From 086cfc8ece61d8c49cbd3dd0b236a495276644eb Mon Sep 17 00:00:00 2001 From: clean99 <53188310+clean99@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:38:42 +0800 Subject: [PATCH 1/5] [lexical-mdast] Bug Fix: Keep edits to imported autolink literals ## Description After importing a bare URL, changing its destination, label or formatting is lost on Markdown export. The saved literal style currently emits only the visible text. Keep the bare form only when its plain text still parses as the same link. Otherwise use the default link serializer. Unchanged literals keep their syntax. ## Changeset `.changeset/mdast-edited-autolink-literals.md` ## Test plan `pnpm run test-unit packages/lexical-mdast --maxWorkers=1` ### Before The added round-trip test with the original exporter: ```text Expected: "[https://one.example](https://changed.example) [caption](https://two.example) [**https://three.example**](https://three.example)" Received: "https://one.example caption https://three.example" ``` ### After ```text Test Files 8 passed (8) Tests 286 passed (286) ``` The regression reimports the export and checks destinations, labels, bold formatting and a second export. Browser suites were not run; this exercises editor state and serialization in the package's Node unit tests. --- .changeset/mdast-edited-autolink-literals.md | 7 +++ packages/lexical-mdast/src/MdastExport.ts | 37 ++++++++++-- .../__tests__/unit/MdastImportExport.test.ts | 59 +++++++++++++++++++ 3 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 .changeset/mdast-edited-autolink-literals.md diff --git a/.changeset/mdast-edited-autolink-literals.md b/.changeset/mdast-edited-autolink-literals.md new file mode 100644 index 00000000000..4a3750b1923 --- /dev/null +++ b/.changeset/mdast-edited-autolink-literals.md @@ -0,0 +1,7 @@ +--- +'@lexical/mdast': patch +--- + +[fix] Preserve edits to imported autolink literals when exporting Markdown + +Verify: Import a bare URL with `MdastAutolinkLiteralExtension`, change its destination, label or text formatting, then export and reimport it. The edits should survive. Unchanged bare URLs and email addresses should keep their original syntax. diff --git a/packages/lexical-mdast/src/MdastExport.ts b/packages/lexical-mdast/src/MdastExport.ts index bb310243bbf..25969254870 100644 --- a/packages/lexical-mdast/src/MdastExport.ts +++ b/packages/lexical-mdast/src/MdastExport.ts @@ -48,6 +48,7 @@ import { IS_ITALIC, IS_STRIKETHROUGH, } from 'lexical'; +import {fromMarkdown} from 'mdast-util-from-markdown'; import {defaultHandlers, toMarkdown} from 'mdast-util-to-markdown'; import {toString as mdastToString} from 'mdast-util-to-string'; @@ -148,6 +149,7 @@ function collectHardBreaks(tree: Root): Set { * options (and contributed `toMarkdownExtensions`) still apply to them. */ const syntaxToMarkdown = ( + compiled: CompiledMdast, hardBreaks: ReadonlySet, ): ToMarkdownExtension => ({ handlers: { @@ -196,11 +198,34 @@ const syntaxToMarkdown = ( // A title can't be expressed in autolink/literal form, so links with a // title always fall through to the default (resource form). const style = node.data && node.data.mdastLinkStyle; - if (style === 'literal' && node.title == null) { - // A bare GFM autolink literal: emit the visible text as-is. The - // gfm-autolink-literal to-markdown extension keeps surrounding - // escaping consistent so it re-parses as the same literal. - return mdastToString(node); + if ( + style === 'literal' && + node.title == null && + node.children.length === 1 && + node.children[0].type === 'text' + ) { + // Preserve the bare form only while it still parses as the same link. + // Edits to its destination, label or formatting need resource syntax. + const text = node.children[0].value; + const tree = fromMarkdown(text, { + extensions: compiled.micromarkExtensions, + mdastExtensions: compiled.mdastExtensions, + }); + const paragraph = tree.children[0]; + const literal = + tree.children.length === 1 && + paragraph.type === 'paragraph' && + paragraph.children.length === 1 + ? paragraph.children[0] + : null; + if ( + literal !== null && + literal.type === 'link' && + literal.url === node.url && + mdastToString(literal) === text + ) { + return text; + } } if (style === 'inline') { // Force `[text](url)` even when the text equals the URL (the @@ -677,7 +702,7 @@ export function createMdastExport(compiled: CompiledMdast): { extensions: [ defaults, ...compiled.toMarkdownExtensions, - syntaxToMarkdown(collectHardBreaks(tree)), + syntaxToMarkdown(compiled, collectHardBreaks(tree)), ], }); // toMarkdown always appends a trailing newline; drop it so callers get the diff --git a/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts b/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts index 54c838efb73..2377f96a95a 100644 --- a/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts +++ b/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts @@ -614,6 +614,65 @@ describe('@lexical/mdast import/export', () => { } }); + it('keeps edits to imported autolink literals', () => { + using editor = buildEditorFromExtensions( + MdastCommonMarkExtension, + MdastAutolinkLiteralExtension, + MdastExtension, + ); + editor.update( + () => + $convertFromMarkdownString( + 'https://one.example https://two.example https://three.example', + ), + {discrete: true}, + ); + const $links = () => + $assertNodeType($getRoot().getFirstChildOrThrow(), $isElementNode) + .getChildren() + .filter($isLinkNode); + editor.update( + () => { + const [destination, label, formatted] = $links(); + destination.setURL('https://changed.example'); + $assertNodeType( + label.getFirstChildOrThrow(), + $isTextNode, + ).setTextContent('caption'); + $assertNodeType( + formatted.getFirstChildOrThrow(), + $isTextNode, + ).setFormat('bold'); + }, + {discrete: true}, + ); + const markdown = editor.read(() => $convertToMarkdownString()); + expect(markdown).toBe( + '[https://one.example](https://changed.example) ' + + '[caption](https://two.example) ' + + '[**https://three.example**](https://three.example)', + ); + editor.update(() => $convertFromMarkdownString(markdown), { + discrete: true, + }); + editor.read(() => { + expect( + $links().map(link => [link.getURL(), link.getTextContent()]), + ).toEqual([ + ['https://changed.example', 'https://one.example'], + ['https://two.example', 'caption'], + ['https://three.example', 'https://three.example'], + ]); + expect( + $assertNodeType( + $links()[2].getFirstChildOrThrow(), + $isTextNode, + ).hasFormat('bold'), + ).toBe(true); + expect($convertToMarkdownString()).toBe(markdown); + }); + }); + describe('mdast tree interop', () => { it('$convertToMdast exposes the mdast tree before serialization', () => { using editor = createEditor(); From 165b860ec2a2e57b9391f2390973b9f25273026f Mon Sep 17 00:00:00 2001 From: clean99 <53188310+clean99@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:14:32 +0800 Subject: [PATCH 2/5] [lexical-mdast] Bug Fix: Keep edits to imported autolink literals ## Description After importing a bare URL, changing its destination, label or formatting is lost on Markdown export. The saved literal style currently emits only the visible text. Keep the bare form only when its plain text still parses as the same link. Otherwise use the default link serializer. Reuse parsed destinations during lookahead and repeated exports, retaining only the literal labels seen in the latest export. Unchanged literals keep their syntax. ## Changeset `.changeset/mdast-edited-autolink-literals.md` ## Test plan `pnpm run test-unit packages/lexical-mdast --maxWorkers=1` ### Before The added round-trip test with the original exporter: ```text Expected: "[https://one.example](https://changed.example) [caption](https://two.example) [**https://three.example**](https://three.example)" Received: "https://one.example caption https://three.example" ``` ### After ```text Test Files 8 passed (8) Tests 287 passed (287) ``` The tests cover edited-link round trips and parser calls during lookahead and repeated exports. `pnpm run ci-check`, package builds and the full unit suite with fixture-only `GIT_CONFIG_GLOBAL=/dev/null` also pass. Browser suites were not run; this exercises editor state and serialization in the package's Node unit tests. --- packages/lexical-mdast/src/MdastExport.ts | 53 ++++++++++++------- .../__tests__/unit/MdastImportExport.test.ts | 42 +++++++++++++++ 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/packages/lexical-mdast/src/MdastExport.ts b/packages/lexical-mdast/src/MdastExport.ts index 25969254870..ffe0df56c06 100644 --- a/packages/lexical-mdast/src/MdastExport.ts +++ b/packages/lexical-mdast/src/MdastExport.ts @@ -149,7 +149,7 @@ function collectHardBreaks(tree: Root): Set { * options (and contributed `toMarkdownExtensions`) still apply to them. */ const syntaxToMarkdown = ( - compiled: CompiledMdast, + getLiteralUrl: (text: string) => string | null, hardBreaks: ReadonlySet, ): ToMarkdownExtension => ({ handlers: { @@ -207,23 +207,7 @@ const syntaxToMarkdown = ( // Preserve the bare form only while it still parses as the same link. // Edits to its destination, label or formatting need resource syntax. const text = node.children[0].value; - const tree = fromMarkdown(text, { - extensions: compiled.micromarkExtensions, - mdastExtensions: compiled.mdastExtensions, - }); - const paragraph = tree.children[0]; - const literal = - tree.children.length === 1 && - paragraph.type === 'paragraph' && - paragraph.children.length === 1 - ? paragraph.children[0] - : null; - if ( - literal !== null && - literal.type === 'link' && - literal.url === node.url && - mdastToString(literal) === text - ) { + if (getLiteralUrl(text) === node.url) { return text; } } @@ -672,6 +656,7 @@ export function createMdastExport(compiled: CompiledMdast): { // The unfiltered exporter has no per-call state and is shared by every // whole-document export; selection exports build a fresh one per call. const documentExporter = createNodeExporter(compiled); + let literalUrls = new Map(); const $toMdast = ( root: ElementNode, @@ -684,6 +669,35 @@ export function createMdastExport(compiled: CompiledMdast): { }); const $serialize = (tree: Root, root: ElementNode): string => { + const previousLiteralUrls = literalUrls; + const nextLiteralUrls = new Map(); + const getLiteralUrl = (text: string): string | null => { + let url = nextLiteralUrls.get(text); + if (url === undefined) { + url = previousLiteralUrls.get(text); + } + if (url === undefined) { + const parsed = fromMarkdown(text, { + extensions: compiled.micromarkExtensions, + mdastExtensions: compiled.mdastExtensions, + }); + const paragraph = parsed.children[0]; + const literal = + parsed.children.length === 1 && + paragraph.type === 'paragraph' && + paragraph.children.length === 1 + ? paragraph.children[0] + : null; + url = + literal !== null && + literal.type === 'link' && + mdastToString(literal) === text + ? literal.url + : null; + } + nextLiteralUrls.set(text, url); + return url; + }; // Emphasis/strong delimiters are document-level: `_` when one was // recorded on import, unless contributed toMarkdownExtensions set them. // Defaults (with the '-' bullet baseline) ride as the FIRST extension so @@ -702,9 +716,10 @@ export function createMdastExport(compiled: CompiledMdast): { extensions: [ defaults, ...compiled.toMarkdownExtensions, - syntaxToMarkdown(compiled, collectHardBreaks(tree)), + syntaxToMarkdown(getLiteralUrl, collectHardBreaks(tree)), ], }); + literalUrls = nextLiteralUrls; // toMarkdown always appends a trailing newline; drop it so callers get the // same shape as `@lexical/markdown`'s `$convertToMarkdownString`. return out.replace(/\n$/, ''); diff --git a/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts b/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts index 2377f96a95a..9c35145c2aa 100644 --- a/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts +++ b/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts @@ -614,6 +614,45 @@ describe('@lexical/mdast import/export', () => { } }); + it('reuses literal validation during lookahead and repeated exports', () => { + let parses = 0; + using editor = buildEditorFromExtensions( + MdastCommonMarkExtension, + MdastAutolinkLiteralExtension, + configExtension(MdastExtension, { + mdastExtensions: [ + { + transforms: [ + () => { + parses++; + }, + ], + }, + ], + }), + ); + const markdown = 'https://lexical.dev www.example.com'; + editor.update(() => $convertFromMarkdownString(markdown), { + discrete: true, + }); + parses = 0; + expect(editor.read(() => $convertToMarkdownString())).toBe(markdown); + expect(parses).toBeLessThanOrEqual(2); + const firstExportParses = parses; + expect(editor.read(() => $convertToMarkdownString())).toBe(markdown); + expect(parses).toBe(firstExportParses); + editor.update( + () => $getRoot().getAllTextNodes()[0].setTextContent('caption'), + {discrete: true}, + ); + const edited = '[caption](https://lexical.dev) www.example.com'; + expect(editor.read(() => $convertToMarkdownString())).toBe(edited); + expect(parses).toBeLessThanOrEqual(firstExportParses + 1); + const editedExportParses = parses; + expect(editor.read(() => $convertToMarkdownString())).toBe(edited); + expect(parses).toBe(editedExportParses); + }); + it('keeps edits to imported autolink literals', () => { using editor = buildEditorFromExtensions( MdastCommonMarkExtension, @@ -627,6 +666,9 @@ describe('@lexical/mdast import/export', () => { ), {discrete: true}, ); + expect(editor.read(() => $convertToMarkdownString())).toBe( + 'https://one.example https://two.example https://three.example', + ); const $links = () => $assertNodeType($getRoot().getFirstChildOrThrow(), $isElementNode) .getChildren() From 6ce92d8823042f4d23e75a381fa9490674c1fd15 Mon Sep 17 00:00:00 2001 From: clean99 <53188310+clean99@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:38:28 +0800 Subject: [PATCH 3/5] [lexical-mdast] Bug Fix: Reuse literal validation across export roots ## Description Alternating subtree exports or a selection export between document exports replaces cached literal results and repeats parsing. Keep up to 1,000 labels per exporter, evicting the oldest instead of replacing the map after each export. Note the existing neighbouring-text limitation without changing literal serialization rules. ## Test plan ### Before `pnpm run test-unit packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts --maxWorkers=1` with the new subtree/selection test and the previous implementation: ```text AssertionError: expected 5 to be 2 // Object.is equality Test Files 1 failed (1) Tests 1 failed | 106 passed (107) ``` ### After `pnpm run test-unit packages/lexical-mdast --maxWorkers=1` ```text Test Files 8 passed (8) Tests 288 passed (288) ``` `pnpm run ci-check`, development/production/www builds and declaration validation pass. The full unit suite with fixture-only `GIT_CONFIG_GLOBAL=/dev/null` passes: 31,522 passed, 470 expected fail, 135 skipped. Browser suites were not rerun locally. --- packages/lexical-mdast/src/MdastExport.ts | 16 +++--- .../__tests__/unit/MdastImportExport.test.ts | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/lexical-mdast/src/MdastExport.ts b/packages/lexical-mdast/src/MdastExport.ts index ffe0df56c06..3cc173324de 100644 --- a/packages/lexical-mdast/src/MdastExport.ts +++ b/packages/lexical-mdast/src/MdastExport.ts @@ -206,6 +206,7 @@ const syntaxToMarkdown = ( ) { // Preserve the bare form only while it still parses as the same link. // Edits to its destination, label or formatting need resource syntax. + // Neighbouring text can still change how the bare form re-parses. const text = node.children[0].value; if (getLiteralUrl(text) === node.url) { return text; @@ -656,7 +657,7 @@ export function createMdastExport(compiled: CompiledMdast): { // The unfiltered exporter has no per-call state and is shared by every // whole-document export; selection exports build a fresh one per call. const documentExporter = createNodeExporter(compiled); - let literalUrls = new Map(); + const literalUrls = new Map(); const $toMdast = ( root: ElementNode, @@ -669,13 +670,8 @@ export function createMdastExport(compiled: CompiledMdast): { }); const $serialize = (tree: Root, root: ElementNode): string => { - const previousLiteralUrls = literalUrls; - const nextLiteralUrls = new Map(); const getLiteralUrl = (text: string): string | null => { - let url = nextLiteralUrls.get(text); - if (url === undefined) { - url = previousLiteralUrls.get(text); - } + let url = literalUrls.get(text); if (url === undefined) { const parsed = fromMarkdown(text, { extensions: compiled.micromarkExtensions, @@ -694,8 +690,11 @@ export function createMdastExport(compiled: CompiledMdast): { mdastToString(literal) === text ? literal.url : null; + literalUrls.set(text, url); + if (literalUrls.size > 1000) { + literalUrls.delete(literalUrls.keys().next().value!); + } } - nextLiteralUrls.set(text, url); return url; }; // Emphasis/strong delimiters are document-level: `_` when one was @@ -719,7 +718,6 @@ export function createMdastExport(compiled: CompiledMdast): { syntaxToMarkdown(getLiteralUrl, collectHardBreaks(tree)), ], }); - literalUrls = nextLiteralUrls; // toMarkdown always appends a trailing newline; drop it so callers get the // same shape as `@lexical/markdown`'s `$convertToMarkdownString`. return out.replace(/\n$/, ''); diff --git a/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts b/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts index 9c35145c2aa..40deec5f810 100644 --- a/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts +++ b/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts @@ -653,6 +653,56 @@ describe('@lexical/mdast import/export', () => { expect(parses).toBe(editedExportParses); }); + it('reuses literal validation across subtree and selection exports', () => { + let parses = 0; + using editor = buildEditorFromExtensions( + MdastCommonMarkExtension, + MdastAutolinkLiteralExtension, + configExtension(MdastExtension, { + mdastExtensions: [ + { + transforms: [ + () => { + parses++; + }, + ], + }, + ], + }), + ); + const markdown = 'https://one.example\n\nhttps://two.example'; + editor.update(() => $convertFromMarkdownString(markdown), { + discrete: true, + }); + parses = 0; + editor.read(() => { + const first = $assertNodeType( + $getRoot().getFirstChildOrThrow(), + $isElementNode, + ); + const last = $assertNodeType( + $getRoot().getLastChildOrThrow(), + $isElementNode, + ); + expect($convertToMarkdownString(first)).toBe('https://one.example'); + expect($convertToMarkdownString(last)).toBe('https://two.example'); + const warmedParses = parses; + expect($convertToMarkdownString(first)).toBe('https://one.example'); + expect($convertToMarkdownString(last)).toBe('https://two.example'); + expect($convertToMarkdownString()).toBe(markdown); + expect(parses).toBe(warmedParses); + }); + editor.update(() => $getRoot().getAllTextNodes()[0].select(0), { + discrete: true, + }); + const warmedParses = parses; + expect(editor.read(() => $convertSelectionToMarkdownString())).toBe( + 'https://one.example', + ); + expect(editor.read(() => $convertToMarkdownString())).toBe(markdown); + expect(parses).toBe(warmedParses); + }); + it('keeps edits to imported autolink literals', () => { using editor = buildEditorFromExtensions( MdastCommonMarkExtension, From 5d6321958dc2d17f670056a28e0f4944aa258850 Mon Sep 17 00:00:00 2001 From: clean99 <53188310+clean99@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:47:19 +0800 Subject: [PATCH 4/5] [lexical-mdast] Bug Fix: Remember the literal labels of the latest document export --- packages/lexical-mdast/src/MdastExport.ts | 31 ++++++++++--- .../__tests__/unit/MdastImportExport.test.ts | 44 +++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/lexical-mdast/src/MdastExport.ts b/packages/lexical-mdast/src/MdastExport.ts index 3cc173324de..dec0eccba92 100644 --- a/packages/lexical-mdast/src/MdastExport.ts +++ b/packages/lexical-mdast/src/MdastExport.ts @@ -41,6 +41,7 @@ import { $isElementNode, $isLineBreakNode, $isRangeSelection, + $isRootNode, $isSlotHost, $isTextNode, IS_BOLD, @@ -657,7 +658,7 @@ export function createMdastExport(compiled: CompiledMdast): { // The unfiltered exporter has no per-call state and is shared by every // whole-document export; selection exports build a fresh one per call. const documentExporter = createNodeExporter(compiled); - const literalUrls = new Map(); + let literalUrls = new Map(); const $toMdast = ( root: ElementNode, @@ -669,7 +670,14 @@ export function createMdastExport(compiled: CompiledMdast): { type: 'root', }); - const $serialize = (tree: Root, root: ElementNode): string => { + const $serialize = ( + tree: Root, + root: ElementNode, + isDocumentExport: boolean, + ): string => { + const nextLiteralUrls = isDocumentExport + ? new Map() + : literalUrls; const getLiteralUrl = (text: string): string | null => { let url = literalUrls.get(text); if (url === undefined) { @@ -691,10 +699,8 @@ export function createMdastExport(compiled: CompiledMdast): { ? literal.url : null; literalUrls.set(text, url); - if (literalUrls.size > 1000) { - literalUrls.delete(literalUrls.keys().next().value!); - } } + nextLiteralUrls.set(text, url); return url; }; // Emphasis/strong delimiters are document-level: `_` when one was @@ -718,6 +724,9 @@ export function createMdastExport(compiled: CompiledMdast): { syntaxToMarkdown(getLiteralUrl, collectHardBreaks(tree)), ], }); + if (isDocumentExport) { + literalUrls = nextLiteralUrls; + } // toMarkdown always appends a trailing newline; drop it so callers get the // same shape as `@lexical/markdown`'s `$convertToMarkdownString`. return out.replace(/\n$/, ''); @@ -728,7 +737,11 @@ export function createMdastExport(compiled: CompiledMdast): { const $exportToMarkdown = (node?: ElementNode): string => { const root = node || $getRoot(); - return $serialize($toMdast(root, documentExporter), root); + return $serialize( + $toMdast(root, documentExporter), + root, + $isRootNode(root), + ); }; const $exportSelectionToMarkdown = ( @@ -748,7 +761,11 @@ export function createMdastExport(compiled: CompiledMdast): { return $withRenderContext([ contextValue(RenderContextMarkdownSelection, selection), ])(() => - $serialize($toMdast(root, createNodeExporter(compiled, selection)), root), + $serialize( + $toMdast(root, createNodeExporter(compiled, selection)), + root, + false, + ), ); }; diff --git a/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts b/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts index 40deec5f810..073a4f9a5f6 100644 --- a/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts +++ b/packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts @@ -703,6 +703,50 @@ describe('@lexical/mdast import/export', () => { expect(parses).toBe(warmedParses); }); + it('retains the literal labels used by the latest document export', () => { + let parses = 0; + using editor = buildEditorFromExtensions( + MdastCommonMarkExtension, + MdastAutolinkLiteralExtension, + configExtension(MdastExtension, { + mdastExtensions: [ + { + transforms: [ + () => { + parses++; + }, + ], + }, + ], + }), + ); + const markdown = Array.from( + {length: 1001}, + (_, i) => `https://example.com/${i}`, + ).join('\n\n'); + editor.update(() => $convertFromMarkdownString(markdown), { + discrete: true, + }); + expect(editor.read(() => $convertToMarkdownString())).toBe(markdown); + const warmedParses = parses; + expect(editor.read(() => $convertToMarkdownString())).toBe(markdown); + expect(parses).toBe(warmedParses); + editor.update(() => $getRoot().getLastChildOrThrow().remove(), { + discrete: true, + }); + const shortened = markdown.slice(0, markdown.lastIndexOf('\n\n')); + expect(editor.read(() => $convertToMarkdownString($getRoot()))).toBe( + shortened, + ); + expect(parses).toBe(warmedParses); + editor.update(() => $convertFromMarkdownString(markdown), { + discrete: true, + }); + parses = 0; + expect(editor.read(() => $convertToMarkdownString())).toBe(markdown); + expect(parses).toBe(1); + }); + it('keeps edits to imported autolink literals', () => { using editor = buildEditorFromExtensions( MdastCommonMarkExtension, From 74e3ed43b9e2392f474b760e782de53b2f95dd29 Mon Sep 17 00:00:00 2001 From: clean99 <53188310+clean99@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:48:05 +0800 Subject: [PATCH 5/5] [lexical-mdast] Docs: Specify autolink export verification cases --- .changeset/mdast-edited-autolink-literals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/mdast-edited-autolink-literals.md b/.changeset/mdast-edited-autolink-literals.md index 4a3750b1923..03407819c24 100644 --- a/.changeset/mdast-edited-autolink-literals.md +++ b/.changeset/mdast-edited-autolink-literals.md @@ -4,4 +4,4 @@ [fix] Preserve edits to imported autolink literals when exporting Markdown -Verify: Import a bare URL with `MdastAutolinkLiteralExtension`, change its destination, label or text formatting, then export and reimport it. The edits should survive. Unchanged bare URLs and email addresses should keep their original syntax. +Verify: Import a bare URL with `MdastAutolinkLiteralExtension`, change its destination, label or text formatting, then export and reimport it. The edits should survive. Unchanged bare URLs and email addresses should keep their original syntax. Also check that `**https://a.example**` exports as `[**https://a.example**](https://a.example)`, literal-styled links loaded from JSON without `MdastAutolinkLiteralExtension` export as ``, and copying `https://one.ex` from `https://one.example` exports as `[https://one.ex](https://one.example)`.