@milkdown/preset-gfm 7.21.2 → 7.22.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.
- package/lib/__test__/keep-table-align.spec.d.ts +2 -0
- package/lib/__test__/keep-table-align.spec.d.ts.map +1 -0
- package/lib/__test__/paste-special-char.spec.d.ts +2 -0
- package/lib/__test__/paste-special-char.spec.d.ts.map +1 -0
- package/lib/__test__/task-list-spread-roundtrip.spec.d.ts +2 -0
- package/lib/__test__/task-list-spread-roundtrip.spec.d.ts.map +1 -0
- package/lib/composed/commands.d.ts +1 -1
- package/lib/index.js +23 -6
- package/lib/index.js.map +1 -1
- package/lib/node/table/command.d.ts +1 -1
- package/lib/node/table/utils/select-line.d.ts.map +1 -1
- package/lib/plugin/keep-table-align-plugin.d.ts.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/__test__/keep-table-align.spec.ts +131 -0
- package/src/__test__/paste-special-char.spec.ts +121 -0
- package/src/__test__/task-list-spread-roundtrip.spec.ts +70 -0
- package/src/mark/strike-through.ts +1 -1
- package/src/node/task-list-item.ts +3 -3
- package/src/plugin/keep-table-align-plugin.ts +33 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest'
|
|
2
|
+
import { defaultValueCtx, Editor, editorViewCtx, rootCtx } from '@milkdown/core'
|
|
3
|
+
import { commonmark } from '@milkdown/preset-commonmark'
|
|
4
|
+
import { expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { gfm } from '..'
|
|
7
|
+
|
|
8
|
+
const TABLE = '| aa | bb |\n| --- | --- |\n| cc | dd |\n'
|
|
9
|
+
|
|
10
|
+
async function createEditor(markdown: string) {
|
|
11
|
+
const root = document.createElement('div')
|
|
12
|
+
document.body.appendChild(root)
|
|
13
|
+
const editor = Editor.make()
|
|
14
|
+
editor.config((ctx) => {
|
|
15
|
+
ctx.set(rootCtx, root)
|
|
16
|
+
ctx.set(defaultValueCtx, markdown)
|
|
17
|
+
})
|
|
18
|
+
editor.use(commonmark)
|
|
19
|
+
editor.use(gfm)
|
|
20
|
+
await editor.create()
|
|
21
|
+
return editor
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const view = (editor: Editor) => editor.ctx.get(editorViewCtx)
|
|
25
|
+
|
|
26
|
+
function posOf(editor: Editor, typeName: string, nth = 0) {
|
|
27
|
+
let seen = 0
|
|
28
|
+
let pos = -1
|
|
29
|
+
view(editor).state.doc.descendants((node, p) => {
|
|
30
|
+
if (node.type.name === typeName && seen++ === nth && pos < 0) pos = p
|
|
31
|
+
return true
|
|
32
|
+
})
|
|
33
|
+
return pos
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Every `alignment` attr in column `col`, header row first.
|
|
37
|
+
function columnAlignments(editor: Editor, col: number) {
|
|
38
|
+
const out: (string | null)[] = []
|
|
39
|
+
view(editor).state.doc.descendants((node) => {
|
|
40
|
+
if (/^table.*row$/.test(node.type.name))
|
|
41
|
+
out.push(node.child(col)?.attrs.alignment ?? null)
|
|
42
|
+
return true
|
|
43
|
+
})
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it('should propagate a header cell alignment to the body cells below it', async () => {
|
|
48
|
+
const editor = await createEditor(TABLE)
|
|
49
|
+
const headerPos = posOf(editor, 'table_header')
|
|
50
|
+
const header = view(editor).state.doc.nodeAt(headerPos)!
|
|
51
|
+
|
|
52
|
+
view(editor).dispatch(
|
|
53
|
+
view(editor).state.tr.setNodeMarkup(headerPos, undefined, {
|
|
54
|
+
...header.attrs,
|
|
55
|
+
alignment: 'center',
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expect(columnAlignments(editor, 0)).toEqual(['center', 'center'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should propagate down the header cell own column, not the first one', async () => {
|
|
63
|
+
const editor = await createEditor(
|
|
64
|
+
'| aa | bb | cc |\n| --- | --- | --- |\n| dd | ee | ff |\n'
|
|
65
|
+
)
|
|
66
|
+
const headerPos = posOf(editor, 'table_header', 1)
|
|
67
|
+
const header = view(editor).state.doc.nodeAt(headerPos)!
|
|
68
|
+
|
|
69
|
+
view(editor).dispatch(
|
|
70
|
+
view(editor).state.tr.setNodeMarkup(headerPos, undefined, {
|
|
71
|
+
...header.attrs,
|
|
72
|
+
alignment: 'right',
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(columnAlignments(editor, 1)).toEqual(['right', 'right'])
|
|
77
|
+
expect(columnAlignments(editor, 0)).toEqual([null, null])
|
|
78
|
+
expect(columnAlignments(editor, 2)).toEqual([null, null])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should still propagate when the same transaction also inserts text', async () => {
|
|
82
|
+
// "Some text changed" must not be read as "nothing structural happened".
|
|
83
|
+
const editor = await createEditor(TABLE)
|
|
84
|
+
const headerPos = posOf(editor, 'table_header')
|
|
85
|
+
const bodyPos = posOf(editor, 'table_cell')
|
|
86
|
+
const header = view(editor).state.doc.nodeAt(headerPos)!
|
|
87
|
+
|
|
88
|
+
view(editor).dispatch(
|
|
89
|
+
view(editor)
|
|
90
|
+
.state.tr.insertText('zz', bodyPos + 2)
|
|
91
|
+
.setNodeMarkup(headerPos, undefined, {
|
|
92
|
+
...header.attrs,
|
|
93
|
+
alignment: 'right',
|
|
94
|
+
})
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(columnAlignments(editor, 0)).toEqual(['right', 'right'])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should not append a transaction when no cell needs re-aligning', async () => {
|
|
101
|
+
const editor = await createEditor(`${TABLE}\npara\n`)
|
|
102
|
+
const state = view(editor).state
|
|
103
|
+
const tr = state.tr.insertText('x', posOf(editor, 'table_cell') + 2)
|
|
104
|
+
|
|
105
|
+
expect(state.applyTransaction(tr).transactions).toHaveLength(1)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should not append a transaction for a structural change to a consistent table', async () => {
|
|
109
|
+
// A structural change reaches the walk, which then finds nothing to realign.
|
|
110
|
+
const editor = await createEditor(
|
|
111
|
+
'| aa | bb |\n| :---: | ---: |\n| cc | dd |\n\npara\n'
|
|
112
|
+
)
|
|
113
|
+
const state = view(editor).state
|
|
114
|
+
const tr = state.tr.replaceWith(
|
|
115
|
+
state.doc.content.size,
|
|
116
|
+
state.doc.content.size,
|
|
117
|
+
state.schema.nodes.paragraph!.create()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
expect(tr.docChanged).toBe(true)
|
|
121
|
+
expect(state.applyTransaction(tr).transactions).toHaveLength(1)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should leave a parsed table internally consistent', async () => {
|
|
125
|
+
const editor = await createEditor(
|
|
126
|
+
'| aa | bb |\n| :--- | ---: |\n| cc | dd |\n'
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(columnAlignments(editor, 0)).toEqual(['left', 'left'])
|
|
130
|
+
expect(columnAlignments(editor, 1)).toEqual(['right', 'right'])
|
|
131
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest'
|
|
2
|
+
import { Editor, editorViewCtx } from '@milkdown/core'
|
|
3
|
+
import { commonmark } from '@milkdown/preset-commonmark'
|
|
4
|
+
import { getMarkdown } from '@milkdown/utils'
|
|
5
|
+
import userEvent from '@testing-library/user-event'
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { gfm } from '..'
|
|
9
|
+
|
|
10
|
+
// https://github.com/Milkdown/milkdown/issues/2400
|
|
11
|
+
// The strikethrough input rule shares the same flaw as the underscore-emphasis
|
|
12
|
+
// rule: its regex was not anchored to the end of the input (`$`), so pasting
|
|
13
|
+
// text that ends with `~` twice and then pressing Space/Enter matched a `~...~`
|
|
14
|
+
// span across the two pastes and corrupted the document.
|
|
15
|
+
|
|
16
|
+
const PASTED = 'qa34 %~'
|
|
17
|
+
|
|
18
|
+
async function createEditor() {
|
|
19
|
+
const editor = Editor.make()
|
|
20
|
+
editor.use(commonmark)
|
|
21
|
+
editor.use(gfm)
|
|
22
|
+
await editor.create()
|
|
23
|
+
return editor
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasStrikethrough(editor: Editor) {
|
|
27
|
+
const { doc } = editor.ctx.get(editorViewCtx).state
|
|
28
|
+
let found = false
|
|
29
|
+
doc.descendants((node) => {
|
|
30
|
+
if (node.marks.some((mark) => mark.type.name === 'strike_through'))
|
|
31
|
+
found = true
|
|
32
|
+
})
|
|
33
|
+
return found
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function paste(editor: Editor, text: string) {
|
|
37
|
+
const view = editor.ctx.get(editorViewCtx)
|
|
38
|
+
view.dispatch(view.state.tr.insertText(text))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('pasting special characters — strikethrough (#2400)', () => {
|
|
42
|
+
it('does not delete characters when pressing Space after two pastes', async () => {
|
|
43
|
+
const editor = await createEditor()
|
|
44
|
+
const view = editor.ctx.get(editorViewCtx)
|
|
45
|
+
|
|
46
|
+
paste(editor, PASTED)
|
|
47
|
+
paste(editor, PASTED)
|
|
48
|
+
|
|
49
|
+
const before = view.state.doc.textContent
|
|
50
|
+
const end = view.state.selection.from
|
|
51
|
+
|
|
52
|
+
// The trailing `deflt` callback is required by the prop type but unused
|
|
53
|
+
// by the input-rule plugin.
|
|
54
|
+
view.someProp('handleTextInput', (f) =>
|
|
55
|
+
f(view, end, end, ' ', () => view.state.tr)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
expect(view.state.doc.textContent).toBe(before)
|
|
59
|
+
expect(hasStrikethrough(editor)).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does not delete characters when pressing Enter after two pastes', async () => {
|
|
63
|
+
const editor = await createEditor()
|
|
64
|
+
const view = editor.ctx.get(editorViewCtx)
|
|
65
|
+
|
|
66
|
+
paste(editor, PASTED)
|
|
67
|
+
paste(editor, PASTED)
|
|
68
|
+
|
|
69
|
+
const before = view.state.doc.textContent
|
|
70
|
+
|
|
71
|
+
const event = new KeyboardEvent('keydown', { key: 'Enter' })
|
|
72
|
+
view.someProp('handleKeyDown', (f) => f(view, event))
|
|
73
|
+
|
|
74
|
+
expect(view.state.doc.textContent).toBe(before)
|
|
75
|
+
expect(hasStrikethrough(editor)).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('still turns ~~word~~ into strikethrough when the closing ~ is typed', async () => {
|
|
79
|
+
const editor = await createEditor()
|
|
80
|
+
const view = editor.ctx.get(editorViewCtx)
|
|
81
|
+
|
|
82
|
+
paste(editor, '~~word~')
|
|
83
|
+
|
|
84
|
+
const end = view.state.selection.from
|
|
85
|
+
view.someProp('handleTextInput', (f) =>
|
|
86
|
+
f(view, end, end, '~', () => view.state.tr)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect(view.state.doc.textContent).toBe('word')
|
|
90
|
+
expect(hasStrikethrough(editor)).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// The `$` anchor added to the strikethrough rule must not regress typing the
|
|
95
|
+
// markdown syntax by hand: these mirror the input-rule e2e tests.
|
|
96
|
+
describe('typing strikethrough still works', () => {
|
|
97
|
+
it('creates strikethrough when typing ~~on the grass~~', async () => {
|
|
98
|
+
const user = userEvent.setup()
|
|
99
|
+
const editor = await createEditor()
|
|
100
|
+
const view = editor.ctx.get(editorViewCtx)
|
|
101
|
+
|
|
102
|
+
await user.type(view.dom, 'The lunatic is ~~on the grass~~')
|
|
103
|
+
|
|
104
|
+
expect(hasStrikethrough(editor)).toBe(true)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('does not create strikethrough for intra-word tildes', async () => {
|
|
108
|
+
const user = userEvent.setup()
|
|
109
|
+
const editor = await createEditor()
|
|
110
|
+
|
|
111
|
+
await user.type(
|
|
112
|
+
editor.ctx.get(editorViewCtx).dom,
|
|
113
|
+
'C:/the/~lunatic~/is/on/the/grass'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
expect(hasStrikethrough(editor)).toBe(false)
|
|
117
|
+
expect(editor.action(getMarkdown())).toBe(
|
|
118
|
+
'C:/the/\\~lunatic\\~/is/on/the/grass\n'
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest'
|
|
2
|
+
import {
|
|
3
|
+
defaultValueCtx,
|
|
4
|
+
Editor,
|
|
5
|
+
editorViewCtx,
|
|
6
|
+
schemaCtx,
|
|
7
|
+
} from '@milkdown/core'
|
|
8
|
+
import { commonmark } from '@milkdown/preset-commonmark'
|
|
9
|
+
import { getMarkdown } from '@milkdown/utils'
|
|
10
|
+
import { describe, expect, it } from 'vitest'
|
|
11
|
+
|
|
12
|
+
import { gfm } from '..'
|
|
13
|
+
|
|
14
|
+
async function createEditor(defaultValue: string) {
|
|
15
|
+
const editor = Editor.make()
|
|
16
|
+
editor.config((ctx) => {
|
|
17
|
+
ctx.set(defaultValueCtx, defaultValue)
|
|
18
|
+
})
|
|
19
|
+
editor.use(commonmark)
|
|
20
|
+
editor.use(gfm)
|
|
21
|
+
await editor.create()
|
|
22
|
+
return editor
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function roundTrip(markdown: string) {
|
|
26
|
+
const editor = await createEditor(markdown)
|
|
27
|
+
return editor.action(getMarkdown())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// https://github.com/Milkdown/milkdown/issues/2419
|
|
31
|
+
// Tight task lists must round-trip as tight, and the checkbox syntax
|
|
32
|
+
// (`- [ ]` / `- [x]`) must survive. The list_item toMarkdown fix must be
|
|
33
|
+
// applied *in place*: preset-gfm delegates unchecked items to the base
|
|
34
|
+
// list_item runner, so replacing the whole runner would drop checkboxes.
|
|
35
|
+
describe('task list spread round-trip (#2419)', () => {
|
|
36
|
+
// The serializer normalizes bullet markers to `*`, so the inputs use `*`
|
|
37
|
+
// to keep the assertions about spread (blank lines) and the surviving
|
|
38
|
+
// checkbox rather than the marker character.
|
|
39
|
+
const cases = [
|
|
40
|
+
'* [ ] a\n* [x] b\n',
|
|
41
|
+
'* [ ] a\n * [ ] b\n* [x] c\n',
|
|
42
|
+
'* [ ] a\n\n* [x] b\n',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
cases.forEach((markdown) => {
|
|
46
|
+
it(`should round-trip ${JSON.stringify(markdown)}`, async () => {
|
|
47
|
+
const output = await roundTrip(markdown)
|
|
48
|
+
expect(output).toBe(markdown)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should keep the checkbox on a tight unchecked task item', async () => {
|
|
53
|
+
const output = await roundTrip('* [ ] todo\n')
|
|
54
|
+
expect(output).toBe('* [ ] todo\n')
|
|
55
|
+
expect(output).not.toBe('* todo\n')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// The `spread` attribute must be a real boolean so schema.nodeFromJSON()
|
|
59
|
+
// does not throw on documents containing task lists.
|
|
60
|
+
cases.forEach((markdown) => {
|
|
61
|
+
it(`should round-trip through nodeFromJSON for ${JSON.stringify(markdown)}`, async () => {
|
|
62
|
+
const editor = await createEditor(markdown)
|
|
63
|
+
const doc = editor.ctx.get(editorViewCtx).state.doc
|
|
64
|
+
const schema = editor.ctx.get(schemaCtx)
|
|
65
|
+
const json = doc.toJSON()
|
|
66
|
+
expect(() => schema.nodeFromJSON(json)).not.toThrow()
|
|
67
|
+
expect(schema.nodeFromJSON(json).eq(doc)).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -71,7 +71,7 @@ withMeta(toggleStrikethroughCommand, {
|
|
|
71
71
|
/// Input rule to create the strikethrough mark.
|
|
72
72
|
export const strikethroughInputRule = $inputRule((ctx) => {
|
|
73
73
|
return markRule(
|
|
74
|
-
/(?<![\w:/])(~{1,2})(.+?)\1(?!\w|\/)
|
|
74
|
+
/(?<![\w:/])(~{1,2})(.+?)\1(?!\w|\/)$/,
|
|
75
75
|
strikethroughSchema.type(ctx)
|
|
76
76
|
)
|
|
77
77
|
})
|
|
@@ -28,7 +28,7 @@ export const extendListItemSchemaForTask = listItemSchema.extendSchema(
|
|
|
28
28
|
return {
|
|
29
29
|
label: dom.dataset.label,
|
|
30
30
|
listType: dom.dataset.listType,
|
|
31
|
-
spread: dom.dataset.spread,
|
|
31
|
+
spread: dom.dataset.spread === 'true',
|
|
32
32
|
checked: dom.dataset.checked
|
|
33
33
|
? dom.dataset.checked === 'true'
|
|
34
34
|
: null,
|
|
@@ -64,7 +64,7 @@ export const extendListItemSchemaForTask = listItemSchema.extendSchema(
|
|
|
64
64
|
const label = node.label != null ? `${node.label}.` : '•'
|
|
65
65
|
const checked = node.checked != null ? Boolean(node.checked) : null
|
|
66
66
|
const listType = node.label != null ? 'ordered' : 'bullet'
|
|
67
|
-
const spread = node.spread
|
|
67
|
+
const spread = node.spread ?? true
|
|
68
68
|
|
|
69
69
|
state.openNode(type, { label, listType, spread, checked })
|
|
70
70
|
state.next(node.children)
|
|
@@ -81,7 +81,7 @@ export const extendListItemSchemaForTask = listItemSchema.extendSchema(
|
|
|
81
81
|
|
|
82
82
|
const label = node.attrs.label
|
|
83
83
|
const listType = node.attrs.listType
|
|
84
|
-
const spread = node.attrs.spread
|
|
84
|
+
const spread = node.attrs.spread
|
|
85
85
|
const checked = node.attrs.checked
|
|
86
86
|
|
|
87
87
|
state.openNode('listItem', undefined, {
|
|
@@ -16,14 +16,31 @@ function getChildIndex(node: Node, parent: Node) {
|
|
|
16
16
|
return index
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// The span that differs between the two docs, in `next` coordinates, or `null`
|
|
20
|
+
// when they are value-identical. Both scans compare children by reference
|
|
21
|
+
// before looking inside them.
|
|
22
|
+
function changedRange(prev: Node, next: Node) {
|
|
23
|
+
const from = prev.content.findDiffStart(next.content)
|
|
24
|
+
if (from == null) return null
|
|
25
|
+
|
|
26
|
+
const diff = prev.content.findDiffEnd(next.content)
|
|
27
|
+
if (!diff) return null
|
|
28
|
+
|
|
29
|
+
// Repeated content ("aa" -> "aaa") lets the end scan overrun the start.
|
|
30
|
+
return { from, to: Math.max(diff.b, from) }
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
export const keepTableAlignPlugin = $prose(() => {
|
|
20
34
|
return new Plugin({
|
|
21
35
|
key: pluginKey,
|
|
22
36
|
appendTransaction: (_tr, oldState, state) => {
|
|
37
|
+
if (oldState.doc === state.doc) return
|
|
38
|
+
|
|
39
|
+
const range = changedRange(oldState.doc, state.doc)
|
|
40
|
+
if (!range) return
|
|
41
|
+
|
|
23
42
|
let tr: Transaction | undefined
|
|
24
43
|
const check = (node: Node, pos: number) => {
|
|
25
|
-
if (!tr) tr = state.tr
|
|
26
|
-
|
|
27
44
|
if (node.type.name !== 'table_cell') return
|
|
28
45
|
|
|
29
46
|
const $pos = state.doc.resolve(pos)
|
|
@@ -40,9 +57,22 @@ export const keepTableAlignPlugin = $prose(() => {
|
|
|
40
57
|
const currentAlign = node.attrs.alignment
|
|
41
58
|
if (align === currentAlign) return
|
|
42
59
|
|
|
60
|
+
// Creating `tr` any earlier appends an empty transaction to every
|
|
61
|
+
// document change.
|
|
62
|
+
if (!tr) tr = state.tr
|
|
43
63
|
tr.setNodeMarkup(pos, undefined, { ...node.attrs, alignment: align })
|
|
44
64
|
}
|
|
45
|
-
|
|
65
|
+
|
|
66
|
+
// `nodesBetween` reports the range's ancestors, so an edit deep inside a
|
|
67
|
+
// cell still reaches its table. Each table it finds is checked in full,
|
|
68
|
+
// because a header cell reaches body cells outside the range.
|
|
69
|
+
state.doc.nodesBetween(range.from, range.to, (node, pos) => {
|
|
70
|
+
if (node.type.name !== 'table') return true
|
|
71
|
+
|
|
72
|
+
state.doc.nodesBetween(pos, pos + node.nodeSize, check)
|
|
73
|
+
// Tables do not nest.
|
|
74
|
+
return false
|
|
75
|
+
})
|
|
46
76
|
|
|
47
77
|
return tr
|
|
48
78
|
},
|