@jvs-milkdown/preset-commonmark 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/lib/__internal__/index.d.ts +3 -0
  4. package/lib/__internal__/index.d.ts.map +1 -0
  5. package/lib/__internal__/serialize-text.d.ts +4 -0
  6. package/lib/__internal__/serialize-text.d.ts.map +1 -0
  7. package/lib/__internal__/with-meta.d.ts +3 -0
  8. package/lib/__internal__/with-meta.d.ts.map +1 -0
  9. package/lib/__test__/html.spec.d.ts +2 -0
  10. package/lib/__test__/html.spec.d.ts.map +1 -0
  11. package/lib/__test__/trailing-space.spec.d.ts +2 -0
  12. package/lib/__test__/trailing-space.spec.d.ts.map +1 -0
  13. package/lib/__test__/vitest.setup.d.ts +1 -0
  14. package/lib/__test__/vitest.setup.d.ts.map +1 -0
  15. package/lib/commands/index.d.ts +20 -0
  16. package/lib/commands/index.d.ts.map +1 -0
  17. package/lib/composed/commands.d.ts +3 -0
  18. package/lib/composed/commands.d.ts.map +1 -0
  19. package/lib/composed/index.d.ts +6 -0
  20. package/lib/composed/index.d.ts.map +1 -0
  21. package/lib/composed/inputrules.d.ts +4 -0
  22. package/lib/composed/inputrules.d.ts.map +1 -0
  23. package/lib/composed/keymap.d.ts +3 -0
  24. package/lib/composed/keymap.d.ts.map +1 -0
  25. package/lib/composed/plugins.d.ts +3 -0
  26. package/lib/composed/plugins.d.ts.map +1 -0
  27. package/lib/composed/schema.d.ts +3 -0
  28. package/lib/composed/schema.d.ts.map +1 -0
  29. package/lib/index.d.ts +8 -0
  30. package/lib/index.d.ts.map +1 -0
  31. package/lib/index.js +2153 -0
  32. package/lib/index.js.map +1 -0
  33. package/lib/mark/emphasis.d.ts +7 -0
  34. package/lib/mark/emphasis.d.ts.map +1 -0
  35. package/lib/mark/index.d.ts +5 -0
  36. package/lib/mark/index.d.ts.map +1 -0
  37. package/lib/mark/inline-code.d.ts +6 -0
  38. package/lib/mark/inline-code.d.ts.map +1 -0
  39. package/lib/mark/link.d.ts +9 -0
  40. package/lib/mark/link.d.ts.map +1 -0
  41. package/lib/mark/strong.d.ts +6 -0
  42. package/lib/mark/strong.d.ts.map +1 -0
  43. package/lib/node/blockquote.d.ts +7 -0
  44. package/lib/node/blockquote.d.ts.map +1 -0
  45. package/lib/node/bullet-list.d.ts +6 -0
  46. package/lib/node/bullet-list.d.ts.map +1 -0
  47. package/lib/node/code-block.d.ts +10 -0
  48. package/lib/node/code-block.d.ts.map +1 -0
  49. package/lib/node/doc.d.ts +2 -0
  50. package/lib/node/doc.d.ts.map +1 -0
  51. package/lib/node/hardbreak.d.ts +5 -0
  52. package/lib/node/hardbreak.d.ts.map +1 -0
  53. package/lib/node/heading.d.ts +11 -0
  54. package/lib/node/heading.d.ts.map +1 -0
  55. package/lib/node/hr.d.ts +5 -0
  56. package/lib/node/hr.d.ts.map +1 -0
  57. package/lib/node/html.d.ts +3 -0
  58. package/lib/node/html.d.ts.map +1 -0
  59. package/lib/node/image.d.ts +11 -0
  60. package/lib/node/image.d.ts.map +1 -0
  61. package/lib/node/index.d.ts +14 -0
  62. package/lib/node/index.d.ts.map +1 -0
  63. package/lib/node/list-item.d.ts +8 -0
  64. package/lib/node/list-item.d.ts.map +1 -0
  65. package/lib/node/ordered-list.d.ts +6 -0
  66. package/lib/node/ordered-list.d.ts.map +1 -0
  67. package/lib/node/paragraph.d.ts +5 -0
  68. package/lib/node/paragraph.d.ts.map +1 -0
  69. package/lib/node/text.d.ts +2 -0
  70. package/lib/node/text.d.ts.map +1 -0
  71. package/lib/plugin/hardbreak-clear-mark-plugin.d.ts +2 -0
  72. package/lib/plugin/hardbreak-clear-mark-plugin.d.ts.map +1 -0
  73. package/lib/plugin/hardbreak-filter-plugin.d.ts +3 -0
  74. package/lib/plugin/hardbreak-filter-plugin.d.ts.map +1 -0
  75. package/lib/plugin/index.d.ts +12 -0
  76. package/lib/plugin/index.d.ts.map +1 -0
  77. package/lib/plugin/inline-nodes-cursor-plugin.d.ts +2 -0
  78. package/lib/plugin/inline-nodes-cursor-plugin.d.ts.map +1 -0
  79. package/lib/plugin/remark-add-order-in-list-plugin.d.ts +2 -0
  80. package/lib/plugin/remark-add-order-in-list-plugin.d.ts.map +1 -0
  81. package/lib/plugin/remark-html-transformer.d.ts +2 -0
  82. package/lib/plugin/remark-html-transformer.d.ts.map +1 -0
  83. package/lib/plugin/remark-inline-link-plugin.d.ts +2 -0
  84. package/lib/plugin/remark-inline-link-plugin.d.ts.map +1 -0
  85. package/lib/plugin/remark-line-break.d.ts +2 -0
  86. package/lib/plugin/remark-line-break.d.ts.map +1 -0
  87. package/lib/plugin/remark-marker-plugin.d.ts +2 -0
  88. package/lib/plugin/remark-marker-plugin.d.ts.map +1 -0
  89. package/lib/plugin/remark-preserve-empty-line.d.ts +2 -0
  90. package/lib/plugin/remark-preserve-empty-line.d.ts.map +1 -0
  91. package/lib/plugin/sync-heading-id-plugin.d.ts +2 -0
  92. package/lib/plugin/sync-heading-id-plugin.d.ts.map +1 -0
  93. package/lib/plugin/sync-list-order-plugin.d.ts +2 -0
  94. package/lib/plugin/sync-list-order-plugin.d.ts.map +1 -0
  95. package/lib/tsconfig.tsbuildinfo +1 -0
  96. package/package.json +44 -0
  97. package/src/__internal__/index.ts +2 -0
  98. package/src/__internal__/serialize-text.ts +21 -0
  99. package/src/__internal__/with-meta.ts +15 -0
  100. package/src/__test__/html.spec.ts +46 -0
  101. package/src/__test__/trailing-space.spec.ts +27 -0
  102. package/src/__test__/vitest.setup.ts +65 -0
  103. package/src/commands/index.ts +140 -0
  104. package/src/composed/commands.ts +72 -0
  105. package/src/composed/index.ts +5 -0
  106. package/src/composed/inputrules.ts +34 -0
  107. package/src/composed/keymap.ts +29 -0
  108. package/src/composed/plugins.ts +35 -0
  109. package/src/composed/schema.ts +92 -0
  110. package/src/index.ts +26 -0
  111. package/src/mark/emphasis.ts +130 -0
  112. package/src/mark/index.ts +4 -0
  113. package/src/mark/inline-code.ts +123 -0
  114. package/src/mark/link.ts +134 -0
  115. package/src/mark/strong.ts +130 -0
  116. package/src/node/blockquote.ts +100 -0
  117. package/src/node/bullet-list.ts +129 -0
  118. package/src/node/code-block.ts +176 -0
  119. package/src/node/doc.ts +26 -0
  120. package/src/node/hardbreak.ts +134 -0
  121. package/src/node/heading.ts +271 -0
  122. package/src/node/hr.ts +87 -0
  123. package/src/node/html.ts +66 -0
  124. package/src/node/image.ts +173 -0
  125. package/src/node/index.ts +14 -0
  126. package/src/node/list-item.ts +244 -0
  127. package/src/node/ordered-list.ts +141 -0
  128. package/src/node/paragraph.ts +136 -0
  129. package/src/node/text.ts +25 -0
  130. package/src/plugin/hardbreak-clear-mark-plugin.ts +58 -0
  131. package/src/plugin/hardbreak-filter-plugin.ts +46 -0
  132. package/src/plugin/index.ts +14 -0
  133. package/src/plugin/inline-nodes-cursor-plugin.ts +103 -0
  134. package/src/plugin/remark-add-order-in-list-plugin.ts +29 -0
  135. package/src/plugin/remark-html-transformer.ts +74 -0
  136. package/src/plugin/remark-inline-link-plugin.ts +20 -0
  137. package/src/plugin/remark-line-break.ts +69 -0
  138. package/src/plugin/remark-marker-plugin.ts +33 -0
  139. package/src/plugin/remark-preserve-empty-line.ts +49 -0
  140. package/src/plugin/sync-heading-id-plugin.ts +67 -0
  141. package/src/plugin/sync-list-order-plugin.ts +112 -0
