@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,153 @@
1
+ import {
2
+ Plugin,
3
+ PluginKey,
4
+ } from '@prosekit/pm/state'
5
+ import {
6
+ describe,
7
+ expect,
8
+ it,
9
+ vi,
10
+ } from 'vitest'
11
+
12
+ import { union } from '../editor/union'
13
+ import { withPriority } from '../editor/with-priority'
14
+ import {
15
+ defineDoc,
16
+ defineParagraph,
17
+ defineText,
18
+ setupTestFromExtension,
19
+ } from '../testing'
20
+ import { Priority } from '../types/priority'
21
+
22
+ import { definePlugin } from './plugin'
23
+
24
+ describe('plugin', () => {
25
+ it('maintains plugin order in state based on priority', () => {
26
+ const plugins = {
27
+ a: new Plugin({ key: new PluginKey('a') }),
28
+ b: new Plugin({ key: new PluginKey('b') }),
29
+ c: new Plugin({ key: new PluginKey('c') }),
30
+ d: new Plugin({ key: new PluginKey('d') }),
31
+ e: new Plugin({ key: new PluginKey('e') }),
32
+ }
33
+
34
+ const extension1 = definePlugin(() => [plugins.a, plugins.d]) // default priority
35
+ const extension2 = withPriority(definePlugin(plugins.b), Priority.highest)
36
+ const extension3 = withPriority(definePlugin([plugins.c]), Priority.lowest)
37
+ const extension4 = definePlugin(plugins.e) // default priority
38
+
39
+ const { editor } = setupTestFromExtension(union(
40
+ defineDoc(),
41
+ defineParagraph(),
42
+ defineText(),
43
+ extension1,
44
+ extension2,
45
+ extension3,
46
+ extension4,
47
+ ))
48
+
49
+ const pluginKeys = editor.state.plugins.map((plugin) => {
50
+ for (const [key, value] of Object.entries(plugins)) {
51
+ if (plugin === value) {
52
+ return key
53
+ }
54
+ }
55
+ return undefined
56
+ }).filter(Boolean)
57
+
58
+ // The plugins with the highest priority should be listed
59
+ // first in state. The plugins with the same priority should be listed in
60
+ // the order of the extensions.
61
+ expect(pluginKeys).toEqual([
62
+ // Highest priority
63
+ 'b',
64
+
65
+ // Default priority, added in the last extension
66
+ 'e',
67
+
68
+ // Default priority, added as the second plugin in the first extension
69
+ 'd',
70
+
71
+ // Default priority, added as the first plugin in the first extension
72
+ 'a',
73
+
74
+ // Lowest priority
75
+ 'c',
76
+ ])
77
+ })
78
+
79
+ it('calls handlers in priority order with highest priority first', () => {
80
+ const callOrder: string[] = []
81
+
82
+ const handleKeyDownA = vi.fn(() => {
83
+ callOrder.push('a')
84
+ return false
85
+ })
86
+ const handleKeyDownB = vi.fn(() => {
87
+ callOrder.push('b')
88
+ return false
89
+ })
90
+ const handleKeyDownC = vi.fn(() => {
91
+ callOrder.push('c')
92
+ return false
93
+ })
94
+ const handleKeyDownD = vi.fn(() => {
95
+ callOrder.push('d')
96
+ return false
97
+ })
98
+ const handleKeyDownE = vi.fn(() => {
99
+ callOrder.push('e')
100
+ return false
101
+ })
102
+
103
+ const plugins = {
104
+ a: new Plugin({ props: { handleKeyDown: handleKeyDownA } }),
105
+ b: new Plugin({ props: { handleKeyDown: handleKeyDownB } }),
106
+ c: new Plugin({ props: { handleKeyDown: handleKeyDownC } }),
107
+ d: new Plugin({ props: { handleKeyDown: handleKeyDownD } }),
108
+ e: new Plugin({ props: { handleKeyDown: handleKeyDownE } }),
109
+ }
110
+
111
+ const extension1 = definePlugin(() => [plugins.a, plugins.d]) // default priority
112
+ const extension2 = withPriority(definePlugin(plugins.b), Priority.highest)
113
+ const extension3 = withPriority(definePlugin([plugins.c]), Priority.lowest)
114
+ const extension4 = definePlugin(plugins.e) // default priority
115
+
116
+ const { editor } = setupTestFromExtension(union(
117
+ defineDoc(),
118
+ defineParagraph(),
119
+ defineText(),
120
+ extension1,
121
+ extension2,
122
+ extension3,
123
+ extension4,
124
+ ))
125
+
126
+ editor.view.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
127
+
128
+ // All handlers should be called since none returns true
129
+ expect(handleKeyDownA).toHaveBeenCalledTimes(1)
130
+ expect(handleKeyDownB).toHaveBeenCalledTimes(1)
131
+ expect(handleKeyDownC).toHaveBeenCalledTimes(1)
132
+ expect(handleKeyDownD).toHaveBeenCalledTimes(1)
133
+ expect(handleKeyDownE).toHaveBeenCalledTimes(1)
134
+
135
+ // The event handlers of the plugins with the highest priority should be called first
136
+ expect(callOrder).toEqual([
137
+ // Highest priority
138
+ 'b',
139
+
140
+ // Default priority, added in the last extension
141
+ 'e',
142
+
143
+ // Default priority, added as the second plugin in the first extension
144
+ 'd',
145
+
146
+ // Default priority, added as the first plugin in the first extension
147
+ 'a',
148
+
149
+ // Lowest priority
150
+ 'c',
151
+ ])
152
+ })
153
+ })
@@ -0,0 +1,81 @@
1
+ import type { Schema } from '@prosekit/pm/model'
2
+ import {
3
+ Plugin,
4
+ type ProseMirrorPlugin,
5
+ } from '@prosekit/pm/state'
6
+
7
+ import { ProseKitError } from '../error'
8
+ import {
9
+ defineFacet,
10
+ type Facet,
11
+ } from '../facets/facet'
12
+ import { defineFacetPayload } from '../facets/facet-extension'
13
+ import {
14
+ stateFacet,
15
+ type StatePayload,
16
+ } from '../facets/state'
17
+ import type { PlainExtension } from '../types/extension'
18
+ import { toReversed } from '../utils/array'
19
+
20
+ /**
21
+ * Adds a ProseMirror plugin to the editor.
22
+ *
23
+ * @param plugin - The ProseMirror plugin to add, or an array of plugins, or a
24
+ * function that returns one or multiple plugins.
25
+ *
26
+ * @public
27
+ */
28
+ export function definePlugin(
29
+ plugin:
30
+ | Plugin
31
+ | Plugin[]
32
+ | ((context: { schema: Schema }) => Plugin | Plugin[]),
33
+ ): PlainExtension {
34
+ return definePluginPayload(plugin)
35
+ }
36
+
37
+ function definePluginPayload(payload: PluginPayload): PlainExtension {
38
+ return defineFacetPayload(pluginFacet, [payload]) as PlainExtension
39
+ }
40
+
41
+ /**
42
+ * @internal
43
+ */
44
+ export type PluginPayload =
45
+ | Plugin
46
+ | Plugin[]
47
+ | ((context: { schema: Schema }) => Plugin | Plugin[])
48
+
49
+ /**
50
+ * @internal
51
+ */
52
+ export const pluginFacet: Facet<PluginPayload, StatePayload> = defineFacet({
53
+ reducer: (payloads): StatePayload => {
54
+ return ({ schema }) => {
55
+ // An array of plugins from lower to higher priority.
56
+ const plugins: ProseMirrorPlugin[] = []
57
+
58
+ for (const payload of payloads) {
59
+ if (payload instanceof Plugin) {
60
+ plugins.push(payload)
61
+ } else if (
62
+ Array.isArray(payload)
63
+ && payload.every((p) => p instanceof Plugin)
64
+ ) {
65
+ plugins.push(...payload)
66
+ } else if (typeof payload === 'function') {
67
+ plugins.push(...[payload({ schema })].flat())
68
+ } else {
69
+ throw new ProseKitError('Invalid plugin')
70
+ }
71
+ }
72
+
73
+ // An array of plugins from higher to lower priority. This matches the
74
+ // order of plugins required by ProseMirror.
75
+ const reversedPlugins = toReversed(plugins)
76
+
77
+ return { plugins: reversedPlugins }
78
+ }
79
+ },
80
+ parent: stateFacet,
81
+ })
@@ -0,0 +1,34 @@
1
+ import type { Attrs } from '@prosekit/pm/model'
2
+
3
+ import type { Extension } from '../types/extension'
4
+
5
+ import { defineNodeSpec } from './node-spec'
6
+
7
+ /**
8
+ * @internal
9
+ */
10
+ export type TextExtension = Extension<{
11
+ Nodes: {
12
+ text: Attrs
13
+ }
14
+ }>
15
+
16
+ /**
17
+ * @public
18
+ *
19
+ * @deprecated Use the following import instead:
20
+ *
21
+ * ```ts
22
+ * import { defineText } from 'prosekit/extensions/text'
23
+ * ```
24
+ */
25
+ export function defineText(): TextExtension {
26
+ console.warn(
27
+ '[prosekit] The `defineText` function from `prosekit/core` is deprecated. Use the following import instead: `import { defineText } from "prosekit/extensions/text"`.',
28
+ )
29
+
30
+ return defineNodeSpec({
31
+ name: 'text',
32
+ group: 'inline',
33
+ })
34
+ }
@@ -0,0 +1,54 @@
1
+ import type { Schema } from '@prosekit/pm/model'
2
+
3
+ import type {
4
+ Extension,
5
+ ExtensionTyping,
6
+ } from '../types/extension'
7
+ import { Priority } from '../types/priority'
8
+
9
+ import type { Facet } from './facet'
10
+ import type { FacetNode } from './facet-node'
11
+ import type { Tuple5 } from './facet-types'
12
+ import { schemaFacet } from './schema'
13
+
14
+ /**
15
+ * @internal
16
+ */
17
+ export abstract class BaseExtension<T extends ExtensionTyping = ExtensionTyping> implements Extension<T> {
18
+ abstract extension: Extension | Extension[]
19
+ priority?: Priority
20
+ _type?: T
21
+
22
+ private trees: Tuple5<FacetNode | null> = [null, null, null, null, null]
23
+
24
+ /**
25
+ * @internal
26
+ */
27
+ abstract createTree(priority: Priority): FacetNode
28
+
29
+ /**
30
+ * @internal
31
+ */
32
+ getTree(priority?: Priority): FacetNode {
33
+ const pri = priority ?? this.priority ?? Priority.default
34
+ return (this.trees[pri] ||= this.createTree(pri))
35
+ }
36
+
37
+ /**
38
+ * @internal
39
+ */
40
+ findFacetOutput<I, O>(facet: Facet<I, O>): Tuple5<O | null> | null {
41
+ let node: FacetNode | undefined = this.getTree()
42
+
43
+ for (const index of facet.path) {
44
+ node = node?.children.get(index)
45
+ }
46
+
47
+ return node?.getOutput() ?? null
48
+ }
49
+
50
+ get schema(): Schema | null {
51
+ const output = this.findFacetOutput(schemaFacet)
52
+ return output?.find(Boolean)?.schema ?? null
53
+ }
54
+ }
@@ -0,0 +1,21 @@
1
+ import type { CommandCreators } from '../types/extension-command'
2
+
3
+ import {
4
+ defineFacet,
5
+ type Facet,
6
+ } from './facet'
7
+ import {
8
+ rootFacet,
9
+ type RootPayload,
10
+ } from './root'
11
+
12
+ type CommandPayload = CommandCreators
13
+
14
+ export const commandFacet: Facet<CommandPayload, RootPayload> = defineFacet({
15
+ reducer: (inputs) => {
16
+ const commands = Object.assign({}, ...inputs) as CommandPayload
17
+ return { commands }
18
+ },
19
+ parent: rootFacet,
20
+ singleton: true,
21
+ })
@@ -0,0 +1,173 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ vi,
6
+ } from 'vitest'
7
+
8
+ import { Priority } from '../types/priority'
9
+ import { isNotNullish } from '../utils/type-assertion'
10
+
11
+ import {
12
+ defineFacet,
13
+ Facet,
14
+ } from './facet'
15
+ import { FacetExtensionImpl } from './facet-extension'
16
+ import {
17
+ subtractFacetNode,
18
+ unionFacetNode,
19
+ } from './facet-node'
20
+ import type { FacetReducer } from './facet-types'
21
+ import { UnionExtensionImpl } from './union-extension'
22
+
23
+ describe('facet extension', () => {
24
+ type FooHandler = (foo: string) => void
25
+ type BarHandler = (bar: string) => void
26
+
27
+ interface RootInput {
28
+ onFoo?: FooHandler
29
+ onBar?: BarHandler
30
+ }
31
+
32
+ interface RootOutput {
33
+ fooHandlers: FooHandler[]
34
+ barHandlers: BarHandler[]
35
+ }
36
+
37
+ type FooInput = FooHandler
38
+ type BarInput = BarHandler
39
+
40
+ const rootReducerImpl: FacetReducer<RootInput, RootOutput> = (input) => {
41
+ const fooHandlers = input.map((i) => i.onFoo).filter(isNotNullish)
42
+ const barHandlers = input.map((i) => i.onBar).filter(isNotNullish)
43
+ return { fooHandlers, barHandlers }
44
+ }
45
+ const rootReducer = vi.fn(rootReducerImpl)
46
+ const rootFacet = new Facet(null, true, rootReducer)
47
+
48
+ // Foo facet uses `reduce` closure to return the same `onFoo` function every time.
49
+ const fooReduce: () => FacetReducer<FooInput, RootInput> = () => {
50
+ let fooHandlers: FooHandler[] | undefined
51
+
52
+ const onFoo = (value: string): void => {
53
+ fooHandlers?.forEach((handler) => handler(value))
54
+ }
55
+
56
+ return (input) => {
57
+ fooHandlers = input
58
+ return { onFoo }
59
+ }
60
+ }
61
+ const fooFacet = defineFacet<FooInput, RootInput>({
62
+ parent: rootFacet,
63
+ reduce: fooReduce,
64
+ })
65
+
66
+ // Bar facet uses `reducer` directly thus the `onBar` function is a different instance every time.
67
+ const barReducer: FacetReducer<BarInput, RootInput> = (input) => {
68
+ return {
69
+ onBar: (value: string) => {
70
+ input.forEach((handler) => handler(value))
71
+ },
72
+ }
73
+ }
74
+ const barFacet = defineFacet<BarInput, RootInput>({
75
+ parent: rootFacet,
76
+ reducer: barReducer,
77
+ })
78
+
79
+ it('can merge payloads', () => {
80
+ const fooHandler1: FooHandler = vi.fn()
81
+ const fooExtension1 = new FacetExtensionImpl(fooFacet, [fooHandler1])
82
+
83
+ const fooHandler2: FooHandler = vi.fn()
84
+ const fooExtension2 = new FacetExtensionImpl(fooFacet, [fooHandler2])
85
+
86
+ const barHandler1: BarHandler = vi.fn()
87
+ const barExtension1 = new FacetExtensionImpl(barFacet, [barHandler1])
88
+
89
+ const extension1 = new UnionExtensionImpl([
90
+ fooExtension1,
91
+ fooExtension2,
92
+ barExtension1,
93
+ ])
94
+
95
+ const tree = extension1.createTree(Priority.default)
96
+ const rootOutput = tree.getSingletonOutput() as RootOutput
97
+ expect(rootOutput.fooHandlers).toHaveLength(1)
98
+ expect(rootOutput.barHandlers).toHaveLength(1)
99
+
100
+ expect(fooHandler1).toHaveBeenCalledTimes(0)
101
+ expect(fooHandler2).toHaveBeenCalledTimes(0)
102
+ expect(barHandler1).toHaveBeenCalledTimes(0)
103
+
104
+ rootOutput.fooHandlers.forEach((handler) => handler('a'))
105
+ expect(fooHandler1).toHaveBeenCalledWith('a')
106
+ expect(fooHandler1).toHaveBeenCalledTimes(1)
107
+ expect(fooHandler2).toHaveBeenCalledWith('a')
108
+ expect(fooHandler2).toHaveBeenCalledTimes(1)
109
+
110
+ rootOutput.barHandlers.forEach((handler) => handler('b'))
111
+ expect(barHandler1).toHaveBeenCalledWith('b')
112
+ expect(barHandler1).toHaveBeenCalledTimes(1)
113
+ })
114
+
115
+ it('can skip unnecessary update', () => {
116
+ const fooHandler1: FooHandler = vi.fn()
117
+ const fooExtension1 = new FacetExtensionImpl(fooFacet, [fooHandler1])
118
+
119
+ const fooHandler2: FooHandler = vi.fn()
120
+ const fooExtension2 = new FacetExtensionImpl(fooFacet, [fooHandler2])
121
+
122
+ const barHandler1: BarHandler = vi.fn()
123
+ const barExtension1 = new FacetExtensionImpl(barFacet, [barHandler1])
124
+
125
+ const barHandler2: BarHandler = vi.fn()
126
+ const barExtension2 = new FacetExtensionImpl(barFacet, [barHandler2])
127
+
128
+ // Initial the root output with fooExtension1 and barExtension1.
129
+ const rootExtension = new UnionExtensionImpl([fooExtension1, barExtension1])
130
+ let tree = rootExtension.createTree(Priority.default)
131
+ let rootOutput = tree.getSingletonOutput() as RootOutput
132
+
133
+ // Save the initial root output.
134
+ const rootFooHandlers1 = [...rootOutput.fooHandlers]
135
+ const rootBarHandlers1 = [...rootOutput.barHandlers]
136
+
137
+ // Add fooExtension2.
138
+ // This should not trigger any updates to the root output.
139
+ tree = unionFacetNode(tree, fooExtension2.getTree())
140
+ rootOutput = tree.getSingletonOutput() as RootOutput
141
+ const rootFooHandlers2 = [...rootOutput.fooHandlers]
142
+ const rootBarHandlers2 = [...rootOutput.barHandlers]
143
+ expect(rootFooHandlers2).toEqual(rootFooHandlers1)
144
+ expect(rootBarHandlers2).toEqual(rootBarHandlers1)
145
+
146
+ // Add barExtension2.
147
+ // This should change rootOutput.barHandlers
148
+ tree = unionFacetNode(tree, barExtension2.getTree())
149
+ rootOutput = tree.getSingletonOutput() as RootOutput
150
+ const rootFooHandlers3 = [...rootOutput.fooHandlers]
151
+ const rootBarHandlers3 = [...rootOutput.barHandlers]
152
+ expect(rootFooHandlers3).toEqual(rootFooHandlers2)
153
+ expect(rootBarHandlers3).not.toEqual(rootBarHandlers2)
154
+
155
+ // Remove fooExtension1
156
+ // This should not trigger any updates to the root output.
157
+ tree = subtractFacetNode(tree, fooExtension1.getTree())
158
+ rootOutput = tree.getSingletonOutput() as RootOutput
159
+ const rootFooHandlers4 = [...rootOutput.fooHandlers]
160
+ const rootBarHandlers4 = [...rootOutput.barHandlers]
161
+ expect(rootFooHandlers4).toEqual(rootFooHandlers3)
162
+ expect(rootBarHandlers4).toEqual(rootBarHandlers3)
163
+
164
+ // Remove barExtension2
165
+ // This should change rootOutput.barHandlers
166
+ tree = subtractFacetNode(tree, barExtension2.getTree())
167
+ rootOutput = tree.getSingletonOutput() as RootOutput
168
+ const rootFooHandlers5 = [...rootOutput.fooHandlers]
169
+ const rootBarHandlers5 = [...rootOutput.barHandlers]
170
+ expect(rootFooHandlers5).toEqual(rootFooHandlers4)
171
+ expect(rootBarHandlers5).not.toEqual(rootBarHandlers4)
172
+ })
173
+ })
@@ -0,0 +1,53 @@
1
+ import type { Extension } from '../types/extension'
2
+ import type { Priority } from '../types/priority'
3
+
4
+ import { BaseExtension } from './base-extension'
5
+ import type { Facet } from './facet'
6
+ import { FacetNode } from './facet-node'
7
+ import type { Tuple5 } from './facet-types'
8
+
9
+ /**
10
+ * @internal
11
+ */
12
+ export class FacetExtensionImpl<Input, Output> extends BaseExtension {
13
+ declare extension: Extension
14
+
15
+ /**
16
+ * @internal
17
+ */
18
+ constructor(
19
+ readonly facet: Facet<Input, Output>,
20
+ readonly payloads: Input[],
21
+ ) {
22
+ super()
23
+ }
24
+
25
+ /**
26
+ * @internal
27
+ */
28
+ createTree(priority: Priority): FacetNode {
29
+ const pri = this.priority ?? priority
30
+
31
+ const inputs: Tuple5<Input[] | null> = [null, null, null, null, null]
32
+ inputs[pri] = [...this.payloads]
33
+
34
+ let node: FacetNode = new FacetNode(this.facet, inputs)
35
+
36
+ while (node.facet.parent) {
37
+ const children = new Map([[node.facet.index, node]])
38
+ node = new FacetNode(node.facet.parent, undefined, children)
39
+ }
40
+
41
+ return node
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @internal
47
+ */
48
+ export function defineFacetPayload<Input>(
49
+ facet: Facet<Input, any>,
50
+ payloads: Input[],
51
+ ): Extension {
52
+ return new FacetExtensionImpl(facet, payloads)
53
+ }