@prosekit/core 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor-KZlceNQ1.d.ts +722 -0
- package/dist/editor-KZlceNQ1.d.ts.map +1 -0
- package/dist/{editor-DlGlYOp-.js → editor-TvRTsFdO.js} +102 -196
- package/dist/editor-TvRTsFdO.js.map +1 -0
- package/dist/prosekit-core-test.d.ts +20 -19
- package/dist/prosekit-core-test.d.ts.map +1 -0
- package/dist/prosekit-core-test.js +5 -8
- package/dist/prosekit-core-test.js.map +1 -0
- package/dist/prosekit-core.d.ts +797 -792
- package/dist/prosekit-core.d.ts.map +1 -0
- package/dist/prosekit-core.js +42 -79
- package/dist/prosekit-core.js.map +1 -0
- package/package.json +14 -12
- package/src/commands/add-mark.ts +53 -0
- package/src/commands/expand-mark.ts +96 -0
- package/src/commands/insert-default-block.spec.ts +102 -0
- package/src/commands/insert-default-block.ts +49 -0
- package/src/commands/insert-node.ts +71 -0
- package/src/commands/insert-text.ts +24 -0
- package/src/commands/remove-mark.ts +54 -0
- package/src/commands/remove-node.ts +43 -0
- package/src/commands/select-all.ts +16 -0
- package/src/commands/set-block-type.ts +64 -0
- package/src/commands/set-node-attrs.ts +68 -0
- package/src/commands/toggle-mark.ts +65 -0
- package/src/commands/toggle-node.ts +47 -0
- package/src/commands/toggle-wrap.spec.ts +35 -0
- package/src/commands/toggle-wrap.ts +42 -0
- package/src/commands/unset-block-type.spec.ts +49 -0
- package/src/commands/unset-block-type.ts +84 -0
- package/src/commands/unset-mark.spec.ts +35 -0
- package/src/commands/unset-mark.ts +38 -0
- package/src/commands/wrap.ts +50 -0
- package/src/editor/action.spec.ts +143 -0
- package/src/editor/action.ts +248 -0
- package/src/editor/editor.spec.ts +186 -0
- package/src/editor/editor.ts +563 -0
- package/src/editor/union.spec.ts +108 -0
- package/src/editor/union.ts +47 -0
- package/src/editor/with-priority.ts +25 -0
- package/src/error.ts +28 -0
- package/src/extensions/clipboard-serializer.ts +107 -0
- package/src/extensions/command.ts +121 -0
- package/src/extensions/default-state.spec.ts +60 -0
- package/src/extensions/default-state.ts +76 -0
- package/src/extensions/doc.ts +31 -0
- package/src/extensions/events/doc-change.ts +34 -0
- package/src/extensions/events/dom-event.spec.ts +70 -0
- package/src/extensions/events/dom-event.ts +117 -0
- package/src/extensions/events/editor-event.ts +293 -0
- package/src/extensions/events/focus.spec.ts +50 -0
- package/src/extensions/events/focus.ts +28 -0
- package/src/extensions/events/plugin-view.ts +132 -0
- package/src/extensions/history.ts +81 -0
- package/src/extensions/keymap-base.ts +60 -0
- package/src/extensions/keymap.spec.ts +125 -0
- package/src/extensions/keymap.ts +96 -0
- package/src/extensions/mark-spec.spec.ts +177 -0
- package/src/extensions/mark-spec.ts +181 -0
- package/src/extensions/mark-view-effect.ts +85 -0
- package/src/extensions/mark-view.ts +43 -0
- package/src/extensions/node-spec.spec.ts +224 -0
- package/src/extensions/node-spec.ts +199 -0
- package/src/extensions/node-view-effect.ts +85 -0
- package/src/extensions/node-view.ts +43 -0
- package/src/extensions/paragraph.ts +61 -0
- package/src/extensions/plugin.spec.ts +153 -0
- package/src/extensions/plugin.ts +81 -0
- package/src/extensions/text.ts +34 -0
- package/src/facets/base-extension.ts +54 -0
- package/src/facets/command.ts +21 -0
- package/src/facets/facet-extension.spec.ts +173 -0
- package/src/facets/facet-extension.ts +53 -0
- package/src/facets/facet-node.spec.ts +265 -0
- package/src/facets/facet-node.ts +185 -0
- package/src/facets/facet-types.ts +9 -0
- package/src/facets/facet.spec.ts +76 -0
- package/src/facets/facet.ts +84 -0
- package/src/facets/root.ts +44 -0
- package/src/facets/schema-spec.ts +30 -0
- package/src/facets/schema.ts +26 -0
- package/src/facets/state.spec.ts +53 -0
- package/src/facets/state.ts +85 -0
- package/src/facets/union-extension.ts +41 -0
- package/src/index.ts +302 -0
- package/src/test/index.ts +4 -0
- package/src/test/test-builder.ts +68 -0
- package/src/test/test-editor.spec.ts +104 -0
- package/src/test/test-editor.ts +113 -0
- package/src/testing/index.ts +283 -0
- package/src/testing/keyboard.ts +5 -0
- package/src/types/any-function.ts +4 -0
- package/src/types/assert-type-equal.ts +8 -0
- package/src/types/attrs.ts +32 -0
- package/src/types/base-node-view-options.ts +33 -0
- package/src/types/dom-node.ts +1 -0
- package/src/types/extension-command.ts +52 -0
- package/src/types/extension-mark.ts +15 -0
- package/src/types/extension-node.ts +15 -0
- package/src/types/extension.spec.ts +56 -0
- package/src/types/extension.ts +168 -0
- package/src/types/model.ts +54 -0
- package/src/types/object-entries.ts +13 -0
- package/src/types/pick-string-literal.spec.ts +10 -0
- package/src/types/pick-string-literal.ts +6 -0
- package/src/types/pick-sub-type.spec.ts +20 -0
- package/src/types/pick-sub-type.ts +6 -0
- package/src/types/priority.ts +12 -0
- package/src/types/setter.ts +4 -0
- package/src/types/simplify-deeper.spec.ts +40 -0
- package/src/types/simplify-deeper.ts +6 -0
- package/src/types/simplify-union.spec.ts +21 -0
- package/src/types/simplify-union.ts +11 -0
- package/src/utils/array-grouping.spec.ts +29 -0
- package/src/utils/array-grouping.ts +25 -0
- package/src/utils/array.ts +21 -0
- package/src/utils/assert.ts +13 -0
- package/src/utils/attrs-match.ts +20 -0
- package/src/utils/can-use-regex-lookbehind.ts +12 -0
- package/src/utils/clsx.spec.ts +14 -0
- package/src/utils/clsx.ts +14 -0
- package/src/utils/collect-children.ts +21 -0
- package/src/utils/collect-nodes.ts +37 -0
- package/src/utils/combine-event-handlers.spec.ts +27 -0
- package/src/utils/combine-event-handlers.ts +27 -0
- package/src/utils/contains-inline-node.ts +17 -0
- package/src/utils/deep-equals.spec.ts +26 -0
- package/src/utils/deep-equals.ts +29 -0
- package/src/utils/default-block-at.ts +15 -0
- package/src/utils/editor-content.spec.ts +47 -0
- package/src/utils/editor-content.ts +77 -0
- package/src/utils/env.ts +6 -0
- package/src/utils/find-parent-node-of-type.ts +29 -0
- package/src/utils/find-parent-node.spec.ts +68 -0
- package/src/utils/find-parent-node.ts +55 -0
- package/src/utils/get-custom-selection.ts +19 -0
- package/src/utils/get-dom-api.ts +56 -0
- package/src/utils/get-id.spec.ts +14 -0
- package/src/utils/get-id.ts +13 -0
- package/src/utils/get-mark-type.ts +20 -0
- package/src/utils/get-node-type.ts +20 -0
- package/src/utils/get-node-types.ts +19 -0
- package/src/utils/includes-mark.ts +18 -0
- package/src/utils/is-at-block-start.ts +26 -0
- package/src/utils/is-in-code-block.ts +18 -0
- package/src/utils/is-mark-absent.spec.ts +53 -0
- package/src/utils/is-mark-absent.ts +42 -0
- package/src/utils/is-mark-active.ts +27 -0
- package/src/utils/is-node-active.ts +25 -0
- package/src/utils/is-subset.spec.ts +12 -0
- package/src/utils/is-subset.ts +11 -0
- package/src/utils/maybe-run.spec.ts +39 -0
- package/src/utils/maybe-run.ts +11 -0
- package/src/utils/merge-objects.spec.ts +30 -0
- package/src/utils/merge-objects.ts +11 -0
- package/src/utils/merge-specs.ts +35 -0
- package/src/utils/object-equal.spec.ts +26 -0
- package/src/utils/object-equal.ts +28 -0
- package/src/utils/output-spec.test.ts +95 -0
- package/src/utils/output-spec.ts +130 -0
- package/src/utils/parse.spec.ts +46 -0
- package/src/utils/parse.ts +321 -0
- package/src/utils/remove-undefined-values.spec.ts +15 -0
- package/src/utils/remove-undefined-values.ts +9 -0
- package/src/utils/set-selection-around.ts +11 -0
- package/src/utils/type-assertion.ts +91 -0
- package/src/utils/unicode.spec.ts +10 -0
- package/src/utils/unicode.ts +4 -0
- package/src/utils/with-skip-code-block.ts +15 -0
- package/dist/editor-OUH5V8BA.d.ts +0 -754
|
@@ -0,0 +1,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
|
+
}
|