diff --git a/config/patches/@tiptap__extension-list@3.31.3.patch b/config/patches/@tiptap__extension-list@3.31.3.patch new file mode 100644 index 00000000000..4f1640a6920 --- /dev/null +++ b/config/patches/@tiptap__extension-list@3.31.3.patch @@ -0,0 +1,102 @@ +diff --git a/dist/index.cjs b/dist/index.cjs +index 283575c0a81fa24ea2d73cb8096de05bce7ab26b..c7b19bda70eececb4688250f8bd11cbe6db6a9aa 100644 +--- a/dist/index.cjs ++++ b/dist/index.cjs +@@ -245,10 +245,9 @@ const ROMAN_NUMERALS = [ + ]; + const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz"; + /** +-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha. +-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha. ++* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose. + */ +-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`; ++const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`; + /** + * Convert a number to lowercase roman numerals. + * @example toRoman(1) // 'i' +diff --git a/dist/index.js b/dist/index.js +index 1fa29e72a755ce527b1840765bbe44e80df38c3d..9a857225b17a51fa6afd14674502e556566bef8c 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -233,10 +233,9 @@ const ROMAN_NUMERALS = [ + ]; + const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz"; + /** +-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha. +-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha. ++* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose. + */ +-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`; ++const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`; + /** + * Convert a number to lowercase roman numerals. + * @example toRoman(1) // 'i' +diff --git a/dist/kit/index.cjs b/dist/kit/index.cjs +index a1f217bd0eb67c84404a4e4b447379eaf3a7f367..1f26c89d35a87bcd98113ad425245a9d86cb7ef0 100644 +--- a/dist/kit/index.cjs ++++ b/dist/kit/index.cjs +@@ -233,10 +233,9 @@ const ROMAN_NUMERALS = [ + ]; + const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz"; + /** +-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha. +-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha. ++* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose. + */ +-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`; ++const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`; + /** + * Convert a number to lowercase roman numerals. + * @example toRoman(1) // 'i' +diff --git a/dist/kit/index.js b/dist/kit/index.js +index ee2b2a9114cb40daa6061340ccd80d128c2981ba..8152d964a20dfac20fc3234f93182325593d027d 100644 +--- a/dist/kit/index.js ++++ b/dist/kit/index.js +@@ -232,10 +232,9 @@ const ROMAN_NUMERALS = [ + ]; + const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz"; + /** +-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha. +-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha. ++* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose. + */ +-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`; ++const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`; + /** + * Convert a number to lowercase roman numerals. + * @example toRoman(1) // 'i' +diff --git a/dist/ordered-list/index.cjs b/dist/ordered-list/index.cjs +index 9373d830293b67e463c9764cec5bacd8c189b473..4292a2b0f1b45fdc0f06d6fe7f351f7484e45974 100644 +--- a/dist/ordered-list/index.cjs ++++ b/dist/ordered-list/index.cjs +@@ -19,10 +19,9 @@ const ROMAN_NUMERALS = [ + ]; + const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz"; + /** +-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha. +-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha. ++* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose. + */ +-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`; ++const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`; + /** + * Convert a number to lowercase roman numerals. + * @example toRoman(1) // 'i' +diff --git a/dist/ordered-list/index.js b/dist/ordered-list/index.js +index ace73267067e8c785e080cbf5bc6655a0527aa36..de1bb3061ec1629b20616f10a42eeba0a4e2222d 100644 +--- a/dist/ordered-list/index.js ++++ b/dist/ordered-list/index.js +@@ -18,10 +18,9 @@ const ROMAN_NUMERALS = [ + ]; + const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz"; + /** +-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha. +-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha. ++* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose. + */ +-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`; ++const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`; + /** + * Convert a number to lowercase roman numerals. + * @example toRoman(1) // 'i' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a23d9eb85d..6e9a7bbb654 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,7 @@ packageExtensionsChecksum: sha256-TrarseUCmMKP0lVjkfyLP2TAvkGZMZzvzPxjKY48nrY= pnpmfileChecksum: sha256-6l6qdgUHKyTLYIQLx/UGfGRnEiFXxIaSlGMgK4mttEw= patchedDependencies: + '@tiptap/extension-list@3.31.3': 294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd '@vscode/windows-process-tree@0.8.0': a09f2e8b2b4e73ef20c317af91f9e06c09abf62fa2c891d99b16f53047f596fd '@xterm/addon-image@0.10.0-beta.301': f088c9207dac42844a542b9da8078be52351cee60585725ef189b9fd30f33f34 '@xterm/addon-ligatures@0.11.0-beta.301': 47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920 @@ -345,7 +346,7 @@ importers: version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-list': specifier: 3.31.3 - version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + version: 3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-mathematics': specifier: 3.31.3 version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@types/katex@0.16.8)(katex@0.18.10) @@ -10399,9 +10400,9 @@ snapshots: '@tiptap/pm': 3.31.3 optional: true - '@tiptap/extension-bullet-list@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': + '@tiptap/extension-bullet-list@3.31.3(@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-list': 3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-code-block-lowlight@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/extension-code-block@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(highlight.js@11.11.1)(lowlight@3.3.0)': dependencies: @@ -10472,15 +10473,15 @@ snapshots: '@tiptap/pm': 3.31.3 linkifyjs: 4.3.3 - '@tiptap/extension-list-item@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': + '@tiptap/extension-list-item@3.31.3(@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-list': 3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-list-keymap@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': + '@tiptap/extension-list-keymap@3.31.3(@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-list': 3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': + '@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) '@tiptap/pm': 3.31.3 @@ -10494,9 +10495,9 @@ snapshots: optionalDependencies: '@types/katex': 0.16.8 - '@tiptap/extension-ordered-list@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': + '@tiptap/extension-ordered-list@3.31.3(@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-list': 3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-paragraph@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: @@ -10577,7 +10578,7 @@ snapshots: '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) '@tiptap/extension-blockquote': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-bold': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) - '@tiptap/extension-bullet-list': 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-bullet-list': 3.31.3(@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) '@tiptap/extension-code': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) '@tiptap/extension-code-block': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-document': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) @@ -10588,10 +10589,10 @@ snapshots: '@tiptap/extension-horizontal-rule': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-italic': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) '@tiptap/extension-link': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-list-item': 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) - '@tiptap/extension-list-keymap': 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) - '@tiptap/extension-ordered-list': 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-list': 3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-list-item': 3.31.3(@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-list-keymap': 3.31.3(@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-ordered-list': 3.31.3(@tiptap/extension-list@3.31.3(patch_hash=294a9c187f6733c78b52c5ac182a8adf90c24c07093a2b40b68725cdb320d7bd)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) '@tiptap/extension-paragraph': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) '@tiptap/extension-strike': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) '@tiptap/extension-text': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d9e70d16421..99459322481 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -136,3 +136,4 @@ patchedDependencies: oxlint-plugin-anti-slop@0.1.2: config/patches/oxlint-plugin-anti-slop@0.1.2.patch monaco-editor@0.55.1: config/patches/monaco-editor@0.55.1.patch sprintf-js@1.1.3: config/patches/sprintf-js@1.1.3.patch + '@tiptap/extension-list@3.31.3': config/patches/@tiptap__extension-list@3.31.3.patch diff --git a/src/renderer/src/components/editor/rich-markdown-blockquote-list.test.ts b/src/renderer/src/components/editor/rich-markdown-blockquote-list.test.ts index 0980dd849de..9dd0a96cbdc 100644 --- a/src/renderer/src/components/editor/rich-markdown-blockquote-list.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-blockquote-list.test.ts @@ -15,10 +15,11 @@ function openMarkdown(source: string): Editor { } describe('rich markdown blockquote list continuations', () => { - it.each(['a.', 'Z)', 'aa.', 'zz)', 'I.', 'iii)', '1.', '3)', '1234567890.'])( + // A no-break space reaches the fallback (only Tiptap reads it); plain spaces are controls. + it.each(['1.\u00a0', '3)\u00a0', '1234567890.\u00a0', '1. ', '3) '])( 'opens %j without throwing during Markdown initialization', (marker) => { - const source = `> ${marker} first\ncontinued` + const source = `> ${marker}first\ncontinued` const codec = createRichMarkdownEditorCodec() const editor = new Editor({ element: null, @@ -39,7 +40,7 @@ describe('rich markdown blockquote list continuations', () => { element: null, extensions: createRichMarkdownExtensions({ codec: expectedCodec }), content: encodeRawMarkdownHtmlForRichEditor( - `> ${marker} first\n> continued`, + `> ${marker}first\n> continued`, expectedCodec ), contentType: 'markdown' @@ -56,49 +57,36 @@ describe('rich markdown blockquote list continuations', () => { ) it.each([ - ['> a. x\nlazy\n> b. y\nlazy2\n', '> 1. x\nlazy\n> 2. y\nlazy2\n'], - ['> a. x\nlazy\n> quoted para\n', '> 1. x\nlazy\n> quoted para\n'], - ['> a. x\n> > nested\nlazy', '> 1. x\n> > nested\nlazy'], + ['> 1.\u00a0x\nlazy\n> 2.\u00a0y\nlazy2\n', '> 1. x\nlazy\n> 2. y\nlazy2\n'], + ['> 1.\u00a0x\nlazy\n> quoted para\n', '> 1. x\nlazy\n> quoted para\n'], + ['> 1.\u00a0x\n> > nested\nlazy', '> 1. x\n> > nested\nlazy'], [ - '> a. x\n> > nested\n> > more\n> b. y\nlazy', + '> 1.\u00a0x\n> > nested\n> > more\n> 2.\u00a0y\nlazy', '> 1. x\n> > nested\n> > more\n> 2. y\nlazy' ], - ['> a. x\nlazy\n > y', '> 1. x\nlazy\n > y'], - ['> a.\tx\nlazy\n > y', '> 1.\tx\nlazy\n > y'], + ['> 1.\u00a0x\nlazy\n > y', '> 1. x\nlazy\n > y'], // Tiptap reads a 5+ space gap as text, not code; compare with the fully quoted form. - ['> a. x\n> > nested\nlazy', '> a. x\n> > nested\n> > lazy'], - ['> iv. x\n> v. y\n> > nested\nlazy', '> iv. x\n> v. y\n> > nested\n> > lazy'] - ])( - 'ends the continued list at the next quoted line like a numeric list: %j', - (source, numeric) => { - const editor = openMarkdown(source) - const control = openMarkdown(numeric) - try { - const withoutListAttrs = (json: unknown): string => - JSON.stringify(json).replace(/"attrs":\{[^}]*\},?/g, '') - expect(withoutListAttrs(editor.getJSON())).toEqual(withoutListAttrs(control.getJSON())) - const withoutMarkers = (markdown: string): string => - markdown.replace(/[a-zA-Z0-9]+[.)](?= )/g, '#.') - expect(withoutMarkers(editor.getMarkdown())).toBe(withoutMarkers(control.getMarkdown())) - } finally { - editor.destroy() - control.destroy() - } + ['> 1.\u00a0 x\n> > nested\nlazy', '> 1.\u00a0 x\n> > nested\n> > lazy'] + ])('ends the continued list at the next quoted line: %j', (source, control) => { + const editor = openMarkdown(source) + const expected = openMarkdown(control) + try { + expect(editor.getJSON()).toEqual(expected.getJSON()) + expect(editor.getMarkdown()).toBe(expected.getMarkdown()) + } finally { + editor.destroy() + expected.destroy() } - ) + }) // Guards a dependency bump: any item start Tiptap accepts must still reach the fallback. it.each([ - '> \ta. x\nlazy', - '> a. x\nlazy', - '> a.\u00a0x\nlazy', + '> \t1. x\nlazy', + '> 1. x\nlazy', '> 1.\u00a0x\nlazy', - '> a.\tx\nlazy', - '> Mr. Smith\nlazy', - '> iv) x\nlazy', - '> zz. x\nlazy', - '> > a. x\nlazy', - '> a. x\r\nlazy' + '> 4)\u00a0x\nlazy', + '> > 1.\u00a0x\nlazy', + '> 1.\u00a0x\r\nlazy' ])('opens %j with whitespace Tiptap accepts but Marked does not', (source) => { const editor = openMarkdown(source) try { @@ -109,7 +97,7 @@ describe('rich markdown blockquote list continuations', () => { }) it('opens many quoted lazy continuations without nesting each into the last', () => { - const editor = openMarkdown('> a. x\nlazy\n'.repeat(1000)) + const editor = openMarkdown('> 1.\u00a0x\nlazy\n'.repeat(1000)) try { expect(JSON.stringify(editor.getJSON()).match(/"blockquote"/g)).toHaveLength(1) expect(editor.getText()).toContain('lazy') diff --git a/src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts b/src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts index 8fb68a0cd1e..b0765c0d93e 100644 --- a/src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts @@ -51,8 +51,8 @@ describe('rich markdown list tokenizers', () => { ) }) - it.each(['a. first\nb. second\n', 'I. first\nII. second\n', ' 3. indented\n'])( - 'preserves upstream ordered marker support for %j', + it.each(['1) first\n2) second\n', '1.\u00a0no-break space\n', ' 3. indented\n'])( + 'matches the base tokenizer for %j', (source) => { expect(getTokenizer(RichMarkdownOrderedList).tokenize(source, [], lexer)).toEqual( getTokenizer(OrderedList).tokenize(source, [], lexer) diff --git a/src/renderer/src/components/editor/rich-markdown-ordered-list.test.ts b/src/renderer/src/components/editor/rich-markdown-ordered-list.test.ts new file mode 100644 index 00000000000..daea66c4ee2 --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-ordered-list.test.ts @@ -0,0 +1,65 @@ +// @vitest-environment happy-dom +import { Editor } from '@tiptap/core' +import { describe, expect, it } from 'vitest' +import { encodeRawMarkdownHtmlForRichEditor } from './raw-markdown-html' +import { createRichMarkdownExtensions } from './rich-markdown-extensions' +import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport' + +function openMarkdown(source: string): Editor { + const codec = createRichMarkdownEditorCodec() + return new Editor({ + element: null, + extensions: createRichMarkdownExtensions({ codec }), + content: encodeRawMarkdownHtmlForRichEditor(source, codec), + contentType: 'markdown' + }) +} + +function orderedListCount(editor: Editor): number { + return JSON.stringify(editor.getJSON()).match(/"type":"orderedList"/g)?.length ?? 0 +} + +describe('rich markdown ordered list markers', () => { + it.each([ + 'Mr. Smith came back.\n\nDr. Jones looked at him.\n\nSt. Louis was a long way off.', + 'A. Smith wrote back.\n\nOK. That works.\n\niv. is not a list here.\n\nI. said so.', + '> a. first\n> continued' + ])('keeps %j as prose when opened and saved', (source) => { + const editor = openMarkdown(source) + try { + expect(orderedListCount(editor)).toBe(0) + expect(editor.getMarkdown()).toBe(source) + } finally { + editor.destroy() + } + }) + + it.each([ + ['1. one\n2. two\n\nDr. Smith said.', 1], + ['1. one\nDr. Smith said.', 1], + ['3. three\n4. four', 1], + ['1. parent\n 1. child\n 2. child two\n2. sibling', 2], + ['1. [ ] task\n2. [x] done', 1] + ] as const)('round-trips numeric list %j unchanged', (source, lists) => { + const editor = openMarkdown(source) + try { + expect(orderedListCount(editor)).toBe(lists) + expect(editor.getMarkdown()).toBe(source) + } finally { + editor.destroy() + } + }) + + it('saves a lettered list pasted as HTML with numeric markers', () => { + const editor = new Editor({ + element: null, + extensions: createRichMarkdownExtensions({ codec: createRichMarkdownEditorCodec() }), + content: '
  1. x

  2. y

' + }) + try { + expect(editor.getMarkdown()).toBe('2. x\n3. y') + } finally { + editor.destroy() + } + }) +}) diff --git a/src/renderer/src/components/editor/rich-markdown-ordered-list.ts b/src/renderer/src/components/editor/rich-markdown-ordered-list.ts index 4ccf53eae77..a454de984ca 100644 --- a/src/renderer/src/components/editor/rich-markdown-ordered-list.ts +++ b/src/renderer/src/components/editor/rich-markdown-ordered-list.ts @@ -6,6 +6,11 @@ const orderedListStart = new RegExp(`^\\s*(?:${ORDERED_LIST_MARKER_PATTERN})[.)] const baseTokenizer = OrderedList.config.markdownTokenizer as MarkdownTokenizer export const RichMarkdownOrderedList = OrderedList.extend({ + addAttributes() { + // Why: Markdown reads only numeric markers, so a lettered HTML paste shows and saves as "1." + const { type: _letteredType, ...attributes } = this.parent?.() ?? {} + return attributes + }, markdownTokenizer: { ...baseTokenizer, tokenize(src, tokens, lexer) { diff --git a/src/renderer/src/components/editor/tiptap-marked-facade.ts b/src/renderer/src/components/editor/tiptap-marked-facade.ts index 40d512a89d1..b90b975294d 100644 --- a/src/renderer/src/components/editor/tiptap-marked-facade.ts +++ b/src/renderer/src/components/editor/tiptap-marked-facade.ts @@ -17,7 +17,7 @@ import { import { ORDERED_LIST_MARKER_PATTERN } from '@tiptap/extension-list' // Same item start Tiptap's ordered list accepts, so every list it built can be re-read here. -const EXTENDED_LIST_ITEM = new RegExp(`^(\\s*(?:${ORDERED_LIST_MARKER_PATTERN})[.)])(\\s+)`) +const TIPTAP_LIST_ITEM = new RegExp(`^(\\s*(?:${ORDERED_LIST_MARKER_PATTERN})[.)])(\\s+)`) function expandColumns(text: string, startColumn = 0): number { let column = startColumn @@ -41,7 +41,7 @@ function sliceBeforeOuterQuote(source: string): string { let contentIndent = 0 let outerQuoteLine = /^>/ for (const [index, line] of lines.entries()) { - const item = EXTENDED_LIST_ITEM.exec(line) + const item = TIPTAP_LIST_ITEM.exec(line) const lineIndent = expandColumns(/^\s*/.exec(line)?.[0] ?? '') if (item && (index === 0 || lineIndent < contentIndent)) { contentIndent = getItemContentIndent(item) @@ -58,11 +58,11 @@ function isListToken(token: Tokens.Generic | undefined): token is Tokens.List { } class BlockquoteListTokenizer extends Tokenizer { - // Why: Marked's blockquote() lazy continuation calls `this.list(newText)!` (still so in - // v18), skipping the registered extended-marker (a., iii)) tokenizers, then reads `.raw`. + // Why: Marked's blockquote() lazy continuation calls `this.list(newText)!` (still so in v18), + // skipping the registered tokenizer that also reads `1.` + no-break space, then reads `.raw`. list(source: string): Tokens.List | undefined { const token = super.list(source) - if (token || !EXTENDED_LIST_ITEM.test(source)) { + if (token || !TIPTAP_LIST_ITEM.test(source)) { return token } const listSource = sliceBeforeOuterQuote(source)