@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.
Files changed (170) hide show
  1. package/dist/editor-KZlceNQ1.d.ts +722 -0
  2. package/dist/editor-KZlceNQ1.d.ts.map +1 -0
  3. package/dist/{editor-DlGlYOp-.js → editor-TvRTsFdO.js} +102 -196
  4. package/dist/editor-TvRTsFdO.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 +5 -8
  8. package/dist/prosekit-core-test.js.map +1 -0
  9. package/dist/prosekit-core.d.ts +797 -792
  10. package/dist/prosekit-core.d.ts.map +1 -0
  11. package/dist/prosekit-core.js +42 -79
  12. package/dist/prosekit-core.js.map +1 -0
  13. package/package.json +14 -12
  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 +125 -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.spec.ts +153 -0
  68. package/src/extensions/plugin.ts +81 -0
  69. package/src/extensions/text.ts +34 -0
  70. package/src/facets/base-extension.ts +54 -0
  71. package/src/facets/command.ts +21 -0
  72. package/src/facets/facet-extension.spec.ts +173 -0
  73. package/src/facets/facet-extension.ts +53 -0
  74. package/src/facets/facet-node.spec.ts +265 -0
  75. package/src/facets/facet-node.ts +185 -0
  76. package/src/facets/facet-types.ts +9 -0
  77. package/src/facets/facet.spec.ts +76 -0
  78. package/src/facets/facet.ts +84 -0
  79. package/src/facets/root.ts +44 -0
  80. package/src/facets/schema-spec.ts +30 -0
  81. package/src/facets/schema.ts +26 -0
  82. package/src/facets/state.spec.ts +53 -0
  83. package/src/facets/state.ts +85 -0
  84. package/src/facets/union-extension.ts +41 -0
  85. package/src/index.ts +302 -0
  86. package/src/test/index.ts +4 -0
  87. package/src/test/test-builder.ts +68 -0
  88. package/src/test/test-editor.spec.ts +104 -0
  89. package/src/test/test-editor.ts +113 -0
  90. package/src/testing/index.ts +283 -0
  91. package/src/testing/keyboard.ts +5 -0
  92. package/src/types/any-function.ts +4 -0
  93. package/src/types/assert-type-equal.ts +8 -0
  94. package/src/types/attrs.ts +32 -0
  95. package/src/types/base-node-view-options.ts +33 -0
  96. package/src/types/dom-node.ts +1 -0
  97. package/src/types/extension-command.ts +52 -0
  98. package/src/types/extension-mark.ts +15 -0
  99. package/src/types/extension-node.ts +15 -0
  100. package/src/types/extension.spec.ts +56 -0
  101. package/src/types/extension.ts +168 -0
  102. package/src/types/model.ts +54 -0
  103. package/src/types/object-entries.ts +13 -0
  104. package/src/types/pick-string-literal.spec.ts +10 -0
  105. package/src/types/pick-string-literal.ts +6 -0
  106. package/src/types/pick-sub-type.spec.ts +20 -0
  107. package/src/types/pick-sub-type.ts +6 -0
  108. package/src/types/priority.ts +12 -0
  109. package/src/types/setter.ts +4 -0
  110. package/src/types/simplify-deeper.spec.ts +40 -0
  111. package/src/types/simplify-deeper.ts +6 -0
  112. package/src/types/simplify-union.spec.ts +21 -0
  113. package/src/types/simplify-union.ts +11 -0
  114. package/src/utils/array-grouping.spec.ts +29 -0
  115. package/src/utils/array-grouping.ts +25 -0
  116. package/src/utils/array.ts +21 -0
  117. package/src/utils/assert.ts +13 -0
  118. package/src/utils/attrs-match.ts +20 -0
  119. package/src/utils/can-use-regex-lookbehind.ts +12 -0
  120. package/src/utils/clsx.spec.ts +14 -0
  121. package/src/utils/clsx.ts +14 -0
  122. package/src/utils/collect-children.ts +21 -0
  123. package/src/utils/collect-nodes.ts +37 -0
  124. package/src/utils/combine-event-handlers.spec.ts +27 -0
  125. package/src/utils/combine-event-handlers.ts +27 -0
  126. package/src/utils/contains-inline-node.ts +17 -0
  127. package/src/utils/deep-equals.spec.ts +26 -0
  128. package/src/utils/deep-equals.ts +29 -0
  129. package/src/utils/default-block-at.ts +15 -0
  130. package/src/utils/editor-content.spec.ts +47 -0
  131. package/src/utils/editor-content.ts +77 -0
  132. package/src/utils/env.ts +6 -0
  133. package/src/utils/find-parent-node-of-type.ts +29 -0
  134. package/src/utils/find-parent-node.spec.ts +68 -0
  135. package/src/utils/find-parent-node.ts +55 -0
  136. package/src/utils/get-custom-selection.ts +19 -0
  137. package/src/utils/get-dom-api.ts +56 -0
  138. package/src/utils/get-id.spec.ts +14 -0
  139. package/src/utils/get-id.ts +13 -0
  140. package/src/utils/get-mark-type.ts +20 -0
  141. package/src/utils/get-node-type.ts +20 -0
  142. package/src/utils/get-node-types.ts +19 -0
  143. package/src/utils/includes-mark.ts +18 -0
  144. package/src/utils/is-at-block-start.ts +26 -0
  145. package/src/utils/is-in-code-block.ts +18 -0
  146. package/src/utils/is-mark-absent.spec.ts +53 -0
  147. package/src/utils/is-mark-absent.ts +42 -0
  148. package/src/utils/is-mark-active.ts +27 -0
  149. package/src/utils/is-node-active.ts +25 -0
  150. package/src/utils/is-subset.spec.ts +12 -0
  151. package/src/utils/is-subset.ts +11 -0
  152. package/src/utils/maybe-run.spec.ts +39 -0
  153. package/src/utils/maybe-run.ts +11 -0
  154. package/src/utils/merge-objects.spec.ts +30 -0
  155. package/src/utils/merge-objects.ts +11 -0
  156. package/src/utils/merge-specs.ts +35 -0
  157. package/src/utils/object-equal.spec.ts +26 -0
  158. package/src/utils/object-equal.ts +28 -0
  159. package/src/utils/output-spec.test.ts +95 -0
  160. package/src/utils/output-spec.ts +130 -0
  161. package/src/utils/parse.spec.ts +46 -0
  162. package/src/utils/parse.ts +321 -0
  163. package/src/utils/remove-undefined-values.spec.ts +15 -0
  164. package/src/utils/remove-undefined-values.ts +9 -0
  165. package/src/utils/set-selection-around.ts +11 -0
  166. package/src/utils/type-assertion.ts +91 -0
  167. package/src/utils/unicode.spec.ts +10 -0
  168. package/src/utils/unicode.ts +4 -0
  169. package/src/utils/with-skip-code-block.ts +15 -0
  170. package/dist/editor-OUH5V8BA.d.ts +0 -754
