@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.
Files changed (168) hide show
  1. package/dist/editor-CfkZ4TNU.d.ts +748 -0
  2. package/dist/editor-CfkZ4TNU.d.ts.map +1 -0
  3. package/dist/{editor-DbMrpnmL.js → editor-CizSwUN8.js} +102 -192
  4. package/dist/editor-CizSwUN8.js.map +1 -0
  5. package/dist/prosekit-core-test.d.ts +20 -19
  6. package/dist/prosekit-core-test.d.ts.map +1 -0
  7. package/dist/prosekit-core-test.js +4 -5
  8. package/dist/prosekit-core-test.js.map +1 -0
  9. package/dist/prosekit-core.d.ts +782 -757
  10. package/dist/prosekit-core.d.ts.map +1 -0
  11. package/dist/prosekit-core.js +30 -45
  12. package/dist/prosekit-core.js.map +1 -0
  13. package/package.json +14 -11
  14. package/src/commands/add-mark.ts +53 -0
  15. package/src/commands/expand-mark.ts +96 -0
  16. package/src/commands/insert-default-block.spec.ts +102 -0
  17. package/src/commands/insert-default-block.ts +49 -0
  18. package/src/commands/insert-node.ts +71 -0
  19. package/src/commands/insert-text.ts +24 -0
  20. package/src/commands/remove-mark.ts +54 -0
  21. package/src/commands/remove-node.ts +43 -0
  22. package/src/commands/select-all.ts +16 -0
  23. package/src/commands/set-block-type.ts +64 -0
  24. package/src/commands/set-node-attrs.ts +68 -0
  25. package/src/commands/toggle-mark.ts +65 -0
  26. package/src/commands/toggle-node.ts +47 -0
  27. package/src/commands/toggle-wrap.spec.ts +35 -0
  28. package/src/commands/toggle-wrap.ts +42 -0
  29. package/src/commands/unset-block-type.spec.ts +49 -0
  30. package/src/commands/unset-block-type.ts +84 -0
  31. package/src/commands/unset-mark.spec.ts +35 -0
  32. package/src/commands/unset-mark.ts +38 -0
  33. package/src/commands/wrap.ts +50 -0
  34. package/src/editor/action.spec.ts +143 -0
  35. package/src/editor/action.ts +248 -0
  36. package/src/editor/editor.spec.ts +186 -0
  37. package/src/editor/editor.ts +563 -0
  38. package/src/editor/union.spec.ts +108 -0
  39. package/src/editor/union.ts +47 -0
  40. package/src/editor/with-priority.ts +25 -0
  41. package/src/error.ts +28 -0
  42. package/src/extensions/clipboard-serializer.ts +107 -0
  43. package/src/extensions/command.ts +121 -0
  44. package/src/extensions/default-state.spec.ts +60 -0
  45. package/src/extensions/default-state.ts +76 -0
  46. package/src/extensions/doc.ts +31 -0
  47. package/src/extensions/events/doc-change.ts +34 -0
  48. package/src/extensions/events/dom-event.spec.ts +70 -0
  49. package/src/extensions/events/dom-event.ts +117 -0
  50. package/src/extensions/events/editor-event.ts +293 -0
  51. package/src/extensions/events/focus.spec.ts +50 -0
  52. package/src/extensions/events/focus.ts +28 -0
  53. package/src/extensions/events/plugin-view.ts +132 -0
  54. package/src/extensions/history.ts +81 -0
  55. package/src/extensions/keymap-base.ts +60 -0
  56. package/src/extensions/keymap.spec.ts +89 -0
  57. package/src/extensions/keymap.ts +96 -0
  58. package/src/extensions/mark-spec.spec.ts +177 -0
  59. package/src/extensions/mark-spec.ts +181 -0
  60. package/src/extensions/mark-view-effect.ts +85 -0
  61. package/src/extensions/mark-view.ts +43 -0
  62. package/src/extensions/node-spec.spec.ts +224 -0
  63. package/src/extensions/node-spec.ts +199 -0
  64. package/src/extensions/node-view-effect.ts +85 -0
  65. package/src/extensions/node-view.ts +43 -0
  66. package/src/extensions/paragraph.ts +61 -0
  67. package/src/extensions/plugin.ts +91 -0
  68. package/src/extensions/text.ts +34 -0
  69. package/src/facets/base-extension.ts +54 -0
  70. package/src/facets/command.ts +21 -0
  71. package/src/facets/facet-extension.spec.ts +173 -0
  72. package/src/facets/facet-extension.ts +53 -0
  73. package/src/facets/facet-node.spec.ts +265 -0
  74. package/src/facets/facet-node.ts +185 -0
  75. package/src/facets/facet-types.ts +9 -0
  76. package/src/facets/facet.spec.ts +76 -0
  77. package/src/facets/facet.ts +84 -0
  78. package/src/facets/root.ts +44 -0
  79. package/src/facets/schema-spec.ts +30 -0
  80. package/src/facets/schema.ts +26 -0
  81. package/src/facets/state.ts +57 -0
  82. package/src/facets/union-extension.ts +41 -0
  83. package/src/index.ts +302 -0
  84. package/src/test/index.ts +4 -0
  85. package/src/test/test-builder.ts +68 -0
  86. package/src/test/test-editor.spec.ts +104 -0
  87. package/src/test/test-editor.ts +113 -0
  88. package/src/testing/index.ts +283 -0
  89. package/src/testing/keyboard.ts +5 -0
  90. package/src/types/any-function.ts +4 -0
  91. package/src/types/assert-type-equal.ts +8 -0
  92. package/src/types/attrs.ts +32 -0
  93. package/src/types/base-node-view-options.ts +33 -0
  94. package/src/types/dom-node.ts +1 -0
  95. package/src/types/extension-command.ts +52 -0
  96. package/src/types/extension-mark.ts +15 -0
  97. package/src/types/extension-node.ts +15 -0
  98. package/src/types/extension.spec.ts +56 -0
  99. package/src/types/extension.ts +168 -0
  100. package/src/types/model.ts +54 -0
  101. package/src/types/object-entries.ts +13 -0
  102. package/src/types/pick-string-literal.spec.ts +10 -0
  103. package/src/types/pick-string-literal.ts +6 -0
  104. package/src/types/pick-sub-type.spec.ts +20 -0
  105. package/src/types/pick-sub-type.ts +6 -0
  106. package/src/types/priority.ts +12 -0
  107. package/src/types/setter.ts +4 -0
  108. package/src/types/simplify-deeper.spec.ts +40 -0
  109. package/src/types/simplify-deeper.ts +6 -0
  110. package/src/types/simplify-union.spec.ts +21 -0
  111. package/src/types/simplify-union.ts +11 -0
  112. package/src/utils/array-grouping.spec.ts +29 -0
  113. package/src/utils/array-grouping.ts +25 -0
  114. package/src/utils/array.ts +21 -0
  115. package/src/utils/assert.ts +13 -0
  116. package/src/utils/attrs-match.ts +20 -0
  117. package/src/utils/can-use-regex-lookbehind.ts +12 -0
  118. package/src/utils/clsx.spec.ts +14 -0
  119. package/src/utils/clsx.ts +12 -0
  120. package/src/utils/collect-children.ts +21 -0
  121. package/src/utils/collect-nodes.ts +37 -0
  122. package/src/utils/combine-event-handlers.spec.ts +27 -0
  123. package/src/utils/combine-event-handlers.ts +27 -0
  124. package/src/utils/contains-inline-node.ts +17 -0
  125. package/src/utils/deep-equals.spec.ts +26 -0
  126. package/src/utils/deep-equals.ts +29 -0
  127. package/src/utils/default-block-at.ts +15 -0
  128. package/src/utils/editor-content.spec.ts +47 -0
  129. package/src/utils/editor-content.ts +77 -0
  130. package/src/utils/env.ts +6 -0
  131. package/src/utils/find-parent-node-of-type.ts +29 -0
  132. package/src/utils/find-parent-node.spec.ts +68 -0
  133. package/src/utils/find-parent-node.ts +55 -0
  134. package/src/utils/get-custom-selection.ts +19 -0
  135. package/src/utils/get-dom-api.ts +56 -0
  136. package/src/utils/get-id.spec.ts +14 -0
  137. package/src/utils/get-id.ts +13 -0
  138. package/src/utils/get-mark-type.ts +20 -0
  139. package/src/utils/get-node-type.ts +20 -0
  140. package/src/utils/get-node-types.ts +19 -0
  141. package/src/utils/includes-mark.ts +18 -0
  142. package/src/utils/is-at-block-start.ts +26 -0
  143. package/src/utils/is-in-code-block.ts +18 -0
  144. package/src/utils/is-mark-absent.spec.ts +53 -0
  145. package/src/utils/is-mark-absent.ts +42 -0
  146. package/src/utils/is-mark-active.ts +27 -0
  147. package/src/utils/is-node-active.ts +25 -0
  148. package/src/utils/is-subset.spec.ts +12 -0
  149. package/src/utils/is-subset.ts +11 -0
  150. package/src/utils/maybe-run.spec.ts +39 -0
  151. package/src/utils/maybe-run.ts +11 -0
  152. package/src/utils/merge-objects.spec.ts +30 -0
  153. package/src/utils/merge-objects.ts +11 -0
  154. package/src/utils/merge-specs.ts +35 -0
  155. package/src/utils/object-equal.spec.ts +26 -0
  156. package/src/utils/object-equal.ts +28 -0
  157. package/src/utils/output-spec.test.ts +95 -0
  158. package/src/utils/output-spec.ts +130 -0
  159. package/src/utils/parse.spec.ts +46 -0
  160. package/src/utils/parse.ts +321 -0
  161. package/src/utils/remove-undefined-values.spec.ts +15 -0
  162. package/src/utils/remove-undefined-values.ts +9 -0
  163. package/src/utils/set-selection-around.ts +11 -0
  164. package/src/utils/type-assertion.ts +91 -0
  165. package/src/utils/unicode.spec.ts +10 -0
  166. package/src/utils/unicode.ts +4 -0
  167. package/src/utils/with-skip-code-block.ts +15 -0
  168. 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
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @internal
3
+ */
4
+ export const isApple: boolean = typeof navigator !== 'undefined'
5
+ ? /Mac|iP(hone|[ao]d)/.test(navigator.platform)
6
+ : false
@@ -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
+ })