fix: codeblock various fixes (#6550)

This commit is contained in:
Alexander Onnikov 2024-09-13 21:48:00 +07:00 committed by GitHub
parent 81fbac2a73
commit 24bd0e9f59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 141 additions and 28 deletions

View File

@ -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)

View File

@ -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",

View File

@ -0,0 +1,32 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import diffview from '@hcengineering/diffview'
import { MarkupNode } from '@hcengineering/text'
import { Component } from '@hcengineering/ui'
export let node: MarkupNode
export let preview = false
$: language = node.attrs?.language
$: content = node.content ?? []
$: value = content.map((node) => node.text).join('/n')
</script>
{#if node}
<pre class="proseCodeBlock" style:margin={preview ? '0' : null}><code
><Component is={diffview.component.Highlight} props={{ value, language }} /></code
></pre>
{/if}

View File

@ -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 @@
<MarkupNodes {nodes} {preview} />
</svelte:element>
{:else if node.type === MarkupNodeType.code_block}
<pre class="proseCodeBlock" style:margin={preview ? '0' : null}><code><MarkupNodes {nodes} {preview} /></code></pre>
<CodeBlockNode {node} {preview} />
{:else if node.type === MarkupNodeType.image}
{@const src = toString(attrs.src)}
{@const alt = toString(attrs.alt)}

View File

@ -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%);

View File

@ -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;

View File

@ -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",

View File

@ -0,0 +1,23 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import dompurify from 'dompurify'
export let value: string
$: sanitized = dompurify.sanitize(value)
</script>
{@html sanitized}

View File

@ -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'

View File

@ -0,0 +1,26 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Html } from '@hcengineering/ui'
import { highlightText } from '../highlight'
export let value: string
export let language: string | undefined = undefined
$: highlighted = highlightText(value, { language })
</script>
<Html value={highlighted} />

View File

@ -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[] {

View File

@ -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<Resources> => ({
component: {
DiffView,
InlineDiffView
InlineDiffView,
Highlight
}
})

View File

@ -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,

View File

@ -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<typeof createLowlight>
@ -26,14 +27,19 @@ const chevronSvg = `<svg width="16" height="16" viewBox="0 0 32 32" fill="curren
<path d="M16 22L6 12L7.4 10.6L16 19.2L24.6 10.6L26 12L16 22Z" />
</svg>`
export const CodeBlockExtension = CodeBlockLowlight.extend<CodeBlockLowlightOptions>({
export const codeBlockHighlightOptions: CodeBlockLowlightOptions = {
...codeBlockOptions,
lowlight: createLowlight(common)
}
export const CodeBlockHighlighExtension = CodeBlockLowlight.extend<CodeBlockLowlightOptions>({
addProseMirrorPlugins () {
return [...(this.parent?.() ?? []), LanguageSelector(this.options)]
}
})
export function LanguageSelector (options: CodeBlockLowlightOptions): Plugin {
return new Plugin({
return new Plugin<DecorationSet>({
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'

View File

@ -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<CodeBlockOptions> | false
@ -66,10 +65,7 @@ export const DefaultKit = Extension.create<DefaultKitOptions>({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
CodeBlockExtension.configure({
...codeBlockOptions,
lowlight: createLowlight(common)
})
CodeBlockHighlighExtension.configure(codeBlockHighlightOptions)
]
}
})

View File

@ -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<Extension<EditorKitOptions, any>> {
}
})
],
[200, CodeBlockExtension.configure(codeBlockOptions)],
[200, CodeBlockHighlighExtension.configure(codeBlockHighlightOptions)],
[210, CodeExtension.configure(codeOptions)],
[220, HardBreakExtension.configure({ shortcuts: mode })]
]