Skip to content

Commit c7cfa7e

Browse files
feat: add preserve empty line plugin (#1765)
* feat: add preserve empty line plugin * [autofix.ci] apply automated fixes * chore: fix e2e test * test: add e2e * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 5e6e9f5 commit c7cfa7e

File tree

7 files changed

+98
-2
lines changed

7 files changed

+98
-2
lines changed

Diff for: e2e/tests/crepe/latex.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ test('latex block preview toggle', async ({ page }) => {
2727

2828
await expect(preview).toBeVisible()
2929
const markdown = await getMarkdown(page)
30-
expect(markdown.trim()).toEqual('$$\nE=mc^2\n$$')
30+
expect(markdown.trim()).toEqual('$$\nE=mc^2\n$$\n\n<br />')
3131

3232
await codeTools.hover()
3333
await expect(previewToggleButton).toBeVisible()

Diff for: e2e/tests/data/preserve-empty-line.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
123
2+
3+
<br />
4+
5+
456
6+
7+
<br />
8+
9+
789

Diff for: e2e/tests/transform/preserve-empty-line.spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect, test } from '@playwright/test'
2+
import { focusEditor, getMarkdown, loadFixture, setMarkdown } from '../misc'
3+
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/preset-commonmark/')
6+
})
7+
8+
test('preserve empty line', async ({ page }) => {
9+
await focusEditor(page)
10+
const markdown = await loadFixture('preserve-empty-line.md')
11+
await setMarkdown(page, markdown)
12+
13+
expect(await page.locator('p').count()).toBe(5)
14+
expect(await page.locator('p').nth(0).textContent()).toBe('123')
15+
expect(await page.locator('p').nth(1).textContent()).toBe('')
16+
expect(await page.locator('p').nth(2).textContent()).toBe('456')
17+
expect(await page.locator('p').nth(3).textContent()).toBe('')
18+
expect(await page.locator('p').nth(4).textContent()).toBe('789')
19+
20+
const markdownOutput = await getMarkdown(page)
21+
expect(markdownOutput.trim()).toBe(markdown.trim())
22+
})

Diff for: packages/plugins/preset-commonmark/src/composed/plugins.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
remarkInlineLinkPlugin,
1010
remarkLineBreak,
1111
remarkMarker,
12+
remarkPreserveEmptyLinePlugin,
1213
syncHeadingIdPlugin,
1314
syncListOrderPlugin,
1415
} from '../plugin'
@@ -26,6 +27,7 @@ export const plugins: MilkdownPlugin[] = [
2627
remarkLineBreak,
2728
remarkHtmlTransformer,
2829
remarkMarker,
30+
remarkPreserveEmptyLinePlugin,
2931

3032
syncHeadingIdPlugin,
3133
syncListOrderPlugin,

Diff for: packages/plugins/preset-commonmark/src/node/paragraph.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { commandsCtx } from '@milkdown/core'
22
import { setBlockType } from '@milkdown/prose/commands'
33
import { $command, $nodeAttr, $nodeSchema, $useKeymap } from '@milkdown/utils'
44
import { serializeText, withMeta } from '../__internal__'
5+
import { remarkPreserveEmptyLinePlugin } from '../plugin/remark-preserve-empty-line'
6+
import type { Ctx } from '@milkdown/ctx'
57

68
/// HTML attributes for paragraph node.
79
export const paragraphAttr = $nodeAttr('paragraph')
@@ -31,12 +33,30 @@ export const paragraphSchema = $nodeSchema('paragraph', (ctx) => ({
3133
match: (node) => node.type.name === 'paragraph',
3234
runner: (state, node) => {
3335
state.openNode('paragraph')
34-
serializeText(state, node)
36+
if (
37+
(!node.content || node.content.size === 0) &&
38+
shouldPreserveEmptyLine(ctx)
39+
) {
40+
state.addNode('html', undefined, '<br />')
41+
} else {
42+
serializeText(state, node)
43+
}
3544
state.closeNode()
3645
},
3746
},
3847
}))
3948

49+
function shouldPreserveEmptyLine(ctx: Ctx) {
50+
let shouldPreserveEmptyLine = false
51+
try {
52+
ctx.get(remarkPreserveEmptyLinePlugin.id)
53+
shouldPreserveEmptyLine = true
54+
} catch (e) {
55+
shouldPreserveEmptyLine = false
56+
}
57+
return shouldPreserveEmptyLine
58+
}
59+
4060
withMeta(paragraphSchema.node, {
4161
displayName: 'NodeSchema<paragraph>',
4262
group: 'Paragraph',

Diff for: packages/plugins/preset-commonmark/src/plugin/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './remark-line-break'
33
export * from './remark-inline-link-plugin'
44
export * from './remark-html-transformer'
55
export * from './remark-marker-plugin'
6+
export * from './remark-preserve-empty-line'
67

78
export * from './inline-nodes-cursor-plugin'
89

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Node } from '@milkdown/transformer'
2+
import { visit } from 'unist-util-visit'
3+
import { $remark } from '@milkdown/utils'
4+
import { withMeta } from '../__internal__'
5+
6+
function visitImage(ast: Node) {
7+
return visit(ast, 'paragraph', (node: Node & { children?: Node[] }) => {
8+
if (node.children?.length !== 1) return
9+
const firstChild = node.children?.[0]
10+
if (!firstChild || firstChild.type !== 'html') return
11+
12+
const { value } = firstChild as Node & {
13+
value: string
14+
}
15+
16+
if (!['<br />', '<br>', '<br/>'].includes(value)) {
17+
return
18+
}
19+
20+
node.children.splice(0, 1)
21+
})
22+
}
23+
24+
/// @internal
25+
/// This plugin is used to preserve the empty line.
26+
/// Markdown will fold the empty line into the previous line by default.
27+
/// This plugin will preserve the empty line by converting `<br />` to `line-break`.
28+
/// This plugin should be used with `linebreakSchema` to work.
29+
export const remarkPreserveEmptyLinePlugin = $remark(
30+
'remark-preserve-empty-line',
31+
() => () => visitImage
32+
)
33+
34+
withMeta(remarkPreserveEmptyLinePlugin.plugin, {
35+
displayName: 'Remark<remarkPreserveEmptyLine>',
36+
group: 'Remark',
37+
})
38+
39+
withMeta(remarkPreserveEmptyLinePlugin.options, {
40+
displayName: 'RemarkConfig<remarkPreserveEmptyLine>',
41+
group: 'Remark',
42+
})

0 commit comments

Comments
 (0)