@@ -0,0 +1,25 @@
1
+ import { $node } from '@jvs-milkdown/utils'
2
+
3
+ import { withMeta } from '../__internal__'
4
+
5
+ /// The bottom-level node.
6
+ export const textSchema = $node('text', () => ({
7
+ group: 'inline',
8
+ parseMarkdown: {
9
+ match: ({ type }) => type === 'text',
10
+ runner: (state, node) => {
11
+ state.addText(node.value as string)
12
+ },
13
+ },
14
+ toMarkdown: {
15
+ match: (node) => node.type.name === 'text',
16
+ runner: (state, node) => {
17
+ state.addNode('text', undefined, node.text as string)
18
+ },
19
+ },
20
+ }))
21
+
22
+ withMeta(textSchema, {
23
+ displayName: 'NodeSchema<text>',
24
+ group: 'Text',
25
+ })
@@ -0,0 +1,58 @@
1
+ import { Plugin, PluginKey } from '@jvs-milkdown/prose/state'
2
+ import { AddMarkStep, ReplaceStep } from '@jvs-milkdown/prose/transform'
3
+ import { $prose } from '@jvs-milkdown/utils'
4
+
5
+ import { withMeta } from '../__internal__'
6
+ import { hardbreakSchema } from '../node'
7
+
8
+ /// This plugin is used to clear the marks around the hardbreak node.
9
+ export const hardbreakClearMarkPlugin = $prose((ctx) => {
10
+ return new Plugin({
11
+ key: new PluginKey('MILKDOWN_HARDBREAK_MARKS'),
12
+ appendTransaction: (trs, _oldState, newState) => {
13
+ if (!trs.length) return
14
+
15
+ const [tr] = trs
16
+ if (!tr) return
17
+
18
+ const [step] = tr.steps
19
+
20
+ const isInsertHr = tr.getMeta('hardbreak')
21
+ if (isInsertHr) {
22
+ if (!(step instanceof ReplaceStep)) return
23
+
24
+ const { from } = step as unknown as { from: number }
25
+ return newState.tr.setNodeMarkup(
26
+ from,
27
+ hardbreakSchema.type(ctx),
28
+ undefined,
29
+ []
30
+ )
31
+ }
32
+
33
+ const isAddMarkStep = step instanceof AddMarkStep
34
+ if (isAddMarkStep) {
35
+ let _tr = newState.tr
36
+ const { from, to } = step as unknown as { from: number; to: number }
37
+ newState.doc.nodesBetween(from, to, (node, pos) => {
38
+ if (node.type === hardbreakSchema.type(ctx))
39
+ _tr = _tr.setNodeMarkup(
40
+ pos,
41
+ hardbreakSchema.type(ctx),
42
+ undefined,
43
+ []
44
+ )
45
+ })
46
+
47
+ return _tr
48
+ }
49
+
50
+ return undefined
51
+ },
52
+ })
53
+ })
54
+
55
+ withMeta(hardbreakClearMarkPlugin, {
56
+ displayName: 'Prose<hardbreakClearMarkPlugin>',
57
+ group: 'Prose',
58
+ })
@@ -0,0 +1,46 @@
1
+ import { Plugin, PluginKey } from '@jvs-milkdown/prose/state'
2
+ import { $ctx, $prose } from '@jvs-milkdown/utils'
3
+
4
+ import { withMeta } from '../__internal__'
5
+
6
+ /// This slice contains the nodes that within which the hardbreak will be ignored.
7
+ export const hardbreakFilterNodes = $ctx(
8
+ ['table', 'code_block'],
9
+ 'hardbreakFilterNodes'
10
+ )
11
+
12
+ withMeta(hardbreakFilterNodes, {
13
+ displayName: 'Ctx<hardbreakFilterNodes>',
14
+ group: 'Prose',
15
+ })
16
+
17
+ /// This plugin is used to filter the hardbreak node.
18
+ /// If the hardbreak is going to be inserted within a node that is in the `hardbreakFilterNodes`, ignore it.
19
+ export const hardbreakFilterPlugin = $prose((ctx) => {
20
+ const notIn = ctx.get(hardbreakFilterNodes.key)
21
+ return new Plugin({
22
+ key: new PluginKey('MILKDOWN_HARDBREAK_FILTER'),
23
+ filterTransaction: (tr, state) => {
24
+ const isInsertHr = tr.getMeta('hardbreak')
25
+ const [step] = tr.steps
26
+ if (isInsertHr && step) {
27
+ const { from } = step as unknown as { from: number }
28
+ const $from = state.doc.resolve(from)
29
+ let curDepth = $from.depth
30
+ let canApply = true
31
+ while (curDepth > 0) {
32
+ if (notIn.includes($from.node(curDepth).type.name)) canApply = false
33
+
34
+ curDepth--
35
+ }
36
+ return canApply
37
+ }
38
+ return true
39
+ },
40
+ })
41
+ })
42
+
43
+ withMeta(hardbreakFilterPlugin, {
44
+ displayName: 'Prose<hardbreakFilterPlugin>',
45
+ group: 'Prose',
46
+ })
@@ -0,0 +1,14 @@
1
+ export * from './remark-add-order-in-list-plugin'
2
+ export * from './remark-line-break'
3
+ export * from './remark-inline-link-plugin'
4
+ export * from './remark-html-transformer'
5
+ export * from './remark-marker-plugin'
6
+ export * from './remark-preserve-empty-line'
7
+
8
+ export * from './inline-nodes-cursor-plugin'
9
+
10
+ export * from './hardbreak-clear-mark-plugin'
11
+ export * from './hardbreak-filter-plugin'
12
+
13
+ export * from './sync-heading-id-plugin'
14
+ export * from './sync-list-order-plugin'
@@ -0,0 +1,103 @@
1
+ import { Plugin, PluginKey } from '@jvs-milkdown/prose/state'
2
+ import { Decoration, DecorationSet } from '@jvs-milkdown/prose/view'
3
+ import { $prose } from '@jvs-milkdown/utils'
4
+
5
+ import { withMeta } from '../__internal__'
6
+
7
+ /// This plugin is to solve the [chrome 98 bug](https://discuss.prosemirror.net/t/cursor-jumps-at-the-end-of-line-when-it-betweens-two-inline-nodes/4641).
8
+ export const inlineNodesCursorPlugin = $prose(() => {
9
+ let lock = false
10
+ const inlineNodesCursorPluginKey = new PluginKey(
11
+ 'MILKDOWN_INLINE_NODES_CURSOR'
12
+ )
13
+ const inlineNodesCursorPlugin: Plugin = new Plugin({
14
+ key: inlineNodesCursorPluginKey,
15
+ state: {
16
+ init() {
17
+ return false
18
+ },
19
+ apply(tr) {
20
+ if (!tr.selection.empty) return false
21
+
22
+ const pos = tr.selection.$from
23
+ const left = pos.nodeBefore
24
+ const right = pos.nodeAfter
25
+ if (
26
+ left &&
27
+ right &&
28
+ left.isInline &&
29
+ !left.isText &&
30
+ right.isInline &&
31
+ !right.isText
32
+ )
33
+ return true
34
+
35
+ return false
36
+ },
37
+ },
38
+ props: {
39
+ handleDOMEvents: {
40
+ compositionend: (view, e) => {
41
+ if (lock) {
42
+ lock = false
43
+ requestAnimationFrame(() => {
44
+ const active = inlineNodesCursorPlugin.getState(view.state)
45
+ if (active) {
46
+ const from = view.state.selection.from
47
+ e.preventDefault()
48
+ view.dispatch(view.state.tr.insertText(e.data || '', from))
49
+ }
50
+ })
51
+
52
+ return true
53
+ }
54
+ return false
55
+ },
56
+ compositionstart: (view) => {
57
+ const active = inlineNodesCursorPlugin.getState(view.state)
58
+ if (active) lock = true
59
+
60
+ return false
61
+ },
62
+ beforeinput: (view, e) => {
63
+ const active = inlineNodesCursorPlugin.getState(view.state)
64
+ if (active && e instanceof InputEvent && e.data && !lock) {
65
+ const from = view.state.selection.from
66
+ e.preventDefault()
67
+ view.dispatch(view.state.tr.insertText(e.data || '', from))
68
+
69
+ return true
70
+ }
71
+
72
+ return false
73
+ },
74
+ },
75
+ decorations(state) {
76
+ const active = inlineNodesCursorPlugin.getState(state)
77
+ if (active) {
78
+ const pos = state.selection.$from
79
+ const position = pos.pos
80
+ const left = document.createElement('span')
81
+ const leftDec = Decoration.widget(position, left, {
82
+ side: -1,
83
+ })
84
+ const right = document.createElement('span')
85
+ const rightDec = Decoration.widget(position, right)
86
+ setTimeout(() => {
87
+ left.contentEditable = 'true'
88
+ right.contentEditable = 'true'
89
+ })
90
+ return DecorationSet.create(state.doc, [leftDec, rightDec])
91
+ }
92
+ return DecorationSet.empty
93
+ },
94
+ },
95
+ })
96
+
97
+ return inlineNodesCursorPlugin
98
+ })
99
+
100
+ withMeta(inlineNodesCursorPlugin, {
101
+ displayName: 'Prose<inlineNodesCursorPlugin>',
102
+ group: 'Prose',
103
+ })
@@ -0,0 +1,29 @@
1
+ import { $remark } from '@jvs-milkdown/utils'
2
+ import { visit } from 'unist-util-visit'
3
+
4
+ import { withMeta } from '../__internal__'
5
+
6
+ /// This plugin is used to add order in list for remark AST.
7
+ export const remarkAddOrderInListPlugin = $remark(
8
+ 'remarkAddOrderInList',
9
+ () => () => (tree) => {
10
+ visit(tree, 'list', (node) => {
11
+ if (node.ordered) {
12
+ const start = node.start ?? 1
13
+ node.children.forEach((child, index) => {
14
+ ;(child as unknown as Record<string, number>).label = index + start
15
+ })
16
+ }
17
+ })
18
+ }
19
+ )
20
+
21
+ withMeta(remarkAddOrderInListPlugin.plugin, {
22
+ displayName: 'Remark<remarkAddOrderInListPlugin>',
23
+ group: 'Remark',
24
+ })
25
+
26
+ withMeta(remarkAddOrderInListPlugin.options, {
27
+ displayName: 'RemarkConfig<remarkAddOrderInListPlugin>',
28
+ group: 'Remark',
29
+ })
@@ -0,0 +1,74 @@
1
+ import type { Node } from '@jvs-milkdown/transformer'
2
+
3
+ import { $remark } from '@jvs-milkdown/utils'
4
+
5
+ import { withMeta } from '../__internal__'
6
+
7
+ const isParent = (node: Node): node is Node & { children: Node[] } =>
8
+ !!(node as Node & { children: Node[] }).children
9
+ const isHTML = (
10
+ node: Node
11
+ ): node is Node & { children: Node[]; value: unknown } => node.type === 'html'
12
+
13
+ function flatMapWithDepth(
14
+ ast: Node,
15
+ fn: (node: Node, index: number, parent: Node | null) => Node[]
16
+ ) {
17
+ return transform(ast, 0, null)[0]
18
+
19
+ function transform(node: Node, index: number, parent: Node | null) {
20
+ if (isParent(node)) {
21
+ const out = []
22
+ for (let i = 0, n = node.children.length; i < n; i++) {
23
+ const nthChild = node.children[i]
24
+ if (nthChild) {
25
+ const xs = transform(nthChild, i, node)
26
+ if (xs) {
27
+ for (let j = 0, m = xs.length; j < m; j++) {
28
+ const item = xs[j]
29
+ if (item) out.push(item)
30
+ }
31
+ }
32
+ }
33
+ }
34
+ node.children = out
35
+ }
36
+
37
+ return fn(node, index, parent)
38
+ }
39
+ }
40
+
41
+ // List of container node types that can contain block-level content
42
+ // and thus may need HTML content to be wrapped in paragraphs
43
+ const BLOCK_CONTAINER_TYPES = ['root', 'blockquote', 'listItem']
44
+
45
+ /// @internal
46
+ /// This plugin should be deprecated after we support HTML.
47
+ export const remarkHtmlTransformer = $remark(
48
+ 'remarkHTMLTransformer',
49
+ () => () => (tree: Node) => {
50
+ flatMapWithDepth(tree, (node, _index, parent) => {
51
+ if (!isHTML(node)) return [node]
52
+
53
+ // If the parent is a block container that expects block content,
54
+ // wrap the HTML in a paragraph node
55
+ if (parent && BLOCK_CONTAINER_TYPES.includes(parent.type)) {
56
+ node.children = [{ ...node }]
57
+ delete node.value
58
+ ;(node as { type: string }).type = 'paragraph'
59
+ }
60
+
61
+ return [node]
62
+ })
63
+ }
64
+ )
65
+
66
+ withMeta(remarkHtmlTransformer.plugin, {
67
+ displayName: 'Remark<remarkHtmlTransformer>',
68
+ group: 'Remark',
69
+ })
70
+
71
+ withMeta(remarkHtmlTransformer.options, {
72
+ displayName: 'RemarkConfig<remarkHtmlTransformer>',
73
+ group: 'Remark',
74
+ })
@@ -0,0 +1,20 @@
1
+ import { $remark } from '@jvs-milkdown/utils'
2
+ import remarkInlineLinks from 'remark-inline-links'
3
+
4
+ import { withMeta } from '../__internal__'
5
+
6
+ /// This plugin wraps [remark-inline-links](https://github.com/remarkjs/remark-inline-links).
7
+ export const remarkInlineLinkPlugin = $remark(
8
+ 'remarkInlineLink',
9
+ () => remarkInlineLinks
10
+ )
11
+
12
+ withMeta(remarkInlineLinkPlugin.plugin, {
13
+ displayName: 'Remark<remarkInlineLinkPlugin>',
14
+ group: 'Remark',
15
+ })
16
+
17
+ withMeta(remarkInlineLinkPlugin.options, {
18
+ displayName: 'RemarkConfig<remarkInlineLinkPlugin>',
19
+ group: 'Remark',
20
+ })
@@ -0,0 +1,69 @@
1
+ import type { Node } from '@jvs-milkdown/transformer'
2
+
3
+ import { $remark } from '@jvs-milkdown/utils'
4
+ import { visit } from 'unist-util-visit'
5
+
6
+ import { withMeta } from '../__internal__'
7
+
8
+ /// This plugin is used to add inline line break for remark AST.
9
+ /// The inline line break should be treated as a `space`.
10
+ /// And the normal line break should be treated as a `LF`.
11
+ export const remarkLineBreak = $remark(
12
+ 'remarkLineBreak',
13
+ () => () => (tree: Node) => {
14
+ const find = /[\t ]*(?:\r?\n|\r)/g
15
+ visit(
16
+ tree,
17
+ 'text',
18
+ (
19
+ node: Node & { value: string },
20
+ index: number,
21
+ parent: Node & { children: Node[] }
22
+ ) => {
23
+ if (!node.value || typeof node.value !== 'string') return
24
+
25
+ const result = []
26
+ let start = 0
27
+
28
+ find.lastIndex = 0
29
+
30
+ let match = find.exec(node.value)
31
+
32
+ while (match) {
33
+ const position = match.index
34
+
35
+ if (start !== position)
36
+ result.push({
37
+ type: 'text',
38
+ value: node.value.slice(start, position),
39
+ })
40
+
41
+ result.push({ type: 'break', data: { isInline: true } })
42
+ start = position + match[0].length
43
+ match = find.exec(node.value)
44
+ }
45
+
46
+ const hasResultAndIndex =
47
+ result.length > 0 && parent && typeof index === 'number'
48
+
49
+ if (!hasResultAndIndex) return
50
+
51
+ if (start < node.value.length)
52
+ result.push({ type: 'text', value: node.value.slice(start) })
53
+
54
+ parent.children.splice(index, 1, ...result)
55
+ return index + result.length
56
+ }
57
+ )
58
+ }
59
+ )
60
+
61
+ withMeta(remarkLineBreak.plugin, {
62
+ displayName: 'Remark<remarkLineBreak>',
63
+ group: 'Remark',
64
+ })
65
+
66
+ withMeta(remarkLineBreak.options, {
67
+ displayName: 'RemarkConfig<remarkLineBreak>',
68
+ group: 'Remark',
69
+ })
@@ -0,0 +1,33 @@
1
+ import type { Node } from '@jvs-milkdown/transformer'
2
+
3
+ import { $remark } from '@jvs-milkdown/utils'
4
+ import { visit } from 'unist-util-visit'
5
+
6
+ import { withMeta } from '../__internal__'
7
+
8
+ /// This plugin is used to keep the marker (`_` and `*`) of emphasis and strong nodes.
9
+ export const remarkMarker = $remark(
10
+ 'remarkMarker',
11
+ () => () => (tree, file) => {
12
+ const getMarker = (node: Node) => {
13
+ return (file.value as string).charAt(node.position!.start.offset!)
14
+ }
15
+ visit(
16
+ tree,
17
+ (node: Node) => ['strong', 'emphasis'].includes(node.type),
18
+ (node: Node) => {
19
+ ;(node as Node & { marker: string }).marker = getMarker(node)
20
+ }
21
+ )
22
+ }
23
+ )
24
+
25
+ withMeta(remarkMarker.plugin, {
26
+ displayName: 'Remark<remarkMarker>',
27
+ group: 'Remark',
28
+ })
29
+
30
+ withMeta(remarkMarker.options, {
31
+ displayName: 'RemarkConfig<remarkMarker>',
32
+ group: 'Remark',
33
+ })
@@ -0,0 +1,49 @@
1
+ import type { Node } from '@jvs-milkdown/transformer'
2
+
3
+ import { $remark } from '@jvs-milkdown/utils'
4
+ import { visitParents } from 'unist-util-visit-parents'
5
+
6
+ import { withMeta } from '../__internal__'
7
+
8
+ function visitEmptyLine(ast: Node) {
9
+ return visitParents(
10
+ ast,
11
+ (node: Node) =>
12
+ node.type === 'html' &&
13
+ ['<br />', '<br>', '<br >', '<br/>'].includes(
14
+ (node as Node & { value: string }).value?.trim()
15
+ ),
16
+ (node: Node, parents: Node[]) => {
17
+ if (!parents.length) return
18
+ const parent = parents[parents.length - 1] as
19
+ | (Node & { children: Node[] })
20
+ | undefined
21
+ if (!parent) return
22
+ const index = parent.children.indexOf(node)
23
+ if (index === -1) return
24
+
25
+ parent.children.splice(index, 1)
26
+ },
27
+ true
28
+ )
29
+ }
30
+
31
+ /// @internal
32
+ /// This plugin is used to preserve the empty line.
33
+ /// Markdown will fold the empty line into the previous line by default.
34
+ /// This plugin will preserve the empty line by converting `<br />` to `line-break`.
35
+ /// This plugin should be used with `linebreakSchema` to work.
36
+ export const remarkPreserveEmptyLinePlugin = $remark(
37
+ 'remark-preserve-empty-line',
38
+ () => () => visitEmptyLine
39
+ )
40
+
41
+ withMeta(remarkPreserveEmptyLinePlugin.plugin, {
42
+ displayName: 'Remark<remarkPreserveEmptyLine>',
43
+ group: 'Remark',
44
+ })
45
+
46
+ withMeta(remarkPreserveEmptyLinePlugin.options, {
47
+ displayName: 'RemarkConfig<remarkPreserveEmptyLine>',
48
+ group: 'Remark',
49
+ })
@@ -0,0 +1,67 @@
1
+ import type { EditorView } from '@jvs-milkdown/prose/view'
2
+
3
+ import { Plugin, PluginKey } from '@jvs-milkdown/prose/state'
4
+ import { $prose } from '@jvs-milkdown/utils'
5
+
6
+ import { withMeta } from '../__internal__'
7
+ import { headingIdGenerator, headingSchema } from '../node/heading'
8
+
9
+ /// This plugin is used to sync the heading id when the heading content changes.
10
+ /// It will use the `headingIdGenerator` to generate the id.
11
+ export const syncHeadingIdPlugin = $prose((ctx) => {
12
+ const headingIdPluginKey = new PluginKey('MILKDOWN_HEADING_ID')
13
+
14
+ const updateId = (view: EditorView) => {
15
+ if (view.composing) return
16
+
17
+ const getId = ctx.get(headingIdGenerator.key)
18
+ const tr = view.state.tr.setMeta('addToHistory', false)
19
+
20
+ let found = false
21
+ const idMap: Record<string, number> = {}
22
+
23
+ view.state.doc.descendants((node, pos) => {
24
+ if (node.type === headingSchema.type(ctx)) {
25
+ if (node.textContent.trim().length === 0) return
26
+
27
+ const attrs = node.attrs
28
+ let id = getId(node)
29
+ if (idMap[id]) {
30
+ idMap[id]! += 1
31
+ id += `-#${idMap[id]}`
32
+ } else {
33
+ idMap[id] = 1
34
+ }
35
+
36
+ if (attrs.id !== id) {
37
+ found = true
38
+ tr.setMeta(headingIdPluginKey, true).setNodeMarkup(pos, undefined, {
39
+ ...attrs,
40
+ id,
41
+ })
42
+ }
43
+ }
44
+ })
45
+
46
+ if (found) view.dispatch(tr)
47
+ }
48
+
49
+ return new Plugin({
50
+ key: headingIdPluginKey,
51
+ view: (view) => {
52
+ updateId(view)
53
+
54
+ return {
55
+ update: (view, prevState) => {
56
+ if (view.state.doc.eq(prevState.doc)) return
57
+ updateId(view)
58
+ },
59
+ }
60
+ },
61
+ })
62
+ })
63
+
64
+ withMeta(syncHeadingIdPlugin, {
65
+ displayName: 'Prose<syncHeadingIdPlugin>',
66
+ group: 'Prose',
67
+ })