@prosekit/core 0.8.3 → 0.8.5
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/dist/editor-KZlceNQ1.d.ts +722 -0
- package/dist/editor-KZlceNQ1.d.ts.map +1 -0
- package/dist/{editor-DlGlYOp-.js → editor-TvRTsFdO.js} +102 -196
- package/dist/editor-TvRTsFdO.js.map +1 -0
- package/dist/prosekit-core-test.d.ts +20 -19
- package/dist/prosekit-core-test.d.ts.map +1 -0
- package/dist/prosekit-core-test.js +5 -8
- package/dist/prosekit-core-test.js.map +1 -0
- package/dist/prosekit-core.d.ts +797 -792
- package/dist/prosekit-core.d.ts.map +1 -0
- package/dist/prosekit-core.js +42 -79
- package/dist/prosekit-core.js.map +1 -0
- package/package.json +14 -12
- package/src/commands/add-mark.ts +53 -0
- package/src/commands/expand-mark.ts +96 -0
- package/src/commands/insert-default-block.spec.ts +102 -0
- package/src/commands/insert-default-block.ts +49 -0
- package/src/commands/insert-node.ts +71 -0
- package/src/commands/insert-text.ts +24 -0
- package/src/commands/remove-mark.ts +54 -0
- package/src/commands/remove-node.ts +43 -0
- package/src/commands/select-all.ts +16 -0
- package/src/commands/set-block-type.ts +64 -0
- package/src/commands/set-node-attrs.ts +68 -0
- package/src/commands/toggle-mark.ts +65 -0
- package/src/commands/toggle-node.ts +47 -0
- package/src/commands/toggle-wrap.spec.ts +35 -0
- package/src/commands/toggle-wrap.ts +42 -0
- package/src/commands/unset-block-type.spec.ts +49 -0
- package/src/commands/unset-block-type.ts +84 -0
- package/src/commands/unset-mark.spec.ts +35 -0
- package/src/commands/unset-mark.ts +38 -0
- package/src/commands/wrap.ts +50 -0
- package/src/editor/action.spec.ts +143 -0
- package/src/editor/action.ts +248 -0
- package/src/editor/editor.spec.ts +186 -0
- package/src/editor/editor.ts +563 -0
- package/src/editor/union.spec.ts +108 -0
- package/src/editor/union.ts +47 -0
- package/src/editor/with-priority.ts +25 -0
- package/src/error.ts +28 -0
- package/src/extensions/clipboard-serializer.ts +107 -0
- package/src/extensions/command.ts +121 -0
- package/src/extensions/default-state.spec.ts +60 -0
- package/src/extensions/default-state.ts +76 -0
- package/src/extensions/doc.ts +31 -0
- package/src/extensions/events/doc-change.ts +34 -0
- package/src/extensions/events/dom-event.spec.ts +70 -0
- package/src/extensions/events/dom-event.ts +117 -0
- package/src/extensions/events/editor-event.ts +293 -0
- package/src/extensions/events/focus.spec.ts +50 -0
- package/src/extensions/events/focus.ts +28 -0
- package/src/extensions/events/plugin-view.ts +132 -0
- package/src/extensions/history.ts +81 -0
- package/src/extensions/keymap-base.ts +60 -0
- package/src/extensions/keymap.spec.ts +125 -0
- package/src/extensions/keymap.ts +96 -0
- package/src/extensions/mark-spec.spec.ts +177 -0
- package/src/extensions/mark-spec.ts +181 -0
- package/src/extensions/mark-view-effect.ts +85 -0
- package/src/extensions/mark-view.ts +43 -0
- package/src/extensions/node-spec.spec.ts +224 -0
- package/src/extensions/node-spec.ts +199 -0
- package/src/extensions/node-view-effect.ts +85 -0
- package/src/extensions/node-view.ts +43 -0
- package/src/extensions/paragraph.ts +61 -0
- package/src/extensions/plugin.spec.ts +153 -0
- package/src/extensions/plugin.ts +81 -0
- package/src/extensions/text.ts +34 -0
- package/src/facets/base-extension.ts +54 -0
- package/src/facets/command.ts +21 -0
- package/src/facets/facet-extension.spec.ts +173 -0
- package/src/facets/facet-extension.ts +53 -0
- package/src/facets/facet-node.spec.ts +265 -0
- package/src/facets/facet-node.ts +185 -0
- package/src/facets/facet-types.ts +9 -0
- package/src/facets/facet.spec.ts +76 -0
- package/src/facets/facet.ts +84 -0
- package/src/facets/root.ts +44 -0
- package/src/facets/schema-spec.ts +30 -0
- package/src/facets/schema.ts +26 -0
- package/src/facets/state.spec.ts +53 -0
- package/src/facets/state.ts +85 -0
- package/src/facets/union-extension.ts +41 -0
- package/src/index.ts +302 -0
- package/src/test/index.ts +4 -0
- package/src/test/test-builder.ts +68 -0
- package/src/test/test-editor.spec.ts +104 -0
- package/src/test/test-editor.ts +113 -0
- package/src/testing/index.ts +283 -0
- package/src/testing/keyboard.ts +5 -0
- package/src/types/any-function.ts +4 -0
- package/src/types/assert-type-equal.ts +8 -0
- package/src/types/attrs.ts +32 -0
- package/src/types/base-node-view-options.ts +33 -0
- package/src/types/dom-node.ts +1 -0
- package/src/types/extension-command.ts +52 -0
- package/src/types/extension-mark.ts +15 -0
- package/src/types/extension-node.ts +15 -0
- package/src/types/extension.spec.ts +56 -0
- package/src/types/extension.ts +168 -0
- package/src/types/model.ts +54 -0
- package/src/types/object-entries.ts +13 -0
- package/src/types/pick-string-literal.spec.ts +10 -0
- package/src/types/pick-string-literal.ts +6 -0
- package/src/types/pick-sub-type.spec.ts +20 -0
- package/src/types/pick-sub-type.ts +6 -0
- package/src/types/priority.ts +12 -0
- package/src/types/setter.ts +4 -0
- package/src/types/simplify-deeper.spec.ts +40 -0
- package/src/types/simplify-deeper.ts +6 -0
- package/src/types/simplify-union.spec.ts +21 -0
- package/src/types/simplify-union.ts +11 -0
- package/src/utils/array-grouping.spec.ts +29 -0
- package/src/utils/array-grouping.ts +25 -0
- package/src/utils/array.ts +21 -0
- package/src/utils/assert.ts +13 -0
- package/src/utils/attrs-match.ts +20 -0
- package/src/utils/can-use-regex-lookbehind.ts +12 -0
- package/src/utils/clsx.spec.ts +14 -0
- package/src/utils/clsx.ts +14 -0
- package/src/utils/collect-children.ts +21 -0
- package/src/utils/collect-nodes.ts +37 -0
- package/src/utils/combine-event-handlers.spec.ts +27 -0
- package/src/utils/combine-event-handlers.ts +27 -0
- package/src/utils/contains-inline-node.ts +17 -0
- package/src/utils/deep-equals.spec.ts +26 -0
- package/src/utils/deep-equals.ts +29 -0
- package/src/utils/default-block-at.ts +15 -0
- package/src/utils/editor-content.spec.ts +47 -0
- package/src/utils/editor-content.ts +77 -0
- package/src/utils/env.ts +6 -0
- package/src/utils/find-parent-node-of-type.ts +29 -0
- package/src/utils/find-parent-node.spec.ts +68 -0
- package/src/utils/find-parent-node.ts +55 -0
- package/src/utils/get-custom-selection.ts +19 -0
- package/src/utils/get-dom-api.ts +56 -0
- package/src/utils/get-id.spec.ts +14 -0
- package/src/utils/get-id.ts +13 -0
- package/src/utils/get-mark-type.ts +20 -0
- package/src/utils/get-node-type.ts +20 -0
- package/src/utils/get-node-types.ts +19 -0
- package/src/utils/includes-mark.ts +18 -0
- package/src/utils/is-at-block-start.ts +26 -0
- package/src/utils/is-in-code-block.ts +18 -0
- package/src/utils/is-mark-absent.spec.ts +53 -0
- package/src/utils/is-mark-absent.ts +42 -0
- package/src/utils/is-mark-active.ts +27 -0
- package/src/utils/is-node-active.ts +25 -0
- package/src/utils/is-subset.spec.ts +12 -0
- package/src/utils/is-subset.ts +11 -0
- package/src/utils/maybe-run.spec.ts +39 -0
- package/src/utils/maybe-run.ts +11 -0
- package/src/utils/merge-objects.spec.ts +30 -0
- package/src/utils/merge-objects.ts +11 -0
- package/src/utils/merge-specs.ts +35 -0
- package/src/utils/object-equal.spec.ts +26 -0
- package/src/utils/object-equal.ts +28 -0
- package/src/utils/output-spec.test.ts +95 -0
- package/src/utils/output-spec.ts +130 -0
- package/src/utils/parse.spec.ts +46 -0
- package/src/utils/parse.ts +321 -0
- package/src/utils/remove-undefined-values.spec.ts +15 -0
- package/src/utils/remove-undefined-values.ts +9 -0
- package/src/utils/set-selection-around.ts +11 -0
- package/src/utils/type-assertion.ts +91 -0
- package/src/utils/unicode.spec.ts +10 -0
- package/src/utils/unicode.ts +4 -0
- package/src/utils/with-skip-code-block.ts +15 -0
- package/dist/editor-OUH5V8BA.d.ts +0 -754
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import OrderedMap from 'orderedmap'
|
|
2
|
+
import {
|
|
3
|
+
expect,
|
|
4
|
+
test,
|
|
5
|
+
} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { deepEquals } from './deep-equals'
|
|
8
|
+
|
|
9
|
+
test('arrays with equal values are equal', () => {
|
|
10
|
+
expect(deepEquals([1, 2], [1, 2])).toBe(true)
|
|
11
|
+
expect(deepEquals([1, 2], [2, 1])).toBe(false)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('nested objects', () => {
|
|
15
|
+
const a = { a: 1, b: { c: 3 } }
|
|
16
|
+
const b = { a: 1, b: { c: 3 } }
|
|
17
|
+
const c = { a: 1, b: { c: 4 } }
|
|
18
|
+
expect(deepEquals(a, b)).toBe(true)
|
|
19
|
+
expect(deepEquals(a, c)).toBe(false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('ordered maps', () => {
|
|
23
|
+
const map1 = OrderedMap.from({ a: 1, b: 2 })
|
|
24
|
+
const map2 = OrderedMap.from({ a: 1, b: 2 })
|
|
25
|
+
expect(deepEquals(map1, map2)).toBe(true)
|
|
26
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import OrderedMap from 'orderedmap'
|
|
2
|
+
|
|
3
|
+
export function deepEquals<T>(a: T, b: T): boolean {
|
|
4
|
+
if (a === b) {
|
|
5
|
+
return true
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!a || !b) {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
13
|
+
return a.length === b.length && a.every((x, i) => deepEquals(x, b[i]))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (a instanceof OrderedMap && b instanceof OrderedMap) {
|
|
17
|
+
return a.size === b.size && deepEquals(a.toObject(), b.toObject())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
21
|
+
const aKeys = Object.keys(a)
|
|
22
|
+
const bKeys = Object.keys(b)
|
|
23
|
+
return (
|
|
24
|
+
aKeys.length === bKeys.length
|
|
25
|
+
&& aKeys.every((key) => deepEquals(a[key as keyof T], b[key as keyof T]))
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContentMatch,
|
|
3
|
+
NodeType,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function defaultBlockAt(match: ContentMatch): NodeType | null {
|
|
10
|
+
for (let i = 0; i < match.edgeCount; i++) {
|
|
11
|
+
const { type } = match.edge(i)
|
|
12
|
+
if (type.isTextblock && !type.hasRequiredAttrs()) return type
|
|
13
|
+
}
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { AllSelection } from '@prosekit/pm/state'
|
|
2
|
+
import {
|
|
3
|
+
expect,
|
|
4
|
+
test,
|
|
5
|
+
} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { setupTest } from '../testing'
|
|
8
|
+
|
|
9
|
+
import { getEditorSelection } from './editor-content'
|
|
10
|
+
|
|
11
|
+
test('getEditorSelection', () => {
|
|
12
|
+
const { n } = setupTest()
|
|
13
|
+
const doc = n.doc(n.paragraph('hello world'))
|
|
14
|
+
expect(getEditorSelection(doc, 'start').toJSON()).toMatchInlineSnapshot(`
|
|
15
|
+
{
|
|
16
|
+
"anchor": 1,
|
|
17
|
+
"head": 1,
|
|
18
|
+
"type": "text",
|
|
19
|
+
}
|
|
20
|
+
`)
|
|
21
|
+
expect(getEditorSelection(doc, 'end').toJSON()).toMatchInlineSnapshot(`
|
|
22
|
+
{
|
|
23
|
+
"anchor": 12,
|
|
24
|
+
"head": 12,
|
|
25
|
+
"type": "text",
|
|
26
|
+
}
|
|
27
|
+
`)
|
|
28
|
+
expect(
|
|
29
|
+
getEditorSelection(doc, {
|
|
30
|
+
anchor: 4,
|
|
31
|
+
head: 8,
|
|
32
|
+
type: 'text',
|
|
33
|
+
}).toJSON(),
|
|
34
|
+
).toMatchInlineSnapshot(`
|
|
35
|
+
{
|
|
36
|
+
"anchor": 4,
|
|
37
|
+
"head": 8,
|
|
38
|
+
"type": "text",
|
|
39
|
+
}
|
|
40
|
+
`)
|
|
41
|
+
expect(getEditorSelection(doc, new AllSelection(doc)).toJSON())
|
|
42
|
+
.toMatchInlineSnapshot(`
|
|
43
|
+
{
|
|
44
|
+
"type": "all",
|
|
45
|
+
}
|
|
46
|
+
`)
|
|
47
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isElementLike } from '@ocavue/utils'
|
|
2
|
+
import type {
|
|
3
|
+
ProseMirrorNode,
|
|
4
|
+
Schema,
|
|
5
|
+
} from '@prosekit/pm/model'
|
|
6
|
+
import { Selection } from '@prosekit/pm/state'
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
NodeJSON,
|
|
10
|
+
SelectionJSON,
|
|
11
|
+
} from '../types/model'
|
|
12
|
+
|
|
13
|
+
import { assert } from './assert'
|
|
14
|
+
import {
|
|
15
|
+
jsonFromElement,
|
|
16
|
+
jsonFromHTML,
|
|
17
|
+
} from './parse'
|
|
18
|
+
import {
|
|
19
|
+
isProseMirrorNode,
|
|
20
|
+
isSelection,
|
|
21
|
+
} from './type-assertion'
|
|
22
|
+
|
|
23
|
+
export function getEditorContentJSON(
|
|
24
|
+
schema: Schema,
|
|
25
|
+
content: NodeJSON | string | HTMLElement,
|
|
26
|
+
): NodeJSON {
|
|
27
|
+
if (typeof content === 'string') {
|
|
28
|
+
return jsonFromHTML(content, { schema })
|
|
29
|
+
} else if (isElementLike(content)) {
|
|
30
|
+
return jsonFromElement(content, { schema })
|
|
31
|
+
} else {
|
|
32
|
+
return content
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getEditorContentNode(
|
|
37
|
+
schema: Schema,
|
|
38
|
+
content: NodeJSON | string | HTMLElement | ProseMirrorNode,
|
|
39
|
+
): ProseMirrorNode {
|
|
40
|
+
if (isProseMirrorNode(content)) {
|
|
41
|
+
return content
|
|
42
|
+
}
|
|
43
|
+
return schema.nodeFromJSON(getEditorContentJSON(schema, content))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getEditorContentDoc(
|
|
47
|
+
schema: Schema,
|
|
48
|
+
content: NodeJSON | string | HTMLElement | ProseMirrorNode,
|
|
49
|
+
): ProseMirrorNode {
|
|
50
|
+
const doc = getEditorContentNode(schema, content)
|
|
51
|
+
assert(
|
|
52
|
+
doc.type.schema === schema,
|
|
53
|
+
'Document schema does not match editor schema',
|
|
54
|
+
)
|
|
55
|
+
assert(
|
|
56
|
+
doc.type === schema.topNodeType,
|
|
57
|
+
`Document type does not match editor top node type. Expected ${schema.topNodeType.name}, got ${doc.type.name}`,
|
|
58
|
+
)
|
|
59
|
+
return doc
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getEditorSelection(
|
|
63
|
+
doc: ProseMirrorNode,
|
|
64
|
+
selection: SelectionJSON | Selection | 'start' | 'end',
|
|
65
|
+
): Selection {
|
|
66
|
+
if (isSelection(selection)) {
|
|
67
|
+
assert(selection.$head.doc === doc, 'Selection and doc do not match')
|
|
68
|
+
return selection
|
|
69
|
+
}
|
|
70
|
+
if (selection === 'start') {
|
|
71
|
+
return Selection.atStart(doc)
|
|
72
|
+
}
|
|
73
|
+
if (selection === 'end') {
|
|
74
|
+
return Selection.atEnd(doc)
|
|
75
|
+
}
|
|
76
|
+
return Selection.fromJSON(doc, selection)
|
|
77
|
+
}
|
package/src/utils/env.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NodeType,
|
|
3
|
+
ResolvedPos,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
findParentNode,
|
|
8
|
+
type FindParentNodeResult,
|
|
9
|
+
} from './find-parent-node'
|
|
10
|
+
import { getNodeType } from './get-node-type'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Finds the closest parent node that matches the given node type.
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export function findParentNodeOfType(
|
|
18
|
+
/**
|
|
19
|
+
* The type of the node to find.
|
|
20
|
+
*/
|
|
21
|
+
type: NodeType | string,
|
|
22
|
+
/**
|
|
23
|
+
* The position to start searching from.
|
|
24
|
+
*/
|
|
25
|
+
$pos: ResolvedPos,
|
|
26
|
+
): FindParentNodeResult | undefined {
|
|
27
|
+
const nodeType = getNodeType($pos.doc.type.schema, type)
|
|
28
|
+
return findParentNode((node) => node.type === nodeType, $pos)
|
|
29
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
it,
|
|
5
|
+
} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { setupTest } from '../testing'
|
|
8
|
+
|
|
9
|
+
import { findParentNode } from './find-parent-node'
|
|
10
|
+
|
|
11
|
+
describe('findParentNode', () => {
|
|
12
|
+
it('finds parent node with cursor directly inside', () => {
|
|
13
|
+
const { editor, n } = setupTest()
|
|
14
|
+
editor.set(n.doc(n.p('foo'), n.p('bar<a>')))
|
|
15
|
+
const found = findParentNode(
|
|
16
|
+
(node) => node.type.name === 'paragraph',
|
|
17
|
+
editor.state.selection.$anchor,
|
|
18
|
+
)
|
|
19
|
+
expect(found).toMatchInlineSnapshot(`
|
|
20
|
+
{
|
|
21
|
+
"depth": 1,
|
|
22
|
+
"node": {
|
|
23
|
+
"content": [
|
|
24
|
+
{
|
|
25
|
+
"text": "bar",
|
|
26
|
+
"type": "text",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
"type": "paragraph",
|
|
30
|
+
},
|
|
31
|
+
"pos": 5,
|
|
32
|
+
"start": 6,
|
|
33
|
+
}
|
|
34
|
+
`)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('can handle the top-level node', () => {
|
|
38
|
+
const { editor, n } = setupTest()
|
|
39
|
+
editor.set(n.doc(n.p('foo<a>')))
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
findParentNode(
|
|
43
|
+
(node) => node.type.name === 'doc',
|
|
44
|
+
editor.state.selection.$anchor,
|
|
45
|
+
),
|
|
46
|
+
).toMatchInlineSnapshot(`
|
|
47
|
+
{
|
|
48
|
+
"depth": 0,
|
|
49
|
+
"node": {
|
|
50
|
+
"content": [
|
|
51
|
+
{
|
|
52
|
+
"content": [
|
|
53
|
+
{
|
|
54
|
+
"text": "foo",
|
|
55
|
+
"type": "text",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
"type": "paragraph",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
"type": "doc",
|
|
62
|
+
},
|
|
63
|
+
"pos": 0,
|
|
64
|
+
"start": 0,
|
|
65
|
+
}
|
|
66
|
+
`)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProseMirrorNode,
|
|
3
|
+
ResolvedPos,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export interface FindParentNodeResult {
|
|
10
|
+
/**
|
|
11
|
+
* The closest parent node that satisfies the predicate.
|
|
12
|
+
*/
|
|
13
|
+
node: ProseMirrorNode
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The position directly before the node.
|
|
17
|
+
*/
|
|
18
|
+
pos: number
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The position at the start of the node.
|
|
22
|
+
*/
|
|
23
|
+
start: number
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The depth of the node.
|
|
27
|
+
*/
|
|
28
|
+
depth: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find the closest parent node that satisfies the predicate.
|
|
33
|
+
*
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
export function findParentNode(
|
|
37
|
+
/**
|
|
38
|
+
* The predicate to test the parent node.
|
|
39
|
+
*/
|
|
40
|
+
predicate: (node: ProseMirrorNode) => boolean,
|
|
41
|
+
/**
|
|
42
|
+
* The position to start searching from.
|
|
43
|
+
*/
|
|
44
|
+
$pos: ResolvedPos,
|
|
45
|
+
): FindParentNodeResult | undefined {
|
|
46
|
+
for (let depth = $pos.depth; depth >= 0; depth -= 1) {
|
|
47
|
+
const node = $pos.node(depth)
|
|
48
|
+
|
|
49
|
+
if (predicate(node)) {
|
|
50
|
+
const pos = depth === 0 ? 0 : $pos.before(depth)
|
|
51
|
+
const start = $pos.start(depth)
|
|
52
|
+
return { node, pos, start, depth }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TextSelection,
|
|
3
|
+
type EditorState,
|
|
4
|
+
type Selection,
|
|
5
|
+
} from '@prosekit/pm/state'
|
|
6
|
+
|
|
7
|
+
export function getCustomSelection(
|
|
8
|
+
state: EditorState,
|
|
9
|
+
from?: number | null,
|
|
10
|
+
to?: number | null,
|
|
11
|
+
): Selection {
|
|
12
|
+
const pos = from ?? to
|
|
13
|
+
if (pos != null) {
|
|
14
|
+
const $from = state.doc.resolve(from ?? pos)
|
|
15
|
+
const $to = state.doc.resolve(to ?? pos)
|
|
16
|
+
return TextSelection.between($from, $to)
|
|
17
|
+
}
|
|
18
|
+
return state.selection
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DOMDocumentNotFoundError } from '../error'
|
|
2
|
+
|
|
3
|
+
function findGlobalBrowserDocument() {
|
|
4
|
+
if (typeof document !== 'undefined') {
|
|
5
|
+
return document
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof globalThis !== 'undefined' && globalThis.document) {
|
|
9
|
+
return globalThis.document
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function findGlobalBrowserWindow() {
|
|
14
|
+
if (typeof window !== 'undefined') {
|
|
15
|
+
return window
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof globalThis !== 'undefined' && globalThis.window) {
|
|
19
|
+
return globalThis.window
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findBrowserDocument(options?: { document?: Document }): Document | undefined {
|
|
24
|
+
return (
|
|
25
|
+
options?.document
|
|
26
|
+
?? findGlobalBrowserDocument()
|
|
27
|
+
?? findGlobalBrowserWindow()?.document
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findBrowserWindow(options?: {
|
|
32
|
+
document?: Document
|
|
33
|
+
}): (Window & typeof globalThis) | undefined {
|
|
34
|
+
return (
|
|
35
|
+
options?.document?.defaultView
|
|
36
|
+
?? findGlobalBrowserWindow()
|
|
37
|
+
?? findBrowserDocument(options)?.defaultView
|
|
38
|
+
?? undefined
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getBrowserDocument(options?: {
|
|
43
|
+
document?: Document
|
|
44
|
+
}): Document {
|
|
45
|
+
const doc = findBrowserDocument(options)
|
|
46
|
+
if (doc) return doc
|
|
47
|
+
throw new DOMDocumentNotFoundError()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getBrowserWindow(options?: {
|
|
51
|
+
document?: Document
|
|
52
|
+
}): Window & typeof globalThis {
|
|
53
|
+
const win = findBrowserWindow(options)
|
|
54
|
+
if (win) return win
|
|
55
|
+
throw new DOMDocumentNotFoundError()
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expect,
|
|
3
|
+
test,
|
|
4
|
+
} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { getId } from './get-id'
|
|
7
|
+
|
|
8
|
+
test('generates sequential ids', () => {
|
|
9
|
+
const first = getId()
|
|
10
|
+
const second = getId()
|
|
11
|
+
const firstNum = Number(first.split(':')[1])
|
|
12
|
+
const secondNum = Number(second.split(':')[1])
|
|
13
|
+
expect(secondNum).toBe((firstNum + 1) % Number.MAX_SAFE_INTEGER)
|
|
14
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
let id = 0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a unique id in the current process that can be used in various places.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*
|
|
8
|
+
* @deprecated Import `getId` from `@ocavue/utils` package instead. Remove it in a future version.
|
|
9
|
+
*/
|
|
10
|
+
export function getId(): string {
|
|
11
|
+
id = (id + 1) % Number.MAX_SAFE_INTEGER
|
|
12
|
+
return `id:${id}`
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MarkType,
|
|
3
|
+
Schema,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
|
|
6
|
+
import { ProseKitError } from '../error'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export function getMarkType(schema: Schema, type: string | MarkType): MarkType {
|
|
12
|
+
if (typeof type === 'string') {
|
|
13
|
+
const markType = schema.marks[type]
|
|
14
|
+
if (!markType) {
|
|
15
|
+
throw new ProseKitError(`Cannot find mark type "${type}"`)
|
|
16
|
+
}
|
|
17
|
+
return markType
|
|
18
|
+
}
|
|
19
|
+
return type
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NodeType,
|
|
3
|
+
Schema,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
|
|
6
|
+
import { ProseKitError } from '../error'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export function getNodeType(schema: Schema, type: string | NodeType): NodeType {
|
|
12
|
+
if (typeof type === 'string') {
|
|
13
|
+
const nodeType = schema.nodes[type]
|
|
14
|
+
if (!nodeType) {
|
|
15
|
+
throw new ProseKitError(`Cannot find ProseMirror node type "${type}"`)
|
|
16
|
+
}
|
|
17
|
+
return nodeType
|
|
18
|
+
}
|
|
19
|
+
return type
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NodeType,
|
|
3
|
+
Schema,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
|
|
6
|
+
import { getNodeType } from './get-node-type'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export function getNodeTypes(
|
|
12
|
+
schema: Schema,
|
|
13
|
+
types: string | NodeType | string[] | NodeType[],
|
|
14
|
+
): NodeType[] {
|
|
15
|
+
if (Array.isArray(types)) {
|
|
16
|
+
return types.map((type) => getNodeType(schema, type))
|
|
17
|
+
}
|
|
18
|
+
return [getNodeType(schema, types)]
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Attrs,
|
|
3
|
+
Mark,
|
|
4
|
+
MarkType,
|
|
5
|
+
} from '@prosekit/pm/model'
|
|
6
|
+
|
|
7
|
+
import { isSubset } from './is-subset'
|
|
8
|
+
|
|
9
|
+
export function includesMark(
|
|
10
|
+
marks: readonly Mark[],
|
|
11
|
+
markType: MarkType,
|
|
12
|
+
attrs?: Attrs | null,
|
|
13
|
+
): boolean {
|
|
14
|
+
attrs = attrs || {}
|
|
15
|
+
return marks.some((mark) => {
|
|
16
|
+
return mark.type === markType && isSubset(attrs, mark.attrs)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ResolvedPos } from '@prosekit/pm/model'
|
|
2
|
+
import type {
|
|
3
|
+
EditorState,
|
|
4
|
+
TextSelection,
|
|
5
|
+
} from '@prosekit/pm/state'
|
|
6
|
+
import type { EditorView } from '@prosekit/pm/view'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether the selection is an empty text selection at the start of a block.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function isAtBlockStart(
|
|
14
|
+
state: EditorState,
|
|
15
|
+
view?: EditorView,
|
|
16
|
+
): ResolvedPos | null {
|
|
17
|
+
// Copy from https://github.com/ProseMirror/prosemirror-commands/blob/1.5.2/src/commands.ts#L15
|
|
18
|
+
const { $cursor } = state.selection as TextSelection
|
|
19
|
+
if (
|
|
20
|
+
!$cursor
|
|
21
|
+
|| (view ? !view.endOfTextblock('backward', state) : $cursor.parentOffset > 0)
|
|
22
|
+
) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
return $cursor
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NodeType } from '@prosekit/pm/model'
|
|
2
|
+
import type { Selection } from '@prosekit/pm/state'
|
|
3
|
+
|
|
4
|
+
function isCodeBlockType(type: NodeType): boolean {
|
|
5
|
+
return !!(type.spec.code && type.isBlock)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if the selection is in a code block.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function isInCodeBlock(selection: Selection): boolean {
|
|
14
|
+
return (
|
|
15
|
+
isCodeBlockType(selection.$from.parent.type)
|
|
16
|
+
|| isCodeBlockType(selection.$to.parent.type)
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expect,
|
|
3
|
+
test,
|
|
4
|
+
} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { setupTest } from '../testing'
|
|
7
|
+
|
|
8
|
+
import { isMarkAbsent } from './is-mark-absent'
|
|
9
|
+
|
|
10
|
+
test('isMarkAbsent', () => {
|
|
11
|
+
const { editor, m, n } = setupTest()
|
|
12
|
+
|
|
13
|
+
const isBoldAbsent = () => {
|
|
14
|
+
const markType = editor.schema.marks.bold
|
|
15
|
+
const { doc, selection } = editor.state
|
|
16
|
+
return isMarkAbsent(doc, selection.from, selection.to, markType)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
editor.set(n.doc(n.p('<a>foo<b>')))
|
|
20
|
+
expect(isBoldAbsent()).toBe(true)
|
|
21
|
+
|
|
22
|
+
editor.set(n.doc(n.p('<a>', m.bold('foo'), '<b>')))
|
|
23
|
+
expect(isBoldAbsent()).toBe(false)
|
|
24
|
+
|
|
25
|
+
editor.set(n.doc(n.p('<a>', 'foo', m.bold('bar'), 'baz', '<b>')))
|
|
26
|
+
expect(isBoldAbsent()).toBe(true)
|
|
27
|
+
|
|
28
|
+
editor.set(n.doc(n.p('<a>', m.bold('foo'), 'bar', '<b>')))
|
|
29
|
+
expect(isBoldAbsent()).toBe(true)
|
|
30
|
+
|
|
31
|
+
editor.set(n.doc(n.p('<a>', 'foo', m.bold('bar'), '<b>')))
|
|
32
|
+
expect(isBoldAbsent()).toBe(true)
|
|
33
|
+
|
|
34
|
+
editor.set(n.doc(n.p('<a>'), n.p('foo'), n.p('<b>')))
|
|
35
|
+
expect(isBoldAbsent()).toBe(true)
|
|
36
|
+
|
|
37
|
+
editor.set(n.doc(n.p('<a>'), n.p('foo'), n.p(m.bold('bar')), n.p('<b>')))
|
|
38
|
+
expect(isBoldAbsent()).toBe(true)
|
|
39
|
+
|
|
40
|
+
editor.set(n.doc(n.p('<a>'), n.p(''), n.p(''), n.p('<b>')))
|
|
41
|
+
expect(isBoldAbsent()).toBe(true)
|
|
42
|
+
|
|
43
|
+
editor.set(
|
|
44
|
+
n.doc(n.p('<a>'), n.p(m.bold('foo')), n.p(m.bold('bar')), n.p('<b>')),
|
|
45
|
+
)
|
|
46
|
+
expect(isBoldAbsent()).toBe(false)
|
|
47
|
+
|
|
48
|
+
editor.set(n.doc(n.p('<a>'), n.p(m.bold('foo')), n.p(), n.p('<b>')))
|
|
49
|
+
expect(isBoldAbsent()).toBe(false)
|
|
50
|
+
|
|
51
|
+
editor.set(n.doc(n.codeBlock('<a>', 'foo', '<b>')))
|
|
52
|
+
expect(isBoldAbsent()).toBe(true)
|
|
53
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Attrs,
|
|
3
|
+
MarkType,
|
|
4
|
+
ProseMirrorNode,
|
|
5
|
+
} from '@prosekit/pm/model'
|
|
6
|
+
|
|
7
|
+
import { includesMark } from './includes-mark'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if the given mark is missing in some part of the range.
|
|
11
|
+
* Returns false if the entire range has the given mark.
|
|
12
|
+
* Returns true if the mark is not allowed in the range.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export function isMarkAbsent(
|
|
17
|
+
node: ProseMirrorNode,
|
|
18
|
+
from: number,
|
|
19
|
+
to: number,
|
|
20
|
+
markType: MarkType,
|
|
21
|
+
attrs?: Attrs | null,
|
|
22
|
+
): boolean {
|
|
23
|
+
let missing = false
|
|
24
|
+
let available = false
|
|
25
|
+
|
|
26
|
+
node.nodesBetween(from, to, (node, pos, parent) => {
|
|
27
|
+
if (missing) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const allowed = parent?.type.allowsMarkType(markType)
|
|
32
|
+
&& !node.marks.some((m) => m.type !== markType && m.type.excludes(markType))
|
|
33
|
+
|
|
34
|
+
if (allowed) {
|
|
35
|
+
available = true
|
|
36
|
+
if (!includesMark(node.marks, markType, attrs)) {
|
|
37
|
+
missing = true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
return available ? missing : true
|
|
42
|
+
}
|