@webjsdev/core 0.7.64 → 0.8.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/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +21 -22
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +6 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +6 -2
- package/index.d.ts +7 -3
- package/index.js +6 -2
- package/package.json +1 -1
- package/src/attribute-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +37 -3
- package/src/binding-prefixes.js +10 -41
- package/src/component/lifecycle.js +261 -101
- package/src/component/reactive.js +5 -4
- package/src/component.d.ts +11 -5
- package/src/directives.d.ts +2 -0
- package/src/directives.js +2 -0
- package/src/escape.js +12 -2
- package/src/form-action.js +7 -7
- package/src/html.d.ts +4 -0
- package/src/html.js +22 -1
- package/src/optimistic.js +3 -3
- package/src/registry.d.ts +5 -0
- package/src/registry.js +35 -4
- package/src/render-client/adopt-slots.js +245 -0
- package/src/render-client/parts.js +53 -19
- package/src/render-client/reconciler.js +28 -12
- package/src/render-client/template-compiler.js +19 -5
- package/src/render-client.d.ts +2 -1
- package/src/render-server/attrs.js +5 -4
- package/src/render-server/compile.js +433 -0
- package/src/render-server/dsd.js +11 -435
- package/src/render-server/errors.js +84 -0
- package/src/render-server/expand.js +346 -0
- package/src/render-server/html-scan.js +19 -358
- package/src/render-server/served-attrs.js +79 -0
- package/src/render-server/stream.js +11 -13
- package/src/render-server/template-renderer.js +13 -936
- package/src/render-server/walk.js +798 -0
- package/src/router-client/anchors.js +3 -1
- package/src/router-client/boundaries.js +15 -1
- package/src/router-client/constants.js +30 -0
- package/src/router-client/fetch-apply.js +21 -0
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +4 -0
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +3 -0
- package/src/router-client/snapshot-cache.js +2 -12
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +49 -9
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- package/src/slot.js +1 -1
- package/src/webjs-config.d.ts +1 -1
- package/src/render-server/slots.js +0 -213
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { BINDING_PREFIXES } from '../binding-prefixes.js';
|
|
2
|
+
import { isVoidElement, isRawtextTag, isTextOnlyTag } from './html-scan.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The SSR template compiler (#1646): one tokenizer pass per template, run
|
|
6
|
+
* ONCE per `TemplateStringsArray` identity and cached, so a render interleaves
|
|
7
|
+
* the cached static chunks with its values instead of walking every character
|
|
8
|
+
* of every template on every render.
|
|
9
|
+
*
|
|
10
|
+
* The state machine is the one `renderTemplate` used to run per render, moved
|
|
11
|
+
* here unchanged in what it decides (where a hole sits: text, comment, raw
|
|
12
|
+
* text, an attribute value, a plain attribute, a prefixed binding) and extended
|
|
13
|
+
* with what the output rescans used to decide afterwards (where an element
|
|
14
|
+
* opens and closes, which content is text rather than markup), so the walker
|
|
15
|
+
* in `walk.js` can expand custom elements in the same pass with no second scan.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here consults the component registry. An op records a tag NAME, and
|
|
18
|
+
* the walker resolves it with `lookup(tag)` at render time, because the
|
|
19
|
+
* registry can grow after a template compiled (`primeModuleUrl` inserts keys
|
|
20
|
+
* before classes register, and `bun --hot` re-registers), and a memo keyed on
|
|
21
|
+
* the registry rendered components as bare tags once already (#1645).
|
|
22
|
+
*
|
|
23
|
+
* Op shapes, all plain objects with a numeric `t`:
|
|
24
|
+
*
|
|
25
|
+
* TEXT { s } a static chunk, emitted verbatim
|
|
26
|
+
* OPEN { tag } a start tag begins at the NEXT chunk's `<`
|
|
27
|
+
* TAG_END { selfClose, close } the start tag's `>` or `/>` (`close` is its text)
|
|
28
|
+
* CLOSE { tag, text } a whole end tag (`text` is its source bytes)
|
|
29
|
+
* HOLE { k, ... } a value position, `k` says where it sits:
|
|
30
|
+
* CHILD text content, escaped or rendered
|
|
31
|
+
* RAW comment or raw text, emitted as a string
|
|
32
|
+
* ATTR { name, kind, tag, lead } a whole-value attribute hole;
|
|
33
|
+
* `kind` is the BINDING_PREFIXES class
|
|
34
|
+
* (`event` / `prop` / `bool`) or `plain`,
|
|
35
|
+
* `lead` the removed `name=` source text
|
|
36
|
+
* VALUE { name, tag } inside a quoted or unquoted value
|
|
37
|
+
* DROP an unsupported position, value ignored
|
|
38
|
+
*
|
|
39
|
+
* A text-only element (`title`, `textarea`, `iframe`, ...) emits no OPEN or
|
|
40
|
+
* CLOSE for anything inside it, and a comment, raw text or attribute value
|
|
41
|
+
* never does either, which is how a tag name written in one of those stays
|
|
42
|
+
* text (#1128, #1133, #1134). Script data honours the double-escaped state
|
|
43
|
+
* (#1134) so its real end is not necessarily the first `</script>`.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
export const TEXT = 0;
|
|
47
|
+
export const OPEN = 1;
|
|
48
|
+
export const TAG_END = 2;
|
|
49
|
+
export const CLOSE = 3;
|
|
50
|
+
export const HOLE = 4;
|
|
51
|
+
|
|
52
|
+
export const CHILD = 0;
|
|
53
|
+
export const RAW = 1;
|
|
54
|
+
export const ATTR = 2;
|
|
55
|
+
export const VALUE = 3;
|
|
56
|
+
export const DROP = 4;
|
|
57
|
+
|
|
58
|
+
/** @typedef {TemplateStringsArray | string[]} Strings */
|
|
59
|
+
/** @typedef {{ t: number, s?: string, tag?: string, selfClose?: boolean, close?: string, text?: string, k?: number, name?: string, kind?: string, lead?: string }} Op */
|
|
60
|
+
|
|
61
|
+
/** @type {WeakMap<object, Op[]>} */
|
|
62
|
+
const cache = new WeakMap();
|
|
63
|
+
|
|
64
|
+
// Test seam: how many templates the tokenizer has walked, so a test can prove
|
|
65
|
+
// a second render of the same template did NOT re-tokenize it.
|
|
66
|
+
let compiled = 0;
|
|
67
|
+
export function _compileCount() { return compiled; }
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Ops for a template, tokenized once per `strings` identity.
|
|
71
|
+
* @param {Strings} strings
|
|
72
|
+
* @returns {Op[]}
|
|
73
|
+
*/
|
|
74
|
+
export function compile(strings) {
|
|
75
|
+
let ops = cache.get(strings);
|
|
76
|
+
if (ops === undefined) {
|
|
77
|
+
ops = tokenize(strings);
|
|
78
|
+
cache.set(strings, ops);
|
|
79
|
+
}
|
|
80
|
+
return ops;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ops for a raw HTML string (an `unsafeHTML` value, a streamed boundary's
|
|
85
|
+
* children, a legacy `injectDSD` caller). Not cached: every string is new.
|
|
86
|
+
* @param {string} html
|
|
87
|
+
* @returns {Op[]}
|
|
88
|
+
*/
|
|
89
|
+
export function compileRaw(html) {
|
|
90
|
+
return tokenize([html]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const WS = /\s/;
|
|
94
|
+
const LETTER = /[a-zA-Z]/;
|
|
95
|
+
const BOUNDARY = /[\s/>]/;
|
|
96
|
+
// A processing-instruction opener (`<?`), matched by regex rather than by a
|
|
97
|
+
// character compare so the sigil guard (no `=== '<sigil>'` literal in a
|
|
98
|
+
// renderer) stays meaningful.
|
|
99
|
+
const QUESTION = /^\?$/;
|
|
100
|
+
// The states between a start tag's `<name` and its `>`.
|
|
101
|
+
const START_TAG_STATES = new Set(['tag-name', 'in-tag', 'attr-name', 'after-name', 'after-eq', 'attr-quoted', 'attr-unquoted']);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {Strings} strings
|
|
105
|
+
* @returns {Op[]}
|
|
106
|
+
*/
|
|
107
|
+
function tokenize(strings) {
|
|
108
|
+
compiled += 1;
|
|
109
|
+
/** @type {Op[]} */
|
|
110
|
+
const ops = [];
|
|
111
|
+
let chunk = '';
|
|
112
|
+
const flush = () => { if (chunk) { ops.push({ t: TEXT, s: chunk }); chunk = ''; } };
|
|
113
|
+
|
|
114
|
+
let state = 'text';
|
|
115
|
+
let attrName = '';
|
|
116
|
+
let attrStart = 0; // index into `chunk` of the attribute's first character
|
|
117
|
+
let attrQuote = '';
|
|
118
|
+
let commentDashes = 0;
|
|
119
|
+
let currentTag = '';
|
|
120
|
+
/** @type {Op | null} */
|
|
121
|
+
let pendingOpen = null; // the OPEN op whose name is still being read
|
|
122
|
+
let openIndex = -1; // index in `ops` of the start tag being read
|
|
123
|
+
let closeStart = 0; // index into `chunk` of a close tag's `<`
|
|
124
|
+
let solidus = false; // a `/` in a tag state that may be a self-closing solidus
|
|
125
|
+
// Raw text (`script` / `style`): ends at `</script>` / `</style>`, with the
|
|
126
|
+
// #1134 escaped and double-escaped states for script data.
|
|
127
|
+
let rawTail = '';
|
|
128
|
+
let rawEscaped = false;
|
|
129
|
+
let rawDbl = false;
|
|
130
|
+
let rawScriptOpen = false; // `<script` just completed; the next char decides
|
|
131
|
+
// Text-only content (`title`, `textarea`, `iframe`, ...): ends at `</tag`.
|
|
132
|
+
let textOnlyTag = '';
|
|
133
|
+
let textOnlyTail = '';
|
|
134
|
+
|
|
135
|
+
const nameDone = () => {
|
|
136
|
+
if (pendingOpen) { pendingOpen.tag = currentTag; pendingOpen = null; }
|
|
137
|
+
};
|
|
138
|
+
/** Push the CLOSE op for the end tag whose bytes start at `closeStart`. */
|
|
139
|
+
const pushClose = (tag) => {
|
|
140
|
+
const text = chunk.slice(closeStart);
|
|
141
|
+
chunk = chunk.slice(0, closeStart);
|
|
142
|
+
flush();
|
|
143
|
+
ops.push({ t: CLOSE, tag, text });
|
|
144
|
+
state = 'text';
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Content state after a start tag. `allowRawtext` is NOT a preference: only
|
|
148
|
+
* the `tag-name` and `in-tag` exits ever entered raw text in the renderer
|
|
149
|
+
* this replaces, while the three attribute exits (`attr-name`, `after-eq`,
|
|
150
|
+
* `attr-unquoted`) forced `text`, so `<script defer>` and
|
|
151
|
+
* `<style media=print>` escaped their bodies. That is an XSS-facing
|
|
152
|
+
* behaviour this compiler keeps exactly: through an attribute exit a
|
|
153
|
+
* raw-text element's holes stay escaped, and only the "is a tag in here an
|
|
154
|
+
* element" question gets the text-only answer. A self-closed text-only tag
|
|
155
|
+
* (`<svg><title/></svg>`) has no content in foreign content, so it opens
|
|
156
|
+
* nothing (#1128).
|
|
157
|
+
*/
|
|
158
|
+
const finishStartTag = (allowRawtext) => {
|
|
159
|
+
const selfClose = solidus;
|
|
160
|
+
const close = selfClose ? '/>' : '>';
|
|
161
|
+
chunk = chunk.slice(0, chunk.length - close.length);
|
|
162
|
+
flush();
|
|
163
|
+
ops.push({ t: TAG_END, selfClose, close });
|
|
164
|
+
solidus = false;
|
|
165
|
+
attrName = '';
|
|
166
|
+
const tag = currentTag;
|
|
167
|
+
if (isRawtextTag(tag) && allowRawtext) {
|
|
168
|
+
state = 'rawtext';
|
|
169
|
+
rawTail = '';
|
|
170
|
+
rawEscaped = false;
|
|
171
|
+
rawDbl = false;
|
|
172
|
+
rawScriptOpen = false;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!selfClose && isTextOnlyTag(tag)) {
|
|
176
|
+
state = 'textonly';
|
|
177
|
+
textOnlyTag = tag;
|
|
178
|
+
textOnlyTail = '';
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
state = 'text';
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
let bindingQuote = '';
|
|
185
|
+
for (let i = 0; i < strings.length; i++) {
|
|
186
|
+
const s = bindingQuote ? strings[i].slice(1) : strings[i];
|
|
187
|
+
bindingQuote = '';
|
|
188
|
+
for (let j = 0; j < s.length; j++) {
|
|
189
|
+
const c = s[j];
|
|
190
|
+
switch (state) {
|
|
191
|
+
case 'text':
|
|
192
|
+
if (c === '<') { flush(); state = 'tag-open'; }
|
|
193
|
+
chunk += c;
|
|
194
|
+
break;
|
|
195
|
+
case 'tag-open':
|
|
196
|
+
if (c === '!') { chunk += c; state = 'bang-1'; }
|
|
197
|
+
else if (QUESTION.test(c)) { chunk += c; state = 'bogus'; }
|
|
198
|
+
else if (c === '/') { chunk += c; state = 'close-open'; }
|
|
199
|
+
else if (LETTER.test(c)) {
|
|
200
|
+
// A start tag. The OPEN op precedes the chunk holding `<name...`.
|
|
201
|
+
chunk = chunk.slice(0, -1);
|
|
202
|
+
flush();
|
|
203
|
+
pendingOpen = { t: OPEN, tag: '' };
|
|
204
|
+
openIndex = ops.length;
|
|
205
|
+
ops.push(pendingOpen);
|
|
206
|
+
chunk = '<' + c;
|
|
207
|
+
currentTag = c.toLowerCase();
|
|
208
|
+
state = 'tag-name';
|
|
209
|
+
} else { chunk += c; state = 'text'; }
|
|
210
|
+
break;
|
|
211
|
+
case 'close-open':
|
|
212
|
+
if (LETTER.test(c)) {
|
|
213
|
+
closeStart = chunk.length - 2;
|
|
214
|
+
chunk += c;
|
|
215
|
+
currentTag = c.toLowerCase();
|
|
216
|
+
state = 'close-name';
|
|
217
|
+
} else {
|
|
218
|
+
// `</` followed by anything else is a bogus comment to the next `>`.
|
|
219
|
+
chunk += c;
|
|
220
|
+
state = c === '>' ? 'text' : 'bogus';
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
case 'close-name':
|
|
224
|
+
chunk += c;
|
|
225
|
+
if (c === '>') pushClose(currentTag);
|
|
226
|
+
else if (WS.test(c) || c === '/') state = 'close-tail';
|
|
227
|
+
else currentTag += c.toLowerCase();
|
|
228
|
+
break;
|
|
229
|
+
case 'close-tail':
|
|
230
|
+
chunk += c;
|
|
231
|
+
if (c === '>') pushClose(currentTag);
|
|
232
|
+
break;
|
|
233
|
+
case 'bang-1':
|
|
234
|
+
chunk += c;
|
|
235
|
+
state = c === '-' ? 'bang-dash' : 'bogus';
|
|
236
|
+
break;
|
|
237
|
+
case 'bang-dash':
|
|
238
|
+
chunk += c;
|
|
239
|
+
state = c === '-' ? 'comment-start' : 'bogus';
|
|
240
|
+
break;
|
|
241
|
+
case 'bogus':
|
|
242
|
+
// `<!doctype ...>`, `<![CDATA[...]]>`, `<?...>`, `</1...>`: to the next `>`.
|
|
243
|
+
chunk += c;
|
|
244
|
+
if (c === '>') state = 'text';
|
|
245
|
+
break;
|
|
246
|
+
case 'comment-start':
|
|
247
|
+
// The spec short forms: `<!-->` and `<!--->` close immediately.
|
|
248
|
+
chunk += c;
|
|
249
|
+
if (c === '>') state = 'text';
|
|
250
|
+
else if (c === '-') state = 'comment-start-dash';
|
|
251
|
+
else { state = 'comment'; commentDashes = 0; }
|
|
252
|
+
break;
|
|
253
|
+
case 'comment-start-dash':
|
|
254
|
+
chunk += c;
|
|
255
|
+
if (c === '>') state = 'text';
|
|
256
|
+
else if (c === '-') { state = 'comment'; commentDashes = 2; }
|
|
257
|
+
else { state = 'comment'; commentDashes = 0; }
|
|
258
|
+
break;
|
|
259
|
+
case 'comment':
|
|
260
|
+
chunk += c;
|
|
261
|
+
if (c === '-') commentDashes += 1;
|
|
262
|
+
else if (c === '>' && commentDashes >= 2) { state = 'text'; commentDashes = 0; }
|
|
263
|
+
else if (c === '!' && commentDashes >= 2) state = 'comment-bang';
|
|
264
|
+
else commentDashes = 0;
|
|
265
|
+
break;
|
|
266
|
+
case 'comment-bang':
|
|
267
|
+
// `--!>` closes like `-->`.
|
|
268
|
+
chunk += c;
|
|
269
|
+
if (c === '>') { state = 'text'; commentDashes = 0; }
|
|
270
|
+
else { state = 'comment'; commentDashes = c === '-' ? 1 : 0; }
|
|
271
|
+
break;
|
|
272
|
+
case 'tag-name':
|
|
273
|
+
chunk += c;
|
|
274
|
+
if (c === '>') { nameDone(); finishStartTag(true); }
|
|
275
|
+
else if (WS.test(c)) { nameDone(); state = 'in-tag'; }
|
|
276
|
+
else if (c === '/') { nameDone(); solidus = true; state = 'in-tag'; }
|
|
277
|
+
else currentTag += c.toLowerCase();
|
|
278
|
+
break;
|
|
279
|
+
case 'in-tag':
|
|
280
|
+
chunk += c;
|
|
281
|
+
if (c === '>') finishStartTag(true);
|
|
282
|
+
else if (c === '/') solidus = true;
|
|
283
|
+
else if (WS.test(c)) solidus = false;
|
|
284
|
+
else {
|
|
285
|
+
solidus = false;
|
|
286
|
+
state = 'attr-name';
|
|
287
|
+
attrName = c;
|
|
288
|
+
attrStart = chunk.length - 1;
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
case 'attr-name':
|
|
292
|
+
if (c === '=') { state = 'after-eq'; chunk += c; }
|
|
293
|
+
else if (WS.test(c)) { state = 'after-name'; chunk += c; }
|
|
294
|
+
else if (c === '/') { state = 'in-tag'; attrName = ''; solidus = true; chunk += c; }
|
|
295
|
+
else if (c === '>') { chunk += c; finishStartTag(false); }
|
|
296
|
+
else { attrName += c; chunk += c; }
|
|
297
|
+
break;
|
|
298
|
+
case 'after-name':
|
|
299
|
+
// Whitespace after an attribute name (after-attribute-name state):
|
|
300
|
+
// an `=` still belongs to that attribute, so `href = ${x}` binds.
|
|
301
|
+
chunk += c;
|
|
302
|
+
if (c === '=') state = 'after-eq';
|
|
303
|
+
else if (WS.test(c)) { /* keep waiting */ }
|
|
304
|
+
// Whitespace already ended the attribute, the renderer this replaces
|
|
305
|
+
// was back in `in-tag` here, and that exit entered raw text, so
|
|
306
|
+
// `<script async >` keeps its holes raw.
|
|
307
|
+
else if (c === '>') finishStartTag(true);
|
|
308
|
+
else if (c === '/') { state = 'in-tag'; attrName = ''; solidus = true; }
|
|
309
|
+
else { state = 'attr-name'; attrName = c; attrStart = chunk.length - 1; }
|
|
310
|
+
break;
|
|
311
|
+
case 'after-eq':
|
|
312
|
+
// Whitespace after `=` is skipped (before-attribute-value state), so
|
|
313
|
+
// `title= "x"` still opens a quoted value. The renderer this replaces
|
|
314
|
+
// dropped to `in-tag` here and lost the attribute's hole.
|
|
315
|
+
if (c === '"' || c === "'") { state = 'attr-quoted'; attrQuote = c; chunk += c; }
|
|
316
|
+
else if (WS.test(c)) { chunk += c; }
|
|
317
|
+
else if (c === '>') { chunk += c; finishStartTag(false); }
|
|
318
|
+
else { state = 'attr-unquoted'; chunk += c; }
|
|
319
|
+
break;
|
|
320
|
+
case 'attr-unquoted':
|
|
321
|
+
if (WS.test(c)) { state = 'in-tag'; attrName = ''; chunk += c; }
|
|
322
|
+
else if (c === '>') { chunk += c; finishStartTag(false); }
|
|
323
|
+
else chunk += c;
|
|
324
|
+
break;
|
|
325
|
+
case 'attr-quoted':
|
|
326
|
+
chunk += c;
|
|
327
|
+
if (c === attrQuote) { state = 'in-tag'; attrName = ''; }
|
|
328
|
+
break;
|
|
329
|
+
case 'rawtext': {
|
|
330
|
+
chunk += c;
|
|
331
|
+
const lc = c.toLowerCase();
|
|
332
|
+
if (currentTag === 'script') {
|
|
333
|
+
// Script data (#1134). `<!--` enters the escaped state, `-->` and
|
|
334
|
+
// `--!>` leave it, a `<script` inside the escaped state enters the
|
|
335
|
+
// double-escaped one where a `</script>` is text, and the element
|
|
336
|
+
// ends at the first `</script>` outside the double-escaped state.
|
|
337
|
+
if (rawScriptOpen) { rawScriptOpen = false; if (BOUNDARY.test(c) && rawEscaped) rawDbl = true; }
|
|
338
|
+
rawTail = (rawTail + lc).slice(-9);
|
|
339
|
+
if (rawTail.endsWith('<!--')) { if (!rawEscaped) rawEscaped = true; }
|
|
340
|
+
else if (rawTail.endsWith('-->') || rawTail.endsWith('--!>')) { rawEscaped = false; rawDbl = false; }
|
|
341
|
+
else if (rawTail.endsWith('<script')) rawScriptOpen = true;
|
|
342
|
+
else if (rawTail.endsWith('</script>')) {
|
|
343
|
+
if (rawDbl) rawDbl = false;
|
|
344
|
+
else { closeStart = chunk.length - 9; pushClose('script'); rawTail = ''; currentTag = ''; }
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
rawTail = (rawTail + lc).slice(-8);
|
|
348
|
+
if (rawTail.endsWith('</style>')) { closeStart = chunk.length - 8; pushClose('style'); rawTail = ''; currentTag = ''; }
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case 'textonly':
|
|
353
|
+
// Text until `</tag` on a name boundary; `plaintext` never ends.
|
|
354
|
+
if (textOnlyTail === '</' + textOnlyTag && BOUNDARY.test(c)) {
|
|
355
|
+
closeStart = chunk.length - textOnlyTail.length;
|
|
356
|
+
chunk += c;
|
|
357
|
+
currentTag = textOnlyTag;
|
|
358
|
+
textOnlyTag = '';
|
|
359
|
+
textOnlyTail = '';
|
|
360
|
+
if (c === '>') pushClose(currentTag);
|
|
361
|
+
else state = 'close-tail';
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
chunk += c;
|
|
365
|
+
if (textOnlyTag !== 'plaintext') textOnlyTail = (textOnlyTail + c.toLowerCase()).slice(-(textOnlyTag.length + 2));
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (i < strings.length - 1) {
|
|
371
|
+
// A value hole sits here. Its op says where, and the walker decides what.
|
|
372
|
+
if (state === 'comment' || state === 'comment-start' || state === 'comment-start-dash' || state === 'comment-bang') {
|
|
373
|
+
flush();
|
|
374
|
+
ops.push({ t: HOLE, k: RAW });
|
|
375
|
+
commentDashes = 0;
|
|
376
|
+
state = 'comment';
|
|
377
|
+
} else if (state === 'rawtext') {
|
|
378
|
+
flush();
|
|
379
|
+
ops.push({ t: HOLE, k: RAW });
|
|
380
|
+
rawTail = '';
|
|
381
|
+
rawScriptOpen = false;
|
|
382
|
+
} else if (state === 'text' || state === 'textonly') {
|
|
383
|
+
flush();
|
|
384
|
+
ops.push({ t: HOLE, k: CHILD });
|
|
385
|
+
textOnlyTail = '';
|
|
386
|
+
} else if (state === 'after-eq') {
|
|
387
|
+
const prefix = attrName[0];
|
|
388
|
+
const kind = BINDING_PREFIXES[prefix];
|
|
389
|
+
const lead = chunk.slice(attrStart);
|
|
390
|
+
chunk = chunk.slice(0, attrStart);
|
|
391
|
+
flush();
|
|
392
|
+
ops.push({
|
|
393
|
+
t: HOLE, k: ATTR, kind: kind === undefined ? 'plain' : kind,
|
|
394
|
+
name: kind === undefined ? attrName : attrName.slice(1), tag: currentTag, lead,
|
|
395
|
+
});
|
|
396
|
+
state = 'in-tag';
|
|
397
|
+
attrName = '';
|
|
398
|
+
} else if (state === 'attr-quoted' && BINDING_PREFIXES[attrName[0]] !== undefined) {
|
|
399
|
+
const lead = chunk.slice(attrStart);
|
|
400
|
+
if (!lead.endsWith('=' + attrQuote) || !strings[i + 1].startsWith(attrQuote)) {
|
|
401
|
+
throw new Error('Event, property and boolean bindings require exactly one expression with no surrounding text.');
|
|
402
|
+
}
|
|
403
|
+
chunk = chunk.slice(0, attrStart);
|
|
404
|
+
flush();
|
|
405
|
+
ops.push({ t: HOLE, k: ATTR, kind: BINDING_PREFIXES[attrName[0]], name: attrName.slice(1), tag: currentTag, lead: lead.slice(0, -1) });
|
|
406
|
+
bindingQuote = attrQuote;
|
|
407
|
+
state = 'in-tag';
|
|
408
|
+
attrName = '';
|
|
409
|
+
} else if (state === 'attr-quoted' || state === 'attr-unquoted') {
|
|
410
|
+
const prefix = chunk.slice(attrStart);
|
|
411
|
+
flush();
|
|
412
|
+
ops.push({ t: HOLE, k: VALUE, name: attrName, tag: currentTag, quote: state === 'attr-quoted' ? attrQuote : '', prefix });
|
|
413
|
+
} else {
|
|
414
|
+
// A hole in a tag name, between attributes, in a close tag or a markup
|
|
415
|
+
// declaration: nothing is emitted for it, as before.
|
|
416
|
+
flush();
|
|
417
|
+
ops.push({ t: HOLE, k: DROP });
|
|
418
|
+
closeStart = 0;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// An unterminated tag, comment or close tag: the bytes stay text, as a
|
|
423
|
+
// browser treats them. A start tag still open here has no TAG_END, so its
|
|
424
|
+
// OPEN is dropped too: the walker would otherwise begin taking the element
|
|
425
|
+
// (a registered tag moves the page built so far aside until the `>`) and
|
|
426
|
+
// never finish, losing everything before it (a truncated `unsafeHTML`).
|
|
427
|
+
if (pendingOpen) pendingOpen.tag = currentTag;
|
|
428
|
+
if (openIndex !== -1 && START_TAG_STATES.has(state)) ops.splice(openIndex, 1);
|
|
429
|
+
flush();
|
|
430
|
+
return ops;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export { isVoidElement };
|