@@ -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
+ })
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @internal
3
+ */
4
+ export function maybeRun<
5
+ Value,
6
+ Args extends unknown[],
7
+ >(value: Value | ((...args: Args) => Value), ...args: Args): Value {
8
+ return typeof value === 'function'
9
+ ? (value as (...args: Args) => Value)(...args)
10
+ : value
11
+ }
@@ -0,0 +1,30 @@
1
+ import {
2
+ expect,
3
+ test,
4
+ } from 'vitest'
5
+
6
+ import { mergeObjects } from './merge-objects'
7
+
8
+ // basic merge
9
+ test('merge simple objects', () => {
10
+ const a = { a: 1 }
11
+ const b = { b: 2 }
12
+ expect(
13
+ mergeObjects<{ a: number; b: number }>(a, b),
14
+ ).toEqual({ a: 1, b: 2 })
15
+ })
16
+
17
+ // undefined values should be removed and overwritten
18
+ test('skip undefined values and override', () => {
19
+ const a = { a: 1, b: undefined as number | undefined }
20
+ const b = { b: 2 }
21
+ expect(
22
+ mergeObjects<{ a: number; b: number | undefined }>(a, b),
23
+ ).toEqual({ a: 1, b: 2 })
24
+ })
25
+
26
+ // null or undefined arguments should be ignored
27
+ test('ignore null and undefined inputs', () => {
28
+ const a = { a: 1 }
29
+ expect(mergeObjects(null, undefined, a)).toEqual({ a: 1 })
30
+ })
@@ -0,0 +1,11 @@
1
+ import { removeUndefinedValues } from './remove-undefined-values'
2
+ import { isNotNullish } from './type-assertion'
3
+
4
+ export function mergeObjects<T extends object>(
5
+ ...objects: Array<Partial<T> | null | undefined>
6
+ ): Partial<T> {
7
+ const filteredObjects = objects
8
+ .filter(isNotNullish)
9
+ .map(removeUndefinedValues)
10
+ return Object.assign({}, ...filteredObjects) as Partial<T>
11
+ }
@@ -0,0 +1,35 @@
1
+ import type {
2
+ MarkSpec,
3
+ NodeSpec,
4
+ } from '@prosekit/pm/model'
5
+
6
+ import { mergeObjects } from './merge-objects'
7
+
8
+ function mergeSpecs(a: NodeSpec, b: NodeSpec): NodeSpec
9
+ function mergeSpecs(a: MarkSpec, b: MarkSpec): MarkSpec
10
+ function mergeSpecs(
11
+ a: NodeSpec | MarkSpec,
12
+ b: NodeSpec | MarkSpec,
13
+ ): NodeSpec | MarkSpec {
14
+ type T = typeof a
15
+
16
+ const attrs: T['attrs'] = {}
17
+ const attrNames = new Set([
18
+ ...Object.keys(a.attrs ?? {}),
19
+ ...Object.keys(b.attrs ?? {}),
20
+ ])
21
+ for (const name of attrNames) {
22
+ const attrSpecA = a.attrs?.[name]
23
+ const attrSpecB = b.attrs?.[name]
24
+ const attrSpecMerged = mergeObjects(attrSpecA, attrSpecB)
25
+ if (attrSpecMerged) {
26
+ attrs[name] = attrSpecMerged
27
+ }
28
+ }
29
+
30
+ const parseDOM: T['parseDOM'] = [...(a.parseDOM ?? []), ...(b.parseDOM ?? [])]
31
+
32
+ return mergeObjects<T>(a, b, { attrs, parseDOM })
33
+ }
34
+
35
+ export { mergeSpecs }
@@ -0,0 +1,26 @@
1
+ import {
2
+ expect,
3
+ test,
4
+ } from 'vitest'
5
+
6
+ import { objectEqual } from './object-equal'
7
+
8
+ test('objects with same keys and values are equal', () => {
9
+ expect(objectEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true)
10
+ })
11
+
12
+ test('different keys result in inequality', () => {
13
+ expect(objectEqual({ a: 1, b: 2 }, { a: 1, c: 2 })).toBe(false)
14
+ })
15
+
16
+ test('nested objects are compared recursively', () => {
17
+ const obj1 = { a: { c: 3 }, b: 2 }
18
+ const obj2 = { a: { c: 3 }, b: 2 }
19
+ expect(objectEqual(obj1, obj2)).toBe(true)
20
+ })
21
+
22
+ test('nested objects with different values are not equal', () => {
23
+ const obj1 = { a: { c: 3 }, b: 2 }
24
+ const obj2 = { a: { c: 4 }, b: 2 }
25
+ expect(objectEqual(obj1, obj2)).toBe(false)
26
+ })
@@ -0,0 +1,28 @@
1
+ export function objectEqual<T>(a: T, b: T): boolean {
2
+ if (a === b) {
3
+ return true
4
+ }
5
+ if (typeof a !== 'object' || typeof b !== 'object') {
6
+ return false
7
+ }
8
+ if (a === null || b === null) {
9
+ return false
10
+ }
11
+ if (Array.isArray(a) || Array.isArray(b)) {
12
+ return false
13
+ }
14
+ const aKeys = Object.keys(a) as (keyof T)[]
15
+ const bKeys = Object.keys(b) as (keyof T)[]
16
+ if (aKeys.length !== bKeys.length) {
17
+ return false
18
+ }
19
+ for (const key of aKeys) {
20
+ if (!bKeys.includes(key)) {
21
+ return false
22
+ }
23
+ if (!objectEqual(a[key], b[key])) {
24
+ return false
25
+ }
26
+ }
27
+ return true
28
+ }
@@ -0,0 +1,95 @@
1
+ import type { DOMOutputSpec } from '@prosekit/pm/model'
2
+ import {
3
+ describe,
4
+ expect,
5
+ it,
6
+ } from 'vitest'
7
+
8
+ import { insertOutputSpecAttrs } from './output-spec'
9
+
10
+ describe('insertOutputSpecAttrs', () => {
11
+ it('should insert attrs into an array without attributes', () => {
12
+ const spec: DOMOutputSpec = ['input']
13
+ const result = insertOutputSpecAttrs(spec, [
14
+ ['data-foo', 'foo'],
15
+ ['checked', ''],
16
+ ['style', 'background-color: red'],
17
+ ['style', 'color: blue'],
18
+ ])
19
+ expect(result).toMatchInlineSnapshot(`
20
+ [
21
+ "input",
22
+ {
23
+ "checked": "",
24
+ "data-foo": "foo",
25
+ "style": "color: blue; background-color: red",
26
+ },
27
+ ]
28
+ `)
29
+ })
30
+
31
+ it('should insert attrs into an array with attributes', () => {
32
+ const spec: DOMOutputSpec = ['input', { type: 'checkbox' }]
33
+ const result = insertOutputSpecAttrs(spec, [
34
+ ['data-foo', 'foo'],
35
+ ['checked', ''],
36
+ ['style', 'background-color: red'],
37
+ ['style', 'color: blue'],
38
+ ])
39
+ expect(result).toMatchInlineSnapshot(`
40
+ [
41
+ "input",
42
+ {
43
+ "checked": "",
44
+ "data-foo": "foo",
45
+ "style": "color: blue; background-color: red",
46
+ "type": "checkbox",
47
+ },
48
+ ]
49
+ `)
50
+ })
51
+
52
+ it('should insert attrs into an element', () => {
53
+ const element = document.createElement('input')
54
+ element.type = 'checkbox'
55
+
56
+ const spec: DOMOutputSpec = element
57
+ const result = insertOutputSpecAttrs(spec, [
58
+ ['data-foo', 'foo'],
59
+ ['checked', ''],
60
+ ['style', 'background-color: red'],
61
+ ['style', 'color: blue'],
62
+ ])
63
+ expect(result).toMatchInlineSnapshot(`
64
+ <input
65
+ checked=""
66
+ data-foo="foo"
67
+ style="color: blue; background-color: red"
68
+ type="checkbox"
69
+ />
70
+ `)
71
+ })
72
+
73
+ it('should insert attrs into an object ', () => {
74
+ const element = document.createElement('input')
75
+ element.type = 'checkbox'
76
+
77
+ const spec: DOMOutputSpec = { dom: element }
78
+ const result = insertOutputSpecAttrs(spec, [
79
+ ['data-foo', 'foo'],
80
+ ['checked', ''],
81
+ ['style', 'background-color: red'],
82
+ ['style', 'color: blue'],
83
+ ])
84
+ expect(result).toMatchInlineSnapshot(`
85
+ {
86
+ "dom": <input
87
+ checked=""
88
+ data-foo="foo"
89
+ style="color: blue; background-color: red"
90
+ type="checkbox"
91
+ />,
92
+ }
93
+ `)
94
+ })
95
+ })
@@ -0,0 +1,130 @@
1
+ import { isElementLike } from '@ocavue/utils'
2
+ import type {
3
+ DOMOutputSpec,
4
+ Mark,
5
+ ProseMirrorNode,
6
+ TagParseRule,
7
+ } from '@prosekit/pm/model'
8
+
9
+ import { isNotNullish } from './type-assertion'
10
+
11
+ interface AttrOptions {
12
+ attr: string
13
+ toDOM?: (value: unknown) => [key: string, value: string] | null | undefined
14
+ parseDOM?: (node: HTMLElement) => unknown
15
+ }
16
+
17
+ export function wrapOutputSpecAttrs<
18
+ T extends ProseMirrorNode | Mark,
19
+ Args extends readonly unknown[],
20
+ >(
21
+ toDOM: (node: T, ...args: Args) => DOMOutputSpec,
22
+ options: AttrOptions[],
23
+ ): (node: T, ...args: Args) => DOMOutputSpec {
24
+ return (node, ...args) => {
25
+ const dom = toDOM(node, ...args)
26
+ const pairs = options
27
+ .map((option) => option.toDOM?.(node.attrs[option.attr]))
28
+ .filter(isNotNullish)
29
+ return insertOutputSpecAttrs(dom, pairs)
30
+ }
31
+ }
32
+
33
+ export function wrapTagParseRuleAttrs(
34
+ rule: TagParseRule,
35
+ options: AttrOptions[],
36
+ ): TagParseRule {
37
+ const existingGetAttrs = rule.getAttrs
38
+ const existingAttrs = rule.attrs
39
+
40
+ return {
41
+ ...rule,
42
+ getAttrs: (dom) => {
43
+ const baseAttrs = existingGetAttrs?.(dom) ?? existingAttrs ?? {}
44
+
45
+ if (baseAttrs === false || !dom || !isElementLike(dom)) {
46
+ return baseAttrs ?? null
47
+ }
48
+
49
+ const insertedAttrs: Record<string, unknown> = {}
50
+
51
+ for (const option of options) {
52
+ if (option.parseDOM) {
53
+ insertedAttrs[option.attr] = option.parseDOM(dom)
54
+ }
55
+ }
56
+
57
+ return { ...baseAttrs, ...insertedAttrs }
58
+ },
59
+ }
60
+ }
61
+
62
+ export function insertOutputSpecAttrs(
63
+ dom: DOMOutputSpec,
64
+ attrs: Array<[key: string, value: string]>,
65
+ ): DOMOutputSpec {
66
+ if (!dom) {
67
+ return dom
68
+ }
69
+
70
+ if (Array.isArray(dom)) {
71
+ const rest = dom.slice(1) as Array<unknown>
72
+ let oldAttrs: Record<string, unknown>
73
+
74
+ if (rest.length > 0 && (rest[0] == null || typeof rest[0] === 'object')) {
75
+ oldAttrs = rest.shift() as Record<string, unknown>
76
+ } else {
77
+ oldAttrs = {}
78
+ }
79
+
80
+ const newAttrs = setObjectAttributes(oldAttrs, attrs)
81
+ return [dom[0], newAttrs, ...rest]
82
+ }
83
+
84
+ if (isElementLike(dom)) {
85
+ return setElementAttributes(dom, attrs)
86
+ }
87
+
88
+ if (typeof dom === 'object' && 'dom' in dom && isElementLike(dom.dom)) {
89
+ return { ...dom, dom: setElementAttributes(dom.dom, attrs) }
90
+ }
91
+
92
+ return dom
93
+ }
94
+
95
+ function setObjectAttributes(
96
+ obj: Record<string, unknown>,
97
+ attrs: Array<[key: string, value: string]>,
98
+ ): Record<string, unknown> {
99
+ obj = { ...obj }
100
+ for (const [key, value] of attrs) {
101
+ const oldValue = obj[key]
102
+ const newValue = key === 'style'
103
+ ? joinStyles(value, typeof oldValue === 'string' ? oldValue : '')
104
+ : value
105
+ obj[key] = newValue
106
+ }
107
+ return obj
108
+ }
109
+
110
+ function setElementAttributes(
111
+ element: Element,
112
+ attrs: Array<[key: string, value: string]>,
113
+ ): Element {
114
+ element = element.cloneNode(true) as Element
115
+ for (const [key, value] of attrs) {
116
+ const oldValue = element.getAttribute(key)
117
+ const newValue = key === 'style'
118
+ ? joinStyles(value, typeof oldValue === 'string' ? oldValue : '')
119
+ : value
120
+ element.setAttribute(key, newValue)
121
+ }
122
+ return element
123
+ }
124
+
125
+ function joinStyles(...styles: string[]) {
126
+ return styles
127
+ .map((style) => style.trim().replace(/;$/, ''))
128
+ .filter(Boolean)
129
+ .join('; ')
130
+ }
@@ -0,0 +1,46 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ test,
5
+ } from 'vitest'
6
+
7
+ import { createEditor } from '../editor/editor'
8
+ import { defineTestExtension } from '../testing'
9
+
10
+ import {
11
+ elementFromHTML,
12
+ htmlFromNode,
13
+ nodeFromElement,
14
+ nodeFromHTML,
15
+ } from './parse'
16
+
17
+ describe('parse', () => {
18
+ const extension = defineTestExtension()
19
+ const editor = createEditor({ extension })
20
+ const schema = editor.schema
21
+ const n = editor.nodes
22
+
23
+ const element = document.createElement('div')
24
+ element.innerHTML = '<p>hello</p>'
25
+
26
+ const node = n.doc(n.paragraph(schema.text('hello')))
27
+
28
+ const html = '<p>hello</p>'
29
+
30
+ test('nodeFromElement', () => {
31
+ expect(node.eq(nodeFromElement(element, { schema }))).toBe(true)
32
+ })
33
+
34
+ test('nodeFromHTML', () => {
35
+ expect(nodeFromHTML(html, { schema }).eq(node)).toBe(true)
36
+ })
37
+
38
+ test('elementFromHTML', () => {
39
+ expect(elementFromHTML(html).innerHTML).toBe(html)
40
+ })
41
+
42
+ test('htmlFromNode', () => {
43
+ expect(htmlFromNode(node)).toBe('<div><p>hello</p></div>')
44
+ expect(htmlFromNode(node.child(0))).toBe('<p>hello</p>')
45
+ })
46
+ })