@webjsdev/core 0.7.64 → 0.7.65

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.
@@ -9,7 +9,7 @@
9
9
  import { SNAPSHOT_CAP } from './constants.js';
10
10
 
11
11
  /** @typedef {{ html: string, scrollX: number, scrollY: number, scrollHeight: number }} Snapshot */
12
- /** @type {Map<string, Snapshot | string>} */
12
+ /** @type {Map<string, Snapshot>} */
13
13
  export const snapshotCache = new Map();
14
14
 
15
15
  /**
@@ -59,9 +59,7 @@ export function snapshotCurrent(url) {
59
59
  }
60
60
 
61
61
  /**
62
- * Look up a cached snapshot by URL. Returns a normalized Snapshot or
63
- * null. Tolerates legacy string entries (e.g. from test fixtures that
64
- * `_snapshotCache.set('/x', 'snap')`).
62
+ * Look up a cached snapshot by URL, or null on a miss.
65
63
  *
66
64
  * @param {string} url
67
65
  * @returns {Snapshot | null}
@@ -73,14 +71,6 @@ export function snapshotGet(url) {
73
71
  // Move-to-front.
74
72
  snapshotCache.delete(key);
75
73
  snapshotCache.set(key, v);
76
- // A legacy string entry carries no offsets and no height. `scrollHeight: 0`
77
- // is stated rather than left undefined so the returned object satisfies the
78
- // `Snapshot` typedef, and because `reserveRestoredHeight` treats a
79
- // non-positive height as "nothing to reserve" and no-ops: there is no
80
- // recorded height to hold, so reserving is not merely skippable, it is
81
- // meaningless. Such an entry restores with no reservation, which is the
82
- // pre-#1428 behaviour and correct for a snapshot that never recorded one.
83
- if (typeof v === 'string') return { html: v, scrollX: 0, scrollY: 0, scrollHeight: 0 };
84
74
  return v;
85
75
  }
86
76
 
package/src/slot.js CHANGED
@@ -74,7 +74,7 @@
74
74
  *
75
75
  * SSR. This module is import-safe in Node (DOM access is guarded on
76
76
  * `typeof HTMLSlotElement !== 'undefined'`). Server-side slot substitution
77
- * lives in render-server.js (injectDSD); slot.js drives the client runtime.
77
+ * lives in render-server/expand.js (`finishSlot`); slot.js drives the client runtime.
78
78
  */
79
79
 
80
80
  // ---------------------------------------------------------------------------
