@lexical/html 0.44.1-nightly.20260519.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.
- package/{DOMRenderExtension.d.ts → dist/DOMRenderExtension.d.ts} +12 -1
- package/dist/DOMRenderRuntime.d.ts +51 -0
- package/dist/LexicalHtml.dev.js +3192 -0
- package/dist/LexicalHtml.dev.mjs +3146 -0
- package/{LexicalHtml.js.flow → dist/LexicalHtml.js.flow} +16 -16
- package/dist/LexicalHtml.mjs +56 -0
- package/dist/LexicalHtml.node.mjs +54 -0
- package/dist/LexicalHtml.prod.js +9 -0
- package/dist/LexicalHtml.prod.mjs +9 -0
- package/dist/RenderContext.d.ts +68 -0
- package/{compileDOMRenderConfigOverrides.d.ts → dist/compileDOMRenderConfigOverrides.d.ts} +1 -1
- package/{constants.d.ts → dist/constants.d.ts} +2 -0
- package/dist/domOverride.d.ts +23 -0
- package/dist/import/CoreImportExtension.d.ts +11 -0
- package/dist/import/DOMImportExtension.d.ts +82 -0
- package/dist/import/HorizontalRuleImportExtension.d.ts +27 -0
- package/dist/import/ImportContext.d.ts +208 -0
- package/dist/import/compileImportRules.d.ts +50 -0
- package/dist/import/coreImportRules.d.ts +25 -0
- package/dist/import/defineImportRule.d.ts +32 -0
- package/dist/import/defineOverlayRules.d.ts +66 -0
- package/dist/import/index.d.ts +38 -0
- package/dist/import/inlineStylesFromStyleSheets.d.ts +28 -0
- package/dist/import/parseCss.d.ts +18 -0
- package/dist/import/runImport.d.ts +19 -0
- package/dist/import/schemas.d.ts +91 -0
- package/dist/import/sel.d.ts +74 -0
- package/dist/import/types.d.ts +394 -0
- package/dist/index.d.ts +44 -0
- package/{types.d.ts → dist/types.d.ts} +96 -8
- package/package.json +33 -18
- package/src/ContextRecord.ts +243 -0
- package/src/DOMRenderExtension.ts +96 -0
- package/src/DOMRenderRuntime.ts +265 -0
- package/src/RenderContext.ts +168 -0
- package/src/compileDOMRenderConfigOverrides.ts +416 -0
- package/src/constants.ts +18 -0
- package/src/domOverride.ts +46 -0
- package/src/import/CoreImportExtension.ts +26 -0
- package/src/import/DOMImportExtension.ts +221 -0
- package/src/import/HorizontalRuleImportExtension.ts +53 -0
- package/src/import/ImportContext.ts +339 -0
- package/src/import/compileImportRules.ts +178 -0
- package/src/import/coreImportRules.ts +485 -0
- package/src/import/defineImportRule.ts +40 -0
- package/src/import/defineOverlayRules.ts +105 -0
- package/src/import/index.ts +96 -0
- package/src/import/inlineStylesFromStyleSheets.ts +104 -0
- package/src/import/parseCss.ts +219 -0
- package/src/import/runImport.ts +245 -0
- package/src/import/schemas.ts +236 -0
- package/src/import/sel.ts +314 -0
- package/src/import/types.ts +471 -0
- package/src/index.ts +555 -0
- package/src/types.ts +470 -0
- package/LexicalHtml.dev.js +0 -914
- package/LexicalHtml.dev.mjs +0 -900
- package/LexicalHtml.mjs +0 -24
- package/LexicalHtml.node.mjs +0 -22
- package/LexicalHtml.prod.js +0 -9
- package/LexicalHtml.prod.mjs +0 -9
- package/RenderContext.d.ts +0 -32
- package/domOverride.d.ts +0 -18
- package/index.d.ts +0 -32
- /package/{ContextRecord.d.ts → dist/ContextRecord.d.ts} +0 -0
- /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
|
+
}
|