Skip to content

Commit 14b3786

Browse files
committed
fix: preserve empty blockquote lines on markdown round-trips
Fixes #920
1 parent a3070c8 commit 14b3786

File tree

4 files changed

+111
-5
lines changed

4 files changed

+111
-5
lines changed

src/examples/bug-920.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react'
2+
import { MDXEditor, MDXEditorMethods, quotePlugin } from '..'
3+
4+
const fixture = `> one
5+
> two
6+
>
7+
> three`
8+
9+
export function RoundTripBlockquoteEmptyLine() {
10+
const ref = React.useRef<MDXEditorMethods>(null)
11+
const [markdown, setMarkdown] = React.useState(fixture)
12+
const [lastReloadedMarkdown, setLastReloadedMarkdown] = React.useState(fixture)
13+
14+
return (
15+
<div style={{ display: 'grid', gap: 16 }}>
16+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
17+
<button
18+
onClick={() => {
19+
setMarkdown(fixture)
20+
setLastReloadedMarkdown(fixture)
21+
ref.current?.setMarkdown(fixture)
22+
}}
23+
>
24+
Reset fixture
25+
</button>
26+
<button
27+
onClick={() => {
28+
const nextMarkdown = ref.current?.getMarkdown() ?? ''
29+
setLastReloadedMarkdown(nextMarkdown)
30+
ref.current?.setMarkdown(nextMarkdown)
31+
}}
32+
>
33+
Reload exported markdown
34+
</button>
35+
</div>
36+
37+
<MDXEditor ref={ref} markdown={markdown} onChange={setMarkdown} plugins={[quotePlugin()]} />
38+
39+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 16 }}>
40+
<label style={{ display: 'grid', gap: 8 }}>
41+
<span>Current markdown</span>
42+
<textarea readOnly value={markdown} rows={6} style={{ fontFamily: 'monospace', width: '100%' }} />
43+
</label>
44+
45+
<label style={{ display: 'grid', gap: 8 }}>
46+
<span>Last reloaded markdown</span>
47+
<textarea readOnly value={lastReloadedMarkdown} rows={6} style={{ fontFamily: 'monospace', width: '100%' }} />
48+
</label>
49+
</div>
50+
</div>
51+
)
52+
}

src/plugins/core/MdastParagraphVisitor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { $createParagraphNode } from 'lexical'
22
import * as Mdast from 'mdast'
33
import { MdastImportVisitor } from '../../importMarkdownToLexical'
44

5-
const lexicalTypesThatShouldSkipParagraphs = ['listitem', 'quote', 'admonition']
5+
const lexicalTypesThatShouldSkipParagraphs = ['listitem', 'admonition']
66

77
export const MdastParagraphVisitor: MdastImportVisitor<Mdast.Paragraph> = {
88
testNode: 'paragraph',

src/plugins/quote/LexicalQuoteVisitor.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import { LexicalExportVisitor } from '../../exportMarkdownFromLexical'
44

55
export const LexicalQuoteVisitor: LexicalExportVisitor<QuoteNode, Mdast.Blockquote> = {
66
testLexicalNode: $isQuoteNode,
7-
visitLexicalNode: ({ lexicalNode, mdastParent, actions }) => {
8-
const paragraph: Mdast.Paragraph = { type: 'paragraph', children: [] }
9-
actions.appendToParent(mdastParent, { type: 'blockquote', children: [paragraph] })
10-
actions.visitChildren(lexicalNode, paragraph)
7+
visitLexicalNode: ({ actions }) => {
8+
actions.addAndStepInto('blockquote')
119
}
1210
}

src/test/core.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ import React from 'react'
22
import { describe, expect, it } from 'vitest'
33
import { MDXEditor, MDXEditorMethods } from '../'
44
import { render } from '@testing-library/react'
5+
import { $getRoot, createEditor, ParagraphNode, TextNode } from 'lexical'
6+
import { QuoteNode } from '@lexical/rich-text'
7+
import { importMarkdownToLexical } from '../importMarkdownToLexical'
8+
import { exportMarkdownFromLexical } from '../exportMarkdownFromLexical'
9+
import { MdastRootVisitor } from '../plugins/core/MdastRootVisitor'
10+
import { MdastParagraphVisitor } from '../plugins/core/MdastParagraphVisitor'
11+
import { MdastTextVisitor } from '../plugins/core/MdastTextVisitor'
12+
import { MdastBreakVisitor } from '../plugins/core/MdastBreakVisitor'
13+
import { LexicalRootVisitor } from '../plugins/core/LexicalRootVisitor'
14+
import { LexicalParagraphVisitor } from '../plugins/core/LexicalParagraphVisitor'
15+
import { LexicalTextVisitor } from '../plugins/core/LexicalTextVisitor'
16+
import { LexicalLinebreakVisitor } from '../plugins/core/LexicalLinebreakVisitor'
17+
import { MdastBlockQuoteVisitor } from '../plugins/quote/MdastBlockQuoteVisitor'
18+
import { LexicalQuoteVisitor } from '../plugins/quote/LexicalQuoteVisitor'
519

620
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
721
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
@@ -34,6 +48,48 @@ describe('markdown import export', () => {
3448
testIdenticalMarkdown(`Hello\n\nWorld`)
3549
})
3650

51+
it('preserves empty lines inside blockquotes across lexical markdown round-trips', () => {
52+
const editor = createEditor({
53+
namespace: 'test-editor',
54+
nodes: [ParagraphNode, TextNode, QuoteNode],
55+
onError(error) {
56+
throw error
57+
}
58+
})
59+
60+
let exportedMarkdown = ''
61+
62+
editor.update(() => {
63+
importMarkdownToLexical({
64+
root: $getRoot(),
65+
markdown: `> one
66+
> two
67+
>
68+
> three`,
69+
visitors: [MdastRootVisitor, MdastParagraphVisitor, MdastTextVisitor, MdastBreakVisitor, MdastBlockQuoteVisitor] as any,
70+
syntaxExtensions: [],
71+
mdastExtensions: [],
72+
jsxComponentDescriptors: [],
73+
directiveDescriptors: [],
74+
codeBlockEditorDescriptors: []
75+
})
76+
77+
exportedMarkdown = exportMarkdownFromLexical({
78+
root: $getRoot(),
79+
visitors: [LexicalRootVisitor, LexicalParagraphVisitor, LexicalTextVisitor, LexicalLinebreakVisitor, LexicalQuoteVisitor] as any,
80+
toMarkdownExtensions: [],
81+
toMarkdownOptions: {},
82+
jsxComponentDescriptors: [],
83+
jsxIsAvailable: false
84+
}).trim()
85+
})
86+
87+
expect(exportedMarkdown).toEqual(`> one
88+
> two
89+
>
90+
> three`)
91+
})
92+
3793
it('works with italics', () => {
3894
testIdenticalMarkdown(`*Hello* World`)
3995
})

0 commit comments

Comments
 (0)