fix(editor): keep paragraphs starting with "Dr.", "A." or "iv." out of ordered lists (#32694)

This commit is contained in:
Kelvin Amoaba
2026-10-23 22:19:53 +00:00
committed by GitHub
parent 18555204fe
commit 4c6cfda0ac
8 changed files with 222 additions and 60 deletions
@@ -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'
+16 -15
View File
@@ -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))
+1
View File
@@ -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
@@ -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')
@@ -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)
@@ -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: '<ol type="a" start="2"><li><p>x</p></li><li><p>y</p></li></ol>'
})
try {
expect(editor.getMarkdown()).toBe('2. x\n3. y')
} finally {
editor.destroy()
}
})
})
@@ -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) {
@@ -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)