@@ -1,213 +0,0 @@
1
- import { escapeAttr } from '../escape.js';
2
- import {
3
- endOfComment, escapeRegex, findClosingTagInString, inertAt, inertRanges, isVoidElement,
4
- } from './html-scan.js';
5
-
6
- /**
7
- * SSR light-DOM slot projection: partition a component's authored children by
8
- * their `slot=""` attribute, then substitute each `<slot>` in the rendered
9
- * template with the projected children or the authored fallback.
10
- *
11
- * String-level throughout, so it depends on `html-scan.js` and nothing else in
12
- * this directory.
13
- */
14
-
15
- /**
16
- * Extract the `slot` attribute value from an attribute string. Returns
17
- * null when the attribute is absent.
18
- *
19
- * @param {string} attrsRaw
20
- * @returns {string | null}
21
- */
22
- function extractSlotAttr(attrsRaw) {
23
- const m = /\bslot\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(attrsRaw);
24
- if (!m) return null;
25
- const value = m[1] ?? m[2] ?? m[3] ?? '';
26
- // Per shadow DOM spec, slot="" (empty) and missing slot attribute both
27
- // route to the default slot. `default` is the framework's reserved alias
28
- // for it (#1015: the client record normalizes it identically, so both
29
- // sides agree end to end).
30
- return value === '' || value === 'default' ? null : value;
31
- }
32
-
33
- /**
34
- * Partition authored inner HTML by each top-level child's `slot=""`
35
- * attribute. Text nodes, comment nodes, and elements without `slot=""`
36
- * all route to the default-slot key (null).
37
- *
38
- * Returns a Map keyed by slot name (null for default) whose values are
39
- * the concatenated HTML strings for that slot in source order.
40
- *
41
- * @param {string} html
42
- * @returns {Map<string|null, string>}
43
- */
44
- export function partitionAuthoredBySlot(html) {
45
- /** @type {Map<string|null, string>} */
46
- const groups = new Map();
47
- let defaultBuf = '';
48
- let cursor = 0;
49
- while (cursor < html.length) {
50
- const lt = html.indexOf('<', cursor);
51
- if (lt === -1) {
52
- defaultBuf += html.slice(cursor);
53
- break;
54
- }
55
- if (lt > cursor) defaultBuf += html.slice(cursor, lt);
56
- const rest = html.slice(lt);
57
- if (rest.startsWith('<!--')) {
58
- // Find the comment's end the same way inertRanges does, rather than with
59
- // a bare `indexOf('-->')`. The two helpers both decide where a comment
60
- // stops, so a bare search makes them DISAGREE on the spec short forms
61
- // (`--!>`, `<!-->`, `<!--->`): this one would run past the real end and
62
- // swallow the slotted children that follow, silently routing a
63
- // `slot="head"` child into the default slot.
64
- const commentEnd = endOfComment(html, lt);
65
- if (commentEnd === -1) {
66
- defaultBuf += rest;
67
- cursor = html.length;
68
- break;
69
- }
70
- defaultBuf += html.slice(lt, commentEnd);
71
- cursor = commentEnd;
72
- continue;
73
- }
74
- if (rest.startsWith('<!') || rest.startsWith('</')) {
75
- const end = html.indexOf('>', lt);
76
- if (end === -1) {
77
- defaultBuf += rest;
78
- cursor = html.length;
79
- break;
80
- }
81
- defaultBuf += html.slice(lt, end + 1);
82
- cursor = end + 1;
83
- continue;
84
- }
85
- const tagMatch = /^<([a-zA-Z][\w-]*)((?:"[^"]*"|'[^']*'|[^>])*?)(\/?)>/.exec(rest);
86
- if (!tagMatch) {
87
- defaultBuf += '<';
88
- cursor = lt + 1;
89
- continue;
90
- }
91
- const [tagFull, tagName, attrsRaw, selfCloseSlash] = tagMatch;
92
- const lower = tagName.toLowerCase();
93
- const isSelfClose = !!selfCloseSlash || isVoidElement(lower);
94
- const slotAttr = extractSlotAttr(attrsRaw);
95
- let elemEnd;
96
- if (isSelfClose) {
97
- elemEnd = lt + tagFull.length;
98
- } else {
99
- const innerStart = lt + tagFull.length;
100
- const closeIdx = findClosingTagInString(html, innerStart, lower);
101
- if (closeIdx === -1) {
102
- // Unclosed element. Take to end of html.
103
- const elementHTML = html.slice(lt);
104
- if (slotAttr !== null) appendStringToMap(groups, slotAttr, elementHTML);
105
- else defaultBuf += elementHTML;
106
- cursor = html.length;
107
- continue;
108
- }
109
- const closeRe = new RegExp(`</${escapeRegex(lower)}\\s*>`, 'i');
110
- const tail = html.slice(closeIdx);
111
- const closeMatch = closeRe.exec(tail);
112
- const closeLen = closeMatch ? closeMatch[0].length : `</${lower}>`.length;
113
- elemEnd = closeIdx + closeLen;
114
- }
115
- const elementHTML = html.slice(lt, elemEnd);
116
- if (slotAttr !== null) appendStringToMap(groups, slotAttr, elementHTML);
117
- else defaultBuf += elementHTML;
118
- cursor = elemEnd;
119
- }
120
- if (defaultBuf.length > 0) groups.set(null, defaultBuf);
121
- return groups;
122
- }
123
-
124
- /** Append a string to a Map<K, string>, concatenating if the key exists. */
125
- function appendStringToMap(map, key, value) {
126
- const existing = map.get(key);
127
- if (existing !== undefined) map.set(key, existing + value);
128
- else map.set(key, value);
129
- }
130
-
131
- /**
132
- * Substitute every `<slot>` tag in `rendered` with a framework-marked
133
- * `<slot data-webjs-light data-projection="actual|fallback">` element
134
- * carrying either the projected children for that slot (from
135
- * `partitioned`) or the slot's authored fallback content. Multiple
136
- * slots with the same name follow the first-wins rule per spec; later
137
- * same-named slots fall back regardless of available projection.
138
- *
139
- * The `ownerTag` (the tag of the component whose template rendered these
140
- * slots) is emitted as `data-wj-slot-owner` so the client resolves template
141
- * ownership on hydration the same way the client renderer stamps SLOT_OWNER,
142
- * which is what makes a FORWARDED slot (rendered by this component but nested
143
- * inside a child) route to this component and not the child (#1023).
144
- *
145
- * @param {string} rendered
146
- * @param {Map<string|null, string>} partitioned
147
- * @param {string} ownerTag
148
- * @returns {string}
149
- */
150
- export function substituteSlotsInRender(rendered, partitioned, ownerTag) {
151
- const ownerAttr = ownerTag ? ` data-wj-slot-owner="${escapeAttr(ownerTag)}"` : '';
152
- /** @type {Set<string|null>} */
153
- const consumedNames = new Set();
154
- let result = '';
155
- let cursor = 0;
156
- const slotRe = /<slot((?:"[^"]*"|'[^']*'|[^>])*?)(\/?)>/gi;
157
- // A `<slot>` written inside a comment is documentation, not a slot (#1128).
158
- // Substituting one is worse here than in the element walk: a commented slot
159
- // has no `</slot>`, so the fallback scan below swallows the rest of the
160
- // template, the component's REAL slot is never substituted, and the authored
161
- // children are dropped from the page entirely.
162
- const inert = inertRanges(rendered);
163
- const isInert = inertAt(inert);
164
- let m;
165
- while ((m = slotRe.exec(rendered)) !== null) {
166
- if (isInert(m.index)) continue;
167
- result += rendered.slice(cursor, m.index);
168
- const [fullOpen, attrsRaw, selfCloseSlash] = m;
169
- const isSelfClose = !!selfCloseSlash;
170
- const nameMatch = /\bname\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(attrsRaw);
171
- const name = nameMatch ? (nameMatch[1] ?? nameMatch[2] ?? nameMatch[3]) : null;
172
- // Strip the `name` attribute from the carried-through attribute
173
- // string so we can re-add it (with escaping) on the framework slot.
174
- const otherAttrs = attrsRaw.replace(/\bname\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/i, '').trim();
175
- let fallback = '';
176
- let totalEnd;
177
- if (isSelfClose) {
178
- totalEnd = m.index + fullOpen.length;
179
- } else {
180
- const innerStart = m.index + fullOpen.length;
181
- const closeIdx = findClosingTagInString(rendered, innerStart, 'slot', inert);
182
- if (closeIdx === -1) {
183
- fallback = rendered.slice(innerStart);
184
- totalEnd = rendered.length;
185
- } else {
186
- fallback = rendered.slice(innerStart, closeIdx);
187
- const closeRe = /<\/slot\s*>/i;
188
- const tail = rendered.slice(closeIdx);
189
- const closeMatch = closeRe.exec(tail);
190
- const closeLen = closeMatch ? closeMatch[0].length : '</slot>'.length;
191
- totalEnd = closeIdx + closeLen;
192
- }
193
- }
194
- // `default` and `''` are the reserved aliases for the default slot
195
- // (#1015), matching the client's keyOfName exactly: the LOOKUP key
196
- // normalizes, while the emitted name attribute stays as authored so the
197
- // output bytes are unchanged for every other app.
198
- const slotKey = name === 'default' || name === '' ? null : name;
199
- const projected = partitioned.get(slotKey);
200
- const nameAttr = name !== null ? ` name="${escapeAttr(name)}"` : '';
201
- const extraAttrs = otherAttrs ? ` ${otherAttrs}` : '';
202
- if (projected !== undefined && !consumedNames.has(slotKey)) {
203
- consumedNames.add(slotKey);
204
- result += `<slot data-webjs-light data-projection="actual"${ownerAttr}${nameAttr}${extraAttrs}>${projected}</slot>`;
205
- } else {
206
- result += `<slot data-webjs-light data-projection="fallback"${ownerAttr}${nameAttr}${extraAttrs}>${fallback}</slot>`;
207
- }
208
- cursor = totalEnd;
209
- slotRe.lastIndex = totalEnd;
210
- }
211
- result += rendered.slice(cursor);
212
- return result;
213
- }