@prosekit/core 0.8.2 → 0.8.4
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-CfkZ4TNU.d.ts +748 -0
- package/dist/editor-CfkZ4TNU.d.ts.map +1 -0
- package/dist/{editor-DbMrpnmL.js → editor-CizSwUN8.js} +102 -192
- package/dist/editor-CizSwUN8.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 +4 -5
- package/dist/prosekit-core-test.js.map +1 -0
- package/dist/prosekit-core.d.ts +782 -757
- package/dist/prosekit-core.d.ts.map +1 -0
- package/dist/prosekit-core.js +30 -45
- package/dist/prosekit-core.js.map +1 -0
- package/package.json +14 -11
- 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 +89 -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.ts +91 -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.ts +57 -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 +12 -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-CjVyjJqw.d.ts +0 -739
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Attrs,
|
|
3
|
+
MarkType,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
import type { EditorState } from '@prosekit/pm/state'
|
|
6
|
+
|
|
7
|
+
import { getMarkType } from './get-mark-type'
|
|
8
|
+
import { includesMark } from './includes-mark'
|
|
9
|
+
import { isMarkAbsent } from './is-mark-absent'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export function isMarkActive(
|
|
15
|
+
state: EditorState,
|
|
16
|
+
type: string | MarkType,
|
|
17
|
+
attrs?: Attrs | null,
|
|
18
|
+
): boolean {
|
|
19
|
+
const { from, $from, to, empty } = state.selection
|
|
20
|
+
const markType = getMarkType(state.schema, type)
|
|
21
|
+
if (empty) {
|
|
22
|
+
const marks = state.storedMarks || $from.marks()
|
|
23
|
+
return includesMark(marks, markType, attrs)
|
|
24
|
+
} else {
|
|
25
|
+
return !isMarkAbsent(state.doc, from, to, markType, attrs)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Attrs,
|
|
3
|
+
NodeType,
|
|
4
|
+
} from '@prosekit/pm/model'
|
|
5
|
+
import type { EditorState } from '@prosekit/pm/state'
|
|
6
|
+
|
|
7
|
+
import { attrsMatch } from './attrs-match'
|
|
8
|
+
import { getNodeType } from './get-node-type'
|
|
9
|
+
|
|
10
|
+
export function isNodeActive(
|
|
11
|
+
state: EditorState,
|
|
12
|
+
type: string | NodeType,
|
|
13
|
+
attrs?: Attrs | null,
|
|
14
|
+
): boolean {
|
|
15
|
+
const $pos = state.selection.$from
|
|
16
|
+
const nodeType = getNodeType(state.schema, type)
|
|
17
|
+
|
|
18
|
+
for (let depth = $pos.depth; depth >= 0; depth--) {
|
|
19
|
+
const node = $pos.node(depth)
|
|
20
|
+
if (node.type === nodeType && (!attrs || attrsMatch(node, attrs))) {
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expect,
|
|
3
|
+
test,
|
|
4
|
+
} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { isSubset } from './is-subset'
|
|
7
|
+
|
|
8
|
+
test('isSubset', () => {
|
|
9
|
+
expect(isSubset({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(true)
|
|
10
|
+
expect(isSubset({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false)
|
|
11
|
+
expect(isSubset({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 4 })).toBe(false)
|
|
12
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if `subset` is a subset of `superset`.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export function isSubset(
|
|
7
|
+
subset: Record<string, unknown>,
|
|
8
|
+
superset: Record<string, unknown>,
|
|
9
|
+
): boolean {
|
|
10
|
+
return Object.keys(subset).every((key) => subset[key] === superset[key])
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expect,
|
|
3
|
+
test,
|
|
4
|
+
vi,
|
|
5
|
+
} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { maybeRun } from './maybe-run'
|
|
8
|
+
|
|
9
|
+
test('executes function argument', () => {
|
|
10
|
+
const fn = vi.fn((x: number) => x + 1)
|
|
11
|
+
expect(maybeRun(fn, 2)).toBe(3)
|
|
12
|
+
expect(fn).toHaveBeenCalledWith(2)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('returns value when not a function', () => {
|
|
16
|
+
expect(maybeRun(5)).toBe(5)
|
|
17
|
+
expect(maybeRun(undefined)).toBeUndefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('provides precise inference', () => {
|
|
21
|
+
const fn: () => number = () => 1
|
|
22
|
+
const num = 2
|
|
23
|
+
const input: number | (() => number) = Math.random() > 0.5 ? fn : num
|
|
24
|
+
const result: number = maybeRun(input)
|
|
25
|
+
expect(result).toBeTypeOf('number')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('can prevent unexpected arguments', () => {
|
|
29
|
+
const fn: (num: number) => number = (num) => num + 1
|
|
30
|
+
|
|
31
|
+
// @ts-expect-error: unexpected string argument
|
|
32
|
+
maybeRun(fn, 'string')
|
|
33
|
+
|
|
34
|
+
// @ts-expect-error: unexpected argument count
|
|
35
|
+
maybeRun(fn)
|
|
36
|
+
|
|
37
|
+
// @ts-expect-error: unexpected argument count
|
|
38
|
+
maybeRun(fn, 1, 2)
|
|
39
|
+
})
|