@webjsdev/core 0.7.65 → 0.9.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 -0
- package/dist/webjs-core-browser.js +6 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +22 -21
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -6
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +18 -9
- package/index.d.ts +26 -10
- package/index.js +21 -9
- package/package.json +15 -2
- package/src/action-effects-client.d.ts +5 -0
- package/src/action-effects-client.js +84 -0
- package/src/action-seed-client.js +5 -5
- package/src/asset-url.js +12 -13
- package/src/async-directive.d.ts +18 -0
- package/src/async-directive.js +88 -0
- package/src/attribute-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +213 -25
- package/src/binding-prefixes.js +10 -53
- package/src/component/lifecycle.js +921 -252
- package/src/component/reactive.js +7 -14
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +160 -44
- package/src/context.d.ts +94 -18
- package/src/context.js +341 -217
- package/src/converters.d.ts +7 -0
- package/src/converters.js +38 -0
- package/src/css.d.ts +32 -6
- package/src/css.js +178 -14
- package/src/directive.d.ts +58 -0
- package/src/directive.js +119 -0
- package/src/directives.d.ts +40 -1
- package/src/directives.js +160 -1
- package/src/form-action.d.ts +13 -0
- package/src/form-action.js +139 -4
- package/src/hot.js +23 -0
- package/src/html.d.ts +8 -0
- package/src/html.js +55 -2
- package/src/is-server.d.ts +2 -0
- package/src/is-server.js +18 -0
- package/src/jsx-guard.js +130 -0
- package/src/metadata.d.ts +22 -3
- package/src/nav.d.ts +4 -1
- package/src/nav.js +44 -11
- package/src/optimistic.js +2 -2
- package/src/refresh-header.js +52 -0
- package/src/registry.js +10 -1
- package/src/render-client/adopt-slots.js +250 -0
- package/src/render-client/parts.js +790 -42
- package/src/render-client/reconciler.js +84 -23
- package/src/render-client/template-compiler.js +152 -43
- package/src/render-client.d.ts +6 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +50 -2
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +100 -20
- package/src/render-server/served-attrs.js +78 -0
- package/src/render-server/walk.js +177 -5
- package/src/render-server.d.ts +3 -0
- package/src/render-server.js +2 -0
- package/src/repeat.d.ts +7 -2
- package/src/repeat.js +20 -6
- 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/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +107 -5
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +49 -9
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +6 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +53 -10
- package/src/routes.d.ts +75 -18
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- package/src/serialize.d.ts +2 -0
- package/src/serialize.js +35 -4
- package/src/signal.d.ts +7 -0
- package/src/signal.js +26 -6
- package/src/slot/polyfills.js +3 -3
- package/src/slot.js +4 -8
- package/src/static-html.d.ts +22 -0
- package/src/static-html.js +144 -0
- package/src/task.d.ts +63 -20
- package/src/task.js +326 -293
- package/src/webjs-config.d.ts +2 -9
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { nothing, noChange } from '../sentinels.js';
|
|
2
|
+
import { isTemplate, MARKER, html } from '../html.js';
|
|
2
3
|
import {
|
|
3
4
|
reconcileFormAction, isBoundFormAction, ABSENT, assertSubmitterHasNoName, assertSubmitterType,
|
|
4
5
|
assertSubmitterHasNoValue, assertSubmitterHasNoStaticFormAction,
|
|
@@ -10,17 +11,69 @@ import { isLive } from '../directives.js';
|
|
|
10
11
|
import { attrHoleValue } from '../binding-prefixes.js';
|
|
11
12
|
import { RENDERING, SLOT_OWNER, SLOT_STATE, drainRendererBackstop, rescueAssignedNodes } from '../slot.js';
|
|
12
13
|
import { compile, submitterActionBindings, INSTANCE } from './template-compiler.js';
|
|
14
|
+
import { commitAdoption, planSlotAdoption, retargetParts } from './adopt-slots.js';
|
|
13
15
|
import {
|
|
14
|
-
applyPart, bindPart, currentRenderRoot, findSlotHost, setCurrentRenderRoot, updateInstance,
|
|
16
|
+
applyPart, bindPart, currentRenderHost, setCurrentRenderHost, currentRenderRoot, findSlotHost, setCurrentRenderRoot, updateInstance,
|
|
17
|
+
releaseInstanceDirectives, setInstanceDirectivesConnected, ROOT_DISCONNECTED,
|
|
15
18
|
} from './parts.js';
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A top-level list as one child binding. One call site, so its strings are a
|
|
22
|
+
* stable identity and a re-render updates the list in place.
|
|
23
|
+
* @param {unknown} list
|
|
24
|
+
*/
|
|
25
|
+
const rootList = (list) => html`${list}`;
|
|
26
|
+
|
|
17
27
|
/**
|
|
18
28
|
* Render a value into a container, reusing DOM where possible.
|
|
19
29
|
*
|
|
20
30
|
* @param {unknown} value
|
|
21
31
|
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
32
|
+
* @param {{host?: object}} [options]
|
|
22
33
|
*/
|
|
23
|
-
export function render(value, container) {
|
|
34
|
+
export function render(value, container, options = {}) {
|
|
35
|
+
renderValue(value, container, options);
|
|
36
|
+
return rootPart(container);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @type {WeakMap<object, { setConnected(connected: boolean): void }>} */
|
|
40
|
+
const rootParts = new WeakMap();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What `render()` returns, lit's `RootPart`: `setConnected(false)` reports
|
|
44
|
+
* every async directive in the container disconnected (and starts the ones a
|
|
45
|
+
* later render creates disconnected), `setConnected(true)` reconnects them.
|
|
46
|
+
* @param {any} container
|
|
47
|
+
*/
|
|
48
|
+
function rootPart(container) {
|
|
49
|
+
let part = rootParts.get(container);
|
|
50
|
+
if (!part) {
|
|
51
|
+
part = {
|
|
52
|
+
setConnected(connected) {
|
|
53
|
+
container[ROOT_DISCONNECTED] = !connected;
|
|
54
|
+
setInstanceDirectivesConnected(container[INSTANCE], connected);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
rootParts.set(container, part);
|
|
58
|
+
}
|
|
59
|
+
return part;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {unknown} value
|
|
64
|
+
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
65
|
+
* @param {{host?: object}} options
|
|
66
|
+
*/
|
|
67
|
+
function renderValue(value, container, options) {
|
|
68
|
+
if (value === noChange) return;
|
|
69
|
+
// Any object that is not a template (an array or other iterable such as a
|
|
70
|
+
// `map()` / `join()` / `range()` generator or a Set, a DOM node, a directive)
|
|
71
|
+
// renders through a child binding, as Lit's `render()` renders every value
|
|
72
|
+
// (#1686). It used to be written as text, "[object Generator]" or
|
|
73
|
+
// "[object Object]".
|
|
74
|
+
if (value !== null && typeof value === 'object' && !isTemplate(value)) {
|
|
75
|
+
value = rootList(value);
|
|
76
|
+
}
|
|
24
77
|
const host = /** @type any */ (container);
|
|
25
78
|
// Open the renderer-write window for the whole commit: every host-receiver
|
|
26
79
|
// write below (and in createInstance / updateInstance / clearInstance / all
|
|
@@ -29,6 +82,8 @@ export function render(value, container) {
|
|
|
29
82
|
// between a renderer commit and an author write.
|
|
30
83
|
const prevRendering = host[RENDERING];
|
|
31
84
|
host[RENDERING] = true;
|
|
85
|
+
const prevHost = currentRenderHost;
|
|
86
|
+
setCurrentRenderHost(options.host);
|
|
32
87
|
const prevRenderRoot = currentRenderRoot;
|
|
33
88
|
setCurrentRenderRoot(container);
|
|
34
89
|
try {
|
|
@@ -44,12 +99,12 @@ export function render(value, container) {
|
|
|
44
99
|
|
|
45
100
|
// Light DOM hydration: if container has SSR content (marked by
|
|
46
101
|
// <!--webjs-hydrate-->), remove the marker and proceed with normal
|
|
47
|
-
// rendering. The content
|
|
48
|
-
//
|
|
102
|
+
// rendering. The content is replaced with identical output, so nothing
|
|
103
|
+
// flashes, except that the SSR slots holding projected children stay in
|
|
104
|
+
// place (createInstance, #1666), so those children never leave the page.
|
|
49
105
|
const firstChild = container.firstChild;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
106
|
+
const hydrating = !!firstChild && firstChild.nodeType === 8 && /** @type {Comment} */ (firstChild).data === 'webjs-hydrate';
|
|
107
|
+
if (hydrating) firstChild.remove();
|
|
53
108
|
|
|
54
109
|
// Pre-set the symbol to an explicit null BEFORE the commit,
|
|
55
110
|
// UNCONDITIONALLY: if createInstance throws after its replaceChildren
|
|
@@ -59,7 +114,7 @@ export function render(value, container) {
|
|
|
59
114
|
// instance on the template-swap path (whose bookends would misclassify
|
|
60
115
|
// the half-committed new roots as unowned and fold them).
|
|
61
116
|
host[INSTANCE] = null;
|
|
62
|
-
const inst = createInstance(tr, container);
|
|
117
|
+
const inst = createInstance(tr, container, hydrating);
|
|
63
118
|
host[INSTANCE] = inst;
|
|
64
119
|
return;
|
|
65
120
|
}
|
|
@@ -68,17 +123,11 @@ export function render(value, container) {
|
|
|
68
123
|
if (prev) clearInstance(prev, container);
|
|
69
124
|
host[INSTANCE] = null;
|
|
70
125
|
container.replaceChildren();
|
|
71
|
-
if (value == null || value ===
|
|
72
|
-
if (Array.isArray(value)) {
|
|
73
|
-
for (const v of value) {
|
|
74
|
-
const text = document.createTextNode(String(v ?? ''));
|
|
75
|
-
container.appendChild(text);
|
|
76
|
-
}
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
126
|
+
if (value == null || value === nothing) return;
|
|
79
127
|
container.appendChild(document.createTextNode(String(value)));
|
|
80
128
|
} finally {
|
|
81
129
|
setCurrentRenderRoot(prevRenderRoot);
|
|
130
|
+
setCurrentRenderHost(prevHost);
|
|
82
131
|
host[RENDERING] = prevRendering;
|
|
83
132
|
// Outermost window closing: drain this commit's childList records off the
|
|
84
133
|
// slot backstop (drainRendererBackstop processes them with a renderer-output
|
|
@@ -203,7 +252,7 @@ function reconcileSubmitterAction(
|
|
|
203
252
|
if (!id) {
|
|
204
253
|
// A function that was MEANT as an action still refuses, so a button never
|
|
205
254
|
// silently submits the form's action instead of its own.
|
|
206
|
-
if (typeof value === 'function') assertIdentifiableAction(null, el.localName);
|
|
255
|
+
if (typeof value === 'function') assertIdentifiableAction(null, el.localName, value);
|
|
207
256
|
releaseSubmitterAction(el);
|
|
208
257
|
// The submission attributes the framework supplied go with the identity, or
|
|
209
258
|
// a released button keeps a `formmethod` / `formenctype` SSR does not emit
|
|
@@ -307,14 +356,19 @@ function resolveHoleValue(v) {
|
|
|
307
356
|
* @param {import('../html.js').TemplateResult} tr
|
|
308
357
|
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
309
358
|
*/
|
|
310
|
-
function createInstance(tr, container) {
|
|
359
|
+
function createInstance(tr, container, hydrating = false) {
|
|
311
360
|
const { templateEl, parts, formActions } = compile(tr);
|
|
312
|
-
const frag = /** @type DocumentFragment */ (templateEl.content
|
|
361
|
+
const frag = /** @type DocumentFragment */ (document.importNode(templateEl.content, true));
|
|
313
362
|
// Bookend markers bound the instance so we can tear it down cleanly.
|
|
314
363
|
const startNode = document.createComment(`${MARKER}s`);
|
|
315
364
|
const endNode = document.createComment(`${MARKER}e`);
|
|
316
365
|
|
|
317
366
|
const bound = parts.map((p) => bindPart(p, frag));
|
|
367
|
+
// Hydration keeps the SSR slots that hold projected children, and their
|
|
368
|
+
// ancestors, in the document (#1666): the parts targeting them are pointed at
|
|
369
|
+
// the live elements now, before any applies.
|
|
370
|
+
const adoption = hydrating ? planSlotAdoption(container, frag, bound) : null;
|
|
371
|
+
if (adoption) retargetParts(adoption, bound);
|
|
318
372
|
const lastValues = [];
|
|
319
373
|
for (let i = 0; i < tr.values.length; i++) {
|
|
320
374
|
applyPart(bound[i], tr.values[i], undefined, tr.values, reconcileFormActions);
|
|
@@ -322,7 +376,11 @@ function createInstance(tr, container) {
|
|
|
322
376
|
}
|
|
323
377
|
reconcileFormActions(formActions, bound, tr.values);
|
|
324
378
|
|
|
325
|
-
|
|
379
|
+
if (adoption) {
|
|
380
|
+
commitAdoption(adoption, /** @type {Element} */ (container), [startNode, ...frag.childNodes, endNode]);
|
|
381
|
+
} else {
|
|
382
|
+
/** @type any */ (container).replaceChildren(startNode, ...frag.childNodes, endNode);
|
|
383
|
+
}
|
|
326
384
|
|
|
327
385
|
// Slot parts have no value-hole to drive applyPart from the loop above.
|
|
328
386
|
// Apply them once now that the fragment is inserted into the live
|
|
@@ -362,8 +420,11 @@ function clearInstance(inst, container) {
|
|
|
362
420
|
// element parts, and rescue any projected children sitting inside
|
|
363
421
|
// slot parts so they survive teardown of a collapsing conditional
|
|
364
422
|
// fragment.
|
|
423
|
+
// Custom directives in the outgoing template are released first (an async
|
|
424
|
+
// one runs its `disconnected()`), as lit does when a part goes away.
|
|
425
|
+
releaseInstanceDirectives(inst);
|
|
365
426
|
for (const p of inst.bound) {
|
|
366
|
-
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher);
|
|
427
|
+
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher, p.handler);
|
|
367
428
|
if (p.kind === 'element') {
|
|
368
429
|
// Guarded for the same reason as the sibling in `disposeInstance`, and
|
|
369
430
|
// the stakes are higher here: this is the container-level teardown
|
|
@@ -375,7 +436,7 @@ function clearInstance(inst, container) {
|
|
|
375
436
|
const prev = /** @type any */ (p).lastTarget;
|
|
376
437
|
if (prev) {
|
|
377
438
|
if (typeof prev === 'function') {
|
|
378
|
-
try { prev(undefined); } catch {}
|
|
439
|
+
try { prev.call(/** @type any */ (p).refHost, undefined); } catch {}
|
|
379
440
|
} else if (typeof prev === 'object') {
|
|
380
441
|
try { prev.value = undefined; } catch {}
|
|
381
442
|
}
|
|
@@ -2,6 +2,7 @@ import { MARKER } from '../html.js';
|
|
|
2
2
|
import { BINDING_PREFIXES, isBindingPrefix } from '../binding-prefixes.js';
|
|
3
3
|
import { isSubmitterReflectedProp } from '../form-action.js';
|
|
4
4
|
import { LIGHT_SLOT_ATTR } from '../slot.js';
|
|
5
|
+
import { assertNoJsxIsms } from '../jsx-guard.js';
|
|
5
6
|
|
|
6
7
|
/** @type {WeakMap<TemplateStringsArray | string[], { templateEl: HTMLTemplateElement, parts: PartDescriptor[], formActions: FormActionRecord[] | null }>} */
|
|
7
8
|
export const templateCache = new WeakMap();
|
|
@@ -12,6 +13,12 @@ export const templateCache = new WeakMap();
|
|
|
12
13
|
*/
|
|
13
14
|
export const submitterActionBindings = new WeakMap();
|
|
14
15
|
export const INSTANCE = Symbol.for('webjs.instance');
|
|
16
|
+
/** Elements whose content the HTML parser reads as text, never markup. */
|
|
17
|
+
const RAW_TEXT_TAGS = new Set(['script', 'style', 'textarea', 'title']);
|
|
18
|
+
/** HTML's own whitespace; `\s` would also match U+00A0, which is attribute text. */
|
|
19
|
+
const HTML_WS = /[ \t\n\f\r]/;
|
|
20
|
+
/** Delimits a raw-text hole's index; a private-use character no parser alters. */
|
|
21
|
+
const RAW_SENTINEL = '\uE000';
|
|
15
22
|
|
|
16
23
|
/**
|
|
17
24
|
* @typedef {{
|
|
@@ -31,13 +38,13 @@ export const INSTANCE = Symbol.for('webjs.instance');
|
|
|
31
38
|
* }} TemplateInstance
|
|
32
39
|
*
|
|
33
40
|
* @typedef {
|
|
34
|
-
* | { kind: 'child', marker: Comment, child?: TemplateInstance | ChildNode[] }
|
|
41
|
+
* | { kind: 'child', marker: Comment, __eventHost?: object, child?: TemplateInstance | ChildNode[] }
|
|
35
42
|
* | { kind: 'attr', el: Element, name: string }
|
|
36
43
|
* | { kind: 'attr-mixed', el: Element, name: string, statics: string[], group: number[] }
|
|
37
|
-
* | { kind: 'event', el: Element, name: string, handler: (
|
|
44
|
+
* | { kind: 'event', el: Element, name: string, handler: (EventListenerOrEventListenerObject & AddEventListenerOptions) | null, eventHost?: object, dispatcher: (e: Event) => void }
|
|
38
45
|
* | { kind: 'prop', el: Element, name: string }
|
|
39
46
|
* | { kind: 'bool', el: Element, name: string }
|
|
40
|
-
* | { kind: 'element', el: Element, lastTarget?: any }
|
|
47
|
+
* | { kind: 'element', el: Element, lastTarget?: any, refHost?: object }
|
|
41
48
|
* | { kind: 'slot', slotEl: HTMLSlotElement, applied: boolean }
|
|
42
49
|
* | { kind: 'noop' }
|
|
43
50
|
* } BoundPart
|
|
@@ -48,6 +55,14 @@ export function compile(tr) {
|
|
|
48
55
|
const { strings } = tr;
|
|
49
56
|
let cached = templateCache.get(strings);
|
|
50
57
|
if (cached) return cached;
|
|
58
|
+
// Only a tagged template's own strings array carries `raw`, so a template
|
|
59
|
+
// result that arrived as data (JSON with the `_$webjs` brand) is refused
|
|
60
|
+
// rather than parsed as markup, lit's `trustFromTemplateString` check.
|
|
61
|
+
if (!Array.isArray(strings) || !Object.prototype.hasOwnProperty.call(strings, 'raw')) {
|
|
62
|
+
throw new Error('[webjs] refusing to render a template result whose strings are not a tagged template\'s (a TemplateResult built from data, not by html``).');
|
|
63
|
+
}
|
|
64
|
+
// `className=`, `htmlFor=`, `<Link>`: JSX that HTML silently ignores.
|
|
65
|
+
assertNoJsxIsms(strings);
|
|
51
66
|
|
|
52
67
|
/** @type {PartDescriptor[]} */
|
|
53
68
|
const parts = [];
|
|
@@ -60,7 +75,15 @@ export function compile(tr) {
|
|
|
60
75
|
/** @type {{ name: string, firstPartIdx: number } | null} */
|
|
61
76
|
let mixedAttr = null;
|
|
62
77
|
let currentTag = '';
|
|
78
|
+
let closingTag = false;
|
|
63
79
|
let rawTail = '';
|
|
80
|
+
// The content state a start tag's `>` opens: raw text for `script` / `style`
|
|
81
|
+
// / `textarea` / `title`, whose content the HTML parser never reads as markup,
|
|
82
|
+
// whichever attribute state the tag ended in (lit's `rawTextElement`).
|
|
83
|
+
const afterTag = () => {
|
|
84
|
+
rawTail = '';
|
|
85
|
+
return (!closingTag && RAW_TEXT_TAGS.has(currentTag)) ? 'rawtext' : 'text';
|
|
86
|
+
};
|
|
64
87
|
|
|
65
88
|
for (let i = 0; i < strings.length; i++) {
|
|
66
89
|
const s = strings[i];
|
|
@@ -74,18 +97,23 @@ export function compile(tr) {
|
|
|
74
97
|
case 'tag-open':
|
|
75
98
|
html += c;
|
|
76
99
|
if (c === '!') state = 'bang-1';
|
|
77
|
-
else if (c === '/') { state = 'tag-name'; currentTag = ''; }
|
|
78
|
-
else if (/[a-zA-Z]/.test(c)) { state = 'tag-name'; currentTag = c.toLowerCase(); }
|
|
100
|
+
else if (c === '/') { state = 'tag-name'; currentTag = ''; closingTag = true; }
|
|
101
|
+
else if (/[a-zA-Z]/.test(c)) { state = 'tag-name'; currentTag = c.toLowerCase(); closingTag = false; }
|
|
79
102
|
else state = 'text';
|
|
80
103
|
break;
|
|
81
104
|
case 'bang-1':
|
|
82
105
|
html += c;
|
|
83
|
-
state = c === '-' ? 'bang-dash' : '
|
|
106
|
+
state = c === '-' ? 'bang-dash' : 'bogus';
|
|
84
107
|
break;
|
|
85
108
|
case 'bang-dash':
|
|
86
109
|
html += c;
|
|
87
110
|
if (c === '-') { state = 'comment'; commentDashes = 0; }
|
|
88
|
-
else state = '
|
|
111
|
+
else state = 'bogus';
|
|
112
|
+
break;
|
|
113
|
+
case 'bogus':
|
|
114
|
+
// A bogus comment (`<!x ...>`, `</ ...>`) runs to the next `>`.
|
|
115
|
+
html += c;
|
|
116
|
+
if (c === '>') state = 'text';
|
|
89
117
|
break;
|
|
90
118
|
case 'comment':
|
|
91
119
|
html += c;
|
|
@@ -95,18 +123,15 @@ export function compile(tr) {
|
|
|
95
123
|
break;
|
|
96
124
|
case 'tag-name':
|
|
97
125
|
html += c;
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} else if (/\s/.test(c)) state = 'in-tag';
|
|
126
|
+
if (closingTag && currentTag === '' && c !== '>' && !/[a-zA-Z]/.test(c)) state = 'bogus';
|
|
127
|
+
else if (c === '>') state = afterTag();
|
|
128
|
+
else if (HTML_WS.test(c)) state = 'in-tag';
|
|
102
129
|
else currentTag += c.toLowerCase();
|
|
103
130
|
break;
|
|
104
131
|
case 'in-tag':
|
|
105
132
|
html += c;
|
|
106
|
-
if (c === '>')
|
|
107
|
-
|
|
108
|
-
if (state === 'rawtext') rawTail = '';
|
|
109
|
-
} else if (!/\s/.test(c) && c !== '/') {
|
|
133
|
+
if (c === '>') state = afterTag();
|
|
134
|
+
else if (!HTML_WS.test(c) && c !== '/') {
|
|
110
135
|
state = 'attr-name';
|
|
111
136
|
attrName = c;
|
|
112
137
|
attrStart = html.length - 1;
|
|
@@ -114,8 +139,10 @@ export function compile(tr) {
|
|
|
114
139
|
break;
|
|
115
140
|
case 'rawtext':
|
|
116
141
|
html += c;
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
// Only the element's OWN end tag closes it: a `</style>` inside a
|
|
143
|
+
// `<script>` is script text.
|
|
144
|
+
rawTail = (rawTail + c.toLowerCase()).slice(-(currentTag.length + 3));
|
|
145
|
+
if (rawTail === `</${currentTag}>`) {
|
|
119
146
|
state = 'text';
|
|
120
147
|
rawTail = '';
|
|
121
148
|
currentTag = '';
|
|
@@ -123,19 +150,28 @@ export function compile(tr) {
|
|
|
123
150
|
break;
|
|
124
151
|
case 'attr-name':
|
|
125
152
|
if (c === '=') { state = 'after-eq'; html += c; }
|
|
126
|
-
else if (
|
|
127
|
-
else if (c === '>') { state =
|
|
153
|
+
else if (HTML_WS.test(c)) { state = 'after-name'; html += c; }
|
|
154
|
+
else if (c === '>') { state = afterTag(); attrName = ''; html += c; }
|
|
128
155
|
else { attrName += c; html += c; }
|
|
129
156
|
break;
|
|
157
|
+
case 'after-name':
|
|
158
|
+
// Whitespace between a name and its `=` (`abc = ${x}`), as the HTML
|
|
159
|
+
// tokenizer allows.
|
|
160
|
+
html += c;
|
|
161
|
+
if (c === '=') state = 'after-eq';
|
|
162
|
+
else if (c === '>') { state = afterTag(); attrName = ''; }
|
|
163
|
+
else if (c === '/') { state = 'in-tag'; attrName = ''; }
|
|
164
|
+
else if (!HTML_WS.test(c)) { state = 'attr-name'; attrName = c; attrStart = html.length - 1; }
|
|
165
|
+
break;
|
|
130
166
|
case 'after-eq':
|
|
131
167
|
if (c === '"' || c === "'") { state = 'attr-quoted'; attrQuote = c; html += c; }
|
|
132
|
-
else if (
|
|
133
|
-
else if (c === '>') { state =
|
|
168
|
+
else if (HTML_WS.test(c)) { html += c; }
|
|
169
|
+
else if (c === '>') { state = afterTag(); attrName = ''; html += c; }
|
|
134
170
|
else { state = 'attr-unquoted'; html += c; }
|
|
135
171
|
break;
|
|
136
172
|
case 'attr-unquoted':
|
|
137
|
-
if (
|
|
138
|
-
else if (c === '>') { state =
|
|
173
|
+
if (HTML_WS.test(c)) { state = 'in-tag'; attrName = ''; html += c; }
|
|
174
|
+
else if (c === '>') { state = afterTag(); attrName = ''; html += c; }
|
|
139
175
|
else html += c;
|
|
140
176
|
break;
|
|
141
177
|
case 'attr-quoted':
|
|
@@ -144,8 +180,10 @@ export function compile(tr) {
|
|
|
144
180
|
break;
|
|
145
181
|
case 'skip-attr':
|
|
146
182
|
// Consume mixed-attribute chars without appending to html.
|
|
147
|
-
// The attribute was replaced with a sentinel on the first hole.
|
|
148
|
-
|
|
183
|
+
// The attribute was replaced with a sentinel on the first hole. An
|
|
184
|
+
// UNQUOTED value (`attrQuote` empty, `a=x${y}` or `a=${x}${y}`) ends
|
|
185
|
+
// at whitespace, `>` or `/>`, as the HTML tokenizer ends it.
|
|
186
|
+
if (attrQuote === '' ? (HTML_WS.test(c) || c === '>' || (c === '/' && s[j + 1] === '>')) : c === attrQuote) {
|
|
149
187
|
// Closing quote: finalize the attr-mixed part.
|
|
150
188
|
if (mixedAttr) {
|
|
151
189
|
const idx0 = mixedAttr.firstPartIdx;
|
|
@@ -160,19 +198,27 @@ export function compile(tr) {
|
|
|
160
198
|
// statics[n] = prefix of strings[last+1] up to closing quote
|
|
161
199
|
const statics = [];
|
|
162
200
|
const s0 = strings[group[0]];
|
|
163
|
-
|
|
164
|
-
|
|
201
|
+
if (attrQuote === '') {
|
|
202
|
+
statics.push(s0.slice(s0.lastIndexOf('=') + 1).trimStart());
|
|
203
|
+
} else {
|
|
204
|
+
const qp = s0.lastIndexOf(attrQuote);
|
|
205
|
+
statics.push(qp >= 0 ? s0.slice(qp + 1) : s0);
|
|
206
|
+
}
|
|
165
207
|
for (let k = 1; k < group.length; k++) {
|
|
166
208
|
statics.push(strings[group[k]]);
|
|
167
209
|
}
|
|
168
210
|
const sLast = strings[group[group.length - 1] + 1];
|
|
169
|
-
const eq = sLast.indexOf(attrQuote);
|
|
211
|
+
const eq = attrQuote === '' ? j : sLast.indexOf(attrQuote);
|
|
170
212
|
statics.push(eq >= 0 ? sLast.slice(0, eq) : sLast);
|
|
171
213
|
|
|
214
|
+
const specialized = isBindingPrefix(mixedAttr.name[0]);
|
|
215
|
+
if (specialized && (group.length !== 1 || statics.some((piece) => piece !== ''))) {
|
|
216
|
+
throw new Error('Event, property and boolean bindings require exactly one expression with no surrounding text.');
|
|
217
|
+
}
|
|
172
218
|
parts[idx0] = {
|
|
173
|
-
kind: 'attr-mixed',
|
|
219
|
+
kind: specialized ? BINDING_PREFIXES[mixedAttr.name[0]] : 'attr-mixed',
|
|
174
220
|
path: [],
|
|
175
|
-
name: mixedAttr.name,
|
|
221
|
+
name: specialized ? mixedAttr.name.slice(1) : mixedAttr.name,
|
|
176
222
|
statics,
|
|
177
223
|
group,
|
|
178
224
|
};
|
|
@@ -187,8 +233,11 @@ export function compile(tr) {
|
|
|
187
233
|
}
|
|
188
234
|
mixedAttr = null;
|
|
189
235
|
}
|
|
190
|
-
state = 'in-tag';
|
|
191
236
|
attrName = '';
|
|
237
|
+
// A quoted value's closing quote was consumed with it; an unquoted
|
|
238
|
+
// value's terminator is markup and is kept.
|
|
239
|
+
if (attrQuote === '') html += c;
|
|
240
|
+
state = (attrQuote === '' && c === '>') ? afterTag() : 'in-tag';
|
|
192
241
|
}
|
|
193
242
|
break;
|
|
194
243
|
}
|
|
@@ -205,28 +254,38 @@ export function compile(tr) {
|
|
|
205
254
|
continue;
|
|
206
255
|
}
|
|
207
256
|
if (state === 'rawtext') {
|
|
208
|
-
// Inside <script
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
257
|
+
// Inside <script> / <style> / <textarea> / <title> the parser reads a
|
|
258
|
+
// comment marker as text, so the hole leaves a text sentinel that
|
|
259
|
+
// `splitRawTextSentinels` turns into a marker comment after parsing.
|
|
260
|
+
// It is then an ordinary child part committing text nodes, which is
|
|
261
|
+
// what lit does and what SSR's raw-text hole emits.
|
|
212
262
|
rawTail = '';
|
|
213
|
-
|
|
263
|
+
html += `${RAW_SENTINEL}${partIdx}${RAW_SENTINEL}`;
|
|
264
|
+
parts.push({ kind: 'child', path: [] });
|
|
214
265
|
continue;
|
|
215
266
|
}
|
|
267
|
+
// An unquoted value that goes on after its hole (`a=${x}${y}`,
|
|
268
|
+
// `a=${x}px`) is interpolated, like a quoted one.
|
|
269
|
+
const next = strings[i + 1];
|
|
270
|
+
const unquotedGoesOn = state === 'after-eq'
|
|
271
|
+
&& (next === '' ? i + 1 < strings.length - 1 : !/^([ \t\n\f\r]|>|\/>)/.test(next));
|
|
216
272
|
if (state === 'text') {
|
|
217
273
|
// Child hole: insert a comment marker. Use bracketed markers so we can
|
|
218
274
|
// later walk all comments and find ours without ambiguity.
|
|
219
275
|
html += `<!--${MARKER}${partIdx}-->`;
|
|
220
276
|
parts.push({ kind: 'child', path: [] });
|
|
221
|
-
} else if (state === 'in-tag') {
|
|
222
|
-
// Element-position hole: `<tag ${expr}
|
|
277
|
+
} else if (state === 'in-tag' || state === 'after-name') {
|
|
278
|
+
// Element-position hole: `<tag ${expr}>`, also after a bare attribute
|
|
279
|
+
// (`<input disabled ${expr}>`). Used by the `ref` directive
|
|
223
280
|
// (and any future element-bound directive). Emit a sentinel attribute
|
|
224
281
|
// on the current open tag; at bind time the attribute is stripped
|
|
225
282
|
// and the element is captured into the part.
|
|
226
283
|
const sentinel = `data-${MARKER}${partIdx}`;
|
|
227
284
|
html += `${sentinel}=""`;
|
|
228
285
|
parts.push({ kind: 'element', path: [] });
|
|
229
|
-
|
|
286
|
+
state = 'in-tag';
|
|
287
|
+
attrName = '';
|
|
288
|
+
} else if (state === 'after-eq' && !unquotedGoesOn) {
|
|
230
289
|
const prefix = attrName[0];
|
|
231
290
|
const name = attrName.slice(1);
|
|
232
291
|
if (isBindingPrefix(prefix)) {
|
|
@@ -245,24 +304,42 @@ export function compile(tr) {
|
|
|
245
304
|
}
|
|
246
305
|
state = 'in-tag';
|
|
247
306
|
attrName = '';
|
|
248
|
-
} else if (state === 'attr-quoted' || state === 'attr-unquoted') {
|
|
307
|
+
} else if (state === 'attr-quoted' || state === 'attr-unquoted' || state === 'after-eq') {
|
|
249
308
|
// First hole inside a quoted attribute value: start mixed-attr tracking.
|
|
250
309
|
// Replace the entire attribute with a sentinel (same as regular attr).
|
|
310
|
+
if (state !== 'attr-quoted') attrQuote = '';
|
|
251
311
|
html = html.slice(0, attrStart);
|
|
252
312
|
const sentinel = `data-${MARKER}${partIdx}`;
|
|
253
313
|
html += `${sentinel}=""`;
|
|
254
314
|
mixedAttr = { name: attrName, firstPartIdx: partIdx };
|
|
255
315
|
parts.push({ kind: 'noop', path: [] }); // patched to attr-mixed at close-quote
|
|
256
316
|
state = 'skip-attr';
|
|
257
|
-
} else
|
|
258
|
-
//
|
|
317
|
+
} else {
|
|
318
|
+
// `skip-attr`: a later hole in the same mixed attribute. Any other
|
|
319
|
+
// state is a hole where no binding can sit (a tag name, `</div ${x}>`):
|
|
320
|
+
// it binds nothing, as in lit's production build, and still takes its
|
|
321
|
+
// index so every later part keeps its own.
|
|
259
322
|
parts.push({ kind: 'noop', path: [] });
|
|
260
323
|
}
|
|
261
324
|
}
|
|
262
325
|
}
|
|
263
326
|
|
|
264
327
|
const templateEl = document.createElement('template');
|
|
265
|
-
|
|
328
|
+
if (tr.svg || tr.mathml) {
|
|
329
|
+
// An svg`` (or mathml``) fragment is parsed inside an <svg> (or <math>)
|
|
330
|
+
// wrapper so its elements land in that namespace, then the wrapper is
|
|
331
|
+
// unwrapped BEFORE the slot and path walks, so every recorded path is
|
|
332
|
+
// relative to the fragment's own root, as it is for an html`` template
|
|
333
|
+
// (lit does the same).
|
|
334
|
+
const wrapTag = tr.svg ? 'svg' : 'math';
|
|
335
|
+
templateEl.innerHTML = `<${wrapTag}>${html}</${wrapTag}>`;
|
|
336
|
+
const wrapper = /** @type {Element} */ (templateEl.content.firstChild);
|
|
337
|
+
wrapper.replaceWith(...wrapper.childNodes);
|
|
338
|
+
} else {
|
|
339
|
+
templateEl.innerHTML = html;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (html.includes(RAW_SENTINEL)) splitRawTextSentinels(templateEl.content);
|
|
266
343
|
|
|
267
344
|
// Mark every <slot> in the template for framework projection and
|
|
268
345
|
// register a SLOT part for each so the slot-apply step can find them on
|
|
@@ -281,6 +358,38 @@ export function compile(tr) {
|
|
|
281
358
|
return cached;
|
|
282
359
|
}
|
|
283
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Replace each raw-text hole's sentinel with a `wjm-<idx>` marker comment, so
|
|
363
|
+
* the path walk records it as a child part. A comment inside `<script>` or
|
|
364
|
+
* `<textarea>` is a DOM child the element's text (`textContent`, `.text`,
|
|
365
|
+
* `.value`) ignores, which is how lit anchors a raw-text binding too.
|
|
366
|
+
*
|
|
367
|
+
* @param {DocumentFragment} root
|
|
368
|
+
*/
|
|
369
|
+
function splitRawTextSentinels(root) {
|
|
370
|
+
/** @type {Text[]} */
|
|
371
|
+
const hits = [];
|
|
372
|
+
/** @param {Node} node */
|
|
373
|
+
const collect = (node) => {
|
|
374
|
+
for (const child of node.childNodes) {
|
|
375
|
+
if (child.nodeType === 3) { if (/** @type {Text} */ (child).data.includes(RAW_SENTINEL)) hits.push(/** @type {Text} */ (child)); }
|
|
376
|
+
else collect(child);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
collect(root);
|
|
380
|
+
for (const text of hits) {
|
|
381
|
+
const pieces = text.data.split(RAW_SENTINEL);
|
|
382
|
+
/** @type {Node[]} */
|
|
383
|
+
const nodes = [];
|
|
384
|
+
// pieces alternate text, index, text, index, ..., text
|
|
385
|
+
for (let k = 0; k < pieces.length; k++) {
|
|
386
|
+
if (k % 2) nodes.push(document.createComment(`${MARKER}${pieces[k]}`));
|
|
387
|
+
else if (pieces[k]) nodes.push(document.createTextNode(pieces[k]));
|
|
388
|
+
}
|
|
389
|
+
text.replaceWith(...nodes);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
284
393
|
/**
|
|
285
394
|
* Walk the compiled template content for <slot> elements (the static ones
|
|
286
395
|
* written into the template, not dynamically-inserted ones). For each:
|
package/src/render-client.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface RenderOptions { host?: object; }
|
|
2
|
+
/** What `render()` returns, lit's `RootPart`: `setConnected(false)` disconnects the async directives the render created, `setConnected(true)` reconnects them. */
|
|
3
|
+
export interface RootPart {
|
|
4
|
+
setConnected(isConnected: boolean): void;
|
|
5
|
+
}
|
|
6
|
+
export function render(value: unknown, container: Element | DocumentFragment | ShadowRoot, options?: RenderOptions): RootPart;
|
|
@@ -67,7 +67,7 @@ export function withHostMarker(opening) {
|
|
|
67
67
|
* `getAttribute` API so it works whether the instance is the server shim or a
|
|
68
68
|
* real `HTMLElement`. Returns the opening tag unchanged when there is nothing
|
|
69
69
|
* to add, so existing SSR output stays byte-identical when no component
|
|
70
|
-
* reflects
|
|
70
|
+
* reflects.
|
|
71
71
|
*
|
|
72
72
|
* @param {string} opening the element's opening tag, ending in `>`
|
|
73
73
|
* @param {any} instance
|
|
@@ -129,7 +129,7 @@ export function applyAttrsToInstance(instance, attrs, Cls) {
|
|
|
129
129
|
// per-component error isolation below, which is deliberate: an author who
|
|
130
130
|
// supplies a converter owns the read, the same rule `_reflectAttribute`
|
|
131
131
|
// states for `toAttribute`. See the comment on `readAttributeValue`.
|
|
132
|
-
instance[propName] = readAttributeValue(def, decodeAttrEntities(sourceValue));
|
|
132
|
+
instance[propName] = readAttributeValue(def, decodeAttrEntities(sourceValue), propName);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|