diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 36367f9c66..006a5fa12e 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1223,6 +1223,9 @@ dependencies: '@types/domhandler': specifier: ^2.4.5 version: 2.4.5 + '@types/dompurify': + specifier: ^3.0.5 + version: 3.0.5 '@types/email-addresses': specifier: ^3.0.0 version: 3.0.0 @@ -1451,6 +1454,9 @@ dependencies: domhandler: specifier: ^5.0.3 version: 5.0.3 + dompurify: + specifier: ^3.1.6 + version: 3.1.6 domutils: specifier: ^3.1.0 version: 3.1.0 @@ -8954,6 +8960,12 @@ packages: resolution: {integrity: sha512-lANhC2grmFG1gBac/8sDAKdIXx+TzAdkJIAjEOSMA+qW3297ybACEbacJnG15aNYfrzDO6fdcoouokqAKsy6aQ==} dev: false + /@types/dompurify@3.0.5: + resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} + dependencies: + '@types/trusted-types': 2.0.7 + dev: false + /@types/domutils@1.7.8: resolution: {integrity: sha512-iZGboDV79ibrO3D625p9yD+VgmMDnyJocdIRJvu9Xz66R8SHfOY/XNgdjY5SFoFiLgILceVfSLt7IUhlk1Vhhg==} dependencies: @@ -9534,6 +9546,10 @@ packages: resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} dev: false + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + dev: false + /@types/unist@2.0.10: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} dev: false @@ -12958,6 +12974,10 @@ packages: domelementtype: 2.3.0 dev: false + /dompurify@3.1.6: + resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} + dev: false + /domutils@1.5.1: resolution: {integrity: sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==} dependencies: @@ -30472,7 +30492,7 @@ packages: dev: false file:projects/presentation.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-r+NP0EMgEeKbfaa4v8P1Iho0cfYqe9PhOBfV6SPd/9xnNPt42nK9Gu4r5so1LTolhEUzbFiKh7zSX1ADL5e/3g==, tarball: file:projects/presentation.tgz} + resolution: {integrity: sha512-ryBht4b1zE/Ik6KZqDL/joAzt3968bkRbGZOt3x+pE929i7yCtHmlMC7W65Nlr1eglhC2JTSy2NiKTNv9yjcuw==, tarball: file:projects/presentation.tgz} id: file:projects/presentation.tgz name: '@rush-temp/presentation' version: 0.0.0 @@ -35107,17 +35127,19 @@ packages: dev: false file:projects/ui.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): - resolution: {integrity: sha512-WtSFJW84fNe+3lwzv2a8CRmyYIsY8B6HHJwg3YKLd7jWHF4T8hYIf892hAEv7kvh/vrZ7elq8E8b1znmCNd7Sw==, tarball: file:projects/ui.tgz} + resolution: {integrity: sha512-umESBjjPj7ES3uF9YcS31H5dwqZtMATByltYeDc+XG+7ovD1SOM11UAjBpHCqj026RvvqcSjE8lAQP1zRXxCoA==, tarball: file:projects/ui.tgz} id: file:projects/ui.tgz name: '@rush-temp/ui' version: 0.0.0 dependencies: + '@types/dompurify': 3.0.5 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) autolinker: 4.0.0 date-fns: 2.30.0 date-fns-tz: 2.0.0(date-fns@2.30.0) + dompurify: 3.1.6 emoji-regex: 10.3.0 eslint: 8.56.0 eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) diff --git a/packages/presentation/package.json b/packages/presentation/package.json index 0609bcfbaa..4bdad6e660 100644 --- a/packages/presentation/package.json +++ b/packages/presentation/package.json @@ -48,6 +48,7 @@ "@hcengineering/ui": "^0.6.15", "@hcengineering/view": "^0.6.13", "@hcengineering/text": "^0.6.5", + "@hcengineering/diffview": "^0.6.0", "@hcengineering/uploader": "^0.6.0", "svelte": "^4.2.12", "@hcengineering/client": "^0.6.18", diff --git a/packages/presentation/src/components/markup/CodeBlockNode.svelte b/packages/presentation/src/components/markup/CodeBlockNode.svelte new file mode 100644 index 0000000000..870f78fea3 --- /dev/null +++ b/packages/presentation/src/components/markup/CodeBlockNode.svelte @@ -0,0 +1,32 @@ + + + +{#if node} +
+{/if} diff --git a/packages/presentation/src/components/markup/NodeContent.svelte b/packages/presentation/src/components/markup/NodeContent.svelte index 2986e53582..db31fd0f8d 100644 --- a/packages/presentation/src/components/markup/NodeContent.svelte +++ b/packages/presentation/src/components/markup/NodeContent.svelte @@ -17,6 +17,7 @@ import { AttrValue, MarkupNode, MarkupNodeType } from '@hcengineering/text' import MarkupNodes from './Nodes.svelte' + import CodeBlockNode from './CodeBlockNode.svelte' import ObjectNode from './ObjectNode.svelte' export let node: MarkupNode @@ -71,7 +72,7 @@ {:else if node.type === MarkupNodeType.code_block} -
+ {:else if node.type === MarkupNodeType.image} {@const src = toString(attrs.src)} {@const alt = toString(attrs.alt)} diff --git a/packages/theme/styles/_colors.scss b/packages/theme/styles/_colors.scss index 02ba32a4dd..a78a677c6b 100644 --- a/packages/theme/styles/_colors.scss +++ b/packages/theme/styles/_colors.scss @@ -77,7 +77,6 @@ --text-editor-highlighted-node-delete-background-color: #F6DCDA; --text-editor-highlighted-node-delete-font-color: #54201C; - --text-editor-inline-code-color: #B02B46; --text-editor-table-marker-color: #bebebf; --theme-clockface-sec-arrow: conic-gradient(at 50% -10px, rgba(255, 0, 0, 0), rgba(255, 0, 0, 0) 49%, #F47758 50%, rgba(255, 0, 0, 0) 51%, rgba(255, 0, 0, 0) 100%); diff --git a/packages/theme/styles/prose.scss b/packages/theme/styles/prose.scss index a75196081a..cc905be1b4 100644 --- a/packages/theme/styles/prose.scss +++ b/packages/theme/styles/prose.scss @@ -345,7 +345,6 @@ table.proseTable { margin: 0 1px; padding: 0 .25rem; font-family: var(--mono-font); - color: var(--text-editor-inline-code-color); background-color: var(--theme-button-default); border: 1px solid var(--theme-button-border); border-radius: .25rem; diff --git a/packages/ui/package.json b/packages/ui/package.json index 3c868d4bcf..cecc1cad46 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -33,6 +33,7 @@ "prettier": "^3.1.0", "typescript": "^5.3.3", "@types/jest": "^29.5.5", + "@types/dompurify": "^3.0.5", "jest": "^29.7.0", "ts-jest": "^29.1.1", "svelte-eslint-parser": "^0.33.1" @@ -47,6 +48,7 @@ "emoji-regex": "^10.1.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", + "dompurify": "^3.1.6", "@hcengineering/analytics": "^0.6.0" }, "repository": "https://github.com/hcenginneing/anticrm", diff --git a/packages/ui/src/components/Html.svelte b/packages/ui/src/components/Html.svelte new file mode 100644 index 0000000000..679ac0e1a1 --- /dev/null +++ b/packages/ui/src/components/Html.svelte @@ -0,0 +1,23 @@ + + + +{@html sanitized} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 848610f51c..2a14bf1aea 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -96,6 +96,7 @@ export { default as DatePresenter } from './components/calendar/DatePresenter.sv export { default as DueDatePresenter } from './components/calendar/DueDatePresenter.svelte' export { default as DateTimePresenter } from './components/calendar/DateTimePresenter.svelte' export { default as TimeInputBox } from './components/calendar/TimeInputBox.svelte' +export { default as Html } from './components/Html.svelte' export { default as StylishEdit } from './components/StylishEdit.svelte' export { default as Grid } from './components/Grid.svelte' export { default as Row } from './components/Row.svelte' diff --git a/plugins/diffview-resources/src/components/Highlight.svelte b/plugins/diffview-resources/src/components/Highlight.svelte new file mode 100644 index 0000000000..cb731cabcf --- /dev/null +++ b/plugins/diffview-resources/src/components/Highlight.svelte @@ -0,0 +1,26 @@ + + + + diff --git a/plugins/diffview-resources/src/highlight/highlight.ts b/plugins/diffview-resources/src/highlight/highlight.ts index 96c37bca6c..71109341fd 100644 --- a/plugins/diffview-resources/src/highlight/highlight.ts +++ b/plugins/diffview-resources/src/highlight/highlight.ts @@ -19,18 +19,18 @@ import { hljsDefineSvelte } from './languages/svelte-hljs' hljs.registerLanguage('svelte', hljsDefineSvelte) export interface HighlightOptions { - language: string + language: string | undefined } export function highlightText (text: string, options: HighlightOptions): string { // We should always use highlighter because it sanitizes the input // We have to always use highlighter to ensure that the input is sanitized - const validLanguage = options.language !== '' && hljs.getLanguage(options.language) !== undefined - const language = validLanguage ? options.language : 'text' + const { language } = options + const validLanguage = language !== undefined && hljs.getLanguage(language) !== undefined - const { value: highlighted } = hljs.highlight(text, { language }) - const normalized = normalizeHighlightTags(highlighted) - return normalized + const { value: highlighted } = validLanguage ? hljs.highlight(text, { language }) : hljs.highlightAuto(text) + + return normalizeHighlightTags(highlighted) } export function highlightLines (lines: string[], options: HighlightOptions): string[] { diff --git a/plugins/diffview-resources/src/index.ts b/plugins/diffview-resources/src/index.ts index 4d1c1636e6..0133d541a9 100644 --- a/plugins/diffview-resources/src/index.ts +++ b/plugins/diffview-resources/src/index.ts @@ -15,10 +15,12 @@ import { type Resources } from '@hcengineering/platform' import DiffView from './components/DiffView.svelte' +import Highlight from './components/Highlight.svelte' import InlineDiffView from './components/InlineDiffView.svelte' export default async (): Promise => ({ component: { DiffView, - InlineDiffView + InlineDiffView, + Highlight } }) diff --git a/plugins/diffview/src/index.ts b/plugins/diffview/src/index.ts index f7be7e7c07..7d73b026f0 100644 --- a/plugins/diffview/src/index.ts +++ b/plugins/diffview/src/index.ts @@ -48,7 +48,8 @@ export interface DiffFileId { export default plugin(diffviewId, { component: { DiffView: '' as AnyComponent, - InlineDiffView: '' as AnyComponent + InlineDiffView: '' as AnyComponent, + Highlight: '' as AnyComponent }, string: { ViewMode: '' as IntlString, diff --git a/plugins/text-editor-resources/src/components/extension/codeblock.ts b/plugins/text-editor-resources/src/components/extension/codeblock.ts index 1b82a0bf9f..95375706c5 100644 --- a/plugins/text-editor-resources/src/components/extension/codeblock.ts +++ b/plugins/text-editor-resources/src/components/extension/codeblock.ts @@ -13,12 +13,13 @@ // limitations under the License. // +import { codeBlockOptions } from '@hcengineering/text' import { DropdownLabelsPopup, getEventPositionElement, showPopup } from '@hcengineering/ui' import { type CodeBlockLowlightOptions, CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight' import { type Node as ProseMirrorNode } from '@tiptap/pm/model' import { Plugin, PluginKey } from '@tiptap/pm/state' import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view' -import { type createLowlight } from 'lowlight' +import { common, createLowlight } from 'lowlight' type Lowlight = ReturnType @@ -26,14 +27,19 @@ const chevronSvg = ` ` -export const CodeBlockExtension = CodeBlockLowlight.extend({ +export const codeBlockHighlightOptions: CodeBlockLowlightOptions = { + ...codeBlockOptions, + lowlight: createLowlight(common) +} + +export const CodeBlockHighlighExtension = CodeBlockLowlight.extend({ addProseMirrorPlugins () { return [...(this.parent?.() ?? []), LanguageSelector(this.options)] } }) export function LanguageSelector (options: CodeBlockLowlightOptions): Plugin { - return new Plugin({ + return new Plugin({ key: new PluginKey('codeblock-language-selector'), props: { decorations (state) { @@ -41,13 +47,14 @@ export function LanguageSelector (options: CodeBlockLowlightOptions): Plugin { } }, state: { - init () { - return DecorationSet.empty + init (config, state) { + return createDecorations(state.doc, options) }, apply (tr, prev) { if (tr.docChanged) { return createDecorations(tr.doc, options) } + return prev } } @@ -84,7 +91,7 @@ function createDecorations (doc: ProseMirrorNode, options: CodeBlockLowlightOpti function createLangButton (language: string | null): HTMLButtonElement { const button = document.createElement('button') - button.className = 'antiButton ghost small sh-no-shape bs-none gap-medium iconR' + button.className = 'antiButton link-bordered small sh-no-shape bs-none gap-medium iconR' button.style.position = 'absolute' button.style.top = '0.375rem' button.style.right = '0.375rem' diff --git a/plugins/text-editor-resources/src/kits/default-kit.ts b/plugins/text-editor-resources/src/kits/default-kit.ts index 8158107a84..facd5681dc 100644 --- a/plugins/text-editor-resources/src/kits/default-kit.ts +++ b/plugins/text-editor-resources/src/kits/default-kit.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { codeBlockOptions, codeOptions } from '@hcengineering/text' +import { codeOptions } from '@hcengineering/text' import { showPopup } from '@hcengineering/ui' import { type Editor, Extension } from '@tiptap/core' import type { CodeOptions } from '@tiptap/extension-code' @@ -25,10 +25,9 @@ import Link from '@tiptap/extension-link' import Typography from '@tiptap/extension-typography' import Underline from '@tiptap/extension-underline' import StarterKit from '@tiptap/starter-kit' -import { common, createLowlight } from 'lowlight' import LinkPopup from '../components/LinkPopup.svelte' -import { CodeBlockExtension } from '../components/extension/codeblock' +import { CodeBlockHighlighExtension, codeBlockHighlightOptions } from '../components/extension/codeblock' export interface DefaultKitOptions { codeBlock?: Partial | false @@ -66,10 +65,7 @@ export const DefaultKit = Extension.create({ openOnClick: true, HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' } }), - CodeBlockExtension.configure({ - ...codeBlockOptions, - lowlight: createLowlight(common) - }) + CodeBlockHighlighExtension.configure(codeBlockHighlightOptions) ] } }) diff --git a/plugins/text-editor-resources/src/kits/editor-kit.ts b/plugins/text-editor-resources/src/kits/editor-kit.ts index 39546867f9..a852f7edde 100644 --- a/plugins/text-editor-resources/src/kits/editor-kit.ts +++ b/plugins/text-editor-resources/src/kits/editor-kit.ts @@ -15,7 +15,7 @@ import { type Class, type Doc, type Ref, type Space } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' import { getBlobRef, getClient } from '@hcengineering/presentation' -import { CodeBlockExtension, codeBlockOptions, CodeExtension, codeOptions } from '@hcengineering/text' +import { CodeExtension, codeOptions } from '@hcengineering/text' import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor' import { type AnyExtension, type Editor, Extension } from '@tiptap/core' import { type Level } from '@tiptap/extension-heading' @@ -23,6 +23,7 @@ import ListKeymap from '@tiptap/extension-list-keymap' import TableHeader from '@tiptap/extension-table-header' import 'prosemirror-codemark/dist/codemark.css' +import { CodeBlockHighlighExtension, codeBlockHighlightOptions } from '../components/extension/codeblock' import { NoteExtension, type NoteOptions } from '../components/extension/note' import { FileExtension, type FileOptions } from '../components/extension/fileExt' import { HardBreakExtension } from '../components/extension/hardBreak' @@ -171,7 +172,7 @@ async function buildEditorKit (): Promise> { } }) ], - [200, CodeBlockExtension.configure(codeBlockOptions)], + [200, CodeBlockHighlighExtension.configure(codeBlockHighlightOptions)], [210, CodeExtension.configure(codeOptions)], [220, HardBreakExtension.configure({ shortcuts: mode })] ]