foldkit 0.146.0 → 0.148.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/buildToken.d.ts +3 -0
- package/dist/buildToken.d.ts.map +1 -0
- package/dist/buildToken.js +21 -0
- package/dist/controlledDomState.d.ts +25 -0
- package/dist/controlledDomState.d.ts.map +1 -0
- package/dist/controlledDomState.js +240 -0
- package/dist/cssStyleProperties.d.ts +6 -0
- package/dist/cssStyleProperties.d.ts.map +1 -0
- package/dist/cssStyleProperties.js +91 -0
- package/dist/customElement/index.d.ts.map +1 -1
- package/dist/customElement/index.js +23 -0
- package/dist/domReflection.d.ts +73 -0
- package/dist/domReflection.d.ts.map +1 -0
- package/dist/domReflection.js +557 -0
- package/dist/experimental/index.d.ts +1 -0
- package/dist/experimental/index.d.ts.map +1 -1
- package/dist/experimental/index.js +1 -0
- package/dist/experimental/machine/machine.d.ts +22 -3
- package/dist/experimental/machine/machine.d.ts.map +1 -1
- package/dist/experimental/machine/machine.js +8 -0
- package/dist/experimental/server/entry.d.ts +73 -0
- package/dist/experimental/server/entry.d.ts.map +1 -0
- package/dist/experimental/server/entry.js +41 -0
- package/dist/experimental/server/host.d.ts +136 -0
- package/dist/experimental/server/host.d.ts.map +1 -0
- package/dist/experimental/server/host.js +359 -0
- package/dist/experimental/server/index.d.ts +5 -0
- package/dist/experimental/server/index.d.ts.map +1 -0
- package/dist/experimental/server/index.js +4 -0
- package/dist/experimental/server/public.d.ts +3 -0
- package/dist/experimental/server/public.d.ts.map +1 -0
- package/dist/experimental/server/public.js +1 -0
- package/dist/experimental/server/serialize.d.ts +43 -0
- package/dist/experimental/server/serialize.d.ts.map +1 -0
- package/dist/experimental/server/serialize.js +786 -0
- package/dist/experimental/server/server.d.ts +242 -0
- package/dist/experimental/server/server.d.ts.map +1 -0
- package/dist/experimental/server/server.js +946 -0
- package/dist/experimental/server/template.d.ts +53 -0
- package/dist/experimental/server/template.d.ts.map +1 -0
- package/dist/experimental/server/template.js +617 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +474 -33
- package/dist/hydrate.d.ts +4 -0
- package/dist/hydrate.d.ts.map +1 -0
- package/dist/hydrate.js +850 -0
- package/dist/hydrationMarker.d.ts +10 -0
- package/dist/hydrationMarker.d.ts.map +1 -0
- package/dist/hydrationMarker.js +9 -0
- package/dist/hydrationMarkers.d.ts +15 -0
- package/dist/hydrationMarkers.d.ts.map +1 -0
- package/dist/hydrationMarkers.js +70 -0
- package/dist/nativeInnerHtml.d.ts +13 -0
- package/dist/nativeInnerHtml.d.ts.map +1 -0
- package/dist/nativeInnerHtml.js +30 -0
- package/dist/propertyProvenance.d.ts +33 -0
- package/dist/propertyProvenance.d.ts.map +1 -0
- package/dist/propertyProvenance.js +78 -0
- package/dist/propsModule.d.ts.map +1 -1
- package/dist/propsModule.js +149 -15
- package/dist/runtime/public.d.ts +2 -2
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/runtime.d.ts +99 -36
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +412 -71
- package/dist/snabbdom/attributes.d.ts.map +1 -1
- package/dist/snabbdom/attributes.js +65 -37
- package/dist/snabbdom/h.d.ts +1 -0
- package/dist/snabbdom/h.d.ts.map +1 -1
- package/dist/snabbdom/h.js +85 -4
- package/dist/snabbdom/style.d.ts.map +1 -1
- package/dist/snabbdom/style.js +53 -34
- package/dist/snabbdom/tovnode.d.ts.map +1 -1
- package/dist/snabbdom/tovnode.js +5 -1
- package/dist/tagName.d.ts +6 -0
- package/dist/tagName.d.ts.map +1 -0
- package/dist/tagName.js +11 -0
- package/dist/test/apps/attributes.d.ts +1 -0
- package/dist/test/apps/attributes.d.ts.map +1 -1
- package/dist/test/apps/attributes.js +8 -1
- package/dist/test/apps/login.js +1 -1
- package/dist/test/matchers.d.ts.map +1 -1
- package/dist/test/matchers.js +2 -1
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +2 -1
- package/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +25 -1
- package/package.json +9 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type RenderedApplication } from './server.js';
|
|
2
|
+
/** Options for {@link injectIntoTemplate}.
|
|
3
|
+
*
|
|
4
|
+
* @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
|
|
5
|
+
*/
|
|
6
|
+
export type InjectIntoTemplateOptions = Readonly<{
|
|
7
|
+
/** The `id` of the exact `<div id="..."></div>` placeholder the rendered
|
|
8
|
+
* markup replaces. Defaults to `'root'`. */
|
|
9
|
+
containerId?: string;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Places a rendered page into an HTML template.
|
|
13
|
+
*
|
|
14
|
+
* The rendered markup (root element plus the flags payload script) replaces
|
|
15
|
+
* the empty container element, so the booting runtime finds the root by its
|
|
16
|
+
* `data-foldkit-app` stamp and hydrates in place. The `Document` head fields
|
|
17
|
+
* are stamped into the shell so the served HTML is correct before the runtime
|
|
18
|
+
* boots: `title` replaces the `<title>` text, `lang` and `dir` are set on the
|
|
19
|
+
* `<html>` element, `canonical` replaces the `href` of a
|
|
20
|
+
* `<link rel="canonical">` element, and `ogUrl` replaces the `content` of a
|
|
21
|
+
* `<meta property="og:url">` element. A field the render omits, or a head
|
|
22
|
+
* element the template does not carry, leaves the template untouched at that
|
|
23
|
+
* spot.
|
|
24
|
+
*
|
|
25
|
+
* The template is parsed with an HTML tokenizer, so every mutation targets a
|
|
26
|
+
* real element rather than a byte pattern. The container contract is
|
|
27
|
+
* deliberately exact: the template must contain one `<div id="root"></div>`
|
|
28
|
+
* placeholder, or the equivalent for `containerId`, with no additional
|
|
29
|
+
* attributes and no content. It must also contain exactly one `<title>`
|
|
30
|
+
* element in its head. Throws when either required location is missing or
|
|
31
|
+
* appears more than once.
|
|
32
|
+
*
|
|
33
|
+
* Pass the `RenderedApplication` returned by `renderToString` unchanged. A
|
|
34
|
+
* hydratable value must parse as exactly one top-level element carrying one
|
|
35
|
+
* nonempty root stamp and build stamp, optionally followed by one matching
|
|
36
|
+
* top-level JSON Flags script. Static output may contain one element, text, or
|
|
37
|
+
* comment root, or no body output. The helper rejects additional top-level
|
|
38
|
+
* content, ambiguous handoff markers, and source that the HTML parser drops,
|
|
39
|
+
* splits, moves, or reconstructs before insertion.
|
|
40
|
+
*
|
|
41
|
+
* This helper is pure with no module state, so a host process may import it
|
|
42
|
+
* directly even when the render itself must stay inside the server entry's
|
|
43
|
+
* module graph.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const page = injectIntoTemplate(template, rendered)
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
|
|
51
|
+
*/
|
|
52
|
+
export declare const injectIntoTemplate: (template: string, rendered: RenderedApplication, options?: InjectIntoTemplateOptions) => string;
|
|
53
|
+
//# sourceMappingURL=template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../../src/experimental/server/template.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,aAAa,CAAA;AA6oBpB;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C;iDAC6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,MAAM,EAChB,UAAU,mBAAmB,EAC7B,UAAU,yBAAyB,KAClC,MA0MF,CAAA"}
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { html as Parse5Html, defaultTreeAdapter, parse, parseFragment, } from 'parse5';
|
|
2
|
+
import { HYDRATION_BUILD_ATTRIBUTE } from '../../buildToken.js';
|
|
3
|
+
import { FOLDKIT_APP_ATTRIBUTE, FOLDKIT_FLAGS_ATTRIBUTE, } from '../../hydrationMarker.js';
|
|
4
|
+
import { escapeAttributeValue, escapeText } from './serialize.js';
|
|
5
|
+
import { __assertNoDeclarativeShadowRoot, __assertNoDocumentStructureEscape, __assertNoLiveBaseElement, } from './server.js';
|
|
6
|
+
const DEFAULT_CONTAINER_ID = 'root';
|
|
7
|
+
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
8
|
+
const RENDERED_FRAGMENT_CONTEXT = defaultTreeAdapter.createElement('div', Parse5Html.NS.HTML, []);
|
|
9
|
+
// NOTE: assembled by concatenation rather than through String.replace, whose
|
|
10
|
+
// replacement string reads `$&` and `` $` `` as patterns. Rendered markup is
|
|
11
|
+
// application data and may contain either.
|
|
12
|
+
const NEUTRAL_CONTEXT_PREFIX = '<!doctype html><html><head><title>x</title></head><body>';
|
|
13
|
+
const NEUTRAL_CONTEXT_SUFFIX = '</body></html>';
|
|
14
|
+
const isElement = (node) => 'tagName' in node;
|
|
15
|
+
const collectMatching = (root, predicate) => {
|
|
16
|
+
const found = [];
|
|
17
|
+
const walk = (node) => {
|
|
18
|
+
for (const child of node.childNodes) {
|
|
19
|
+
if (isElement(child)) {
|
|
20
|
+
if (predicate(child)) {
|
|
21
|
+
found.push(child);
|
|
22
|
+
}
|
|
23
|
+
walk(child);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
walk(root);
|
|
28
|
+
return found;
|
|
29
|
+
};
|
|
30
|
+
const collectMatchingIncludingTemplateContent = (root, predicate) => {
|
|
31
|
+
const found = [];
|
|
32
|
+
const walk = (node) => {
|
|
33
|
+
for (const child of node.childNodes) {
|
|
34
|
+
if (!isElement(child)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (predicate(child)) {
|
|
38
|
+
found.push(child);
|
|
39
|
+
}
|
|
40
|
+
walk(child);
|
|
41
|
+
if (child.tagName === 'template' && 'content' in child) {
|
|
42
|
+
walk(child.content);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
walk(root);
|
|
47
|
+
return found;
|
|
48
|
+
};
|
|
49
|
+
const firstMatching = (root, predicate) => collectMatching(root, predicate)[0];
|
|
50
|
+
const attributeValue = (element, name) => element.attrs.find(attribute => attribute.name === name)?.value;
|
|
51
|
+
const elementChildren = (node) => node.childNodes.filter(isElement);
|
|
52
|
+
const parentOf = (element) => {
|
|
53
|
+
const parent = element.parentNode;
|
|
54
|
+
return parent !== null && 'tagName' in parent ? parent : undefined;
|
|
55
|
+
};
|
|
56
|
+
// Each step names the element itself and where it sits among its parent's
|
|
57
|
+
// element children, outermost last.
|
|
58
|
+
const ancestorPath = (element) => {
|
|
59
|
+
const path = [];
|
|
60
|
+
let current = element;
|
|
61
|
+
while (current !== undefined) {
|
|
62
|
+
const parent = parentOf(current);
|
|
63
|
+
path.push({
|
|
64
|
+
tagName: current.tagName.toLowerCase(),
|
|
65
|
+
index: parent === undefined ? 0 : elementChildren(parent).indexOf(current),
|
|
66
|
+
});
|
|
67
|
+
if (parent === undefined) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
current = parent;
|
|
71
|
+
}
|
|
72
|
+
return path;
|
|
73
|
+
};
|
|
74
|
+
// Each step is resolved by the index it recorded, not by the first element that
|
|
75
|
+
// happens to carry the tag name. Two sibling `<section>`s are an ordinary
|
|
76
|
+
// template, and taking the first would both reject the valid one and let an
|
|
77
|
+
// earlier same-tag sibling stand in for the position the placeholder actually
|
|
78
|
+
// held, so a corrupted injection could be validated against an intact copy.
|
|
79
|
+
const elementAtPath = (document, path) => {
|
|
80
|
+
let node;
|
|
81
|
+
let children = elementChildren(document);
|
|
82
|
+
for (const step of [...path].reverse()) {
|
|
83
|
+
const child = children[step.index];
|
|
84
|
+
if (child === undefined ||
|
|
85
|
+
child.tagName.toLowerCase() !== step.tagName ||
|
|
86
|
+
(node !== undefined && child.namespaceURI !== node.namespaceURI)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
node = child;
|
|
90
|
+
children = elementChildren(child);
|
|
91
|
+
}
|
|
92
|
+
return node;
|
|
93
|
+
};
|
|
94
|
+
const shapeOfElement = (element) => ({
|
|
95
|
+
kind: 'Element',
|
|
96
|
+
tagName: element.tagName.toLowerCase(),
|
|
97
|
+
namespace: element.namespaceURI,
|
|
98
|
+
attributes: [...element.attrs]
|
|
99
|
+
.map(attribute => [attribute.name, attribute.value])
|
|
100
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
101
|
+
children: shapesOf(element),
|
|
102
|
+
});
|
|
103
|
+
const shapesOf = (node) => {
|
|
104
|
+
const shapes = [];
|
|
105
|
+
let text = '';
|
|
106
|
+
const flush = () => {
|
|
107
|
+
if (text !== '') {
|
|
108
|
+
shapes.push({ kind: 'Text', text });
|
|
109
|
+
}
|
|
110
|
+
text = '';
|
|
111
|
+
};
|
|
112
|
+
for (const child of node.childNodes) {
|
|
113
|
+
if (isElement(child)) {
|
|
114
|
+
flush();
|
|
115
|
+
shapes.push(shapeOfElement(child));
|
|
116
|
+
}
|
|
117
|
+
else if (child.nodeName === '#text' && 'value' in child) {
|
|
118
|
+
text += child.value;
|
|
119
|
+
}
|
|
120
|
+
else if (child.nodeName === '#comment' && 'data' in child) {
|
|
121
|
+
flush();
|
|
122
|
+
shapes.push({ kind: 'Comment', text: child.data });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
flush();
|
|
126
|
+
return shapes;
|
|
127
|
+
};
|
|
128
|
+
const isSameShape = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
129
|
+
// The insertion contexts this API supports, stated rather than inferred. A
|
|
130
|
+
// placeholder reached from `<body>` through nothing but flow containers parses
|
|
131
|
+
// the same way a body does, which is the context `renderToString` already
|
|
132
|
+
// validated the rendered markup against. Every other context (a `<form>`, a
|
|
133
|
+
// `<table>`, a `<select>`, foreign content, template content) makes the final
|
|
134
|
+
// parse depend on where the markup landed, and is refused with a clear error
|
|
135
|
+
// rather than modeled.
|
|
136
|
+
const NEUTRAL_ANCESTOR_TAGS = new Set([
|
|
137
|
+
'article',
|
|
138
|
+
'aside',
|
|
139
|
+
'body',
|
|
140
|
+
'div',
|
|
141
|
+
'footer',
|
|
142
|
+
'header',
|
|
143
|
+
'main',
|
|
144
|
+
'section',
|
|
145
|
+
]);
|
|
146
|
+
const assertNeutralInsertionContext = (container, containerId) => {
|
|
147
|
+
let current = parentOf(container);
|
|
148
|
+
while (current !== undefined) {
|
|
149
|
+
const tagName = current.tagName.toLowerCase();
|
|
150
|
+
if (tagName === 'html') {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!NEUTRAL_ANCESTOR_TAGS.has(tagName) ||
|
|
154
|
+
current.namespaceURI !== HTML_NAMESPACE) {
|
|
155
|
+
throw new Error(`[foldkit] injectIntoTemplate found the <div id="${containerId}"></div> placeholder inside <${tagName}>, which is not a supported insertion context. ` +
|
|
156
|
+
'HTML parsing rearranges what an element with a restrictive content ' +
|
|
157
|
+
'model may hold, so the rendered application could be moved, dropped, ' +
|
|
158
|
+
'or reshaped once it sat there. Place the placeholder in the body or ' +
|
|
159
|
+
'inside plain flow containers (div, main, section, article, aside, ' +
|
|
160
|
+
'header, footer).');
|
|
161
|
+
}
|
|
162
|
+
current = parentOf(current);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
// Declarative shadow DOM is refused by `renderToString` itself, which is where
|
|
166
|
+
// every rendered page passes through. The same check runs here as well, since
|
|
167
|
+
// `injectIntoTemplate` accepts any `RenderedApplication` a caller hands it,
|
|
168
|
+
// including one this process did not produce.
|
|
169
|
+
// The rendered markup describes a subtree. Whether the finished page still
|
|
170
|
+
// describes it depends on where the placeholder sat: HTML parsing drops a
|
|
171
|
+
// `<form>` nested in another `<form>`, foster-parents a `<div>` out of a
|
|
172
|
+
// `<table>`, and empties a `<select>` of anything that is not an option. The
|
|
173
|
+
// splice itself is always correct, so the only way to see any of that is to
|
|
174
|
+
// parse the finished page and compare.
|
|
175
|
+
//
|
|
176
|
+
// The comparison is of the placeholder's whole parent, not of the root alone.
|
|
177
|
+
// What the template says that parent should hold after injection is its own
|
|
178
|
+
// children with the placeholder replaced by what the rendered markup describes
|
|
179
|
+
// on its own, in the neutral context `renderToString` already validated it
|
|
180
|
+
// against. Comparing the whole parent covers the rendered subtree, the nodes
|
|
181
|
+
// around it, and the boundaries between them, and it identifies the injection by
|
|
182
|
+
// the position its placeholder held rather than by any attribute, so a marker
|
|
183
|
+
// authored inside the markup cannot stand in for a root the parser dropped, and
|
|
184
|
+
// another application's root elsewhere in the document is not counted against
|
|
185
|
+
// this one.
|
|
186
|
+
//
|
|
187
|
+
// Both parses run with scripting enabled and disabled, because a `<noscript>`
|
|
188
|
+
// in the rendered markup changes the tree for exactly the visitors it targets.
|
|
189
|
+
const renderedShapes = (renderedHtml, isScriptingEnabled) => {
|
|
190
|
+
const neutral = parse(`${NEUTRAL_CONTEXT_PREFIX}${renderedHtml}${NEUTRAL_CONTEXT_SUFFIX}`, { scriptingEnabled: isScriptingEnabled });
|
|
191
|
+
const body = firstMatching(neutral, element => element.tagName === 'body');
|
|
192
|
+
return body === undefined ? [] : shapesOf(body);
|
|
193
|
+
};
|
|
194
|
+
// Adjacent text runs merge, because a parser reads back-to-back text as one
|
|
195
|
+
// node however it was written. Splicing the rendered shapes into the template's
|
|
196
|
+
// own children can put text beside text, so the result is merged the same way
|
|
197
|
+
// before it is compared.
|
|
198
|
+
const mergeAdjacentText = (shapes) => {
|
|
199
|
+
const merged = [];
|
|
200
|
+
for (const shape of shapes) {
|
|
201
|
+
const previous = merged[merged.length - 1];
|
|
202
|
+
if (shape.kind === 'Text' && previous?.kind === 'Text') {
|
|
203
|
+
merged[merged.length - 1] = {
|
|
204
|
+
kind: 'Text',
|
|
205
|
+
text: previous.text + shape.text,
|
|
206
|
+
};
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
merged.push(shape);
|
|
210
|
+
}
|
|
211
|
+
return merged.filter(shape => shape.kind !== 'Text' || shape.text !== '');
|
|
212
|
+
};
|
|
213
|
+
// The shapes of a node's children, plus where in that list a given child lands.
|
|
214
|
+
const shapesAround = (parent, child) => {
|
|
215
|
+
const shapes = shapesOf(parent);
|
|
216
|
+
let index = 0;
|
|
217
|
+
let isTextOpen = false;
|
|
218
|
+
for (const candidate of parent.childNodes) {
|
|
219
|
+
if (isElement(candidate)) {
|
|
220
|
+
if (candidate === child) {
|
|
221
|
+
return { shapes, index };
|
|
222
|
+
}
|
|
223
|
+
isTextOpen = false;
|
|
224
|
+
index += 1;
|
|
225
|
+
}
|
|
226
|
+
else if (candidate.nodeName === '#text' && 'value' in candidate) {
|
|
227
|
+
if (!isTextOpen && candidate.value !== '') {
|
|
228
|
+
isTextOpen = true;
|
|
229
|
+
index += 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (candidate.nodeName === '#comment') {
|
|
233
|
+
isTextOpen = false;
|
|
234
|
+
index += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { shapes, index };
|
|
238
|
+
};
|
|
239
|
+
const assertInjectionIsFaithful = (page, template, renderedHtml, containerId, isScriptingEnabled) => {
|
|
240
|
+
const parsedTemplate = parse(template, {
|
|
241
|
+
scriptingEnabled: isScriptingEnabled,
|
|
242
|
+
});
|
|
243
|
+
const container = firstMatching(parsedTemplate, element => isContainerPlaceholder(element, containerId));
|
|
244
|
+
const containerParent = container === undefined ? undefined : parentOf(container);
|
|
245
|
+
if (container === undefined || containerParent === undefined) {
|
|
246
|
+
// The placeholder does not survive this parser mode at all, so whatever
|
|
247
|
+
// replaced it does not either: markup before it swallowed the rest of the
|
|
248
|
+
// shell. Losing the application root for the visitors of one mode is the
|
|
249
|
+
// failure this check exists to catch, not a reason to skip it.
|
|
250
|
+
throw new Error(`[foldkit] The <div id="${containerId}"></div> placeholder does not survive when a browser parses the template with scripting ${isScriptingEnabled ? 'enabled' : 'disabled'}. ` +
|
|
251
|
+
'Markup before it leaves an element open (an unterminated <textarea>, ' +
|
|
252
|
+
'<style>, or comment inside a <noscript> fallback), so the application ' +
|
|
253
|
+
'root and the rest of the shell are swallowed as its content. Ensure ' +
|
|
254
|
+
'the template is complete and balanced in both parser modes.');
|
|
255
|
+
}
|
|
256
|
+
const { shapes: templateShapes, index } = shapesAround(containerParent, container);
|
|
257
|
+
const expected = mergeAdjacentText([
|
|
258
|
+
...templateShapes.slice(0, index),
|
|
259
|
+
...renderedShapes(renderedHtml, isScriptingEnabled),
|
|
260
|
+
...templateShapes.slice(index + 1),
|
|
261
|
+
]);
|
|
262
|
+
const parsedPage = parse(page, { scriptingEnabled: isScriptingEnabled });
|
|
263
|
+
const pageParent = elementAtPath(parsedPage, ancestorPath(containerParent));
|
|
264
|
+
const actual = pageParent === undefined ? [] : mergeAdjacentText(shapesOf(pageParent));
|
|
265
|
+
const isFaithful = expected.length === actual.length &&
|
|
266
|
+
expected.every((shape, offset) => {
|
|
267
|
+
const candidate = actual[offset];
|
|
268
|
+
return candidate !== undefined && isSameShape(shape, candidate);
|
|
269
|
+
});
|
|
270
|
+
if (!isFaithful) {
|
|
271
|
+
throw new Error(`[foldkit] injectIntoTemplate produced a page that no longer describes the rendered markup${isScriptingEnabled ? '' : ' when a browser parses it with scripting disabled'}. ` +
|
|
272
|
+
'HTML parsing moved, dropped, or reshaped the rendered nodes once they ' +
|
|
273
|
+
`sat where the <div id="${containerId}"></div> placeholder was, which ` +
|
|
274
|
+
'happens when the placeholder is nested inside an element with a ' +
|
|
275
|
+
'restrictive content model (a <form> inside another <form>, anything ' +
|
|
276
|
+
'inside a <table> or <select>, or foreign content such as <svg> or ' +
|
|
277
|
+
'<math>). Move the placeholder to the body or another neutral container.');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const withAttribute = (attributes, name, value) => {
|
|
281
|
+
const exists = attributes.some(attribute => attribute.name === name);
|
|
282
|
+
if (exists) {
|
|
283
|
+
return attributes.map(attribute => attribute.name === name ? { name, value } : attribute);
|
|
284
|
+
}
|
|
285
|
+
return [...attributes, { name, value }];
|
|
286
|
+
};
|
|
287
|
+
const renderAttributes = (attributes) => attributes
|
|
288
|
+
.map(attribute => ` ${attribute.name}="${escapeAttributeValue(attribute.value)}"`)
|
|
289
|
+
.join('');
|
|
290
|
+
const renderStartTag = (tagName, attributes) => `<${tagName}${renderAttributes(attributes)}>`;
|
|
291
|
+
const renderVoidElement = (tagName, attributes) => `<${tagName}${renderAttributes(attributes)} />`;
|
|
292
|
+
const applyMutations = (template, mutations) => {
|
|
293
|
+
const ordered = [...mutations].sort((left, right) => right.start - left.start);
|
|
294
|
+
let result = template;
|
|
295
|
+
for (const mutation of ordered) {
|
|
296
|
+
result =
|
|
297
|
+
result.slice(0, mutation.start) +
|
|
298
|
+
mutation.replacement +
|
|
299
|
+
result.slice(mutation.end);
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
};
|
|
303
|
+
// A valid container is `<div id="{containerId}"></div>` with no other
|
|
304
|
+
// attributes and no children, so the rendered application replaces the exact
|
|
305
|
+
// placeholder and nothing else.
|
|
306
|
+
const ASCII_WHITESPACE_ONLY = /^[ \t\n\f\r]*$/;
|
|
307
|
+
const isIgnorableTopLevelText = (node) => node.nodeName === '#text' &&
|
|
308
|
+
'value' in node &&
|
|
309
|
+
ASCII_WHITESPACE_ONLY.test(node.value);
|
|
310
|
+
const sourceLocationOf = (node) => {
|
|
311
|
+
if (!('sourceCodeLocation' in node)) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const location = node.sourceCodeLocation;
|
|
315
|
+
return location === null || location === undefined ? undefined : location;
|
|
316
|
+
};
|
|
317
|
+
const hasGeneratedDescendant = (node) => {
|
|
318
|
+
for (const child of node.childNodes) {
|
|
319
|
+
if (sourceLocationOf(child) === undefined) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
if (isElement(child)) {
|
|
323
|
+
if (hasGeneratedDescendant(child)) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
if (child.tagName === 'template' &&
|
|
327
|
+
'content' in child &&
|
|
328
|
+
hasGeneratedDescendant(child.content)) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return false;
|
|
334
|
+
};
|
|
335
|
+
const assertRenderedSourceIsPreserved = (renderedHtml, parsed) => {
|
|
336
|
+
let endOffset = 0;
|
|
337
|
+
for (const child of parsed.childNodes) {
|
|
338
|
+
const location = sourceLocationOf(child);
|
|
339
|
+
if (location === undefined || location.startOffset < endOffset) {
|
|
340
|
+
throw new Error('[foldkit] injectIntoTemplate received rendered markup that a browser ' +
|
|
341
|
+
'reshapes before insertion. The rendered application must be one ' +
|
|
342
|
+
'complete parser-stable root, with only its optional Flags payload ' +
|
|
343
|
+
'beside it.');
|
|
344
|
+
}
|
|
345
|
+
if (!ASCII_WHITESPACE_ONLY.test(renderedHtml.slice(endOffset, location.startOffset))) {
|
|
346
|
+
throw new Error('[foldkit] injectIntoTemplate received rendered markup whose source ' +
|
|
347
|
+
'does not survive parsing. An element was dropped, moved, or split ' +
|
|
348
|
+
'before the application reached the template.');
|
|
349
|
+
}
|
|
350
|
+
endOffset = location.endOffset;
|
|
351
|
+
}
|
|
352
|
+
if (!ASCII_WHITESPACE_ONLY.test(renderedHtml.slice(endOffset))) {
|
|
353
|
+
throw new Error('[foldkit] injectIntoTemplate received rendered markup whose trailing ' +
|
|
354
|
+
'source does not survive parsing. An element was dropped, moved, or ' +
|
|
355
|
+
'split before the application reached the template.');
|
|
356
|
+
}
|
|
357
|
+
if (hasGeneratedDescendant(parsed)) {
|
|
358
|
+
throw new Error('[foldkit] injectIntoTemplate received rendered markup that makes the ' +
|
|
359
|
+
'HTML parser insert or reconstruct nodes. Pass the unchanged output ' +
|
|
360
|
+
'of renderToString rather than hand-authored or parser-unstable markup.');
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
// The runtime id the rendered markup stamps on its root, or `undefined` when it
|
|
364
|
+
// carries no stamp (a render that nothing will hydrate).
|
|
365
|
+
const runtimeIdInMode = (renderedHtml, isScriptingEnabled) => {
|
|
366
|
+
const parsed = parseFragment(RENDERED_FRAGMENT_CONTEXT, renderedHtml, {
|
|
367
|
+
scriptingEnabled: isScriptingEnabled,
|
|
368
|
+
sourceCodeLocationInfo: true,
|
|
369
|
+
});
|
|
370
|
+
assertRenderedSourceIsPreserved(renderedHtml, parsed);
|
|
371
|
+
const applicationMarkers = collectMatchingIncludingTemplateContent(parsed, element => attributeValue(element, FOLDKIT_APP_ATTRIBUTE) !== undefined);
|
|
372
|
+
const flagsMarkers = collectMatchingIncludingTemplateContent(parsed, element => attributeValue(element, FOLDKIT_FLAGS_ATTRIBUTE) !== undefined);
|
|
373
|
+
const buildMarkers = collectMatchingIncludingTemplateContent(parsed, element => attributeValue(element, HYDRATION_BUILD_ATTRIBUTE) !== undefined);
|
|
374
|
+
if (applicationMarkers.length === 0 && flagsMarkers.length === 0) {
|
|
375
|
+
if (buildMarkers.length > 0) {
|
|
376
|
+
throw new Error('[foldkit] injectIntoTemplate received a build marker without a ' +
|
|
377
|
+
'matching server-rendered application root.');
|
|
378
|
+
}
|
|
379
|
+
const significant = parsed.childNodes.filter(node => !isIgnorableTopLevelText(node));
|
|
380
|
+
if (significant.length > 1) {
|
|
381
|
+
throw new Error('[foldkit] injectIntoTemplate received static rendered markup with ' +
|
|
382
|
+
'more than one top-level node. renderToString produces one static ' +
|
|
383
|
+
'root, one text or comment root, or no body output.');
|
|
384
|
+
}
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
const root = applicationMarkers.at(0);
|
|
388
|
+
const runtimeId = root === undefined ? undefined : attributeValue(root, FOLDKIT_APP_ATTRIBUTE);
|
|
389
|
+
const isTopLevelRoot = root !== undefined && parsed.childNodes.some(node => node === root);
|
|
390
|
+
const areFlagsTopLevel = flagsMarkers.every(element => parsed.childNodes.some(node => node === element));
|
|
391
|
+
const isFlagsPairValid = flagsMarkers.every(element => element.tagName === 'script' &&
|
|
392
|
+
attributeValue(element, 'type') === 'application/json' &&
|
|
393
|
+
attributeValue(element, FOLDKIT_FLAGS_ATTRIBUTE) === runtimeId);
|
|
394
|
+
const buildId = root === undefined
|
|
395
|
+
? undefined
|
|
396
|
+
: attributeValue(root, HYDRATION_BUILD_ATTRIBUTE);
|
|
397
|
+
const isBuildPairValid = buildId !== undefined &&
|
|
398
|
+
buildId !== '' &&
|
|
399
|
+
buildMarkers.length === 1 &&
|
|
400
|
+
buildMarkers.at(0) === root;
|
|
401
|
+
const significant = parsed.childNodes.filter(node => !isIgnorableTopLevelText(node));
|
|
402
|
+
const hasOnlyOwnedTopLevelNodes = significant.every(node => node === root || (isElement(node) && flagsMarkers.includes(node)));
|
|
403
|
+
if (runtimeId === undefined ||
|
|
404
|
+
runtimeId === '' ||
|
|
405
|
+
applicationMarkers.length !== 1 ||
|
|
406
|
+
!isTopLevelRoot ||
|
|
407
|
+
flagsMarkers.length > 1 ||
|
|
408
|
+
!areFlagsTopLevel ||
|
|
409
|
+
!isFlagsPairValid ||
|
|
410
|
+
!isBuildPairValid ||
|
|
411
|
+
!hasOnlyOwnedTopLevelNodes ||
|
|
412
|
+
significant.length !== 1 + flagsMarkers.length) {
|
|
413
|
+
throw new Error('[foldkit] injectIntoTemplate received rendered markup with ambiguous ' +
|
|
414
|
+
'Foldkit root or Flags markers. A rendered application must carry ' +
|
|
415
|
+
'exactly one top-level data-foldkit-app root with a nonempty ' +
|
|
416
|
+
'data-foldkit-build marker, at most one matching top-level ' +
|
|
417
|
+
'script[type="application/json"][data-foldkit-flags] payload, and no ' +
|
|
418
|
+
'other top-level content. Application content cannot author any of ' +
|
|
419
|
+
'those markers.');
|
|
420
|
+
}
|
|
421
|
+
return runtimeId;
|
|
422
|
+
};
|
|
423
|
+
const runtimeIdOf = (renderedHtml) => {
|
|
424
|
+
const enabled = runtimeIdInMode(renderedHtml, true);
|
|
425
|
+
const disabled = runtimeIdInMode(renderedHtml, false);
|
|
426
|
+
if (enabled !== disabled) {
|
|
427
|
+
throw new Error('[foldkit] injectIntoTemplate received rendered markup whose root and ' +
|
|
428
|
+
'Flags ownership changes when scripting is disabled.');
|
|
429
|
+
}
|
|
430
|
+
return enabled;
|
|
431
|
+
};
|
|
432
|
+
const isContainerPlaceholder = (element, containerId) => element.tagName === 'div' &&
|
|
433
|
+
element.attrs.length === 1 &&
|
|
434
|
+
element.attrs[0]?.name === 'id' &&
|
|
435
|
+
element.attrs[0]?.value === containerId &&
|
|
436
|
+
element.childNodes.length === 0;
|
|
437
|
+
/**
|
|
438
|
+
* Places a rendered page into an HTML template.
|
|
439
|
+
*
|
|
440
|
+
* The rendered markup (root element plus the flags payload script) replaces
|
|
441
|
+
* the empty container element, so the booting runtime finds the root by its
|
|
442
|
+
* `data-foldkit-app` stamp and hydrates in place. The `Document` head fields
|
|
443
|
+
* are stamped into the shell so the served HTML is correct before the runtime
|
|
444
|
+
* boots: `title` replaces the `<title>` text, `lang` and `dir` are set on the
|
|
445
|
+
* `<html>` element, `canonical` replaces the `href` of a
|
|
446
|
+
* `<link rel="canonical">` element, and `ogUrl` replaces the `content` of a
|
|
447
|
+
* `<meta property="og:url">` element. A field the render omits, or a head
|
|
448
|
+
* element the template does not carry, leaves the template untouched at that
|
|
449
|
+
* spot.
|
|
450
|
+
*
|
|
451
|
+
* The template is parsed with an HTML tokenizer, so every mutation targets a
|
|
452
|
+
* real element rather than a byte pattern. The container contract is
|
|
453
|
+
* deliberately exact: the template must contain one `<div id="root"></div>`
|
|
454
|
+
* placeholder, or the equivalent for `containerId`, with no additional
|
|
455
|
+
* attributes and no content. It must also contain exactly one `<title>`
|
|
456
|
+
* element in its head. Throws when either required location is missing or
|
|
457
|
+
* appears more than once.
|
|
458
|
+
*
|
|
459
|
+
* Pass the `RenderedApplication` returned by `renderToString` unchanged. A
|
|
460
|
+
* hydratable value must parse as exactly one top-level element carrying one
|
|
461
|
+
* nonempty root stamp and build stamp, optionally followed by one matching
|
|
462
|
+
* top-level JSON Flags script. Static output may contain one element, text, or
|
|
463
|
+
* comment root, or no body output. The helper rejects additional top-level
|
|
464
|
+
* content, ambiguous handoff markers, and source that the HTML parser drops,
|
|
465
|
+
* splits, moves, or reconstructs before insertion.
|
|
466
|
+
*
|
|
467
|
+
* This helper is pure with no module state, so a host process may import it
|
|
468
|
+
* directly even when the render itself must stay inside the server entry's
|
|
469
|
+
* module graph.
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```typescript
|
|
473
|
+
* const page = injectIntoTemplate(template, rendered)
|
|
474
|
+
* ```
|
|
475
|
+
*
|
|
476
|
+
* @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
|
|
477
|
+
*/
|
|
478
|
+
export const injectIntoTemplate = (template, rendered, options) => {
|
|
479
|
+
const containerId = options?.containerId ?? DEFAULT_CONTAINER_ID;
|
|
480
|
+
const document = parse(template, { sourceCodeLocationInfo: true });
|
|
481
|
+
const containers = collectMatching(document, element => isContainerPlaceholder(element, containerId));
|
|
482
|
+
if (containers.length === 0) {
|
|
483
|
+
throw new Error(`[foldkit] injectIntoTemplate found no exact <div id="${containerId}"></div> placeholder in the template. ` +
|
|
484
|
+
'Add that markup where the application root belongs, or pass the container id the template uses.');
|
|
485
|
+
}
|
|
486
|
+
if (containers.length > 1) {
|
|
487
|
+
throw new Error(`[foldkit] injectIntoTemplate found more than one <div id="${containerId}"></div> placeholder in the template. ` +
|
|
488
|
+
'Keep exactly one placeholder for each application root.');
|
|
489
|
+
}
|
|
490
|
+
// The runtime id names an application across the whole page: it pairs a root
|
|
491
|
+
// with its Flags payload, and it keys the Model and scroll position HMR
|
|
492
|
+
// preserves. Two roots sharing one is not a second application, it is the
|
|
493
|
+
// same application claimed twice, so the second injection is refused rather
|
|
494
|
+
// than producing a page whose parts silently take each other's state. This
|
|
495
|
+
// holds whether or not either application declares Flags.
|
|
496
|
+
//
|
|
497
|
+
// Distinct ids are not a licence to hydrate two applications on one page.
|
|
498
|
+
// Each page-owning application rewrites the document's metadata and installs
|
|
499
|
+
// its own navigation listeners, which no id divides between them.
|
|
500
|
+
const renderedRuntimeId = runtimeIdOf(rendered.html);
|
|
501
|
+
if (renderedRuntimeId !== undefined) {
|
|
502
|
+
const existing = collectMatching(document, element => attributeValue(element, FOLDKIT_APP_ATTRIBUTE) === renderedRuntimeId);
|
|
503
|
+
if (existing.length > 0) {
|
|
504
|
+
throw new Error(`[foldkit] injectIntoTemplate is placing an application stamped ` +
|
|
505
|
+
`"${renderedRuntimeId}", but the page already holds a root with ` +
|
|
506
|
+
'that id. A runtime id names one application for the whole page: it ' +
|
|
507
|
+
'pairs a root with its Flags payload and keys the Model and scroll ' +
|
|
508
|
+
'position hot reloading preserves, so two roots sharing one would ' +
|
|
509
|
+
"take each other's state. Give each application its own " +
|
|
510
|
+
'`runtimeId` when rendering.');
|
|
511
|
+
}
|
|
512
|
+
const existingFlags = collectMatching(document, element => attributeValue(element, FOLDKIT_FLAGS_ATTRIBUTE) === renderedRuntimeId);
|
|
513
|
+
if (existingFlags.length > 0) {
|
|
514
|
+
throw new Error(`[foldkit] injectIntoTemplate is placing an application stamped ` +
|
|
515
|
+
`"${renderedRuntimeId}", but the page already holds a Flags ` +
|
|
516
|
+
'payload with that id. A runtime id pairs exactly one root with at ' +
|
|
517
|
+
'most one generated payload, so a stale or application-authored ' +
|
|
518
|
+
'marker would make hydration ambiguous. Remove the conflicting ' +
|
|
519
|
+
'script[data-foldkit-flags].');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const idMatches = collectMatching(document, element => attributeValue(element, 'id') === containerId);
|
|
523
|
+
if (idMatches.length > 1) {
|
|
524
|
+
throw new Error(`[foldkit] injectIntoTemplate found more than one element with id="${containerId}" in the template. ` +
|
|
525
|
+
'The runtime finds the application root by this id once the placeholder is replaced, so a second element sharing it would resolve to the wrong element. Keep the id unique to the placeholder.');
|
|
526
|
+
}
|
|
527
|
+
const head = firstMatching(document, element => element.tagName === 'head');
|
|
528
|
+
const titles = head === undefined
|
|
529
|
+
? []
|
|
530
|
+
: collectMatching(head, element => element.tagName === 'title');
|
|
531
|
+
if (titles.length === 0) {
|
|
532
|
+
throw new Error('[foldkit] injectIntoTemplate found no <title> element in the template head. ' +
|
|
533
|
+
'Add exactly one <title> where the rendered Document title belongs.');
|
|
534
|
+
}
|
|
535
|
+
if (titles.length > 1) {
|
|
536
|
+
throw new Error('[foldkit] injectIntoTemplate found more than one <title> element in the template head. ' +
|
|
537
|
+
'Keep exactly one title for the rendered Document.');
|
|
538
|
+
}
|
|
539
|
+
const mutations = [];
|
|
540
|
+
const container = containers[0];
|
|
541
|
+
assertNeutralInsertionContext(container, containerId);
|
|
542
|
+
__assertNoDeclarativeShadowRoot(rendered.html);
|
|
543
|
+
__assertNoDocumentStructureEscape(rendered.html);
|
|
544
|
+
__assertNoLiveBaseElement(rendered.html);
|
|
545
|
+
const containerLocation = container.sourceCodeLocation;
|
|
546
|
+
if (containerLocation != null) {
|
|
547
|
+
mutations.push({
|
|
548
|
+
start: containerLocation.startOffset,
|
|
549
|
+
end: containerLocation.endOffset,
|
|
550
|
+
replacement: rendered.html,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const title = titles[0];
|
|
554
|
+
const titleLocation = title.sourceCodeLocation;
|
|
555
|
+
if (titleLocation?.startTag != null && titleLocation.endTag != null) {
|
|
556
|
+
mutations.push({
|
|
557
|
+
start: titleLocation.startTag.endOffset,
|
|
558
|
+
end: titleLocation.endTag.startOffset,
|
|
559
|
+
replacement: escapeText(rendered.title),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const html = firstMatching(document, element => element.tagName === 'html');
|
|
563
|
+
if (rendered.lang !== undefined || rendered.dir !== undefined) {
|
|
564
|
+
// HTML lets the <html> start tag be omitted, in which case parse5 builds an
|
|
565
|
+
// implicit html element with no start-tag location to mutate. Rather than
|
|
566
|
+
// silently drop the language or direction the page requested, fail so the
|
|
567
|
+
// author adds an explicit tag.
|
|
568
|
+
const startTag = html?.sourceCodeLocation?.startTag;
|
|
569
|
+
if (html === undefined || startTag == null) {
|
|
570
|
+
throw new Error('[foldkit] injectIntoTemplate cannot stamp the language or direction ' +
|
|
571
|
+
'because the template has no explicit <html> start tag to mutate. ' +
|
|
572
|
+
'Add an <html> tag to the template so lang and dir can be set.');
|
|
573
|
+
}
|
|
574
|
+
let attributes = html.attrs;
|
|
575
|
+
if (rendered.lang !== undefined) {
|
|
576
|
+
attributes = withAttribute(attributes, 'lang', rendered.lang);
|
|
577
|
+
}
|
|
578
|
+
if (rendered.dir !== undefined) {
|
|
579
|
+
attributes = withAttribute(attributes, 'dir', rendered.dir);
|
|
580
|
+
}
|
|
581
|
+
mutations.push({
|
|
582
|
+
start: startTag.startOffset,
|
|
583
|
+
end: startTag.endOffset,
|
|
584
|
+
replacement: renderStartTag('html', attributes),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (rendered.canonical !== undefined && head !== undefined) {
|
|
588
|
+
const canonical = firstMatching(head, element => element.tagName === 'link' &&
|
|
589
|
+
attributeValue(element, 'rel')?.toLowerCase() === 'canonical');
|
|
590
|
+
if (canonical?.sourceCodeLocation != null) {
|
|
591
|
+
mutations.push({
|
|
592
|
+
start: canonical.sourceCodeLocation.startOffset,
|
|
593
|
+
end: canonical.sourceCodeLocation.endOffset,
|
|
594
|
+
replacement: renderVoidElement('link', withAttribute(canonical.attrs, 'href', rendered.canonical)),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (rendered.ogUrl !== undefined && head !== undefined) {
|
|
599
|
+
const ogUrl = firstMatching(head, element => element.tagName === 'meta' &&
|
|
600
|
+
attributeValue(element, 'property')?.toLowerCase() === 'og:url');
|
|
601
|
+
if (ogUrl?.sourceCodeLocation != null) {
|
|
602
|
+
mutations.push({
|
|
603
|
+
start: ogUrl.sourceCodeLocation.startOffset,
|
|
604
|
+
end: ogUrl.sourceCodeLocation.endOffset,
|
|
605
|
+
replacement: renderVoidElement('meta', withAttribute(ogUrl.attrs, 'content', rendered.ogUrl)),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const page = applyMutations(template, mutations);
|
|
610
|
+
// Checked for every render, hydratable or not. Static markup is placed into a
|
|
611
|
+
// document the same way, and markup the parser drops is lost with no
|
|
612
|
+
// hydration to rebuild it.
|
|
613
|
+
for (const isScriptingEnabled of [true, false]) {
|
|
614
|
+
assertInjectionIsFaithful(page, template, rendered.html, containerId, isScriptingEnabled);
|
|
615
|
+
}
|
|
616
|
+
return page;
|
|
617
|
+
};
|