diff --git a/package-lock.json b/package-lock.json index 5ed1a00a..ee7e2d45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,10 @@ "@patternfly/react-icons": "6.4.0", "copy-webpack-plugin": "14.0.0", "css-loader": "^7.1.4", + "dompurify": "3.4.0", "js-yaml": "^4.1.1", "lodash": "^4.18.1", + "marked": "14.0.0", "react": "17.0.2", "react-dom": "17.0.2", "react-i18next": "11.18.6", @@ -2668,8 +2670,7 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/unist": { "version": "2.0.11", @@ -5952,11 +5953,10 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", + "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -7508,6 +7508,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9819,7 +9820,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, diff --git a/package.json b/package.json index 24e625b8..b6e027cd 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,10 @@ "@patternfly/react-icons": "6.4.0", "copy-webpack-plugin": "14.0.0", "css-loader": "^7.1.4", + "dompurify": "3.4.0", "js-yaml": "^4.1.1", "lodash": "^4.18.1", + "marked": "14.0.0", "react": "17.0.2", "react-dom": "17.0.2", "react-i18next": "11.18.6", @@ -67,6 +69,7 @@ "webpack-dev-server": "5.2.3" }, "overrides": { + "dompurify": "3.4.0", "monaco-editor": "0.55.1", "react": "17.0.2", "react-dom": "17.0.2" diff --git a/src/clipboard.ts b/src/clipboard.ts index fd1058d5..c22024fb 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -1,8 +1,33 @@ -export const copyToClipboard = (value: string): void => { +import DOMPurify from 'dompurify'; +import { marked } from 'marked'; + +const supportsHtmlClipboard = (): boolean => + typeof ClipboardItem !== 'undefined' && + typeof ClipboardItem.supports === 'function' && + ClipboardItem.supports('text/html'); + +export const copyToClipboard = async (value: string): Promise => { try { - navigator.clipboard.writeText(value); + if (typeof ClipboardItem !== 'undefined' && navigator.clipboard?.write) { + const blobs: Record = { + 'text/plain': new Blob([value], { type: 'text/plain' }), + }; + if (supportsHtmlClipboard()) { + const html = DOMPurify.sanitize(marked.parse(value) as string); + blobs['text/html'] = new Blob([html], { type: 'text/html' }); + } + await navigator.clipboard.write([new ClipboardItem(blobs)]); + } else { + await navigator.clipboard.writeText(value); + } } catch (err) { // eslint-disable-next-line no-console - console.error('Failed to copy to clipboard: ', err); + console.warn('Rich clipboard write failed, falling back to plain text: ', err); + try { + await navigator.clipboard.writeText(value); + } catch (fallbackErr) { + // eslint-disable-next-line no-console + console.error('Failed to copy to clipboard: ', fallbackErr); + } } }; diff --git a/tsconfig.json b/tsconfig.json index aee3c1e6..26f5f27f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "noImplicitAny": false, "strictNullChecks": false, "noUnusedLocals": true, - "sourceMap": false + "sourceMap": false, + "allowSyntheticDefaultImports": true }, "include": ["src", "types.d.ts"] }