@milkdown/preset-commonmark 7.15.1 → 7.15.2

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.
@@ -0,0 +1,46 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import type { EditorView } from '@milkdown/prose/view'
3
+
4
+ import { defaultValueCtx, Editor, editorViewCtx } from '@milkdown/core'
5
+ import { expect, it } from 'vitest'
6
+
7
+ import { commonmark } from '..'
8
+
9
+ function createEditor() {
10
+ const editor = Editor.make()
11
+ editor.use(commonmark)
12
+ return editor
13
+ }
14
+
15
+ const htmlInBlockquote = {
16
+ name: 'htmlInBlockquote',
17
+ defaultValue: `
18
+ > <p>Hello, world!</p>
19
+ `,
20
+ check: (view: EditorView) => {
21
+ expect(view.dom.querySelector('blockquote')).toBeInTheDocument()
22
+ },
23
+ }
24
+
25
+ const htmlInListItem = {
26
+ name: 'htmlInListItem',
27
+ defaultValue: `
28
+ * <p>List item with HTML</p>
29
+ `,
30
+ check: (view: EditorView) => {
31
+ expect(view.dom.querySelector('li')).toBeInTheDocument()
32
+ },
33
+ }
34
+
35
+ ;[htmlInBlockquote, htmlInListItem].forEach(({ name, defaultValue, check }) => {
36
+ it(`should render html in ${name}`, async () => {
37
+ const editor = createEditor()
38
+ editor.config((ctx) => {
39
+ ctx.set(defaultValueCtx, defaultValue)
40
+ })
41
+
42
+ await editor.create()
43
+
44
+ check(editor.ctx.get(editorViewCtx))
45
+ })
46
+ })
@@ -91,7 +91,7 @@ withMeta(emphasisStarInputRule, {
91
91
 
92
92
  /// Input rule for use `_` to create emphasis mark.
93
93
  export const emphasisUnderscoreInputRule = $inputRule((ctx) => {
94
- return markRule(/(?:^|[^_])_([^_]+)_$/, emphasisSchema.type(ctx), {
94
+ return markRule(/\b_(?![_\s])(.*?[^_\s])_\b/, emphasisSchema.type(ctx), {
95
95
  getAttr: () => ({
96
96
  marker: '_',
97
97
  }),
@@ -86,13 +86,20 @@ withMeta(toggleStrongCommand, {
86
86
 
87
87
  /// A input rule that will capture the strong mark.
88
88
  export const strongInputRule = $inputRule((ctx) => {
89
- return markRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, strongSchema.type(ctx), {
90
- getAttr: (match) => {
91
- return {
92
- marker: match[0].startsWith('*') ? '*' : '_',
93
- }
94
- },
95
- })
89
+ // Avoid matching when the opening delimiter is directly adjacent to alphanumeric characters,
90
+ // colon or slash (to prevent matches inside file paths, URLs, or intra-word like `a**b**c`).
91
+ // Also ensure the closing delimiter is not followed by such characters (mirrors strike-through rule).
92
+ return markRule(
93
+ /(?<![\w:/])(?:\*\*|__)([^*_]+?)(?:\*\*|__)(?![\w/])$/,
94
+ strongSchema.type(ctx),
95
+ {
96
+ getAttr: (match) => {
97
+ return {
98
+ marker: match[0].startsWith('*') ? '*' : '_',
99
+ }
100
+ },
101
+ }
102
+ )
96
103
  })
97
104
 
98
105
  withMeta(strongInputRule, {
@@ -38,6 +38,10 @@ function flatMapWithDepth(
38
38
  }
39
39
  }
40
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
+
41
45
  /// @internal
42
46
  /// This plugin should be deprecated after we support HTML.
43
47
  export const remarkHtmlTransformer = $remark(
@@ -46,7 +50,9 @@ export const remarkHtmlTransformer = $remark(
46
50
  flatMapWithDepth(tree, (node, _index, parent) => {
47
51
  if (!isHTML(node)) return [node]
48
52
 
49
- if (parent?.type === 'root') {
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)) {
50
56
  node.children = [{ ...node }]
51
57
  delete node.value
52
58
  ;(node as { type: string }).type = 'paragraph'
@@ -1,4 +1,5 @@
1
- import type { EditorView } from '@milkdown/prose/view'
1
+ import type { Node } from '@milkdown/prose/model'
2
+ import type { EditorState, Transaction } from '@milkdown/prose/state'
2
3
 
3
4
  import { Plugin, PluginKey } from '@milkdown/prose/state'
4
5
  import { $prose } from '@milkdown/utils'
@@ -10,13 +11,24 @@ import { orderedListSchema } from '../node/ordered-list'
10
11
 
11
12
  /// This plugin is used to keep the label of list item up to date in ordered list.
12
13
  export const syncListOrderPlugin = $prose((ctx) => {
13
- const syncOrderLabel = (view: EditorView) => {
14
- if (view.composing || !view.editable) return
14
+ const syncOrderLabel = (
15
+ transactions: readonly Transaction[],
16
+ _oldState: EditorState,
17
+ newState: EditorState
18
+ ) => {
19
+ // Skip if composing or not editable
20
+ if (
21
+ !newState.selection ||
22
+ transactions.some(
23
+ (tr) => tr.getMeta('addToHistory') === false || !tr.isGeneric
24
+ )
25
+ )
26
+ return null
15
27
 
16
28
  const orderedListType = orderedListSchema.type(ctx)
17
29
  const bulletListType = bulletListSchema.type(ctx)
18
30
  const listItemType = listItemSchema.type(ctx)
19
- const state = view.state
31
+
20
32
  const handleNodeItem = (
21
33
  attrs: Record<string, any>,
22
34
  index: number
@@ -31,57 +43,64 @@ export const syncListOrderPlugin = $prose((ctx) => {
31
43
  return changed
32
44
  }
33
45
 
34
- let tr = state.tr
46
+ let tr = newState.tr
35
47
  let needDispatch = false
36
- state.doc.descendants((node, pos, parent, index) => {
37
- if (node.type === bulletListType) {
38
- const base = node.maybeChild(0)
39
- if (base?.type === listItemType && base.attrs.listType === 'ordered') {
40
- needDispatch = true
41
- tr.setNodeMarkup(pos, orderedListType, { spread: 'true' })
42
48
 
43
- node.descendants((child, pos, _parent, index) => {
44
- if (child.type === listItemType) {
45
- const attrs = { ...child.attrs }
46
- const changed = handleNodeItem(attrs, index)
47
- if (changed) tr = tr.setNodeMarkup(pos, undefined, attrs)
48
- }
49
- return false
50
- })
51
- }
52
- } else if (
53
- node.type === listItemType &&
54
- parent?.type === orderedListType
55
- ) {
56
- const attrs = { ...node.attrs }
57
- let changed = false
58
- if (attrs.listType !== 'ordered') {
59
- attrs.listType = 'ordered'
60
- changed = true
61
- }
49
+ newState.doc.descendants(
50
+ (node: Node, pos: number, parent: Node | null, index: number) => {
51
+ if (node.type === bulletListType) {
52
+ const base = node.maybeChild(0)
53
+ if (
54
+ base?.type === listItemType &&
55
+ base.attrs.listType === 'ordered'
56
+ ) {
57
+ needDispatch = true
58
+ tr.setNodeMarkup(pos, orderedListType, { spread: 'true' })
62
59
 
63
- const base = parent?.maybeChild(0)
64
- if (base) changed = handleNodeItem(attrs, index)
60
+ node.descendants(
61
+ (
62
+ child: Node,
63
+ pos: number,
64
+ _parent: Node | null,
65
+ index: number
66
+ ) => {
67
+ if (child.type === listItemType) {
68
+ const attrs = { ...child.attrs }
69
+ const changed = handleNodeItem(attrs, index)
70
+ if (changed) tr = tr.setNodeMarkup(pos, undefined, attrs)
71
+ }
72
+ return false
73
+ }
74
+ )
75
+ }
76
+ } else if (
77
+ node.type === listItemType &&
78
+ parent?.type === orderedListType
79
+ ) {
80
+ const attrs = { ...node.attrs }
81
+ let changed = false
82
+ if (attrs.listType !== 'ordered') {
83
+ attrs.listType = 'ordered'
84
+ changed = true
85
+ }
65
86
 
66
- if (changed) {
67
- tr = tr.setNodeMarkup(pos, undefined, attrs)
68
- needDispatch = true
87
+ const base = parent?.maybeChild(0)
88
+ if (base) changed = handleNodeItem(attrs, index)
89
+
90
+ if (changed) {
91
+ tr = tr.setNodeMarkup(pos, undefined, attrs)
92
+ needDispatch = true
93
+ }
69
94
  }
70
95
  }
71
- })
96
+ )
72
97
 
73
- if (needDispatch) view.dispatch(tr.setMeta('addToHistory', false))
98
+ return needDispatch ? tr.setMeta('addToHistory', false) : null
74
99
  }
100
+
75
101
  return new Plugin({
76
102
  key: new PluginKey('MILKDOWN_KEEP_LIST_ORDER'),
77
- view: (view) => {
78
- syncOrderLabel(view)
79
- return {
80
- update: (view) => {
81
- syncOrderLabel(view)
82
- },
83
- }
84
- },
103
+ appendTransaction: syncOrderLabel,
85
104
  })
86
105
  })
87
106