diff --git a/src/renderer/src/components/editor/raw-markdown-html-line-break.test.ts b/src/renderer/src/components/editor/raw-markdown-html-line-break.test.ts new file mode 100644 index 0000000000..7a49603b56 --- /dev/null +++ b/src/renderer/src/components/editor/raw-markdown-html-line-break.test.ts @@ -0,0 +1,102 @@ +// @vitest-environment happy-dom + +import { describe, expect, it } from 'vitest' +import { Editor } from '@tiptap/core' +import type { Node as ProseMirrorNode } from '@tiptap/pm/model' +import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport' +import { createRichMarkdownExtensions } from './rich-markdown-extensions' +import { encodeRawMarkdownHtmlForRichEditor } from './raw-markdown-html' + +function createEditor(markdown: string): Editor { + const codec = createRichMarkdownEditorCodec() + return new Editor({ + element: null, + extensions: createRichMarkdownExtensions({ codec }), + content: encodeRawMarkdownHtmlForRichEditor(markdown, codec), + contentType: 'markdown' + }) +} + +function findRawInlineNodes(editor: Editor): ProseMirrorNode[] { + const nodes: ProseMirrorNode[] = [] + editor.state.doc.descendants((node) => { + if (node.type.name === 'rawMarkdownHtmlInline') { + nodes.push(node) + } + }) + return nodes +} + +function renderTag(node: ProseMirrorNode): string { + const spec = node.type.spec.toDOM?.(node) + return Array.isArray(spec) ? String(spec[0]) : '' +} + +const TABLE_WITH_BREAKS = + '| Field | Description |\n' + + '| --- | --- |\n' + + '| verdict | Values:

`normal`
`defect` |\n' + +describe('raw markdown
line breaks in the rich editor', () => { + it('renders
and
inside a table cell as real line breaks, not literal text', () => { + const editor = createEditor(TABLE_WITH_BREAKS) + try { + const breakNodes = findRawInlineNodes(editor) + expect(breakNodes.length).toBeGreaterThan(0) + // Every
/
in the cell renders as an actual
element so the + // values appear on their own lines rather than as raw "
" text. + for (const node of breakNodes) { + expect(renderTag(node)).toBe('br') + } + } finally { + editor.destroy() + } + }) + + it('keeps non-break inline HTML as an inert literal span', () => { + const editor = createEditor('Before hi after\n') + try { + const [spanNode] = findRawInlineNodes(editor) + expect(spanNode).toBeDefined() + expect(renderTag(spanNode)).toBe('span') + } finally { + editor.destroy() + } + }) + + it('round-trips verbatim
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('/g)).toEqual(['
', '
', '
']) + // 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
after\n') + try { + const rendered = document.createElement('div') + rendered.innerHTML = editor.getHTML() + const breakNode = rendered.querySelector('br[data-raw-markdown-html-inline]') + expect(breakNode).not.toBeNull() + breakNode!.setAttribute('data-raw-markdown-html-value', '') + + editor.commands.setContent(rendered.innerHTML) + const output = (editor as Editor & { getMarkdown(): string }).getMarkdown() + expect(output).toContain('Before
after') + expect(output).not.toContain('