@nextwebwg/html-next 1.0.0-alpha.1
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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/ast.d.ts +7 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -0
- package/dist/browser-loader.bundle.js +9 -0
- package/dist/browser-loader.d.ts +35 -0
- package/dist/browser-loader.d.ts.map +1 -0
- package/dist/browser-loader.js +83 -0
- package/dist/browser-loader.js.map +1 -0
- package/dist/browser-source.d.ts +10 -0
- package/dist/browser-source.d.ts.map +1 -0
- package/dist/browser-source.js +97 -0
- package/dist/browser-source.js.map +1 -0
- package/dist/browser.d.ts +5 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +9 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +1 -0
- package/dist/component-styles-build.d.ts +11 -0
- package/dist/component-styles-build.d.ts.map +1 -0
- package/dist/component-styles-build.js +74 -0
- package/dist/component-styles-build.js.map +1 -0
- package/dist/component-styles.d.ts +37 -0
- package/dist/component-styles.d.ts.map +1 -0
- package/dist/component-styles.js +236 -0
- package/dist/component-styles.js.map +1 -0
- package/dist/contract-platform.d.ts +5 -0
- package/dist/contract-platform.d.ts.map +1 -0
- package/dist/contract-platform.js +8 -0
- package/dist/contract-platform.js.map +1 -0
- package/dist/contract.d.ts +14 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +234 -0
- package/dist/contract.js.map +1 -0
- package/dist/controller-files.d.ts +10 -0
- package/dist/controller-files.d.ts.map +1 -0
- package/dist/controller-files.js +54 -0
- package/dist/controller-files.js.map +1 -0
- package/dist/controller.d.ts +15 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +33 -0
- package/dist/controller.js.map +1 -0
- package/dist/data.d.ts +28 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +100 -0
- package/dist/data.js.map +1 -0
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +13 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/expression.d.ts +80 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +474 -0
- package/dist/expression.js.map +1 -0
- package/dist/freeze.d.ts +2 -0
- package/dist/freeze.d.ts.map +1 -0
- package/dist/freeze.js +8 -0
- package/dist/freeze.js.map +1 -0
- package/dist/generate.d.ts +11 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +41 -0
- package/dist/generate.js.map +1 -0
- package/dist/generated/dom-properties.d.ts +8 -0
- package/dist/generated/dom-properties.d.ts.map +1 -0
- package/dist/generated/dom-properties.js +1410 -0
- package/dist/generated/dom-properties.js.map +1 -0
- package/dist/generated-runtime.d.ts +42 -0
- package/dist/generated-runtime.d.ts.map +1 -0
- package/dist/generated-runtime.js +266 -0
- package/dist/generated-runtime.js.map +1 -0
- package/dist/graph.d.ts +37 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +81 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/language.d.ts +7 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +43 -0
- package/dist/language.js.map +1 -0
- package/dist/names.d.ts +4 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +19 -0
- package/dist/names.js.map +1 -0
- package/dist/node-loader.d.ts +22 -0
- package/dist/node-loader.d.ts.map +1 -0
- package/dist/node-loader.js +109 -0
- package/dist/node-loader.js.map +1 -0
- package/dist/package-config.d.ts +22 -0
- package/dist/package-config.d.ts.map +1 -0
- package/dist/package-config.js +2 -0
- package/dist/package-config.js.map +1 -0
- package/dist/package.d.ts +9 -0
- package/dist/package.d.ts.map +1 -0
- package/dist/package.js +208 -0
- package/dist/package.js.map +1 -0
- package/dist/parser.d.ts +11 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +938 -0
- package/dist/parser.js.map +1 -0
- package/dist/platform.d.ts +3 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +26 -0
- package/dist/platform.js.map +1 -0
- package/dist/reactivity.d.ts +88 -0
- package/dist/reactivity.d.ts.map +1 -0
- package/dist/reactivity.js +594 -0
- package/dist/reactivity.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +39 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolve.d.ts +24 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +65 -0
- package/dist/resolve.js.map +1 -0
- package/dist/runtime.d.ts +110 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1926 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sanitize.d.ts +8 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +38 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/source-graph.d.ts +5 -0
- package/dist/source-graph.d.ts.map +1 -0
- package/dist/source-graph.js +60 -0
- package/dist/source-graph.js.map +1 -0
- package/dist/source-parser.d.ts +4 -0
- package/dist/source-parser.d.ts.map +1 -0
- package/dist/source-parser.js +38 -0
- package/dist/source-parser.js.map +1 -0
- package/dist/source.d.ts +4 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +6 -0
- package/dist/source.js.map +1 -0
- package/dist/targets/backend.d.ts +28 -0
- package/dist/targets/backend.d.ts.map +1 -0
- package/dist/targets/backend.js +42 -0
- package/dist/targets/backend.js.map +1 -0
- package/dist/targets/docs.d.ts +3 -0
- package/dist/targets/docs.d.ts.map +1 -0
- package/dist/targets/docs.js +85 -0
- package/dist/targets/docs.js.map +1 -0
- package/dist/targets/native-reactive.d.ts +41 -0
- package/dist/targets/native-reactive.d.ts.map +1 -0
- package/dist/targets/native-reactive.js +169 -0
- package/dist/targets/native-reactive.js.map +1 -0
- package/dist/targets/shared.d.ts +15 -0
- package/dist/targets/shared.d.ts.map +1 -0
- package/dist/targets/shared.js +122 -0
- package/dist/targets/shared.js.map +1 -0
- package/dist/targets/vanilla.d.ts +6 -0
- package/dist/targets/vanilla.d.ts.map +1 -0
- package/dist/targets/vanilla.js +442 -0
- package/dist/targets/vanilla.js.map +1 -0
- package/dist/targets/vue.d.ts +3 -0
- package/dist/targets/vue.d.ts.map +1 -0
- package/dist/targets/vue.js +577 -0
- package/dist/targets/vue.js.map +1 -0
- package/dist/template.d.ts +172 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +2 -0
- package/dist/template.js.map +1 -0
- package/dist/type-system.d.ts +80 -0
- package/dist/type-system.d.ts.map +1 -0
- package/dist/type-system.js +414 -0
- package/dist/type-system.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +38 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +297 -0
- package/dist/validate.js.map +1 -0
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +6 -0
- package/dist/validation.js.map +1 -0
- package/dist/validity-css.d.ts +6 -0
- package/dist/validity-css.d.ts.map +1 -0
- package/dist/validity-css.js +11 -0
- package/dist/validity-css.js.map +1 -0
- package/dist/validity.d.ts +45 -0
- package/dist/validity.d.ts.map +1 -0
- package/dist/validity.js +445 -0
- package/dist/validity.js.map +1 -0
- package/package.json +124 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,1926 @@
|
|
|
1
|
+
import { parseBrowserComponent } from "./browser-source.js";
|
|
2
|
+
import { DataResource } from "./data.js";
|
|
3
|
+
import { fail } from "./diagnostics.js";
|
|
4
|
+
import { ABSENT, UndeclaredName, evaluate, evaluateCompiled, toAttribute, toText, truthy, } from "./expression.js";
|
|
5
|
+
import { kebabCase } from "./names.js";
|
|
6
|
+
import { createComputed, createEffect, createSignal, ReactiveScope, } from "./reactivity.js";
|
|
7
|
+
import { hasExecutableUrl, isUrlAttribute, sanitizeFragment } from "./sanitize.js";
|
|
8
|
+
import { addAttributeToken, COMPONENT_ATTRIBUTE, compileComponentStyles, markProjectedRoot, stateAttribute, stateAttributeValue, } from "./component-styles.js";
|
|
9
|
+
import { parseTypeExpression, parseTypedValue, serializeTypedValue } from "./type-system.js";
|
|
10
|
+
const contentOnly = new WeakSet();
|
|
11
|
+
const runtimeInstances = new WeakMap();
|
|
12
|
+
const definitionAttributes = new WeakMap();
|
|
13
|
+
const runtimeKey = Symbol.for("@nextwebwg/html-next.runtime.v1");
|
|
14
|
+
const lifecycleKey = Symbol.for("@nextwebwg/html-next.lifecycle.v1");
|
|
15
|
+
function documentState(root) {
|
|
16
|
+
return root[runtimeKey] ??= {};
|
|
17
|
+
}
|
|
18
|
+
function runtimeInstance(element) {
|
|
19
|
+
return runtimeInstances.get(element);
|
|
20
|
+
}
|
|
21
|
+
function registryFor(root) {
|
|
22
|
+
const state = documentState(root);
|
|
23
|
+
let registry = state.registry;
|
|
24
|
+
if (registry === undefined || registry.root !== root.documentElement) {
|
|
25
|
+
registry = { root: root.documentElement, definitions: new Map(), discoverySelector: undefined };
|
|
26
|
+
state.registry = registry;
|
|
27
|
+
}
|
|
28
|
+
return registry;
|
|
29
|
+
}
|
|
30
|
+
/** The props and state each definition's `:host-state()` rules test, recorded when its styles compile. */
|
|
31
|
+
const stateNamesByDefinition = new WeakMap();
|
|
32
|
+
function compileStyles(css, definition, document) {
|
|
33
|
+
const compiled = compileComponentStyles(css, definition, document);
|
|
34
|
+
stateNamesByDefinition.set(definition, compiled.stateNames);
|
|
35
|
+
return compiled.css;
|
|
36
|
+
}
|
|
37
|
+
function registerDefinition(registry, tag, definition) {
|
|
38
|
+
registry.definitions.set(tag, definition);
|
|
39
|
+
if (registry.discoverySelector !== undefined)
|
|
40
|
+
registry.discoverySelector += `,${tag}`;
|
|
41
|
+
}
|
|
42
|
+
function discoverySelector(registry) {
|
|
43
|
+
return registry.discoverySelector ??=
|
|
44
|
+
[
|
|
45
|
+
"template[component]",
|
|
46
|
+
"[data-component]",
|
|
47
|
+
...Array.from(registry.definitions.keys()),
|
|
48
|
+
].join(",");
|
|
49
|
+
}
|
|
50
|
+
function parseDefinition(wrapper, index) {
|
|
51
|
+
const tag = wrapper.getAttribute("component") ?? "";
|
|
52
|
+
const source = `${wrapper.ownerDocument.URL}#template[component="${tag}"][${index + 1}]`;
|
|
53
|
+
if (wrapper.hasAttribute("src")) {
|
|
54
|
+
fail("HL001", "External definitions require the application-owned graph resolver.", source);
|
|
55
|
+
}
|
|
56
|
+
const definition = parseBrowserComponent(wrapper, source);
|
|
57
|
+
const style = Array.from(wrapper.content.children).find((element) => element.localName === "style");
|
|
58
|
+
return {
|
|
59
|
+
wrapper,
|
|
60
|
+
style,
|
|
61
|
+
definition,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Installs an already validated external/package graph without reparsing or executing it. */
|
|
65
|
+
export function installComponentGraph(graph, root = document) {
|
|
66
|
+
const registry = registryFor(root);
|
|
67
|
+
let installed = 0;
|
|
68
|
+
for (const node of graph.nodes.values()) {
|
|
69
|
+
if (node.shadowedByCustomElement)
|
|
70
|
+
continue;
|
|
71
|
+
const tag = node.definition.contract.tag;
|
|
72
|
+
if (registry.definitions.has(tag))
|
|
73
|
+
fail("HR001", `More than one definition declares <${tag}>.`);
|
|
74
|
+
const style = node.definition.css === "" ? undefined : root.createElement("style");
|
|
75
|
+
if (style !== undefined) {
|
|
76
|
+
style.textContent = compileStyles(node.definition.css, node.definition, root);
|
|
77
|
+
root.head.append(style);
|
|
78
|
+
}
|
|
79
|
+
registerDefinition(registry, tag, {
|
|
80
|
+
definition: node.definition,
|
|
81
|
+
style,
|
|
82
|
+
});
|
|
83
|
+
installed += 1;
|
|
84
|
+
}
|
|
85
|
+
return installed;
|
|
86
|
+
}
|
|
87
|
+
function invocationValue(prop, input, attributePresent = false) {
|
|
88
|
+
// Bare boolean attributes retain HTML presence semantics. Explicit values
|
|
89
|
+
// are invocation strings and must still pass through the declared type.
|
|
90
|
+
const candidate = prop.type === "boolean" && attributePresent && input === "" ? true : input;
|
|
91
|
+
const parsed = parseTypedValue(candidate, prop.type);
|
|
92
|
+
if (!parsed.ok) {
|
|
93
|
+
const detail = parsed.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ");
|
|
94
|
+
fail("HR002", `A prop invocation value does not satisfy its declared type. ${detail}`);
|
|
95
|
+
}
|
|
96
|
+
return parsed.value;
|
|
97
|
+
}
|
|
98
|
+
function assignedPropValue(name, prop, input) {
|
|
99
|
+
if (input !== undefined)
|
|
100
|
+
return invocationValue(prop, input);
|
|
101
|
+
if (prop.required)
|
|
102
|
+
fail("HC020", `Required prop \`${name}\` was not provided.`);
|
|
103
|
+
return (prop.default === undefined ? ABSENT : prop.default);
|
|
104
|
+
}
|
|
105
|
+
function propAttributeNames(definition, hydration) {
|
|
106
|
+
let names = definitionAttributes.get(definition);
|
|
107
|
+
if (names === undefined) {
|
|
108
|
+
const invocation = Object.create(null);
|
|
109
|
+
const hydrated = Object.create(null);
|
|
110
|
+
for (const name of Object.keys(definition.contract.props)) {
|
|
111
|
+
const attributeName = kebabCase(name);
|
|
112
|
+
invocation[attributeName] = name;
|
|
113
|
+
hydrated[`data-${attributeName}`] = name;
|
|
114
|
+
if (attributeName !== name.toLowerCase())
|
|
115
|
+
hydrated[`data-${name.toLowerCase()}`] = name;
|
|
116
|
+
}
|
|
117
|
+
names = [invocation, hydrated];
|
|
118
|
+
definitionAttributes.set(definition, names);
|
|
119
|
+
}
|
|
120
|
+
return names[hydration ? 1 : 0];
|
|
121
|
+
}
|
|
122
|
+
function readInvocation(invocation, definition, hydration = false) {
|
|
123
|
+
const contract = definition.contract;
|
|
124
|
+
const names = propAttributeNames(definition, hydration);
|
|
125
|
+
const values = Object.create(null);
|
|
126
|
+
const passThrough = [];
|
|
127
|
+
let requestedRoot;
|
|
128
|
+
for (const attribute of Array.from(invocation.attributes)) {
|
|
129
|
+
if (attribute.name.toLowerCase() === "as" && definition.root?.kind === "native") {
|
|
130
|
+
requestedRoot = attribute.value.toLowerCase();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const propName = names[attribute.name.toLowerCase()];
|
|
134
|
+
if (propName === undefined) {
|
|
135
|
+
if (!hydration)
|
|
136
|
+
passThrough.push(attribute);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
values[propName] = invocationValue(contract.props[propName], attribute.value, !hydration);
|
|
140
|
+
}
|
|
141
|
+
const rootName = definition.root?.kind === "native"
|
|
142
|
+
? hydration ? invocation.localName : requestedRoot ?? definition.root.element
|
|
143
|
+
: definition.template.name;
|
|
144
|
+
if (definition.root?.kind === "native" &&
|
|
145
|
+
!definition.root.choices.includes(rootName)) {
|
|
146
|
+
fail(hydration ? "HR005" : "HR002", `<${contract.tag}> cannot use root <${rootName}>; expected one of ${definition.root.choices.join(", ")}.`);
|
|
147
|
+
}
|
|
148
|
+
// Props are attributes on the invocation (or, when hydrating, the data-* reflection of the
|
|
149
|
+
// author's explicit attributes). They are never read from JavaScript properties.
|
|
150
|
+
const explicit = new Set(Object.keys(values).filter((name) => values[name] !== undefined));
|
|
151
|
+
const scope = new ReactiveScope();
|
|
152
|
+
for (const [name, prop] of Object.entries(contract.props)) {
|
|
153
|
+
if (prop.required && values[name] === undefined) {
|
|
154
|
+
fail("HC020", `Required prop \`${name}\` was not provided.`);
|
|
155
|
+
}
|
|
156
|
+
// The effective value seen by expressions: passed value, default, or first-class absence.
|
|
157
|
+
scope.set(name, (values[name] !== undefined
|
|
158
|
+
? values[name]
|
|
159
|
+
: prop.default === undefined ? ABSENT : prop.default));
|
|
160
|
+
}
|
|
161
|
+
const declarations = definition.declarations ?? [];
|
|
162
|
+
for (const declaration of declarations) {
|
|
163
|
+
if (declaration.kind === "state" || declaration.kind === "computed" ||
|
|
164
|
+
declaration.kind === "data")
|
|
165
|
+
scope.set(declaration.name, null);
|
|
166
|
+
}
|
|
167
|
+
for (const declaration of declarations) {
|
|
168
|
+
if (declaration.kind === "data") {
|
|
169
|
+
scope.set(declaration.name, { pending: true, value: null, error: null, ok: false });
|
|
170
|
+
}
|
|
171
|
+
else if (declaration.kind === "state") {
|
|
172
|
+
scope.set(declaration.name, declaration.expression === undefined ? null : evalValue(declaration.expression.source, scope));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const effects = [];
|
|
176
|
+
for (const declaration of declarations) {
|
|
177
|
+
if (declaration.kind !== "computed" || declaration.expression === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
effects.push(scope.defineComputed(declaration.name, () => evaluateCompiled(declaration.expression, scope)));
|
|
180
|
+
}
|
|
181
|
+
const definitionBase = (() => {
|
|
182
|
+
try {
|
|
183
|
+
return new URL(definition.source.file, invocation.ownerDocument.baseURI).href;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return invocation.ownerDocument.baseURI;
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
for (const declaration of declarations) {
|
|
190
|
+
if (declaration.kind !== "data" || declaration.source === undefined)
|
|
191
|
+
continue;
|
|
192
|
+
const data = declaration;
|
|
193
|
+
const dataSource = declaration.source;
|
|
194
|
+
const resource = new DataResource({
|
|
195
|
+
source: dataSource,
|
|
196
|
+
baseURL: definitionBase,
|
|
197
|
+
...(data.type === undefined ? {} : { type: data.type }),
|
|
198
|
+
...(data.debounce === undefined ? {} : { debounce: Number(data.debounce) }),
|
|
199
|
+
...(data.poll === undefined ? {} : { poll: Number(data.poll) }),
|
|
200
|
+
onState: (state) => scope.set(data.name, state),
|
|
201
|
+
});
|
|
202
|
+
effects.push(createEffect(scope.scheduler, () => {
|
|
203
|
+
const parameters = Object.fromEntries(data.parameters.map((parameter) => [
|
|
204
|
+
parameter.name,
|
|
205
|
+
evaluateCompiled(parameter.expression, scope),
|
|
206
|
+
]));
|
|
207
|
+
resource.update(parameters);
|
|
208
|
+
return () => resource.disconnect();
|
|
209
|
+
}, 0));
|
|
210
|
+
}
|
|
211
|
+
return { scope, passThrough, effects, rootName, explicit };
|
|
212
|
+
}
|
|
213
|
+
/** A child scope layer whose locals shadow the parent (for $each/$with/$match aliases). */
|
|
214
|
+
function layer(parent, locals) {
|
|
215
|
+
return parent.fork(Object.entries(locals));
|
|
216
|
+
}
|
|
217
|
+
function evalValue(expression, scope) {
|
|
218
|
+
try {
|
|
219
|
+
return evaluate(expression, scope);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error instanceof UndeclaredName)
|
|
223
|
+
fail("HB001", error.message);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
228
|
+
// Bound attribute names reach the runtime lowercased (`:viewBox` is tokenized as `:viewbox`), so
|
|
229
|
+
// let the HTML parser apply its own SVG attribute adjustment table rather than shipping a copy.
|
|
230
|
+
const svgAttributeNames = new Map();
|
|
231
|
+
function attributeNameFor(element, name) {
|
|
232
|
+
if (element.namespaceURI !== SVG_NAMESPACE)
|
|
233
|
+
return name;
|
|
234
|
+
let adjusted = svgAttributeNames.get(name);
|
|
235
|
+
if (adjusted === undefined) {
|
|
236
|
+
const parser = element.ownerDocument.createElement("template");
|
|
237
|
+
parser.innerHTML = `<svg ${name}>`;
|
|
238
|
+
adjusted = parser.content.firstChild.attributes[0]?.name ?? name;
|
|
239
|
+
svgAttributeNames.set(name, adjusted);
|
|
240
|
+
}
|
|
241
|
+
return adjusted;
|
|
242
|
+
}
|
|
243
|
+
/** Create an element in the namespace its template position implies. */
|
|
244
|
+
function createTemplateElement(document, name, context) {
|
|
245
|
+
if (name === "svg" || context.namespace === SVG_NAMESPACE) {
|
|
246
|
+
return document.createElementNS(SVG_NAMESPACE, name);
|
|
247
|
+
}
|
|
248
|
+
return document.createElement(name);
|
|
249
|
+
}
|
|
250
|
+
/** The context for an element's children: entering `<svg>` switches to SVG, `<foreignObject>` back to HTML. */
|
|
251
|
+
function childContextFor(element, context) {
|
|
252
|
+
const namespace = element.namespaceURI === SVG_NAMESPACE && element.localName !== "foreignObject"
|
|
253
|
+
? SVG_NAMESPACE
|
|
254
|
+
: undefined;
|
|
255
|
+
if (namespace === context.namespace)
|
|
256
|
+
return context;
|
|
257
|
+
// Inherit so live fields (committed, root) keep reading from the shared render context.
|
|
258
|
+
return Object.create(context, { namespace: { value: namespace, enumerable: true } });
|
|
259
|
+
}
|
|
260
|
+
function ownEffect(context, scope, run, priority = 1) {
|
|
261
|
+
context.effects.push(createEffect(scope.scheduler, run, priority));
|
|
262
|
+
}
|
|
263
|
+
function setWritablePath(scope, path, value) {
|
|
264
|
+
const [root, ...segments] = path;
|
|
265
|
+
if (typeof root !== "string")
|
|
266
|
+
return;
|
|
267
|
+
if (segments.length === 0) {
|
|
268
|
+
scope.set(root, value);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
let target = scope.get(root);
|
|
272
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
273
|
+
const segment = segments[index];
|
|
274
|
+
const key = typeof segment === "object" ? evaluateCompiled(segment.expression, scope) : segment;
|
|
275
|
+
if ((typeof key !== "string" && typeof key !== "number") || target == null)
|
|
276
|
+
return;
|
|
277
|
+
target = target[key];
|
|
278
|
+
}
|
|
279
|
+
const last = segments.at(-1);
|
|
280
|
+
const key = typeof last === "object" ? evaluateCompiled(last.expression, scope) : last;
|
|
281
|
+
if ((typeof key === "string" || typeof key === "number") && target != null)
|
|
282
|
+
target[key] = value;
|
|
283
|
+
}
|
|
284
|
+
function controlValue(element) {
|
|
285
|
+
if (element instanceof HTMLInputElement) {
|
|
286
|
+
if (element.type === "checkbox" || element.type === "radio")
|
|
287
|
+
return element.checked;
|
|
288
|
+
if (element.type === "number" || element.type === "range") {
|
|
289
|
+
return Number.isNaN(element.valueAsNumber) ? null : element.valueAsNumber;
|
|
290
|
+
}
|
|
291
|
+
return element.value;
|
|
292
|
+
}
|
|
293
|
+
if (element instanceof HTMLSelectElement) {
|
|
294
|
+
return element.multiple
|
|
295
|
+
? Array.from(element.selectedOptions, (option) => option.value)
|
|
296
|
+
: element.value;
|
|
297
|
+
}
|
|
298
|
+
if (element instanceof HTMLTextAreaElement)
|
|
299
|
+
return element.value;
|
|
300
|
+
return element.value ?? element.getAttribute("value");
|
|
301
|
+
}
|
|
302
|
+
function applyBoundControlValue(element, name, value) {
|
|
303
|
+
const lowerName = name.toLowerCase();
|
|
304
|
+
if (lowerName === "checked" && element instanceof HTMLInputElement) {
|
|
305
|
+
element.checked = truthy(value);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
if (lowerName === "value" && element instanceof HTMLSelectElement && element.multiple) {
|
|
309
|
+
const selected = new Set(Array.isArray(value) ? value.map(String) : []);
|
|
310
|
+
for (const option of Array.from(element.options))
|
|
311
|
+
option.selected = selected.has(option.value);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (lowerName === "value" &&
|
|
315
|
+
(element instanceof HTMLInputElement ||
|
|
316
|
+
element instanceof HTMLTextAreaElement ||
|
|
317
|
+
element instanceof HTMLSelectElement)) {
|
|
318
|
+
element.value = value == null ? "" : String(value);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
function runHandler(declaration, element, scope, context) {
|
|
324
|
+
for (const step of declaration.steps) {
|
|
325
|
+
if (step.guard !== undefined && !truthy(evaluateCompiled(step.guard, scope)))
|
|
326
|
+
continue;
|
|
327
|
+
if (step.kind === "set") {
|
|
328
|
+
setWritablePath(scope, step.writablePath, evaluateCompiled(step.value, scope));
|
|
329
|
+
}
|
|
330
|
+
else if (step.kind === "dispatch") {
|
|
331
|
+
const declaration = eventDeclaration(context.definition, step.event);
|
|
332
|
+
const detail = step.value === undefined ? undefined : evaluateCompiled(step.value, scope);
|
|
333
|
+
if (declaration !== undefined && detail !== undefined) {
|
|
334
|
+
const parsed = parseTypedValue(detail, parseTypeExpression(declaration.type));
|
|
335
|
+
if (!parsed.ok)
|
|
336
|
+
fail("HR002", `Event \`${step.event}\` detail does not satisfy its declared type.`);
|
|
337
|
+
}
|
|
338
|
+
dispatchComponentEvent(context.root ?? element, step.event, detail, declaration);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const target = context.refs[step.target];
|
|
342
|
+
if (step.kind === "focus")
|
|
343
|
+
target?.focus();
|
|
344
|
+
else
|
|
345
|
+
target?.reportValidity?.();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function eventDeclaration(definition, name) {
|
|
350
|
+
return (definition.declarations ?? []).find((candidate) => candidate.kind === "event" && candidate.name === name);
|
|
351
|
+
}
|
|
352
|
+
/** Undeclared events keep the permissive default: bubbling, composed, and not cancelable. */
|
|
353
|
+
function dispatchComponentEvent(target, event, detail, declaration) {
|
|
354
|
+
return target.dispatchEvent(new CustomEvent(event, {
|
|
355
|
+
detail,
|
|
356
|
+
bubbles: declaration?.bubbles ?? true,
|
|
357
|
+
composed: declaration?.composed ?? true,
|
|
358
|
+
cancelable: declaration?.cancelable ?? false,
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
function eventPasses(event, element, modifiers) {
|
|
362
|
+
if (modifiers.includes("self") && event.target !== element)
|
|
363
|
+
return false;
|
|
364
|
+
if (event instanceof MouseEvent) {
|
|
365
|
+
const buttonFilters = modifiers.filter((modifier) => ["left", "middle", "right"].includes(modifier));
|
|
366
|
+
const buttons = { left: 0, middle: 1, right: 2 };
|
|
367
|
+
if (buttonFilters.length > 0 && !buttonFilters.some((filter) => event.button === buttons[filter]))
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
const systemKeys = ["ctrl", "shift", "alt", "meta"];
|
|
371
|
+
for (const key of systemKeys) {
|
|
372
|
+
if (modifiers.includes(key) && !event[`${key}Key`])
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
if (modifiers.includes("exact") &&
|
|
376
|
+
systemKeys.some((key) => !modifiers.includes(key) && event[`${key}Key`]))
|
|
377
|
+
return false;
|
|
378
|
+
if (event instanceof KeyboardEvent) {
|
|
379
|
+
const keyFilters = modifiers.filter((modifier) => ["enter", "escape", "space", "tab", "up", "down", "left", "right"].includes(modifier));
|
|
380
|
+
const keyNames = {
|
|
381
|
+
enter: "Enter", escape: "Escape", space: " ", tab: "Tab",
|
|
382
|
+
up: "ArrowUp", down: "ArrowDown", left: "ArrowLeft", right: "ArrowRight",
|
|
383
|
+
};
|
|
384
|
+
if (keyFilters.length > 0 && !keyFilters.some((filter) => event.key === keyNames[filter]))
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
function bindEvents(element, node, scope, context) {
|
|
390
|
+
for (const binding of node.events ?? []) {
|
|
391
|
+
const declaration = (context.definition.declarations ?? []).find((candidate) => candidate.kind === "handler" && candidate.name === binding.handler);
|
|
392
|
+
const listener = (event) => {
|
|
393
|
+
if (!eventPasses(event, element, binding.modifiers))
|
|
394
|
+
return;
|
|
395
|
+
if (binding.modifiers.includes("prevent"))
|
|
396
|
+
event.preventDefault();
|
|
397
|
+
if (binding.modifiers.includes("stop"))
|
|
398
|
+
event.stopPropagation();
|
|
399
|
+
runHandler(declaration, element, scope, context);
|
|
400
|
+
};
|
|
401
|
+
if (binding.name === "connect" || binding.name === "disconnect") {
|
|
402
|
+
const callbacks = binding.name === "connect"
|
|
403
|
+
? context.connectCallbacks
|
|
404
|
+
: context.disconnectCallbacks;
|
|
405
|
+
callbacks.add(() => listener(new Event(binding.name)));
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
ownEffect(context, scope, () => {
|
|
409
|
+
element.addEventListener(binding.name, listener, {
|
|
410
|
+
capture: binding.modifiers.includes("capture"),
|
|
411
|
+
passive: binding.modifiers.includes("passive"),
|
|
412
|
+
once: binding.modifiers.includes("once"),
|
|
413
|
+
});
|
|
414
|
+
return () => element.removeEventListener(binding.name, listener, {
|
|
415
|
+
capture: binding.modifiers.includes("capture"),
|
|
416
|
+
});
|
|
417
|
+
}, 2);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function setAttribute(element, name, value) {
|
|
421
|
+
name = attributeNameFor(element, name);
|
|
422
|
+
if (value === null || (isUrlAttribute(name) && hasExecutableUrl(value))) {
|
|
423
|
+
element.removeAttribute(name);
|
|
424
|
+
}
|
|
425
|
+
else
|
|
426
|
+
element.setAttribute(name, value);
|
|
427
|
+
}
|
|
428
|
+
/** Set an element's whole content from a `$value` (escaped text) or `$html` (sanitized) directive. */
|
|
429
|
+
function applyContent(element, directive, scope, document) {
|
|
430
|
+
const value = evalValue(directive.expression, scope);
|
|
431
|
+
if (directive.name === "value")
|
|
432
|
+
element.textContent = toText(value);
|
|
433
|
+
else
|
|
434
|
+
element.replaceChildren(sanitizeFragment(toText(value), document, (node) => contentOnly.add(node)));
|
|
435
|
+
}
|
|
436
|
+
/** A `<template $value>`/`<template $html>` produces inline nodes with no wrapper element. */
|
|
437
|
+
function inlineDirective(directive, scope, document) {
|
|
438
|
+
const value = evalValue(directive.expression, scope);
|
|
439
|
+
if (directive.name === "value")
|
|
440
|
+
return document.createTextNode(toText(value));
|
|
441
|
+
return sanitizeFragment(toText(value), document, (node) => contentOnly.add(node));
|
|
442
|
+
}
|
|
443
|
+
function compareValues(a, b) {
|
|
444
|
+
if (typeof a === "number" && typeof b === "number")
|
|
445
|
+
return a - b;
|
|
446
|
+
return toText(a).localeCompare(toText(b));
|
|
447
|
+
}
|
|
448
|
+
/** Apply the `$each` modifiers: `$where` filter, `$sort` (comma keys, `-` = descending), `$limit`. */
|
|
449
|
+
function shapeList(items, flow, scope) {
|
|
450
|
+
let result = items.slice();
|
|
451
|
+
if (flow.where !== undefined) {
|
|
452
|
+
const where = flow.where;
|
|
453
|
+
result = result.filter((item) => truthy(evalValue(where, layer(scope, { [flow.item]: item }))));
|
|
454
|
+
}
|
|
455
|
+
if (flow.sort !== undefined) {
|
|
456
|
+
const keys = flow.sort.split(",").map((raw) => raw.trim()).filter((key) => key !== "");
|
|
457
|
+
// A `$sort` key is a bare field path on the item (unlike `$key`, which is an expression):
|
|
458
|
+
// `price,-name` sorts by item.price ascending then item.name descending. A scalar-item
|
|
459
|
+
// list sorts by the value itself; the key then only carries the ascending/descending sign.
|
|
460
|
+
const sortValue = (item, field) => item !== null && typeof item === "object" && !Array.isArray(item)
|
|
461
|
+
? evalValue(`${flow.item}.${field}`, layer(scope, { [flow.item]: item }))
|
|
462
|
+
: item;
|
|
463
|
+
result.sort((a, b) => {
|
|
464
|
+
for (const key of keys) {
|
|
465
|
+
const descending = key.startsWith("-");
|
|
466
|
+
const field = descending ? key.slice(1).trim() : key;
|
|
467
|
+
const order = compareValues(sortValue(a, field), sortValue(b, field));
|
|
468
|
+
if (order !== 0)
|
|
469
|
+
return descending ? -order : order;
|
|
470
|
+
}
|
|
471
|
+
return 0;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (flow.limit !== undefined) {
|
|
475
|
+
const limit = evalValue(flow.limit, scope);
|
|
476
|
+
if (typeof limit === "number")
|
|
477
|
+
result = result.slice(0, Math.max(0, Math.trunc(limit)));
|
|
478
|
+
}
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
function materialize(nodes, document) {
|
|
482
|
+
const fragment = document.createDocumentFragment();
|
|
483
|
+
fragment.append(...nodes);
|
|
484
|
+
return Array.from(fragment.childNodes);
|
|
485
|
+
}
|
|
486
|
+
function clearRange(start, end) {
|
|
487
|
+
let current = start.nextSibling;
|
|
488
|
+
while (current !== null && current !== end) {
|
|
489
|
+
const next = current.nextSibling;
|
|
490
|
+
current.remove();
|
|
491
|
+
current = next;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function renderDynamicNode(node, scope, document, passThrough, context) {
|
|
495
|
+
if (node.flow?.kind === "each") {
|
|
496
|
+
return renderEachRegion(node, scope, document, passThrough, context);
|
|
497
|
+
}
|
|
498
|
+
const start = document.createComment("html-next:start");
|
|
499
|
+
const end = document.createComment("html-next:end");
|
|
500
|
+
const fragment = document.createDocumentFragment();
|
|
501
|
+
fragment.append(start, end);
|
|
502
|
+
let childEffects = [];
|
|
503
|
+
ownEffect(context, scope, () => {
|
|
504
|
+
for (const effect of childEffects)
|
|
505
|
+
effect.stop();
|
|
506
|
+
childEffects = [];
|
|
507
|
+
clearRange(start, end);
|
|
508
|
+
const effectsStart = context.effects.length;
|
|
509
|
+
let rendered = [];
|
|
510
|
+
if (node.flow?.kind === "if") {
|
|
511
|
+
if (truthy(evalValue(node.flow.test, scope))) {
|
|
512
|
+
const { flow: _flow, ...body } = node;
|
|
513
|
+
rendered = renderInstance(body, scope, document, passThrough, context);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else if (node.flow?.kind === "with") {
|
|
517
|
+
const local = scope.fork([[node.flow.alias, evalValue(node.flow.expr, scope)]]);
|
|
518
|
+
const { flow: _flow, ...body } = node;
|
|
519
|
+
rendered = renderInstance(body, local, document, passThrough, context);
|
|
520
|
+
}
|
|
521
|
+
else if (node.flow?.kind === "match") {
|
|
522
|
+
rendered = renderMatch(node, scope, document, context);
|
|
523
|
+
}
|
|
524
|
+
childEffects = context.effects.slice(effectsStart);
|
|
525
|
+
end.before(...materialize(rendered, document));
|
|
526
|
+
});
|
|
527
|
+
return [fragment];
|
|
528
|
+
}
|
|
529
|
+
function moveBlockBefore(block, reference) {
|
|
530
|
+
if (block.end.nextSibling === reference)
|
|
531
|
+
return;
|
|
532
|
+
const nodes = [];
|
|
533
|
+
let current = block.start;
|
|
534
|
+
while (current !== null) {
|
|
535
|
+
nodes.push(current);
|
|
536
|
+
if (current === block.end)
|
|
537
|
+
break;
|
|
538
|
+
current = current.nextSibling;
|
|
539
|
+
}
|
|
540
|
+
const parent = reference.parentNode;
|
|
541
|
+
if (parent === null)
|
|
542
|
+
return;
|
|
543
|
+
const moveBefore = parent.moveBefore;
|
|
544
|
+
for (const node of nodes) {
|
|
545
|
+
if (moveBefore === undefined)
|
|
546
|
+
parent.insertBefore(node, reference);
|
|
547
|
+
else
|
|
548
|
+
moveBefore.call(parent, node, reference);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function removeBlock(block) {
|
|
552
|
+
for (const effect of block.effects)
|
|
553
|
+
effect.stop();
|
|
554
|
+
let current = block.start;
|
|
555
|
+
while (current !== null) {
|
|
556
|
+
const next = current.nextSibling;
|
|
557
|
+
current.parentNode?.removeChild(current);
|
|
558
|
+
if (current === block.end)
|
|
559
|
+
break;
|
|
560
|
+
current = next;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/** Mark the longest subsequence of retained blocks that is already in DOM order. */
|
|
564
|
+
function stableBlockPositions(previous) {
|
|
565
|
+
let last = -1;
|
|
566
|
+
let ordered = true;
|
|
567
|
+
for (const position of previous) {
|
|
568
|
+
if (position < 0)
|
|
569
|
+
continue;
|
|
570
|
+
if (position < last)
|
|
571
|
+
ordered = false;
|
|
572
|
+
last = position;
|
|
573
|
+
}
|
|
574
|
+
if (ordered)
|
|
575
|
+
return undefined;
|
|
576
|
+
const tails = [];
|
|
577
|
+
const predecessors = new Int32Array(previous.length).fill(-1);
|
|
578
|
+
for (let index = 0; index < previous.length; index += 1) {
|
|
579
|
+
const position = previous[index];
|
|
580
|
+
if (position < 0)
|
|
581
|
+
continue;
|
|
582
|
+
let low = 0;
|
|
583
|
+
let high = tails.length;
|
|
584
|
+
while (low < high) {
|
|
585
|
+
const middle = (low + high) >>> 1;
|
|
586
|
+
if (previous[tails[middle]] < position)
|
|
587
|
+
low = middle + 1;
|
|
588
|
+
else
|
|
589
|
+
high = middle;
|
|
590
|
+
}
|
|
591
|
+
if (low > 0)
|
|
592
|
+
predecessors[index] = tails[low - 1];
|
|
593
|
+
tails[low] = index;
|
|
594
|
+
}
|
|
595
|
+
const stable = new Uint8Array(previous.length);
|
|
596
|
+
let cursor = tails.at(-1) ?? -1;
|
|
597
|
+
while (cursor >= 0) {
|
|
598
|
+
stable[cursor] = 1;
|
|
599
|
+
cursor = predecessors[cursor];
|
|
600
|
+
}
|
|
601
|
+
return stable;
|
|
602
|
+
}
|
|
603
|
+
function renderEachRegion(node, scope, document, passThrough, context) {
|
|
604
|
+
const flow = node.flow;
|
|
605
|
+
const start = document.createComment("html-next:each-start");
|
|
606
|
+
const end = document.createComment("html-next:each-end");
|
|
607
|
+
const fragment = document.createDocumentFragment();
|
|
608
|
+
fragment.append(start, end);
|
|
609
|
+
let blocks = new Map();
|
|
610
|
+
ownEffect(context, scope, () => {
|
|
611
|
+
const value = evalValue(flow.list, scope);
|
|
612
|
+
const items = Array.isArray(value) ? shapeList(value, flow, scope) : [];
|
|
613
|
+
const next = new Map();
|
|
614
|
+
const keyed = flow.key !== undefined;
|
|
615
|
+
const ordered = keyed ? [] : undefined;
|
|
616
|
+
let oldPositions;
|
|
617
|
+
if (keyed) {
|
|
618
|
+
oldPositions = new Map();
|
|
619
|
+
let position = 0;
|
|
620
|
+
for (const key of blocks.keys())
|
|
621
|
+
oldPositions.set(key, position++);
|
|
622
|
+
}
|
|
623
|
+
const previous = keyed ? [] : undefined;
|
|
624
|
+
const { flow: _flow, ...body } = node;
|
|
625
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
626
|
+
const item = items[index];
|
|
627
|
+
const locals = {
|
|
628
|
+
[flow.item]: item,
|
|
629
|
+
loop: { index, first: index === 0, last: index === items.length - 1, count: items.length },
|
|
630
|
+
};
|
|
631
|
+
if (flow.index !== undefined)
|
|
632
|
+
locals[flow.index] = index;
|
|
633
|
+
let local;
|
|
634
|
+
let key = index;
|
|
635
|
+
if (flow.key !== undefined) {
|
|
636
|
+
local = scope.fork(Object.entries(locals));
|
|
637
|
+
key = evalValue(flow.key, local);
|
|
638
|
+
}
|
|
639
|
+
if (next.has(key))
|
|
640
|
+
fail("HR004", `A keyed list produced duplicate key \`${toText(key)}\`.`);
|
|
641
|
+
let block = blocks.get(key);
|
|
642
|
+
if (block === undefined) {
|
|
643
|
+
local ??= scope.fork(Object.entries(locals));
|
|
644
|
+
const effectsStart = context.effects.length;
|
|
645
|
+
const rendered = materialize(renderInstance(body, local, document, passThrough, context), document);
|
|
646
|
+
const blockStart = document.createComment("html-next:item-start");
|
|
647
|
+
const blockEnd = document.createComment("html-next:item-end");
|
|
648
|
+
end.before(blockStart, ...rendered, blockEnd);
|
|
649
|
+
block = {
|
|
650
|
+
start: blockStart,
|
|
651
|
+
end: blockEnd,
|
|
652
|
+
scope: local,
|
|
653
|
+
effects: context.effects.slice(effectsStart),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
block.scope.set(flow.item, item);
|
|
658
|
+
if (flow.index !== undefined)
|
|
659
|
+
block.scope.set(flow.index, index);
|
|
660
|
+
block.scope.set("loop", locals.loop);
|
|
661
|
+
}
|
|
662
|
+
next.set(key, block);
|
|
663
|
+
ordered?.push(block);
|
|
664
|
+
previous?.push(oldPositions?.get(key) ?? -1);
|
|
665
|
+
}
|
|
666
|
+
for (const [key, block] of blocks)
|
|
667
|
+
if (!next.has(key))
|
|
668
|
+
removeBlock(block);
|
|
669
|
+
if (ordered !== undefined && previous !== undefined) {
|
|
670
|
+
const stable = stableBlockPositions(previous);
|
|
671
|
+
let reference = end;
|
|
672
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
673
|
+
const block = ordered[index];
|
|
674
|
+
if (previous[index] < 0 || stable !== undefined && stable[index] !== 1) {
|
|
675
|
+
moveBlockBefore(block, reference);
|
|
676
|
+
}
|
|
677
|
+
reference = block.start;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
blocks = next;
|
|
681
|
+
});
|
|
682
|
+
return [fragment];
|
|
683
|
+
}
|
|
684
|
+
function renderNode(node, scope, document, passThrough, context, candidate) {
|
|
685
|
+
if (node.flow?.kind === "if" ||
|
|
686
|
+
node.flow?.kind === "each" ||
|
|
687
|
+
node.flow?.kind === "with" ||
|
|
688
|
+
node.flow?.kind === "match") {
|
|
689
|
+
// Framework renderers retain ownership of structural branches and their
|
|
690
|
+
// reconciliation anchors. During adoption, keep the framework's current
|
|
691
|
+
// node; installing a second reactive branch would invalidate its next
|
|
692
|
+
// update target and can move projected content into the wrong region.
|
|
693
|
+
if (context.committed && context.frameworkOwned && candidate !== undefined)
|
|
694
|
+
return [candidate];
|
|
695
|
+
return renderDynamicNode(node, scope, document, passThrough, context);
|
|
696
|
+
}
|
|
697
|
+
// Only `$when`/`$else` arms remain; outside a `$match` their marker is ignored.
|
|
698
|
+
return renderInstance(node, scope, document, passThrough, context, candidate);
|
|
699
|
+
}
|
|
700
|
+
function renderMatch(node, scope, document, context) {
|
|
701
|
+
const flow = node.flow;
|
|
702
|
+
const matchScope = flow.expr !== undefined ? layer(scope, { [flow.alias]: evalValue(flow.expr, scope) }) : scope;
|
|
703
|
+
let chosen;
|
|
704
|
+
for (const child of node.children) {
|
|
705
|
+
if (child.kind !== "element")
|
|
706
|
+
continue;
|
|
707
|
+
if (child.flow?.kind === "when" && truthy(evalValue(child.flow.test, matchScope))) {
|
|
708
|
+
chosen = child;
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
if (child.flow?.kind === "else") {
|
|
712
|
+
chosen = child;
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (chosen === undefined)
|
|
717
|
+
return [];
|
|
718
|
+
// Render the winning arm, ignoring its own $when/$else marker.
|
|
719
|
+
const { flow: _armFlow, ...armNode } = chosen;
|
|
720
|
+
const rendered = renderInstance(armNode, matchScope, document, [], context);
|
|
721
|
+
if (node.name === "template")
|
|
722
|
+
return rendered;
|
|
723
|
+
// $match on a real element wraps the winning arm in that element.
|
|
724
|
+
const wrapper = createTemplateElement(document, node.name, context);
|
|
725
|
+
for (const attribute of node.attributes) {
|
|
726
|
+
if (attribute.kind === "literal")
|
|
727
|
+
wrapper.setAttribute(attribute.name, attribute.value);
|
|
728
|
+
}
|
|
729
|
+
wrapper.append(...rendered);
|
|
730
|
+
return [wrapper];
|
|
731
|
+
}
|
|
732
|
+
/** Render one instance of a node (its structural flow already resolved) into 0+ nodes. */
|
|
733
|
+
function renderInstance(node, scope, document, passThrough, context, candidate) {
|
|
734
|
+
const contentDirective = node.attributes.find((attribute) => attribute.kind === "directive");
|
|
735
|
+
// A <template> is a fragment carrier: it contributes no wrapper element to the output.
|
|
736
|
+
if (node.name === "template") {
|
|
737
|
+
if (contentDirective !== undefined) {
|
|
738
|
+
const text = document.createTextNode("");
|
|
739
|
+
ownEffect(context, scope, () => {
|
|
740
|
+
const value = evalValue(contentDirective.expression, scope);
|
|
741
|
+
if (contentDirective.name === "value")
|
|
742
|
+
text.data = toText(value);
|
|
743
|
+
});
|
|
744
|
+
return contentDirective.name === "value"
|
|
745
|
+
? [text]
|
|
746
|
+
: [inlineDirective(contentDirective, scope, document)];
|
|
747
|
+
}
|
|
748
|
+
return renderChildren(node.children, scope, document, context);
|
|
749
|
+
}
|
|
750
|
+
const elementName = node === context.definition.template ? context.rootName : node.name;
|
|
751
|
+
if (context.frameworkOwned &&
|
|
752
|
+
candidate instanceof Element &&
|
|
753
|
+
candidate.localName !== elementName &&
|
|
754
|
+
(candidate.getAttribute("data-component") ?? "").split(/\s+/).includes(node.name)) {
|
|
755
|
+
// A framework renders nested declarative components as their native roots,
|
|
756
|
+
// not as the authored invocation tag. That child owns its already-adopted
|
|
757
|
+
// subtree; walking the parent's invocation shape would move its projected
|
|
758
|
+
// nodes into a disconnected synthetic element.
|
|
759
|
+
return [candidate];
|
|
760
|
+
}
|
|
761
|
+
if (context.hydrationRanges !== undefined &&
|
|
762
|
+
!context.frameworkOwned &&
|
|
763
|
+
candidate instanceof Element &&
|
|
764
|
+
candidate.localName !== elementName &&
|
|
765
|
+
(candidate.getAttribute("data-component") ?? "").split(/\s+/).includes(node.name)) {
|
|
766
|
+
// A nested component the server already lowered. Keep its root, and bind this
|
|
767
|
+
// definition's nodes that were projected into it: they sit in the nested root's slot ranges (or its
|
|
768
|
+
// carrier), exactly where lowering put them.
|
|
769
|
+
const nested = serverRanges(candidate, false);
|
|
770
|
+
const slotOf = (child) => child.kind === "element"
|
|
771
|
+
? child.attributes.find((attribute) => attribute.kind === "literal" && attribute.name === "slot")?.value ?? ""
|
|
772
|
+
: "";
|
|
773
|
+
const rendered = nested?.ranges.filter((range) => !range.fallback) ?? [];
|
|
774
|
+
const walk = (children, existing) => {
|
|
775
|
+
let cursor = 0;
|
|
776
|
+
for (const child of children)
|
|
777
|
+
cursor += renderTemplateNode(child, scope, document, context, existing[cursor]).length;
|
|
778
|
+
};
|
|
779
|
+
for (const range of rendered)
|
|
780
|
+
walk(node.children.filter((child) => slotOf(child) === range.slot), range.content);
|
|
781
|
+
const renderedSlots = new Set(rendered.map((range) => range.slot));
|
|
782
|
+
walk(node.children.filter((child) => !renderedSlots.has(slotOf(child))), nested?.carried ?? []);
|
|
783
|
+
return [candidate];
|
|
784
|
+
}
|
|
785
|
+
const adopted = candidate instanceof Element && candidate.localName === elementName;
|
|
786
|
+
const element = adopted ? candidate : createTemplateElement(document, elementName, context);
|
|
787
|
+
if (node === context.definition.template)
|
|
788
|
+
context.root = element;
|
|
789
|
+
const existingChildren = adopted ? Array.from(element.childNodes) : [];
|
|
790
|
+
const controlState = adopted && (element instanceof HTMLInputElement ||
|
|
791
|
+
element instanceof HTMLTextAreaElement ||
|
|
792
|
+
element instanceof HTMLSelectElement) ? {
|
|
793
|
+
value: element.value,
|
|
794
|
+
focused: element.ownerDocument.activeElement === element,
|
|
795
|
+
...(element instanceof HTMLInputElement ? { checked: element.checked } : {}),
|
|
796
|
+
...(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
|
|
797
|
+
? { selectionStart: element.selectionStart, selectionEnd: element.selectionEnd }
|
|
798
|
+
: {}),
|
|
799
|
+
} : undefined;
|
|
800
|
+
if (node.ref !== undefined)
|
|
801
|
+
context.refs[node.ref] = element;
|
|
802
|
+
for (const attribute of node.attributes) {
|
|
803
|
+
if (attribute.kind === "literal")
|
|
804
|
+
element.setAttribute(attribute.name, attribute.value);
|
|
805
|
+
}
|
|
806
|
+
// The invocation's attributes win over the template's literals; class and style combine. Bound
|
|
807
|
+
// attributes, applied next, are the component's own output.
|
|
808
|
+
for (const attribute of passThrough) {
|
|
809
|
+
const own = attribute.name === "class" || attribute.name === "style" ? element.getAttribute(attribute.name) : null;
|
|
810
|
+
element.setAttribute(attribute.name, own === null || own === "" ? attribute.value : `${own}${attribute.name === "class" ? " " : "; "}${attribute.value}`);
|
|
811
|
+
}
|
|
812
|
+
for (const attribute of node.attributes) {
|
|
813
|
+
if (attribute.kind === "attribute") {
|
|
814
|
+
ownEffect(context, scope, () => {
|
|
815
|
+
const value = evalValue(attribute.expression, scope);
|
|
816
|
+
if (attribute.target === "class") {
|
|
817
|
+
element.classList.toggle(attribute.name, truthy(value));
|
|
818
|
+
}
|
|
819
|
+
else if (attribute.target === "style") {
|
|
820
|
+
element.style.setProperty(attribute.name, toText(value));
|
|
821
|
+
}
|
|
822
|
+
else if (attribute.twoWay === true && applyBoundControlValue(element, attribute.name, value)) {
|
|
823
|
+
// Native form-control properties carry the live value; no duplicate attribute write.
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
setAttribute(element, attribute.name, toAttribute(value, attribute.name));
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
if (attribute.twoWay === true && attribute.writablePath !== undefined) {
|
|
830
|
+
const eventName = element instanceof HTMLSelectElement ||
|
|
831
|
+
(element instanceof HTMLInputElement && ["checkbox", "radio", "file"].includes(element.type))
|
|
832
|
+
? "change"
|
|
833
|
+
: "input";
|
|
834
|
+
const listener = () => {
|
|
835
|
+
if (element instanceof HTMLInputElement && element.type === "radio" && !element.checked)
|
|
836
|
+
return;
|
|
837
|
+
setWritablePath(scope, attribute.writablePath, controlValue(element));
|
|
838
|
+
};
|
|
839
|
+
ownEffect(context, scope, () => {
|
|
840
|
+
element.addEventListener(eventName, listener);
|
|
841
|
+
return () => element.removeEventListener(eventName, listener);
|
|
842
|
+
}, 2);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
else if (attribute.kind === "property") {
|
|
846
|
+
ownEffect(context, scope, () => {
|
|
847
|
+
element[attribute.name] = evalValue(attribute.expression, scope);
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
// Content directives are handled below.
|
|
851
|
+
}
|
|
852
|
+
if (contentDirective !== undefined) {
|
|
853
|
+
ownEffect(context, scope, () => applyContent(element, contentDirective, scope, document));
|
|
854
|
+
bindEvents(element, node, scope, context);
|
|
855
|
+
return [element];
|
|
856
|
+
}
|
|
857
|
+
let renderedChildren = [];
|
|
858
|
+
let cursor = 0;
|
|
859
|
+
const childContext = childContextFor(element, context);
|
|
860
|
+
for (const child of node.children) {
|
|
861
|
+
let candidateIndex = cursor;
|
|
862
|
+
if (context.frameworkOwned && child.kind === "element") {
|
|
863
|
+
// Frameworks may retain whitespace, hydration anchors, and branch sentinels between
|
|
864
|
+
// authored elements. Match the framework's owned element by shape instead of treating
|
|
865
|
+
// its raw childNodes offset as the declarative-template offset; otherwise effects and
|
|
866
|
+
// listeners are installed on a disconnected replacement that the framework never uses.
|
|
867
|
+
let matchingIndex = existingChildren.findIndex((candidate, index) => {
|
|
868
|
+
if (index < cursor)
|
|
869
|
+
return false;
|
|
870
|
+
return candidate instanceof Element && (candidate.localName === child.name ||
|
|
871
|
+
(candidate.getAttribute("data-component") ?? "").split(/\s+/).includes(child.name));
|
|
872
|
+
});
|
|
873
|
+
if (matchingIndex < 0 && child.flow !== undefined) {
|
|
874
|
+
matchingIndex = existingChildren.findIndex((candidate, index) => index >= cursor && candidate instanceof Comment);
|
|
875
|
+
}
|
|
876
|
+
if (matchingIndex >= 0)
|
|
877
|
+
candidateIndex = matchingIndex;
|
|
878
|
+
}
|
|
879
|
+
const rendered = renderTemplateNode(child, scope, document, childContext, existingChildren[candidateIndex]);
|
|
880
|
+
renderedChildren.push(...rendered);
|
|
881
|
+
cursor = candidateIndex + rendered.length;
|
|
882
|
+
}
|
|
883
|
+
if (adopted && !context.frameworkOwned) {
|
|
884
|
+
// Structural renderers use DocumentFragments. Reconcile their children,
|
|
885
|
+
// not the fragment carrier, because inserting a fragment consumes it and
|
|
886
|
+
// would otherwise make the following-child count stale.
|
|
887
|
+
renderedChildren = renderedChildren.flatMap((child) => child.nodeType === 11 ? Array.from(child.childNodes) : [child]);
|
|
888
|
+
for (let index = 0; index < renderedChildren.length; index += 1) {
|
|
889
|
+
const expected = renderedChildren[index];
|
|
890
|
+
if (element.childNodes[index] !== expected) {
|
|
891
|
+
element.insertBefore(expected, element.childNodes[index] ?? null);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
while (element.childNodes.length > renderedChildren.length)
|
|
895
|
+
element.lastChild.remove();
|
|
896
|
+
}
|
|
897
|
+
else if (!adopted) {
|
|
898
|
+
element.append(...renderedChildren);
|
|
899
|
+
}
|
|
900
|
+
if (controlState !== undefined) {
|
|
901
|
+
element.value = controlState.value;
|
|
902
|
+
if (element instanceof HTMLInputElement && "checked" in controlState) {
|
|
903
|
+
element.checked = controlState.checked;
|
|
904
|
+
}
|
|
905
|
+
if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) &&
|
|
906
|
+
"selectionStart" in controlState &&
|
|
907
|
+
typeof controlState.selectionStart === "number" &&
|
|
908
|
+
typeof controlState.selectionEnd === "number") {
|
|
909
|
+
if (controlState.focused)
|
|
910
|
+
element.focus({ preventScroll: true });
|
|
911
|
+
element.setSelectionRange(controlState.selectionStart, controlState.selectionEnd);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
bindEvents(element, node, scope, context);
|
|
915
|
+
return [element];
|
|
916
|
+
}
|
|
917
|
+
function renderChildren(children, scope, document, context) {
|
|
918
|
+
const out = [];
|
|
919
|
+
for (const child of children)
|
|
920
|
+
out.push(...renderTemplateNode(child, scope, document, context));
|
|
921
|
+
return out;
|
|
922
|
+
}
|
|
923
|
+
function projectedSlotName(node, context) {
|
|
924
|
+
return context.projectedSlotNames.get(node) ??
|
|
925
|
+
(node instanceof Element ? node.getAttribute("slot") ?? "" : "");
|
|
926
|
+
}
|
|
927
|
+
function renderSlot(node, scope, document, context) {
|
|
928
|
+
const name = node.nameExpression === undefined
|
|
929
|
+
? node.name ?? ""
|
|
930
|
+
: toText(evaluateCompiled(node.nameExpression, scope));
|
|
931
|
+
const assigned = context.projectedNodes.filter((candidate) => projectedSlotName(candidate, context) === name);
|
|
932
|
+
// Rendered form (spec: live-browser-distributable.md, "Rendered form"): every rendered slot is
|
|
933
|
+
// delimited, so server output can rebuild the same instance.
|
|
934
|
+
const hydrating = context.hydrationRanges?.shift();
|
|
935
|
+
if (hydrating !== undefined) {
|
|
936
|
+
// Adopt the server's range whole: its markers, and either the consumer's nodes or the fallback.
|
|
937
|
+
if (hydrating.fallback) {
|
|
938
|
+
const adopted = [];
|
|
939
|
+
let cursor = 0;
|
|
940
|
+
for (const child of node.fallback ?? []) {
|
|
941
|
+
const out = renderTemplateNode(child, scope, document, context, hydrating.content[cursor]);
|
|
942
|
+
adopted.push(...out);
|
|
943
|
+
cursor += out.length;
|
|
944
|
+
}
|
|
945
|
+
return [hydrating.markers[0], ...adopted, ...hydrating.markers.slice(1)];
|
|
946
|
+
}
|
|
947
|
+
for (const candidate of hydrating.content)
|
|
948
|
+
markProjectedRoot(candidate);
|
|
949
|
+
return hydrating.markers.length === 1
|
|
950
|
+
? [hydrating.markers[0]]
|
|
951
|
+
: [hydrating.markers[0], ...hydrating.content, hydrating.markers[1]];
|
|
952
|
+
}
|
|
953
|
+
const quoted = (value) => `"${value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)}"`;
|
|
954
|
+
// Where the parser does not produce PIs, write the comment it would produce, so a lowered DOM and a
|
|
955
|
+
// hydrated DOM hold the same nodes.
|
|
956
|
+
const instruction = (target, data) => renderedFormMark(document, target, data);
|
|
957
|
+
const ranged = (nodes, fallback) => {
|
|
958
|
+
if (nodes.length === 0)
|
|
959
|
+
return [instruction("marker", `slot=${quoted(name)}`)];
|
|
960
|
+
const data = `slot=${quoted(name)}${fallback ? ' fallback=""' : ""}`;
|
|
961
|
+
return [instruction("start", data), ...nodes, instruction("end", "")];
|
|
962
|
+
};
|
|
963
|
+
if (assigned.length === 0) {
|
|
964
|
+
return ranged(renderChildren(node.fallback ?? [], scope, document, context), true);
|
|
965
|
+
}
|
|
966
|
+
if (context.committed) {
|
|
967
|
+
for (const candidate of assigned)
|
|
968
|
+
markProjectedRoot(candidate);
|
|
969
|
+
return ranged([...assigned], false);
|
|
970
|
+
}
|
|
971
|
+
const anchor = document.createComment(`html-next:slot:${name}`);
|
|
972
|
+
context.slotInsertions.push({ anchor, nodes: assigned });
|
|
973
|
+
return ranged([anchor], false);
|
|
974
|
+
}
|
|
975
|
+
function pseudoAttributes(data) {
|
|
976
|
+
const attributes = new Map();
|
|
977
|
+
let rest = data.trim();
|
|
978
|
+
const references = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
979
|
+
while (rest !== "") {
|
|
980
|
+
const match = /^([A-Za-z_:][-A-Za-z0-9._:]*)\s*=\s*(?:"([^"<]*)"|'([^'<]*)')(?:\s+|$)/.exec(rest);
|
|
981
|
+
if (match === null || attributes.has(match[1]))
|
|
982
|
+
return new Map();
|
|
983
|
+
const value = (match[2] ?? match[3]).replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (_, body) => body.startsWith("#x") ? String.fromCodePoint(parseInt(body.slice(2), 16))
|
|
984
|
+
: body.startsWith("#") ? String.fromCodePoint(Number(body.slice(1))) : references[body]);
|
|
985
|
+
attributes.set(match[1], value);
|
|
986
|
+
rest = rest.slice(match[0].length);
|
|
987
|
+
}
|
|
988
|
+
return attributes;
|
|
989
|
+
}
|
|
990
|
+
const piParsingByDocument = new WeakMap();
|
|
991
|
+
function documentParsesInstructions(document) {
|
|
992
|
+
let piParsing = piParsingByDocument.get(document);
|
|
993
|
+
if (piParsing === undefined) {
|
|
994
|
+
const probe = document.createElement("div");
|
|
995
|
+
probe.innerHTML = '<?probe x="1"?>';
|
|
996
|
+
piParsing = probe.firstChild?.nodeType === 7;
|
|
997
|
+
piParsingByDocument.set(document, piParsing);
|
|
998
|
+
}
|
|
999
|
+
return piParsing;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* A rendered-form mark: a processing instruction, or, where the parser does not produce them, the
|
|
1003
|
+
* comment it would produce instead, so a lowered DOM and a hydrated DOM hold the same nodes.
|
|
1004
|
+
*/
|
|
1005
|
+
function renderedFormMark(document, target, data) {
|
|
1006
|
+
return documentParsesInstructions(document)
|
|
1007
|
+
? document.createProcessingInstruction(target, data)
|
|
1008
|
+
: document.createComment(`?${target}${data === "" ? "" : ` ${data}`}?`);
|
|
1009
|
+
}
|
|
1010
|
+
function serverMark(node) {
|
|
1011
|
+
if (node.nodeType === 7) {
|
|
1012
|
+
const pi = node;
|
|
1013
|
+
return { target: pi.target, attributes: pseudoAttributes(pi.data) };
|
|
1014
|
+
}
|
|
1015
|
+
if (node.nodeType !== 8)
|
|
1016
|
+
return undefined;
|
|
1017
|
+
if (documentParsesInstructions(node.ownerDocument))
|
|
1018
|
+
return undefined; // a real comment is never a marker where PIs parse
|
|
1019
|
+
const match = /^\?([A-Za-z][-A-Za-z0-9]*)(?:\s+([\s\S]*?))?\s*\??$/.exec(node.data);
|
|
1020
|
+
return match === null ? undefined : { target: match[1], attributes: pseudoAttributes(match[2] ?? "") };
|
|
1021
|
+
}
|
|
1022
|
+
/** The slot ranges a server-rendered root owns, in document order, and its carried projection. */
|
|
1023
|
+
function serverRanges(root, consume = true) {
|
|
1024
|
+
const ranges = [];
|
|
1025
|
+
const inRanges = new Set();
|
|
1026
|
+
const collect = (nodes, into) => {
|
|
1027
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
1028
|
+
const node = nodes[index];
|
|
1029
|
+
const mark = serverMark(node);
|
|
1030
|
+
if (mark?.target === "marker" && mark.attributes.has("slot")) {
|
|
1031
|
+
into.push({ slot: mark.attributes.get("slot"), fallback: false, markers: [node], content: [] });
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (mark?.target === "start") {
|
|
1035
|
+
let depth = 1;
|
|
1036
|
+
const content = [];
|
|
1037
|
+
let end;
|
|
1038
|
+
for (index += 1; index < nodes.length; index += 1) {
|
|
1039
|
+
const inner = serverMark(nodes[index]);
|
|
1040
|
+
if (inner?.target === "start")
|
|
1041
|
+
depth += 1;
|
|
1042
|
+
else if (inner?.target === "end" && --depth === 0) {
|
|
1043
|
+
end = nodes[index];
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
content.push(nodes[index]);
|
|
1047
|
+
}
|
|
1048
|
+
if (mark.attributes.has("slot")) {
|
|
1049
|
+
into.push({ slot: mark.attributes.get("slot"), fallback: mark.attributes.has("fallback"), markers: end ? [node, end] : [node], content });
|
|
1050
|
+
for (const child of content)
|
|
1051
|
+
inRanges.add(child);
|
|
1052
|
+
}
|
|
1053
|
+
else
|
|
1054
|
+
collect(content, into); // a page's own range is transparent
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (!(node instanceof Element))
|
|
1058
|
+
continue;
|
|
1059
|
+
if (node !== root && node.hasAttribute("data-component")) {
|
|
1060
|
+
const nested = serverRanges(node, false);
|
|
1061
|
+
for (const range of nested?.ranges ?? [])
|
|
1062
|
+
collect(range.content, into);
|
|
1063
|
+
}
|
|
1064
|
+
else
|
|
1065
|
+
collect(Array.from(node.childNodes), into);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
collect(Array.from(root.childNodes), ranges);
|
|
1069
|
+
// The carrier is the <template> child that follows a `carrier` mark, outside every range.
|
|
1070
|
+
const carrier = Array.from(root.children).find((child) => child instanceof HTMLTemplateElement && !inRanges.has(child) &&
|
|
1071
|
+
child.previousSibling !== null && serverMark(child.previousSibling)?.target === "carrier");
|
|
1072
|
+
if (ranges.length === 0 && carrier === undefined)
|
|
1073
|
+
return undefined;
|
|
1074
|
+
const carried = [];
|
|
1075
|
+
if (carrier !== undefined && consume) {
|
|
1076
|
+
for (const child of Array.from(carrier.content.childNodes))
|
|
1077
|
+
carried.push(root.ownerDocument.adoptNode(child));
|
|
1078
|
+
carrier.previousSibling.remove();
|
|
1079
|
+
carrier.remove();
|
|
1080
|
+
}
|
|
1081
|
+
else if (carrier !== undefined)
|
|
1082
|
+
carried.push(...Array.from(carrier.content.childNodes));
|
|
1083
|
+
return { ranges, carried };
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Serializes the rendered form. Like getHTML({ serializableShadowRoots }), it writes what
|
|
1087
|
+
* the live DOM does not hold: each component root's projected nodes that no slot currently renders,
|
|
1088
|
+
* in an inert trailing <template>.
|
|
1089
|
+
*/
|
|
1090
|
+
export function serializeRenderedForm(container) {
|
|
1091
|
+
const clone = container.cloneNode(true);
|
|
1092
|
+
const originals = [container, ...Array.from(container.querySelectorAll("*"))];
|
|
1093
|
+
const copies = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
1094
|
+
originals.forEach((original, index) => {
|
|
1095
|
+
const projection = runtimeInstance(original)?.projection;
|
|
1096
|
+
if (projection === undefined)
|
|
1097
|
+
return;
|
|
1098
|
+
const unrendered = projection.nodes.filter((node) => !original.contains(node));
|
|
1099
|
+
if (unrendered.length === 0)
|
|
1100
|
+
return;
|
|
1101
|
+
const carrier = clone.ownerDocument.createElement("template");
|
|
1102
|
+
for (const node of unrendered)
|
|
1103
|
+
carrier.content.append(node.cloneNode(true));
|
|
1104
|
+
copies[index].append(renderedFormMark(clone.ownerDocument, "carrier", ""), carrier);
|
|
1105
|
+
});
|
|
1106
|
+
return clone.innerHTML;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Diagnostic: an instance's internal shape (definition, explicit props, prop values, projected nodes per
|
|
1110
|
+
* slot). Two instances with equal shapes behave identically; conformance tests compare them.
|
|
1111
|
+
*/
|
|
1112
|
+
export function inspectInstance(element) {
|
|
1113
|
+
const instance = runtimeInstance(element);
|
|
1114
|
+
if (instance === undefined)
|
|
1115
|
+
return undefined;
|
|
1116
|
+
const props = {};
|
|
1117
|
+
for (const name of Object.keys(instance.definition.contract.props))
|
|
1118
|
+
props[name] = instance.scope.get(name);
|
|
1119
|
+
const slots = {};
|
|
1120
|
+
for (const node of instance.projection?.nodes ?? []) {
|
|
1121
|
+
const slot = instance.projection.slotNames.get(node) ?? (node instanceof Element ? node.getAttribute("slot") ?? "" : "");
|
|
1122
|
+
(slots[slot] ??= []).push(node instanceof Element ? node.outerHTML.replace(/ data-slotted=""/g, "") : node.textContent ?? "");
|
|
1123
|
+
}
|
|
1124
|
+
// Order across slots is not observable; order within a slot is.
|
|
1125
|
+
const sorted = Object.fromEntries(Object.entries(slots).sort(([a], [b]) => a.localeCompare(b)));
|
|
1126
|
+
return { tag: instance.definition.contract.tag, explicit: [...instance.explicit].sort(), props, slots: sorted };
|
|
1127
|
+
}
|
|
1128
|
+
function renderTemplateNode(node, scope, document, context, candidate) {
|
|
1129
|
+
if (node.kind === "text") {
|
|
1130
|
+
const text = candidate instanceof Text ? candidate : document.createTextNode("");
|
|
1131
|
+
text.data = node.value;
|
|
1132
|
+
return [text];
|
|
1133
|
+
}
|
|
1134
|
+
if (node.kind === "slot")
|
|
1135
|
+
return renderSlot(node, scope, document, context);
|
|
1136
|
+
return renderNode(node, scope, document, [], context, candidate);
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Keeps `data-<tag>-state` in step with the resolved props and state the definition's `:host-state()`
|
|
1140
|
+
* rules test, so styles see defaults and state as well as explicit props.
|
|
1141
|
+
*/
|
|
1142
|
+
function installStateAttribute(root, instance) {
|
|
1143
|
+
const names = stateNamesByDefinition.get(instance.definition) ?? [];
|
|
1144
|
+
if (names.length === 0)
|
|
1145
|
+
return;
|
|
1146
|
+
const attribute = stateAttribute(instance.definition.contract.tag);
|
|
1147
|
+
instance.effects.push(createEffect(instance.scope.scheduler, () => {
|
|
1148
|
+
const value = stateAttributeValue(names, (name) => instance.scope.get(name));
|
|
1149
|
+
if (value === "")
|
|
1150
|
+
root.removeAttribute(attribute);
|
|
1151
|
+
else if (root.getAttribute(attribute) !== value)
|
|
1152
|
+
root.setAttribute(attribute, value);
|
|
1153
|
+
}, 2));
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Reflects the author's explicit props onto the lowered root as `data-<name>` so the element records
|
|
1157
|
+
* which options produced it (and server output can be hydrated). Defaults are never written, and no
|
|
1158
|
+
* JavaScript properties are added: the root keeps its native properties untouched. Later attribute
|
|
1159
|
+
* writes by the author are parsed back into the scope.
|
|
1160
|
+
*/
|
|
1161
|
+
function installPropReflection(root, instance) {
|
|
1162
|
+
const props = instance.definition.contract.props;
|
|
1163
|
+
const attributeNames = {};
|
|
1164
|
+
const reflected = {};
|
|
1165
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
1166
|
+
const attributeName = `data-${kebabCase(name)}`;
|
|
1167
|
+
attributeNames[attributeName] = name;
|
|
1168
|
+
// When the template binds this attribute itself it is template output and always shows the
|
|
1169
|
+
// effective value (defaults included), matching the compiled runtime's `bound` props.
|
|
1170
|
+
const bound = instance.definition.template.attributes.some((binding) => binding.kind === "attribute" && binding.name === attributeName);
|
|
1171
|
+
instance.effects.push(createEffect(instance.scope.scheduler, () => {
|
|
1172
|
+
const value = instance.scope.get(name);
|
|
1173
|
+
if (!bound && !instance.explicit.has(name))
|
|
1174
|
+
return;
|
|
1175
|
+
// Null is "no value" at the attribute boundary: it removes the attribute rather than
|
|
1176
|
+
// writing text that would not parse back.
|
|
1177
|
+
const serialized = value === undefined || value === ABSENT || value === null
|
|
1178
|
+
? null
|
|
1179
|
+
: serializeTypedValue(value, prop.type);
|
|
1180
|
+
reflected[attributeName] = serialized;
|
|
1181
|
+
if (serialized === null)
|
|
1182
|
+
root.removeAttribute(attributeName);
|
|
1183
|
+
else
|
|
1184
|
+
root.setAttribute(attributeName, serialized);
|
|
1185
|
+
}, 2));
|
|
1186
|
+
}
|
|
1187
|
+
const Observer = root.ownerDocument.defaultView?.MutationObserver;
|
|
1188
|
+
const attributeFilter = Object.keys(attributeNames);
|
|
1189
|
+
if (Observer === undefined || attributeFilter.length === 0)
|
|
1190
|
+
return;
|
|
1191
|
+
const observer = new Observer((records) => {
|
|
1192
|
+
for (const record of records) {
|
|
1193
|
+
const attributeName = record.attributeName;
|
|
1194
|
+
if (attributeName === null)
|
|
1195
|
+
continue;
|
|
1196
|
+
const name = attributeNames[attributeName];
|
|
1197
|
+
if (name === undefined)
|
|
1198
|
+
continue;
|
|
1199
|
+
const value = root.getAttribute(attributeName);
|
|
1200
|
+
if (attributeName in reflected && reflected[attributeName] === value) {
|
|
1201
|
+
delete reflected[attributeName];
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
delete reflected[attributeName];
|
|
1205
|
+
const prop = props[name];
|
|
1206
|
+
if (value === null)
|
|
1207
|
+
instance.explicit.delete(name);
|
|
1208
|
+
else
|
|
1209
|
+
instance.explicit.add(name);
|
|
1210
|
+
instance.scope.set(name, value === null ? assignedPropValue(name, prop, undefined) : invocationValue(prop, value, true));
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
const connect = () => observer.observe(root, {
|
|
1214
|
+
attributes: true,
|
|
1215
|
+
attributeFilter,
|
|
1216
|
+
});
|
|
1217
|
+
const disconnect = () => observer.disconnect();
|
|
1218
|
+
instance.connectCallbacks.add(connect);
|
|
1219
|
+
instance.disconnectCallbacks.add(disconnect);
|
|
1220
|
+
}
|
|
1221
|
+
function installPublicMethods(root, instance) {
|
|
1222
|
+
for (const declaration of instance.definition.declarations ?? []) {
|
|
1223
|
+
if (declaration.kind !== "method")
|
|
1224
|
+
continue;
|
|
1225
|
+
Object.defineProperty(root, declaration.name, {
|
|
1226
|
+
configurable: true,
|
|
1227
|
+
enumerable: false,
|
|
1228
|
+
value: (...args) => {
|
|
1229
|
+
if (instance.controllerModule === undefined) {
|
|
1230
|
+
return Promise.reject(new TypeError(`Controller method \`${declaration.name}\` is not ready for <${instance.definition.contract.tag}>.`));
|
|
1231
|
+
}
|
|
1232
|
+
return instance.controllerModule.then((module) => {
|
|
1233
|
+
const method = module[declaration.exportName];
|
|
1234
|
+
if (typeof method !== "function") {
|
|
1235
|
+
fail("HJ003", `Controller does not export method \`${declaration.exportName}\`.`);
|
|
1236
|
+
}
|
|
1237
|
+
return Reflect.apply(method, undefined, [getComponentHost(root), ...args]);
|
|
1238
|
+
});
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function prepareRuntimeInvocation(invocation, definition, hydration, projectedNodes, projectedSlotNames = new WeakMap(), frameworkOwned = false) {
|
|
1244
|
+
const focusedControl = hydration && invocation.contains(invocation.ownerDocument.activeElement)
|
|
1245
|
+
? invocation.ownerDocument.activeElement
|
|
1246
|
+
: null;
|
|
1247
|
+
const focusedSelection = focusedControl instanceof HTMLInputElement || focusedControl instanceof HTMLTextAreaElement
|
|
1248
|
+
? [focusedControl.selectionStart, focusedControl.selectionEnd]
|
|
1249
|
+
: undefined;
|
|
1250
|
+
const { scope, passThrough, effects, rootName, explicit } = readInvocation(invocation, definition, hydration);
|
|
1251
|
+
let hydratedNodes = projectedNodes;
|
|
1252
|
+
let hydrationRanges;
|
|
1253
|
+
if (hydration && hydratedNodes === undefined) {
|
|
1254
|
+
const server = serverRanges(invocation);
|
|
1255
|
+
if (server !== undefined) {
|
|
1256
|
+
// The rendered form names every slot range, and the serializer carried the
|
|
1257
|
+
// projected nodes no slot currently renders. Together they are the authored projection.
|
|
1258
|
+
hydrationRanges = server.ranges;
|
|
1259
|
+
const nodes = [];
|
|
1260
|
+
for (const range of server.ranges) {
|
|
1261
|
+
if (range.fallback)
|
|
1262
|
+
continue;
|
|
1263
|
+
for (const node of range.content) {
|
|
1264
|
+
projectedSlotNames.set(node, range.slot);
|
|
1265
|
+
nodes.push(node);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
nodes.push(...server.carried);
|
|
1269
|
+
hydratedNodes = nodes;
|
|
1270
|
+
}
|
|
1271
|
+
else if ((definition.slots ?? []).length > 0) {
|
|
1272
|
+
// Only the root carries a component marker, so nothing tells template output from projected
|
|
1273
|
+
// content without slot marks. Guessing would build a different instance.
|
|
1274
|
+
fail("HR005", `<${definition.contract.tag}> has slots but its server-rendered root has no slot marks.`);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
hydratedNodes = [];
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const children = hydration ? hydratedNodes : Array.from(invocation.childNodes);
|
|
1281
|
+
const instance = {
|
|
1282
|
+
definition,
|
|
1283
|
+
scope,
|
|
1284
|
+
refs: {},
|
|
1285
|
+
effects,
|
|
1286
|
+
connectCallbacks: new Set(),
|
|
1287
|
+
disconnectCallbacks: new Set(),
|
|
1288
|
+
connected: false,
|
|
1289
|
+
explicit,
|
|
1290
|
+
frameworkOwned,
|
|
1291
|
+
projection: { nodes: hydration ? hydratedNodes : Array.from(invocation.childNodes), slotNames: projectedSlotNames },
|
|
1292
|
+
};
|
|
1293
|
+
const context = {
|
|
1294
|
+
definition,
|
|
1295
|
+
effects,
|
|
1296
|
+
refs: instance.refs,
|
|
1297
|
+
connectCallbacks: instance.connectCallbacks,
|
|
1298
|
+
disconnectCallbacks: instance.disconnectCallbacks,
|
|
1299
|
+
projectedNodes: children,
|
|
1300
|
+
projectedSlotNames,
|
|
1301
|
+
slotInsertions: [],
|
|
1302
|
+
rootName,
|
|
1303
|
+
frameworkOwned,
|
|
1304
|
+
committed: hydration,
|
|
1305
|
+
hydrationRanges,
|
|
1306
|
+
};
|
|
1307
|
+
const rendered = renderNode(definition.template, scope, invocation.ownerDocument, passThrough, context, hydration ? invocation : undefined);
|
|
1308
|
+
context.hydrationRanges = undefined;
|
|
1309
|
+
const nativeRoot = rendered[0];
|
|
1310
|
+
if (hydration && nativeRoot !== invocation) {
|
|
1311
|
+
fail("HR005", `Server markup for <${definition.contract.tag}> has an incompatible root.`);
|
|
1312
|
+
}
|
|
1313
|
+
if (focusedControl instanceof HTMLElement) {
|
|
1314
|
+
focusedControl.focus({ preventScroll: true });
|
|
1315
|
+
if (focusedSelection !== undefined &&
|
|
1316
|
+
(focusedControl instanceof HTMLInputElement || focusedControl instanceof HTMLTextAreaElement) &&
|
|
1317
|
+
typeof focusedSelection[0] === "number" &&
|
|
1318
|
+
typeof focusedSelection[1] === "number")
|
|
1319
|
+
focusedControl.setSelectionRange(focusedSelection[0], focusedSelection[1]);
|
|
1320
|
+
}
|
|
1321
|
+
addAttributeToken(nativeRoot, COMPONENT_ATTRIBUTE, definition.contract.tag);
|
|
1322
|
+
instance.element = nativeRoot;
|
|
1323
|
+
return {
|
|
1324
|
+
invocation,
|
|
1325
|
+
nativeRoot,
|
|
1326
|
+
context,
|
|
1327
|
+
definition,
|
|
1328
|
+
instance,
|
|
1329
|
+
replace: !hydration,
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function commitRuntimeInvocations(registry, prepared) {
|
|
1333
|
+
// Commit ancestors first so their slot insertion moves nested live invocations before
|
|
1334
|
+
// descendants replace themselves. Discovery order does not determine nested survival.
|
|
1335
|
+
prepared.sort((left, right) => {
|
|
1336
|
+
if (left.invocation.contains(right.invocation))
|
|
1337
|
+
return -1;
|
|
1338
|
+
if (right.invocation.contains(left.invocation))
|
|
1339
|
+
return 1;
|
|
1340
|
+
return 0;
|
|
1341
|
+
});
|
|
1342
|
+
for (const invocation of prepared) {
|
|
1343
|
+
for (const insertion of invocation.context.slotInsertions) {
|
|
1344
|
+
for (const child of insertion.nodes)
|
|
1345
|
+
markProjectedRoot(child);
|
|
1346
|
+
insertion.anchor.replaceWith(...insertion.nodes);
|
|
1347
|
+
}
|
|
1348
|
+
if (invocation.replace)
|
|
1349
|
+
invocation.invocation.replaceWith(invocation.nativeRoot);
|
|
1350
|
+
invocation.context.committed = true;
|
|
1351
|
+
runtimeInstances.set(invocation.nativeRoot, invocation.instance);
|
|
1352
|
+
installPropReflection(invocation.nativeRoot, invocation.instance);
|
|
1353
|
+
installStateAttribute(invocation.nativeRoot, invocation.instance);
|
|
1354
|
+
installPublicMethods(invocation.nativeRoot, invocation.instance);
|
|
1355
|
+
connectRuntimeInstance(invocation.instance);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function collectWithin(scope, selector, elements) {
|
|
1359
|
+
if (scope.nodeType === 1 && scope.matches(selector))
|
|
1360
|
+
elements.add(scope);
|
|
1361
|
+
for (const element of scope.querySelectorAll(selector))
|
|
1362
|
+
elements.add(element);
|
|
1363
|
+
}
|
|
1364
|
+
function visitComponentRoots(scope, visit) {
|
|
1365
|
+
const element = scope.nodeType === 1 ? scope : undefined;
|
|
1366
|
+
if (element?.matches("[data-component]") === true)
|
|
1367
|
+
visit(element);
|
|
1368
|
+
if (element?.childElementCount === 0)
|
|
1369
|
+
return;
|
|
1370
|
+
for (const descendant of scope.querySelectorAll("[data-component]"))
|
|
1371
|
+
visit(descendant);
|
|
1372
|
+
}
|
|
1373
|
+
function lowerScopes(root, scopes, shouldLower) {
|
|
1374
|
+
const registry = registryFor(root);
|
|
1375
|
+
const discovered = new Set();
|
|
1376
|
+
const selector = discoverySelector(registry);
|
|
1377
|
+
for (const scope of scopes)
|
|
1378
|
+
collectWithin(scope, selector, discovered);
|
|
1379
|
+
const definitions = [];
|
|
1380
|
+
for (const element of discovered) {
|
|
1381
|
+
if (element.localName === "template" && element.hasAttribute("component") && !contentOnly.has(element)) {
|
|
1382
|
+
definitions.push(parseDefinition(element, definitions.length));
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const newDefinitions = new Map();
|
|
1386
|
+
for (const live of definitions) {
|
|
1387
|
+
const tag = live.definition.contract.tag;
|
|
1388
|
+
if (registry.definitions.has(tag) || newDefinitions.has(tag)) {
|
|
1389
|
+
fail("HR001", `More than one definition declares <${tag}>.`);
|
|
1390
|
+
}
|
|
1391
|
+
newDefinitions.set(tag, live);
|
|
1392
|
+
}
|
|
1393
|
+
const prepared = [];
|
|
1394
|
+
const roots = new Set();
|
|
1395
|
+
const prepare = (live, element, hydration) => {
|
|
1396
|
+
const { definition } = live;
|
|
1397
|
+
if (contentOnly.has(element) ||
|
|
1398
|
+
root.defaultView?.customElements.get(definition.contract.tag) !== undefined ||
|
|
1399
|
+
shouldLower?.(element, definition, hydration) === false)
|
|
1400
|
+
return false;
|
|
1401
|
+
prepared.push(prepareRuntimeInvocation(element, definition, hydration));
|
|
1402
|
+
return true;
|
|
1403
|
+
};
|
|
1404
|
+
const collect = (byTag, elements) => {
|
|
1405
|
+
for (const element of elements) {
|
|
1406
|
+
const live = byTag.get(element.localName);
|
|
1407
|
+
if (live !== undefined)
|
|
1408
|
+
prepare(live, element, false);
|
|
1409
|
+
if (!element.hasAttribute("data-component"))
|
|
1410
|
+
continue;
|
|
1411
|
+
const existing = runtimeInstance(element);
|
|
1412
|
+
if (existing !== undefined) {
|
|
1413
|
+
if (!existing.frameworkOwned)
|
|
1414
|
+
roots.add(element);
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
let accepted = false;
|
|
1418
|
+
for (const tag of new Set((element.getAttribute("data-component") ?? "").split(/\s+/))) {
|
|
1419
|
+
const owner = byTag.get(tag);
|
|
1420
|
+
if (owner !== undefined && prepare(owner, element, true))
|
|
1421
|
+
accepted = true;
|
|
1422
|
+
}
|
|
1423
|
+
if (accepted)
|
|
1424
|
+
roots.add(element);
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
collect(registry.definitions, discovered);
|
|
1428
|
+
// A newly discovered definition also applies to matching invocations that predate it.
|
|
1429
|
+
if (newDefinitions.size > 0) {
|
|
1430
|
+
const selector = [
|
|
1431
|
+
"[data-component]",
|
|
1432
|
+
...Array.from(newDefinitions.keys()),
|
|
1433
|
+
].join(",");
|
|
1434
|
+
const existing = new Set();
|
|
1435
|
+
collectWithin(root, selector, existing);
|
|
1436
|
+
collect(newDefinitions, existing);
|
|
1437
|
+
}
|
|
1438
|
+
for (const live of definitions) {
|
|
1439
|
+
registerDefinition(registry, live.definition.contract.tag, live);
|
|
1440
|
+
if (live.style !== undefined) {
|
|
1441
|
+
live.style.textContent = compileStyles(live.style.textContent ?? "", live.definition, live.wrapper.ownerDocument);
|
|
1442
|
+
live.wrapper.ownerDocument.head.append(live.style);
|
|
1443
|
+
}
|
|
1444
|
+
live.wrapper.remove();
|
|
1445
|
+
}
|
|
1446
|
+
commitRuntimeInvocations(registry, prepared);
|
|
1447
|
+
const lowered = prepared.map((invocation) => invocation.nativeRoot);
|
|
1448
|
+
for (const element of lowered)
|
|
1449
|
+
roots.add(element);
|
|
1450
|
+
return { lowered, roots: Array.from(roots) };
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Performs one explicit lowering pass, retaining definitions in a document registry for
|
|
1454
|
+
* later passes. It does not observe mutations or register Custom Elements.
|
|
1455
|
+
*/
|
|
1456
|
+
export function lowerDocument(root = document) {
|
|
1457
|
+
return lowerScopes(root, [root]).lowered.length;
|
|
1458
|
+
}
|
|
1459
|
+
function subscribeDocumentMutations(root, subscriber) {
|
|
1460
|
+
const state = documentState(root);
|
|
1461
|
+
let hub = state.mutationHub;
|
|
1462
|
+
if (hub === undefined) {
|
|
1463
|
+
const Observer = root.defaultView?.MutationObserver;
|
|
1464
|
+
if (Observer === undefined) {
|
|
1465
|
+
fail("HR003", "Automatic component management requires a browser MutationObserver.");
|
|
1466
|
+
}
|
|
1467
|
+
const subscribers = new Set();
|
|
1468
|
+
const observer = new Observer((mutations) => {
|
|
1469
|
+
for (const notify of Array.from(subscribers))
|
|
1470
|
+
notify(mutations);
|
|
1471
|
+
});
|
|
1472
|
+
hub = { observer, subscribers };
|
|
1473
|
+
state.mutationHub = hub;
|
|
1474
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
1475
|
+
}
|
|
1476
|
+
hub.subscribers.add(subscriber);
|
|
1477
|
+
let subscribed = true;
|
|
1478
|
+
return () => {
|
|
1479
|
+
if (!subscribed)
|
|
1480
|
+
return;
|
|
1481
|
+
subscribed = false;
|
|
1482
|
+
hub.subscribers.delete(subscriber);
|
|
1483
|
+
if (hub.subscribers.size === 0) {
|
|
1484
|
+
hub.observer.disconnect();
|
|
1485
|
+
delete state.mutationHub;
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function coordinatorFor(root) {
|
|
1490
|
+
const state = documentState(root);
|
|
1491
|
+
const existing = state.lifecycle;
|
|
1492
|
+
if (existing !== undefined)
|
|
1493
|
+
return existing;
|
|
1494
|
+
let size = 0;
|
|
1495
|
+
const synchronize = (element) => {
|
|
1496
|
+
const record = element[lifecycleKey];
|
|
1497
|
+
if (record === undefined)
|
|
1498
|
+
return;
|
|
1499
|
+
if (element.isConnected && record.disconnect === undefined) {
|
|
1500
|
+
record.disconnect = record.connect(element);
|
|
1501
|
+
}
|
|
1502
|
+
else if (!element.isConnected && record.disconnect !== undefined) {
|
|
1503
|
+
record.disconnect();
|
|
1504
|
+
record.disconnect = undefined;
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
const stopObservation = subscribeDocumentMutations(root, (mutations) => {
|
|
1508
|
+
const changed = [];
|
|
1509
|
+
const collect = (node) => {
|
|
1510
|
+
if (node.nodeType !== 1)
|
|
1511
|
+
return;
|
|
1512
|
+
const element = node;
|
|
1513
|
+
if (element[lifecycleKey] !== undefined)
|
|
1514
|
+
changed.push(element);
|
|
1515
|
+
visitComponentRoots(element, (descendant) => {
|
|
1516
|
+
if (descendant[lifecycleKey] !== undefined)
|
|
1517
|
+
changed.push(descendant);
|
|
1518
|
+
});
|
|
1519
|
+
};
|
|
1520
|
+
for (const mutation of mutations) {
|
|
1521
|
+
for (const node of mutation.addedNodes)
|
|
1522
|
+
collect(node);
|
|
1523
|
+
for (const node of mutation.removedNodes)
|
|
1524
|
+
collect(node);
|
|
1525
|
+
}
|
|
1526
|
+
for (const element of changed)
|
|
1527
|
+
synchronize(element);
|
|
1528
|
+
});
|
|
1529
|
+
const coordinator = {
|
|
1530
|
+
add(element, record) {
|
|
1531
|
+
const target = element;
|
|
1532
|
+
const previous = target[lifecycleKey];
|
|
1533
|
+
if (previous === record)
|
|
1534
|
+
return;
|
|
1535
|
+
previous?.disconnect?.();
|
|
1536
|
+
if (previous === undefined)
|
|
1537
|
+
size += 1;
|
|
1538
|
+
target[lifecycleKey] = record;
|
|
1539
|
+
synchronize(element);
|
|
1540
|
+
},
|
|
1541
|
+
remove(element, record) {
|
|
1542
|
+
const target = element;
|
|
1543
|
+
if (target[lifecycleKey] !== record)
|
|
1544
|
+
return;
|
|
1545
|
+
record.disconnect?.();
|
|
1546
|
+
delete target[lifecycleKey];
|
|
1547
|
+
size -= 1;
|
|
1548
|
+
if (size === 0) {
|
|
1549
|
+
stopObservation();
|
|
1550
|
+
delete state.lifecycle;
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
};
|
|
1554
|
+
state.lifecycle = coordinator;
|
|
1555
|
+
return coordinator;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Gives generated Vanilla roots native-like connection lifecycle without installing one
|
|
1559
|
+
* observer per instance. Runtime copies in the same realm share one coordinator per document.
|
|
1560
|
+
*/
|
|
1561
|
+
export function manageComponentLifecycle(element, definition, options = {}) {
|
|
1562
|
+
const coordinator = coordinatorFor(element.ownerDocument);
|
|
1563
|
+
const record = {
|
|
1564
|
+
connect: (current) => attachComponent(current, definition, options),
|
|
1565
|
+
disconnect: undefined,
|
|
1566
|
+
};
|
|
1567
|
+
coordinator.add(element, record);
|
|
1568
|
+
let stopped = false;
|
|
1569
|
+
return () => {
|
|
1570
|
+
if (stopped)
|
|
1571
|
+
return;
|
|
1572
|
+
stopped = true;
|
|
1573
|
+
coordinator.remove(element, record);
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
/** Registers already parsed package definitions without manufacturing live `<template>` nodes. */
|
|
1577
|
+
export function registerComponentDefinitions(definitions, root = document) {
|
|
1578
|
+
const registry = registryFor(root);
|
|
1579
|
+
for (const definition of definitions) {
|
|
1580
|
+
const existing = registry.definitions.get(definition.contract.tag);
|
|
1581
|
+
if (existing !== undefined) {
|
|
1582
|
+
if (JSON.stringify(existing.definition) !== JSON.stringify(definition)) {
|
|
1583
|
+
fail("HR001", `More than one definition declares <${definition.contract.tag}>.`);
|
|
1584
|
+
}
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
registerDefinition(registry, definition.contract.tag, {
|
|
1588
|
+
definition,
|
|
1589
|
+
style: undefined,
|
|
1590
|
+
});
|
|
1591
|
+
if (definition.css !== "") {
|
|
1592
|
+
const style = root.createElement("style");
|
|
1593
|
+
style.textContent = compileStyles(definition.css, definition, root);
|
|
1594
|
+
root.head.append(style);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Framework-host adapter. The framework emits the declared native root and owns its outer
|
|
1600
|
+
* lifetime; this function adopts that root into the same runtime used by live HTML.
|
|
1601
|
+
*/
|
|
1602
|
+
export function attachComponent(element, definition, options = {}) {
|
|
1603
|
+
const root = element.ownerDocument;
|
|
1604
|
+
const registry = registryFor(root);
|
|
1605
|
+
const existing = registry.definitions.get(definition.contract.tag);
|
|
1606
|
+
if (existing === undefined) {
|
|
1607
|
+
registerDefinition(registry, definition.contract.tag, {
|
|
1608
|
+
definition,
|
|
1609
|
+
style: undefined,
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
else if (JSON.stringify(existing.definition) !== JSON.stringify(definition)) {
|
|
1613
|
+
fail("HR001", `More than one definition declares <${definition.contract.tag}>.`);
|
|
1614
|
+
}
|
|
1615
|
+
// Server-rendered roots are claimed by whichever arrives first. When document observation hydrated
|
|
1616
|
+
// this root before its framework attached, the framework takes it over: hydration leaves the DOM
|
|
1617
|
+
// as rendered, so the observer's instance (and its controller) is released and the root is
|
|
1618
|
+
// re-attached as framework-owned.
|
|
1619
|
+
const observed = runtimeInstance(element);
|
|
1620
|
+
if (observed !== undefined && !observed.frameworkOwned) {
|
|
1621
|
+
documentState(root).release?.(element);
|
|
1622
|
+
disconnectRuntimeInstance(observed);
|
|
1623
|
+
runtimeInstances.delete(element);
|
|
1624
|
+
}
|
|
1625
|
+
const instance = runtimeInstance(element);
|
|
1626
|
+
if (instance === undefined) {
|
|
1627
|
+
const projected = (options.projected ?? []).map(([node]) => node);
|
|
1628
|
+
const projectedSlotNames = new WeakMap();
|
|
1629
|
+
for (const [node, slot] of options.projected ?? []) {
|
|
1630
|
+
projectedSlotNames.set(node, slot);
|
|
1631
|
+
markProjectedRoot(node);
|
|
1632
|
+
}
|
|
1633
|
+
addAttributeToken(element, COMPONENT_ATTRIBUTE, definition.contract.tag);
|
|
1634
|
+
// The framework's explicit props become the same data-* attributes hydration reads; defaults
|
|
1635
|
+
// stay implicit, exactly as for HTML authors.
|
|
1636
|
+
for (const [name, prop] of Object.entries(definition.contract.props)) {
|
|
1637
|
+
const value = options.props?.[name];
|
|
1638
|
+
if (value !== undefined && value !== null)
|
|
1639
|
+
element.setAttribute(`data-${kebabCase(name)}`, serializeTypedValue(value, prop.type));
|
|
1640
|
+
}
|
|
1641
|
+
commitRuntimeInvocations(registry, [
|
|
1642
|
+
prepareRuntimeInvocation(element, definition, true, projected, projectedSlotNames, true),
|
|
1643
|
+
]);
|
|
1644
|
+
}
|
|
1645
|
+
const attached = runtimeInstance(element);
|
|
1646
|
+
if (attached === undefined)
|
|
1647
|
+
fail("HR005", `Could not attach <${definition.contract.tag}> to its native root.`);
|
|
1648
|
+
updateComponentProps(element, options.props ?? {});
|
|
1649
|
+
connectRuntimeInstance(attached);
|
|
1650
|
+
let controllerCleanup;
|
|
1651
|
+
let disposed = false;
|
|
1652
|
+
if (options.controller !== undefined) {
|
|
1653
|
+
const module = Promise.resolve(options.controller);
|
|
1654
|
+
setControllerModule(element, module);
|
|
1655
|
+
void Promise.resolve(options.controller.default(getComponentHost(element))).then((cleanup) => {
|
|
1656
|
+
if (typeof cleanup !== "function")
|
|
1657
|
+
return;
|
|
1658
|
+
if (disposed)
|
|
1659
|
+
cleanup();
|
|
1660
|
+
else
|
|
1661
|
+
controllerCleanup = cleanup;
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
return () => {
|
|
1665
|
+
if (disposed)
|
|
1666
|
+
return;
|
|
1667
|
+
disposed = true;
|
|
1668
|
+
controllerCleanup?.();
|
|
1669
|
+
disconnectRuntimeInstance(attached);
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* The framework-adapter prop channel. A framework's props are the equivalent of authored
|
|
1674
|
+
* attributes: each defined value becomes explicit (and is reflected as `data-<name>`), and
|
|
1675
|
+
* `undefined` returns the prop to its implicit default. This is not a page-authoring API.
|
|
1676
|
+
*/
|
|
1677
|
+
export function updateComponentProps(element, props) {
|
|
1678
|
+
const instance = runtimeInstance(element);
|
|
1679
|
+
if (instance === undefined)
|
|
1680
|
+
return;
|
|
1681
|
+
for (const [name, input] of Object.entries(props)) {
|
|
1682
|
+
const prop = instance.definition.contract.props[name];
|
|
1683
|
+
if (prop === undefined)
|
|
1684
|
+
continue;
|
|
1685
|
+
const attributeName = `data-${kebabCase(name)}`;
|
|
1686
|
+
const value = assignedPropValue(name, prop, input);
|
|
1687
|
+
// Null has no attribute form: like undefined, it leaves no explicit data-* attribute.
|
|
1688
|
+
if (input === undefined || input === null) {
|
|
1689
|
+
instance.explicit.delete(name);
|
|
1690
|
+
// An attribute the template binds is its own output (it shows the default); leave it be.
|
|
1691
|
+
const bound = instance.definition.template.attributes.some((binding) => binding.kind === "attribute" && binding.name === attributeName);
|
|
1692
|
+
if (!bound)
|
|
1693
|
+
element.removeAttribute(attributeName);
|
|
1694
|
+
}
|
|
1695
|
+
else {
|
|
1696
|
+
instance.explicit.add(name);
|
|
1697
|
+
element.setAttribute(attributeName, serializeTypedValue(input, prop.type));
|
|
1698
|
+
}
|
|
1699
|
+
if (!Object.is(instance.scope.get(name), value))
|
|
1700
|
+
instance.scope.set(name, value);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
function connectRuntimeInstance(instance) {
|
|
1704
|
+
if (instance.connected)
|
|
1705
|
+
return;
|
|
1706
|
+
instance.connected = true;
|
|
1707
|
+
for (const effect of instance.effects)
|
|
1708
|
+
effect.resume();
|
|
1709
|
+
for (const callback of instance.connectCallbacks)
|
|
1710
|
+
callback();
|
|
1711
|
+
instance.element?.dispatchEvent(new Event("connect"));
|
|
1712
|
+
}
|
|
1713
|
+
function disconnectRuntimeInstance(instance) {
|
|
1714
|
+
if (!instance.connected)
|
|
1715
|
+
return;
|
|
1716
|
+
instance.element?.dispatchEvent(new Event("disconnect"));
|
|
1717
|
+
for (const callback of instance.disconnectCallbacks)
|
|
1718
|
+
callback();
|
|
1719
|
+
instance.connected = false;
|
|
1720
|
+
for (const effect of instance.effects)
|
|
1721
|
+
effect.pause();
|
|
1722
|
+
}
|
|
1723
|
+
/** Returns the private lifecycle host for a lowered root; page code normally never needs it. */
|
|
1724
|
+
export function getComponentHost(element) {
|
|
1725
|
+
const instance = runtimeInstance(element);
|
|
1726
|
+
if (instance === undefined)
|
|
1727
|
+
return undefined;
|
|
1728
|
+
if (instance.host !== undefined)
|
|
1729
|
+
return instance.host;
|
|
1730
|
+
const writable = new Set((instance.definition.declarations ?? [])
|
|
1731
|
+
.filter((declaration) => declaration.kind === "state")
|
|
1732
|
+
.map((declaration) => declaration.name));
|
|
1733
|
+
const state = new Proxy({}, {
|
|
1734
|
+
get: (_target, key) => {
|
|
1735
|
+
if (typeof key !== "string")
|
|
1736
|
+
return undefined;
|
|
1737
|
+
const value = instance.scope.get(key);
|
|
1738
|
+
return value === ABSENT ? undefined : value;
|
|
1739
|
+
},
|
|
1740
|
+
set: (_target, key, value) => {
|
|
1741
|
+
if (typeof key !== "string" || !writable.has(key)) {
|
|
1742
|
+
throw new TypeError(`Only declared state roots are writable; \`${String(key)}\` is read-only.`);
|
|
1743
|
+
}
|
|
1744
|
+
instance.scope.set(key, value);
|
|
1745
|
+
return true;
|
|
1746
|
+
},
|
|
1747
|
+
has: (_target, key) => typeof key === "string" && instance.scope.has(key),
|
|
1748
|
+
});
|
|
1749
|
+
const elements = new Proxy({}, {
|
|
1750
|
+
get: (_target, key) => {
|
|
1751
|
+
if (typeof key !== "string")
|
|
1752
|
+
return undefined;
|
|
1753
|
+
const form = element instanceof HTMLFormElement ? element : element.querySelector("form");
|
|
1754
|
+
return form?.elements.namedItem(key) ?? element.querySelector(`[name="${CSS.escape(key)}"]`) ?? undefined;
|
|
1755
|
+
},
|
|
1756
|
+
});
|
|
1757
|
+
const host = {
|
|
1758
|
+
element,
|
|
1759
|
+
state,
|
|
1760
|
+
refs: instance.refs,
|
|
1761
|
+
elements,
|
|
1762
|
+
signal(initialValue) {
|
|
1763
|
+
return createSignal(initialValue);
|
|
1764
|
+
},
|
|
1765
|
+
computed(compute) {
|
|
1766
|
+
const computed = createComputed(instance.scope.scheduler, compute);
|
|
1767
|
+
// A controller may finish asynchronous setup after its element disconnects. Keep any
|
|
1768
|
+
// owner created during that gap dormant, and put computed owners before effects so every
|
|
1769
|
+
// derived value can track normally when the instance reconnects.
|
|
1770
|
+
if (!instance.connected)
|
|
1771
|
+
computed.pause();
|
|
1772
|
+
instance.effects.unshift(computed);
|
|
1773
|
+
return computed;
|
|
1774
|
+
},
|
|
1775
|
+
effect(run) {
|
|
1776
|
+
const effect = createEffect(instance.scope.scheduler, run, 2, instance.connected);
|
|
1777
|
+
instance.effects.push(effect);
|
|
1778
|
+
return () => effect.stop();
|
|
1779
|
+
},
|
|
1780
|
+
on(event, listener) {
|
|
1781
|
+
if (event === "connect") {
|
|
1782
|
+
const callback = listener;
|
|
1783
|
+
instance.connectCallbacks.add(callback);
|
|
1784
|
+
if (instance.connected)
|
|
1785
|
+
listener();
|
|
1786
|
+
return () => instance.connectCallbacks.delete(callback);
|
|
1787
|
+
}
|
|
1788
|
+
if (event === "disconnect") {
|
|
1789
|
+
const callback = listener;
|
|
1790
|
+
instance.disconnectCallbacks.add(callback);
|
|
1791
|
+
return () => instance.disconnectCallbacks.delete(callback);
|
|
1792
|
+
}
|
|
1793
|
+
element.addEventListener(event, listener);
|
|
1794
|
+
return () => element.removeEventListener(event, listener);
|
|
1795
|
+
},
|
|
1796
|
+
dispatch(event, detail) {
|
|
1797
|
+
return dispatchComponentEvent(element, event, detail, eventDeclaration(instance.definition, event));
|
|
1798
|
+
},
|
|
1799
|
+
};
|
|
1800
|
+
instance.host = Object.freeze(host);
|
|
1801
|
+
return instance.host;
|
|
1802
|
+
}
|
|
1803
|
+
/** Supplies the already application-approved controller module to a lowered instance. */
|
|
1804
|
+
export function setControllerModule(element, module) {
|
|
1805
|
+
const instance = runtimeInstance(element);
|
|
1806
|
+
if (instance === undefined)
|
|
1807
|
+
fail("HJ003", "A controller can attach only to a lowered component root.");
|
|
1808
|
+
instance.controllerModule = module;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Discover inline definitions and instances added after boot. This browser-only entrypoint
|
|
1812
|
+
* owns observation; explicit lowering and compiled/AOT targets do not install observers.
|
|
1813
|
+
* Controller loading and its host are provided by runtime lifecycle integration, not by
|
|
1814
|
+
* evaluating authored markup. Stop disconnects observation and disposes connected roots.
|
|
1815
|
+
*/
|
|
1816
|
+
export function observeDocument(root = document, options = {}) {
|
|
1817
|
+
const state = documentState(root);
|
|
1818
|
+
if (state.observer !== undefined)
|
|
1819
|
+
fail("HR003", "This document is already being observed.");
|
|
1820
|
+
const connected = new Map();
|
|
1821
|
+
const report = options.onError ?? ((error) => console.error(error));
|
|
1822
|
+
let stopped = false;
|
|
1823
|
+
const disconnect = (element) => {
|
|
1824
|
+
const dispose = connected.get(element);
|
|
1825
|
+
connected.delete(element);
|
|
1826
|
+
const instance = runtimeInstance(element);
|
|
1827
|
+
if (instance !== undefined)
|
|
1828
|
+
disconnectRuntimeInstance(instance);
|
|
1829
|
+
try {
|
|
1830
|
+
dispose?.();
|
|
1831
|
+
}
|
|
1832
|
+
catch (error) {
|
|
1833
|
+
report(error);
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
const connect = (element) => {
|
|
1837
|
+
const instance = runtimeInstance(element);
|
|
1838
|
+
if (instance === undefined || instance.frameworkOwned || connected.has(element) || !root.contains(element))
|
|
1839
|
+
return;
|
|
1840
|
+
// Record first so callback mutations cannot connect an instance twice.
|
|
1841
|
+
connected.set(element, undefined);
|
|
1842
|
+
try {
|
|
1843
|
+
connectRuntimeInstance(instance);
|
|
1844
|
+
const dispose = options.onConnect?.(element, instance.definition);
|
|
1845
|
+
if (stopped)
|
|
1846
|
+
dispose?.();
|
|
1847
|
+
else
|
|
1848
|
+
connected.set(element, dispose);
|
|
1849
|
+
}
|
|
1850
|
+
catch (error) {
|
|
1851
|
+
report(error);
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
const synchronize = (mutations) => {
|
|
1855
|
+
if (stopped)
|
|
1856
|
+
return;
|
|
1857
|
+
const scopes = [];
|
|
1858
|
+
if (mutations === undefined) {
|
|
1859
|
+
scopes.push(root);
|
|
1860
|
+
}
|
|
1861
|
+
else {
|
|
1862
|
+
const removed = [];
|
|
1863
|
+
for (const mutation of mutations) {
|
|
1864
|
+
for (const node of mutation.removedNodes) {
|
|
1865
|
+
if (node.nodeType !== 1)
|
|
1866
|
+
continue;
|
|
1867
|
+
visitComponentRoots(node, (element) => {
|
|
1868
|
+
if (connected.has(element))
|
|
1869
|
+
removed.push(element);
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
for (const node of mutation.addedNodes) {
|
|
1873
|
+
if (node.nodeType !== 1)
|
|
1874
|
+
continue;
|
|
1875
|
+
scopes.push(node);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
for (const element of removed)
|
|
1879
|
+
if (!root.contains(element))
|
|
1880
|
+
disconnect(element);
|
|
1881
|
+
}
|
|
1882
|
+
if (scopes.length > 0) {
|
|
1883
|
+
try {
|
|
1884
|
+
for (const element of lowerScopes(root, scopes, options.shouldLower).roots) {
|
|
1885
|
+
if (stopped)
|
|
1886
|
+
break;
|
|
1887
|
+
connect(element);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
catch (error) {
|
|
1891
|
+
report(error);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
state.release = (element) => {
|
|
1896
|
+
if (connected.has(element))
|
|
1897
|
+
disconnect(element);
|
|
1898
|
+
};
|
|
1899
|
+
const stopObservation = subscribeDocumentMutations(root, synchronize);
|
|
1900
|
+
const stop = () => {
|
|
1901
|
+
if (stopped)
|
|
1902
|
+
return;
|
|
1903
|
+
stopped = true;
|
|
1904
|
+
stopObservation();
|
|
1905
|
+
delete state.observer;
|
|
1906
|
+
delete state.release;
|
|
1907
|
+
for (const dispose of connected.values()) {
|
|
1908
|
+
try {
|
|
1909
|
+
dispose?.();
|
|
1910
|
+
}
|
|
1911
|
+
catch (error) {
|
|
1912
|
+
report(error);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
for (const element of connected.keys()) {
|
|
1916
|
+
const instance = runtimeInstance(element);
|
|
1917
|
+
if (instance !== undefined)
|
|
1918
|
+
disconnectRuntimeInstance(instance);
|
|
1919
|
+
}
|
|
1920
|
+
connected.clear();
|
|
1921
|
+
};
|
|
1922
|
+
state.observer = stop;
|
|
1923
|
+
synchronize();
|
|
1924
|
+
return stop;
|
|
1925
|
+
}
|
|
1926
|
+
//# sourceMappingURL=runtime.js.map
|