mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
fix(editor): validate preserved break source
Use the editor model's supported DOM parse-rule type instead of a private duplicate. Exercise the rendered-DOM round trip and reject forged preservation attributes before they can become hidden Markdown source.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
@@ -62,16 +64,39 @@ describe('raw markdown <br> line breaks in the rich editor', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('round-trips the verbatim <br> tags in a table cell on save', () => {
|
||||
it('round-trips verbatim <br> tags after the rendered DOM is parsed again', () => {
|
||||
const editor = createEditor(TABLE_WITH_BREAKS)
|
||||
try {
|
||||
const rendered = document.createElement('div')
|
||||
rendered.innerHTML = editor.getHTML()
|
||||
expect(rendered.querySelectorAll('br[data-raw-markdown-html-inline]')).toHaveLength(3)
|
||||
expect(rendered.textContent).not.toContain('<br')
|
||||
|
||||
editor.commands.setContent(rendered.innerHTML)
|
||||
const output = (editor as Editor & { getMarkdown(): string }).getMarkdown()
|
||||
expect(output).toContain('<br/>')
|
||||
expect(output).toContain('<br>')
|
||||
expect(output.match(/<br\s*\/?>/g)).toEqual(['<br/>', '<br/>', '<br>'])
|
||||
// The break must not silently collapse into a space on serialization.
|
||||
expect(output).not.toContain('`normal` `defect`')
|
||||
} finally {
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not accept arbitrary Markdown source from a forged break marker', () => {
|
||||
const editor = createEditor('Before<br/>after\n')
|
||||
try {
|
||||
const rendered = document.createElement('div')
|
||||
rendered.innerHTML = editor.getHTML()
|
||||
rendered
|
||||
.querySelector('br[data-raw-markdown-html-inline]')
|
||||
?.setAttribute('data-raw-markdown-html-value', '<script>alert(1)</script>')
|
||||
|
||||
editor.commands.setContent(rendered.innerHTML)
|
||||
const output = (editor as Editor & { getMarkdown(): string }).getMarkdown()
|
||||
expect(output).toContain('Before<br>after')
|
||||
expect(output).not.toContain('<script>')
|
||||
} finally {
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mergeAttributes } from '@tiptap/core'
|
||||
import type { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import type { DOMOutputSpec, Node as ProseMirrorNode, TagParseRule } from '@tiptap/pm/model'
|
||||
import type { RichMarkdownSourceKind } from './rich-markdown-source-transport'
|
||||
|
||||
// Why: preserves the exact authored <br> spelling on the rendered break node so
|
||||
@@ -14,16 +14,17 @@ type RawMarkdownSourceDomConfig = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
type RawMarkdownSourceParseRule = {
|
||||
tag: string
|
||||
priority?: number
|
||||
getAttrs: (element: HTMLElement) => { value: string }
|
||||
}
|
||||
|
||||
function isHtmlLineBreak(value: string): boolean {
|
||||
return /^<br\s*\/?>$/i.test(value.trim())
|
||||
}
|
||||
|
||||
function parsedHtmlLineBreakValue(element: HTMLElement): string {
|
||||
const value = element.getAttribute(HTML_LINE_BREAK_VALUE_ATTR)
|
||||
// Why: DOM input can come from the clipboard, so only trusted break syntax
|
||||
// may become hidden Markdown source through the internal preservation marker.
|
||||
return value && isHtmlLineBreak(value) ? value : '<br>'
|
||||
}
|
||||
|
||||
function nodeValue(node: ProseMirrorNode): string {
|
||||
return typeof node.attrs.value === 'string' ? node.attrs.value : ''
|
||||
}
|
||||
@@ -34,10 +35,8 @@ function rendersLineBreaks(config: RawMarkdownSourceDomConfig): boolean {
|
||||
return config.inline && config.kind === 'inline-html'
|
||||
}
|
||||
|
||||
export function rawMarkdownSourceParseRules(
|
||||
config: RawMarkdownSourceDomConfig
|
||||
): RawMarkdownSourceParseRule[] {
|
||||
const rules: RawMarkdownSourceParseRule[] = [
|
||||
export function rawMarkdownSourceParseRules(config: RawMarkdownSourceDomConfig): TagParseRule[] {
|
||||
const rules: TagParseRule[] = [
|
||||
{
|
||||
tag: `${config.inline ? 'span' : 'div'}[${config.marker}]`,
|
||||
getAttrs: (element: HTMLElement) => ({ value: element.textContent ?? '' })
|
||||
@@ -50,9 +49,7 @@ export function rawMarkdownSourceParseRules(
|
||||
rules.push({
|
||||
tag: `br[${config.marker}]`,
|
||||
priority: 100,
|
||||
getAttrs: (element: HTMLElement) => ({
|
||||
value: element.getAttribute(HTML_LINE_BREAK_VALUE_ATTR) ?? '<br>'
|
||||
})
|
||||
getAttrs: (element: HTMLElement) => ({ value: parsedHtmlLineBreakValue(element) })
|
||||
})
|
||||
}
|
||||
return rules
|
||||
|
||||
Reference in New Issue
Block a user