@lexical/html 0.44.1-nightly.20260518.0 → 0.45.0

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 (66) hide show
  1. package/{DOMRenderExtension.d.ts → dist/DOMRenderExtension.d.ts} +12 -1
  2. package/dist/DOMRenderRuntime.d.ts +51 -0
  3. package/dist/LexicalHtml.dev.js +3192 -0
  4. package/dist/LexicalHtml.dev.mjs +3146 -0
  5. package/{LexicalHtml.js.flow → dist/LexicalHtml.js.flow} +16 -16
  6. package/dist/LexicalHtml.mjs +56 -0
  7. package/dist/LexicalHtml.node.mjs +54 -0
  8. package/dist/LexicalHtml.prod.js +9 -0
  9. package/dist/LexicalHtml.prod.mjs +9 -0
  10. package/dist/RenderContext.d.ts +68 -0
  11. package/{compileDOMRenderConfigOverrides.d.ts → dist/compileDOMRenderConfigOverrides.d.ts} +1 -1
  12. package/{constants.d.ts → dist/constants.d.ts} +2 -0
  13. package/dist/domOverride.d.ts +23 -0
  14. package/dist/import/CoreImportExtension.d.ts +11 -0
  15. package/dist/import/DOMImportExtension.d.ts +82 -0
  16. package/dist/import/HorizontalRuleImportExtension.d.ts +27 -0
  17. package/dist/import/ImportContext.d.ts +208 -0
  18. package/dist/import/compileImportRules.d.ts +50 -0
  19. package/dist/import/coreImportRules.d.ts +25 -0
  20. package/dist/import/defineImportRule.d.ts +32 -0
  21. package/dist/import/defineOverlayRules.d.ts +66 -0
  22. package/dist/import/index.d.ts +38 -0
  23. package/dist/import/inlineStylesFromStyleSheets.d.ts +28 -0
  24. package/dist/import/parseCss.d.ts +18 -0
  25. package/dist/import/runImport.d.ts +19 -0
  26. package/dist/import/schemas.d.ts +91 -0
  27. package/dist/import/sel.d.ts +74 -0
  28. package/dist/import/types.d.ts +394 -0
  29. package/dist/index.d.ts +44 -0
  30. package/{types.d.ts → dist/types.d.ts} +96 -8
  31. package/package.json +33 -18
  32. package/src/ContextRecord.ts +243 -0
  33. package/src/DOMRenderExtension.ts +96 -0
  34. package/src/DOMRenderRuntime.ts +265 -0
  35. package/src/RenderContext.ts +168 -0
  36. package/src/compileDOMRenderConfigOverrides.ts +416 -0
  37. package/src/constants.ts +18 -0
  38. package/src/domOverride.ts +46 -0
  39. package/src/import/CoreImportExtension.ts +26 -0
  40. package/src/import/DOMImportExtension.ts +221 -0
  41. package/src/import/HorizontalRuleImportExtension.ts +53 -0
  42. package/src/import/ImportContext.ts +339 -0
  43. package/src/import/compileImportRules.ts +178 -0
  44. package/src/import/coreImportRules.ts +485 -0
  45. package/src/import/defineImportRule.ts +40 -0
  46. package/src/import/defineOverlayRules.ts +105 -0
  47. package/src/import/index.ts +96 -0
  48. package/src/import/inlineStylesFromStyleSheets.ts +104 -0
  49. package/src/import/parseCss.ts +219 -0
  50. package/src/import/runImport.ts +245 -0
  51. package/src/import/schemas.ts +236 -0
  52. package/src/import/sel.ts +314 -0
  53. package/src/import/types.ts +471 -0
  54. package/src/index.ts +555 -0
  55. package/src/types.ts +470 -0
  56. package/LexicalHtml.dev.js +0 -914
  57. package/LexicalHtml.dev.mjs +0 -900
  58. package/LexicalHtml.mjs +0 -24
  59. package/LexicalHtml.node.mjs +0 -22
  60. package/LexicalHtml.prod.js +0 -9
  61. package/LexicalHtml.prod.mjs +0 -9
  62. package/RenderContext.d.ts +0 -32
  63. package/domOverride.d.ts +0 -18
  64. package/index.d.ts +0 -32
  65. /package/{ContextRecord.d.ts → dist/ContextRecord.d.ts} +0 -0
  66. /package/{LexicalHtml.js → dist/LexicalHtml.js} +0 -0
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import type {
9
+ AnyContextConfigPairOrUpdater,
10
+ AnyContextSymbol,
11
+ ContextConfig,
12
+ ContextConfigPair,
13
+ ContextConfigUpdater,
14
+ ContextRecord,
15
+ } from './types';
16
+
17
+ import {$getEditor, createState, type LexicalEditor} from 'lexical';
18
+
19
+ let activeContext: undefined | EditorContext;
20
+
21
+ type WithContext<Ctx extends AnyContextSymbol> = {
22
+ [K in Ctx]?: undefined | ContextRecord<Ctx>;
23
+ };
24
+
25
+ /**
26
+ * @experimental
27
+ *
28
+ * The LexicalEditor with context
29
+ */
30
+ export type EditorContext = {
31
+ editor: LexicalEditor;
32
+ } & WithContext<AnyContextSymbol>;
33
+
34
+ /**
35
+ * @experimental
36
+ *
37
+ * @param contextRecord The ContextRecord
38
+ * @param cfg The configuration
39
+ * @returns The value or defaultValue of cfg
40
+ */
41
+ export function getContextValue<Ctx extends AnyContextSymbol, V>(
42
+ contextRecord: undefined | ContextRecord<Ctx>,
43
+ cfg: ContextConfig<Ctx, V>,
44
+ ): V {
45
+ const {key} = cfg;
46
+ return contextRecord && key in contextRecord
47
+ ? (contextRecord[key] as V)
48
+ : cfg.defaultValue;
49
+ }
50
+
51
+ /**
52
+ * @experimental
53
+ *
54
+ * Read and delete cfg from this layer of context
55
+ *
56
+ * @param contextRecord The ContextRecord
57
+ * @param cfg The configuration
58
+ * @returns The value of the configuration that was removed
59
+ */
60
+ export function popOwnContextValue<Ctx extends AnyContextSymbol, V>(
61
+ contextRecord: ContextRecord<Ctx>,
62
+ cfg: ContextConfig<Ctx, V>,
63
+ ): undefined | V {
64
+ const rval = getOwnContextValue(contextRecord, cfg);
65
+ delete contextRecord[cfg.key];
66
+ return rval;
67
+ }
68
+
69
+ /**
70
+ * @experimental
71
+ *
72
+ * Get the value without a default
73
+ *
74
+ * @param contextRecord The ContextRecord
75
+ * @param cfg The configuration
76
+ * @returns The current value in this context or `undefined` if not set
77
+ */
78
+ export function getOwnContextValue<Ctx extends AnyContextSymbol, V>(
79
+ contextRecord: ContextRecord<Ctx>,
80
+ cfg: ContextConfig<Ctx, V>,
81
+ ): undefined | V {
82
+ const {key} = cfg;
83
+ return key in contextRecord ? (contextRecord[key] as V) : undefined;
84
+ }
85
+
86
+ function getEditorContext(editor: LexicalEditor): undefined | EditorContext {
87
+ return activeContext && activeContext.editor === editor
88
+ ? activeContext
89
+ : undefined;
90
+ }
91
+
92
+ /**
93
+ * @experimental
94
+ *
95
+ * @param sym The symbol for this ContextRecord (e.g. DOMRenderContextSymbol)
96
+ * @param editor The editor
97
+ * @returns The current context or undefined
98
+ */
99
+ export function getContextRecord<Ctx extends AnyContextSymbol>(
100
+ sym: Ctx,
101
+ editor: LexicalEditor,
102
+ ): undefined | ContextRecord<Ctx> {
103
+ const editorContext = getEditorContext(editor);
104
+ return editorContext && editorContext[sym];
105
+ }
106
+
107
+ function toPair<Ctx extends AnyContextSymbol, V>(
108
+ contextRecord: undefined | ContextRecord<Ctx>,
109
+ pairOrUpdater: ContextConfigPair<Ctx, V> | ContextConfigUpdater<Ctx, V>,
110
+ ): ContextConfigPair<Ctx, V> {
111
+ if ('cfg' in pairOrUpdater) {
112
+ const {cfg, updater} = pairOrUpdater;
113
+ return [cfg, updater(getContextValue(contextRecord, cfg))];
114
+ }
115
+ return pairOrUpdater;
116
+ }
117
+
118
+ /**
119
+ * Construct a new context from a parent context and pairs
120
+ *
121
+ * @param pairs The pairs and updaters to build the context from
122
+ * @param parent The parent context
123
+ * @returns The new context
124
+ */
125
+ export function contextFromPairs<Ctx extends AnyContextSymbol>(
126
+ pairs: readonly AnyContextConfigPairOrUpdater<Ctx>[],
127
+ parent: undefined | ContextRecord<Ctx>,
128
+ ): undefined | ContextRecord<Ctx> {
129
+ let rval = parent;
130
+ for (const pairOrUpdater of pairs) {
131
+ const [k, v] = toPair(rval, pairOrUpdater);
132
+ const key = k.key;
133
+ if (rval === parent && getContextValue(rval, k) === v) {
134
+ continue;
135
+ }
136
+ // If we haven't branched away from `parent` yet, create a fresh child
137
+ // context so we never mutate the caller's parent record. Subsequent
138
+ // pairs in this loop accumulate into the same child. Inside the loop
139
+ // `rval` is non-null after the first iteration, since createChildContext
140
+ // never returns null/undefined.
141
+ const ctx: ContextRecord<Ctx> =
142
+ rval === parent || rval === undefined ? createChildContext(parent) : rval;
143
+ ctx[key] = v;
144
+ rval = ctx;
145
+ }
146
+ return rval;
147
+ }
148
+
149
+ function createChildContext<Ctx extends AnyContextSymbol>(
150
+ parent: undefined | ContextRecord<Ctx>,
151
+ ): ContextRecord<Ctx> {
152
+ return Object.create(parent || null);
153
+ }
154
+
155
+ /**
156
+ * Create a context config pair that sets a value in the render context.
157
+ * @experimental
158
+ */
159
+ export function contextValue<Ctx extends AnyContextSymbol, V>(
160
+ cfg: ContextConfig<Ctx, V>,
161
+ value: V,
162
+ ): ContextConfigPair<Ctx, V> {
163
+ return [cfg, value];
164
+ }
165
+
166
+ /**
167
+ * Create a context config updater that transforms a value in the render context.
168
+ * @experimental
169
+ */
170
+ export function contextUpdater<Ctx extends AnyContextSymbol, V>(
171
+ cfg: ContextConfig<Ctx, V>,
172
+ updater: (prev: V) => V,
173
+ ): ContextConfigUpdater<Ctx, V> {
174
+ return {cfg, updater};
175
+ }
176
+
177
+ /**
178
+ * @internal
179
+ * @experimental
180
+ * @__NO_SIDE_EFFECTS__
181
+ */
182
+ export function $withFullContext<Ctx extends AnyContextSymbol, T>(
183
+ sym: Ctx,
184
+ contextRecord: ContextRecord<Ctx>,
185
+ f: () => T,
186
+ editor: LexicalEditor = $getEditor(),
187
+ ): T {
188
+ const prevDOMContext = activeContext;
189
+ const parentEditorContext = getEditorContext(editor);
190
+ try {
191
+ activeContext = {...parentEditorContext, editor, [sym]: contextRecord};
192
+ return f();
193
+ } finally {
194
+ activeContext = prevDOMContext;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * @internal
200
+ * @experimental
201
+ * @__NO_SIDE_EFFECTS__
202
+ */
203
+ export function $withContext<Ctx extends AnyContextSymbol>(
204
+ sym: Ctx,
205
+ $defaults: (editor: LexicalEditor) => undefined | ContextRecord<Ctx> = () =>
206
+ undefined,
207
+ ) {
208
+ return (
209
+ cfg: readonly AnyContextConfigPairOrUpdater<Ctx>[],
210
+ editor = $getEditor(),
211
+ ): (<T>(f: () => T) => T) => {
212
+ return f => {
213
+ const parentEditorContext = getEditorContext(editor);
214
+ const parentContextRecord =
215
+ parentEditorContext && parentEditorContext[sym];
216
+ const contextRecord = contextFromPairs(
217
+ cfg,
218
+ parentContextRecord || $defaults(editor),
219
+ );
220
+ if (!contextRecord || contextRecord === parentContextRecord) {
221
+ return f();
222
+ }
223
+ return $withFullContext(sym, contextRecord, f, editor);
224
+ };
225
+ };
226
+ }
227
+
228
+ /**
229
+ * @experimental
230
+ * @internal
231
+ * @__NO_SIDE_EFFECTS__
232
+ */
233
+ export function createContextState<Tag extends symbol, V>(
234
+ tag: Tag,
235
+ name: string,
236
+ getDefaultValue: () => V,
237
+ isEqual?: (a: V, b: V) => boolean,
238
+ ): ContextConfig<Tag, V> {
239
+ return Object.assign(
240
+ createState(Symbol(name), {isEqual, parse: getDefaultValue}),
241
+ {[tag]: true} as const,
242
+ );
243
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import type {DOMRenderConfig, DOMRenderExtensionOutput} from './types';
9
+ import type {InitialEditorConfig} from 'lexical';
10
+
11
+ import {defineExtension, RootNode, shallowMergeConfig} from 'lexical';
12
+
13
+ import {compileDOMRenderConfigOverrides} from './compileDOMRenderConfigOverrides';
14
+ import {DOMRenderExtensionName} from './constants';
15
+ import {
16
+ createEditorContextRecord,
17
+ DOMRenderRuntimeImpl,
18
+ filterEditorInstalled,
19
+ } from './DOMRenderRuntime';
20
+
21
+ /** @internal The result returned from {@link DOMRenderExtension}'s `init`. */
22
+ interface DOMRenderInitResult {
23
+ /**
24
+ * The `nodes` and base `dom` captured from the editor config before `dom`
25
+ * is overwritten with the compiled config — the only fields the runtime
26
+ * needs to recompile.
27
+ */
28
+ initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>;
29
+ }
30
+
31
+ /**
32
+ * @experimental
33
+ *
34
+ * An extension that allows overriding the render and export behavior for an
35
+ * editor. This is highly experimental and subject to change from one version
36
+ * to the next.
37
+ **/
38
+ export const DOMRenderExtension = defineExtension<
39
+ DOMRenderConfig,
40
+ typeof DOMRenderExtensionName,
41
+ DOMRenderExtensionOutput,
42
+ DOMRenderInitResult
43
+ >({
44
+ build(editor, config, state) {
45
+ const {initialEditorConfig} = state.getInitResult();
46
+ const editorContext = createEditorContextRecord(config.contextDefaults);
47
+ const runtime = new DOMRenderRuntimeImpl(
48
+ editor,
49
+ initialEditorConfig,
50
+ config.overrides,
51
+ editorContext,
52
+ );
53
+ return {defaults: editorContext, runtime};
54
+ },
55
+ config: {
56
+ contextDefaults: [],
57
+ overrides: [],
58
+ },
59
+ html: {
60
+ // Define a RootNode export for $generateDOMFromRoot
61
+ export: new Map([
62
+ [
63
+ RootNode,
64
+ () => {
65
+ const element = document.createElement('div');
66
+ element.role = 'textbox';
67
+ return {element};
68
+ },
69
+ ],
70
+ ]),
71
+ },
72
+ init(editorConfig, config) {
73
+ // Capture the user's base `dom` (before we overwrite it) and `nodes` so the
74
+ // runtime can recompile from scratch when overrides toggle.
75
+ const initialEditorConfig: DOMRenderInitResult['initialEditorConfig'] = {
76
+ dom: editorConfig.dom,
77
+ nodes: editorConfig.nodes,
78
+ };
79
+ const editorContext = createEditorContextRecord(config.contextDefaults);
80
+ const installed = filterEditorInstalled(config.overrides, editorContext);
81
+ editorConfig.dom = compileDOMRenderConfigOverrides(editorConfig, {
82
+ overrides: installed,
83
+ });
84
+ return {initialEditorConfig};
85
+ },
86
+ mergeConfig(config, partial) {
87
+ const merged = shallowMergeConfig(config, partial);
88
+ for (const k of ['overrides', 'contextDefaults'] as const) {
89
+ if (partial[k]) {
90
+ (merged[k] as unknown[]) = [...config[k], ...partial[k]];
91
+ }
92
+ }
93
+ return merged;
94
+ },
95
+ name: DOMRenderExtensionName,
96
+ });
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+ import type {
9
+ AnyDOMRenderMatch,
10
+ AnyRenderStateConfigPairOrUpdater,
11
+ ContextRecord,
12
+ DOMRenderRuntime,
13
+ RenderContextReader,
14
+ RenderStateConfig,
15
+ } from './types';
16
+ import type {
17
+ EditorDOMRenderConfig,
18
+ InitialEditorConfig,
19
+ Klass,
20
+ LexicalEditor,
21
+ LexicalNode,
22
+ } from 'lexical';
23
+
24
+ import {
25
+ $fullReconcile,
26
+ $isLexicalNode,
27
+ DEFAULT_EDITOR_DOM_CONFIG,
28
+ } from 'lexical';
29
+
30
+ import {compileDOMRenderConfigOverrides} from './compileDOMRenderConfigOverrides';
31
+ import {DOMRenderContextSymbol} from './constants';
32
+ import {
33
+ contextFromPairs,
34
+ getContextRecord,
35
+ getContextValue,
36
+ } from './ContextRecord';
37
+
38
+ type RenderContextRecord = ContextRecord<typeof DOMRenderContextSymbol>;
39
+
40
+ function makeReader(record: RenderContextRecord): RenderContextReader {
41
+ return {
42
+ get<V>(cfg: RenderStateConfig<V>): V {
43
+ return getContextValue(record, cfg);
44
+ },
45
+ };
46
+ }
47
+
48
+ /**
49
+ * The mutable, writable editor-level context record. Reads of a render state
50
+ * during reconciliation (and as the base layer of a session) fall through to
51
+ * this record, and it is the layer the `disabledForEditor` predicates read.
52
+ *
53
+ * @internal
54
+ */
55
+ export function createEditorContextRecord(
56
+ contextDefaults: readonly AnyRenderStateConfigPairOrUpdater[],
57
+ ): RenderContextRecord {
58
+ const parent = Object.create(null) as RenderContextRecord;
59
+ return contextFromPairs(contextDefaults, parent) || parent;
60
+ }
61
+
62
+ /**
63
+ * Filter the configured overrides down to those that are resident in the
64
+ * editor's render config, removing any whose `disabledForEditor` predicate
65
+ * returns `true` for the given editor context.
66
+ *
67
+ * @internal
68
+ */
69
+ export function filterEditorInstalled(
70
+ overrides: readonly AnyDOMRenderMatch[],
71
+ record: RenderContextRecord,
72
+ ): AnyDOMRenderMatch[] {
73
+ const reader = makeReader(record);
74
+ return overrides.filter(
75
+ o => !(o.disabledForEditor && o.disabledForEditor(reader)),
76
+ );
77
+ }
78
+
79
+ function sameOverrides(
80
+ a: readonly AnyDOMRenderMatch[],
81
+ b: readonly AnyDOMRenderMatch[],
82
+ ): boolean {
83
+ if (a.length !== b.length) {
84
+ return false;
85
+ }
86
+ for (let i = 0; i < a.length; i++) {
87
+ if (a[i] !== b[i]) {
88
+ return false;
89
+ }
90
+ }
91
+ return true;
92
+ }
93
+
94
+ function symmetricDiff(
95
+ prev: readonly AnyDOMRenderMatch[],
96
+ next: readonly AnyDOMRenderMatch[],
97
+ ): AnyDOMRenderMatch[] {
98
+ const prevSet = new Set(prev);
99
+ const nextSet = new Set(next);
100
+ const changed: AnyDOMRenderMatch[] = [];
101
+ for (const o of prev) {
102
+ if (!nextSet.has(o)) {
103
+ changed.push(o);
104
+ }
105
+ }
106
+ for (const o of next) {
107
+ if (!prevSet.has(o)) {
108
+ changed.push(o);
109
+ }
110
+ }
111
+ return changed;
112
+ }
113
+
114
+ /**
115
+ * Build a predicate matching the nodes an override targets — `'*'` matches
116
+ * everything, a node class matches by `instanceof`, and a guard is used as-is.
117
+ */
118
+ function nodeMatcher(o: AnyDOMRenderMatch): (node: LexicalNode) => boolean {
119
+ if (o.nodes === '*') {
120
+ return () => true;
121
+ }
122
+ const matchers = o.nodes.map(match => {
123
+ const klass = match as Klass<LexicalNode>;
124
+ return $isLexicalNode(klass.prototype)
125
+ ? (node: LexicalNode) => node instanceof klass
126
+ : (match as (node: LexicalNode) => boolean);
127
+ });
128
+ return node => matchers.some(f => f(node));
129
+ }
130
+
131
+ /**
132
+ * Build a predicate matching the nodes whose DOM must be recreated for the
133
+ * given override change, or `null` when no live re-render is needed.
134
+ *
135
+ * `$createDOM`/`$getDOMSlot` produce the element and slot, and `$decorateDOM`
136
+ * may add DOM that only a fresh `$createDOM` can revert — so toggling any of
137
+ * them recreates the affected nodes. `$updateDOM` is diff-driven and applies on
138
+ * the next node update, and export-only hooks ($exportDOM/$shouldInclude/…)
139
+ * don't touch the live DOM, so neither needs a re-render. Recreating every
140
+ * affected node is the simple, always-correct choice; toggles are rare, so the
141
+ * cost is acceptable and can be optimized later if needed.
142
+ */
143
+ function recreatePredicate(
144
+ changed: readonly AnyDOMRenderMatch[],
145
+ ): ((node: LexicalNode) => boolean) | null {
146
+ const matchers: ((node: LexicalNode) => boolean)[] = [];
147
+ for (const o of changed) {
148
+ if (o.$createDOM || o.$getDOMSlot || o.$decorateDOM) {
149
+ matchers.push(nodeMatcher(o));
150
+ }
151
+ }
152
+ return matchers.length === 0 ? null : node => matchers.some(f => f(node));
153
+ }
154
+
155
+ /**
156
+ * Per-editor runtime backing {@link DOMRenderExtension}'s conditional
157
+ * overrides and imperative editor context. See {@link DOMRenderRuntime}.
158
+ *
159
+ * @internal
160
+ */
161
+ export class DOMRenderRuntimeImpl implements DOMRenderRuntime {
162
+ readonly editor: LexicalEditor;
163
+ /**
164
+ * The `nodes` and base `dom` captured at `init` (before `dom` was
165
+ * overwritten with the compiled config) — the clean base for every recompile.
166
+ */
167
+ readonly initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>;
168
+ readonly overrides: readonly AnyDOMRenderMatch[];
169
+ readonly editorContext: RenderContextRecord;
170
+ readonly hasSessionGates: boolean;
171
+ installed: readonly AnyDOMRenderMatch[];
172
+
173
+ /** Memoized session configs keyed by the set of session-disabled overrides. */
174
+ private readonly sessionCache = new Map<string, EditorDOMRenderConfig>();
175
+
176
+ constructor(
177
+ editor: LexicalEditor,
178
+ initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>,
179
+ overrides: readonly AnyDOMRenderMatch[],
180
+ editorContext: RenderContextRecord,
181
+ ) {
182
+ this.editor = editor;
183
+ this.initialEditorConfig = initialEditorConfig;
184
+ this.overrides = overrides;
185
+ this.editorContext = editorContext;
186
+ this.installed = filterEditorInstalled(overrides, editorContext);
187
+ this.hasSessionGates = overrides.some(o => o.disabledForSession);
188
+ }
189
+
190
+ setContextValue<V>(cfg: RenderStateConfig<V>, value: V): void {
191
+ const prev = this.installed;
192
+ this.editorContext[cfg.key] = value;
193
+ const next = filterEditorInstalled(this.overrides, this.editorContext);
194
+ if (sameOverrides(prev, next)) {
195
+ return;
196
+ }
197
+ const changed = symmetricDiff(prev, next);
198
+ this.installed = next;
199
+ this.sessionCache.clear();
200
+ const dom = compileDOMRenderConfigOverrides(this.initialEditorConfig, {
201
+ overrides: next as AnyDOMRenderMatch[],
202
+ });
203
+ this.editor._config.dom = dom;
204
+
205
+ const recreate = recreatePredicate(changed);
206
+ if (!recreate) {
207
+ // $updateDOM-only or export-only change: the recompiled config is enough.
208
+ return;
209
+ }
210
+
211
+ // Re-render through a full reconcile, which reuses the existing node
212
+ // instances (no node-map mutation, so no spurious mutation/collaboration
213
+ // changes). The affected nodes must be unmounted and recreated — the removed
214
+ // override may have produced or decorated DOM that only a fresh $createDOM
215
+ // reverts — so install a transient $updateDOM that reports a recreate for
216
+ // matching nodes.
217
+ //
218
+ // This mutates the (shared) active config, so the reconcile MUST run and
219
+ // finish synchronously before the original is restored on the next line —
220
+ // hence `discrete`, and hence this must not be called from within an
221
+ // editor.update (where the commit would defer). A deferred update would
222
+ // either restore the wrapper before the reconcile reads it (no recreate) or
223
+ // leave it armed across a window where an unrelated reconcile would
224
+ // spuriously recreate matching nodes. No history tag is needed: a full
225
+ // reconcile marks no nodes dirty, which history merges/discards without
226
+ // pushing.
227
+ const base = dom.$updateDOM;
228
+ dom.$updateDOM = (nextNode, prevNode, el, editor) =>
229
+ recreate(nextNode) ? true : base(nextNode, prevNode, el, editor);
230
+ this.editor.update($fullReconcile, {discrete: true});
231
+ dom.$updateDOM = base;
232
+ }
233
+
234
+ getSessionConfig(): EditorDOMRenderConfig {
235
+ const resident = this.editor._config.dom || DEFAULT_EDITOR_DOM_CONFIG;
236
+ if (!this.hasSessionGates) {
237
+ return resident;
238
+ }
239
+ const reader = makeReader(
240
+ getContextRecord(DOMRenderContextSymbol, this.editor) ||
241
+ this.editorContext,
242
+ );
243
+ const disabledKeys: string[] = [];
244
+ const sessionSet: AnyDOMRenderMatch[] = [];
245
+ this.installed.forEach((o, i) => {
246
+ if (o.disabledForSession && o.disabledForSession(reader)) {
247
+ disabledKeys.push(String(i));
248
+ } else {
249
+ sessionSet.push(o);
250
+ }
251
+ });
252
+ if (disabledKeys.length === 0) {
253
+ return resident;
254
+ }
255
+ const key = disabledKeys.join(',');
256
+ let cfg = this.sessionCache.get(key);
257
+ if (!cfg) {
258
+ cfg = compileDOMRenderConfigOverrides(this.initialEditorConfig, {
259
+ overrides: sessionSet,
260
+ });
261
+ this.sessionCache.set(key, cfg);
262
+ }
263
+ return cfg;
264
+ }
265
+ }