@hyperframes/inspector 0.8.156 → 0.8.158
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/inspection.js +42 -14
- package/dist/live.js +385 -44
- package/dist/service.js +484 -87
- package/dist/types/core/src/runtime/inspectionTypes.d.ts +1 -0
- package/dist/types/core/src/runtime/timelineValues.d.ts +1 -1
- package/dist/types/inspector/src/authoredState.d.ts +3 -0
- package/dist/types/inspector/src/elementMap.d.ts +2 -0
- package/dist/types/inspector/src/engine.d.ts +2 -1
- package/dist/types/inspector/src/pageQueries.d.ts +1 -0
- package/dist/types/inspector/src/sources.d.ts +17 -9
- package/package.json +3 -3
package/dist/live.js
CHANGED
|
@@ -4,6 +4,263 @@ import { createRequire } from "node:module"; const require = createRequire(impor
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
6
6
|
|
|
7
|
+
// src/authoredState.ts
|
|
8
|
+
var PLAIN_CLASS = /^[a-zA-Z_-][\w-]*$/;
|
|
9
|
+
function withAuthoredState(snapshot, sources) {
|
|
10
|
+
const byHfId = group(sources.htmlTags, (tag) => tag.hfId);
|
|
11
|
+
const byId = group(sources.htmlTags, (tag) => tag.id);
|
|
12
|
+
const live = snapshot.nodes;
|
|
13
|
+
const matched = live.map((node) => sourceTag(node, byHfId, byId));
|
|
14
|
+
const generated = generatedInsideText(live, matched, sources);
|
|
15
|
+
const nodes = live.map(
|
|
16
|
+
(node, index) => generated.has(index) ? steadyGenerated(node, live, generated) : authoredNode(node, matched[index], sources, live)
|
|
17
|
+
);
|
|
18
|
+
return { ...snapshot, nodes };
|
|
19
|
+
}
|
|
20
|
+
function authoredNode(node, tag, sources, live) {
|
|
21
|
+
if (!tag) return node;
|
|
22
|
+
const text = directText(sources, tag) ?? node.text;
|
|
23
|
+
const id = node.id || tag.id || "";
|
|
24
|
+
const classes = authoredClasses(node, tag, live);
|
|
25
|
+
const next = { ...node, text, id };
|
|
26
|
+
return classes ? { ...next, ...classes } : next;
|
|
27
|
+
}
|
|
28
|
+
function generatedInsideText(live, matched, sources) {
|
|
29
|
+
const generated = /* @__PURE__ */ new Set();
|
|
30
|
+
for (const [index, tag] of matched.entries()) {
|
|
31
|
+
if (!tag || !directText(sources, tag)) continue;
|
|
32
|
+
for (const child of live[index].children)
|
|
33
|
+
if (!matched[child]) markGenerated(child, live, matched, generated);
|
|
34
|
+
}
|
|
35
|
+
return generated;
|
|
36
|
+
}
|
|
37
|
+
function markGenerated(index, live, matched, generated) {
|
|
38
|
+
if (generated.has(index) || matched[index]) return;
|
|
39
|
+
generated.add(index);
|
|
40
|
+
for (const child of live[index].children) markGenerated(child, live, matched, generated);
|
|
41
|
+
}
|
|
42
|
+
function steadyGenerated(node, live, generated) {
|
|
43
|
+
const siblings = node.parent === null ? [node] : live[node.parent].children.map((index) => live[index]);
|
|
44
|
+
const siblingMatchCount = siblings.filter(
|
|
45
|
+
(other) => generated.has(other.index) && other.tag === node.tag
|
|
46
|
+
).length;
|
|
47
|
+
return {
|
|
48
|
+
...node,
|
|
49
|
+
id: "",
|
|
50
|
+
classes: [],
|
|
51
|
+
text: "",
|
|
52
|
+
generated: true,
|
|
53
|
+
groupSelector: node.tag,
|
|
54
|
+
relative: `${node.tag}:nth-child(${node.position})`,
|
|
55
|
+
siblingMatchCount
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function group(tags, key) {
|
|
59
|
+
const groups = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const tag of tags) {
|
|
61
|
+
const value = key(tag);
|
|
62
|
+
if (value) groups.set(value, [...groups.get(value) ?? [], tag]);
|
|
63
|
+
}
|
|
64
|
+
return groups;
|
|
65
|
+
}
|
|
66
|
+
function sourceTag(node, byHfId, byId) {
|
|
67
|
+
const named = node.hfId && byHfId.get(node.hfId) || node.id && byId.get(node.id) || [];
|
|
68
|
+
const matches = named.filter((tag) => tag.tag === node.tag);
|
|
69
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
70
|
+
}
|
|
71
|
+
var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title", "xmp", "noembed", "noframes"]);
|
|
72
|
+
var VOID = /* @__PURE__ */ new Set([
|
|
73
|
+
"area",
|
|
74
|
+
"base",
|
|
75
|
+
"br",
|
|
76
|
+
"col",
|
|
77
|
+
"embed",
|
|
78
|
+
"hr",
|
|
79
|
+
"img",
|
|
80
|
+
"input",
|
|
81
|
+
"link",
|
|
82
|
+
"meta",
|
|
83
|
+
"param",
|
|
84
|
+
"source",
|
|
85
|
+
"track",
|
|
86
|
+
"wbr"
|
|
87
|
+
]);
|
|
88
|
+
function directText(sources, tag) {
|
|
89
|
+
const text = sources.files.get(tag.file)?.text;
|
|
90
|
+
if (text === void 0) return null;
|
|
91
|
+
if (VOID.has(tag.tag)) return "";
|
|
92
|
+
return normalizedInner(text, tag);
|
|
93
|
+
}
|
|
94
|
+
function normalizedInner(text, tag) {
|
|
95
|
+
const inner = sliceUntilClose(text, tag.end, tag.tag);
|
|
96
|
+
if (inner === null) return null;
|
|
97
|
+
return decodeEntities(textNodes(inner)).replace(/\s+/g, " ").trim();
|
|
98
|
+
}
|
|
99
|
+
function sliceUntilClose(html, from, tag) {
|
|
100
|
+
const end = closeOffset(html, from, tag, 1);
|
|
101
|
+
return end === null ? null : html.slice(from, end);
|
|
102
|
+
}
|
|
103
|
+
function closeOffset(html, from, tag, depth) {
|
|
104
|
+
let cursor = from;
|
|
105
|
+
let level = depth;
|
|
106
|
+
for (; ; ) {
|
|
107
|
+
const step = closeStep(html, cursor, tag, level);
|
|
108
|
+
if (!step) return null;
|
|
109
|
+
if (step.done) return step.at;
|
|
110
|
+
cursor = step.at;
|
|
111
|
+
level = step.depth;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function closeStep(html, from, tag, depth) {
|
|
115
|
+
const found = nextMarkup(html, from);
|
|
116
|
+
if (!found) return null;
|
|
117
|
+
if (found.kind !== "tag") return { at: found.end, depth, done: false };
|
|
118
|
+
if (isRawOpen(found)) return rawStep(html, found, depth);
|
|
119
|
+
return tagStep(found, tag, depth);
|
|
120
|
+
}
|
|
121
|
+
function isRawOpen(found) {
|
|
122
|
+
return !found.closing && RAW_TEXT.has(found.name);
|
|
123
|
+
}
|
|
124
|
+
function rawStep(html, found, depth) {
|
|
125
|
+
const rawEnd = rawElementEnd(html.toLowerCase(), found.name, found.bracket);
|
|
126
|
+
if (rawEnd < 0) return null;
|
|
127
|
+
return { at: rawEnd, depth, done: false };
|
|
128
|
+
}
|
|
129
|
+
function tagStep(found, tag, depth) {
|
|
130
|
+
const same = found.name === tag && !VOID.has(found.name);
|
|
131
|
+
if (!same || !found.closing) return openStep(found, depth, same);
|
|
132
|
+
return closingStep(found, depth);
|
|
133
|
+
}
|
|
134
|
+
function closingStep(found, depth) {
|
|
135
|
+
if (depth === 1) return { at: found.start, depth, done: true };
|
|
136
|
+
return { at: found.bracket + 1, depth: depth - 1, done: false };
|
|
137
|
+
}
|
|
138
|
+
function openStep(found, depth, same) {
|
|
139
|
+
const nextDepth = same ? depth + 1 : depth;
|
|
140
|
+
return { at: found.bracket + 1, depth: nextDepth, done: false };
|
|
141
|
+
}
|
|
142
|
+
function textNodes(inner) {
|
|
143
|
+
const found = nextMarkup(inner, 0);
|
|
144
|
+
if (!found) return inner;
|
|
145
|
+
return inner.slice(0, found.start) + textAfter(inner, found);
|
|
146
|
+
}
|
|
147
|
+
function textAfter(inner, found) {
|
|
148
|
+
if (found.kind !== "tag") return textNodes(inner.slice(found.end));
|
|
149
|
+
return textAfterTag(inner, found);
|
|
150
|
+
}
|
|
151
|
+
function textAfterTag(inner, found) {
|
|
152
|
+
if (skipsTag(found)) return textNodes(inner.slice(found.bracket + 1));
|
|
153
|
+
if (RAW_TEXT.has(found.name)) return textAfterRaw(inner, found);
|
|
154
|
+
return textAfterChild(inner, found);
|
|
155
|
+
}
|
|
156
|
+
function skipsTag(found) {
|
|
157
|
+
return found.closing || VOID.has(found.name);
|
|
158
|
+
}
|
|
159
|
+
function textAfterRaw(inner, found) {
|
|
160
|
+
const rawEnd = rawElementEnd(inner.toLowerCase(), found.name, found.bracket);
|
|
161
|
+
return textNodes(inner.slice(rawEnd < 0 ? inner.length : rawEnd));
|
|
162
|
+
}
|
|
163
|
+
function textAfterChild(inner, found) {
|
|
164
|
+
const child = sliceUntilClose(inner, found.bracket + 1, found.name);
|
|
165
|
+
if (child === null) return "";
|
|
166
|
+
const closeBracket = inner.indexOf(">", found.bracket + 1 + child.length);
|
|
167
|
+
return textNodes(inner.slice(closeBracket < 0 ? inner.length : closeBracket + 1));
|
|
168
|
+
}
|
|
169
|
+
function nextMarkup(html, from) {
|
|
170
|
+
const start = html.indexOf("<", from);
|
|
171
|
+
if (start < 0) return null;
|
|
172
|
+
return markupAt(html, start);
|
|
173
|
+
}
|
|
174
|
+
function markupAt(html, start) {
|
|
175
|
+
const skipped = skippedMarkup(html, start);
|
|
176
|
+
if (skipped) return skipped;
|
|
177
|
+
return tagMarkup(html, start);
|
|
178
|
+
}
|
|
179
|
+
function skippedMarkup(html, start) {
|
|
180
|
+
if (html.startsWith("<!--", start)) return boundedSkip(html, start, "-->", 4);
|
|
181
|
+
if (html.startsWith("<!", start) || html.startsWith("<?", start))
|
|
182
|
+
return boundedSkip(html, start, ">", 1);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function boundedSkip(html, start, marker, from) {
|
|
186
|
+
const end = html.indexOf(marker, start + from);
|
|
187
|
+
return end < 0 ? null : { kind: "skip", start, end: end + marker.length };
|
|
188
|
+
}
|
|
189
|
+
function tagMarkup(html, start) {
|
|
190
|
+
const name = /^<\/?([a-zA-Z][^\s/>]*)/.exec(html.slice(start, start + 128));
|
|
191
|
+
if (!name) return { kind: "skip", start, end: start + 1 };
|
|
192
|
+
const bracket = closingBracket(html, start + name[0].length);
|
|
193
|
+
if (bracket < 0) return null;
|
|
194
|
+
return {
|
|
195
|
+
kind: "tag",
|
|
196
|
+
start,
|
|
197
|
+
name: name[1].toLowerCase(),
|
|
198
|
+
closing: html[start + 1] === "/",
|
|
199
|
+
bracket
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function rawElementEnd(lower, tag, openBracket) {
|
|
203
|
+
const marker = `</${tag}`;
|
|
204
|
+
const at2 = nextRawClose(lower, marker, openBracket + 1);
|
|
205
|
+
if (at2 < 0) return -1;
|
|
206
|
+
const bracket = lower.indexOf(">", at2 + marker.length);
|
|
207
|
+
return bracket < 0 ? -1 : bracket + 1;
|
|
208
|
+
}
|
|
209
|
+
function nextRawClose(lower, marker, from) {
|
|
210
|
+
const at2 = lower.indexOf(marker, from);
|
|
211
|
+
if (at2 < 0) return -1;
|
|
212
|
+
const next = lower[at2 + marker.length];
|
|
213
|
+
if (closesRawText(next)) return at2;
|
|
214
|
+
return nextRawClose(lower, marker, at2 + marker.length);
|
|
215
|
+
}
|
|
216
|
+
function closesRawText(next) {
|
|
217
|
+
return next !== void 0 && /[\s/>]/.test(next);
|
|
218
|
+
}
|
|
219
|
+
function closingBracket(html, from) {
|
|
220
|
+
let quote = "";
|
|
221
|
+
for (let i = from; i < html.length; i++) {
|
|
222
|
+
const c = html[i];
|
|
223
|
+
if (quote) quote = c === quote ? "" : quote;
|
|
224
|
+
else if (c === '"' || c === "'") quote = c;
|
|
225
|
+
else if (c === ">") return i;
|
|
226
|
+
}
|
|
227
|
+
return -1;
|
|
228
|
+
}
|
|
229
|
+
function decodeEntities(value) {
|
|
230
|
+
return value.replace(
|
|
231
|
+
/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi,
|
|
232
|
+
(entity, body) => {
|
|
233
|
+
const named = {
|
|
234
|
+
amp: "&",
|
|
235
|
+
lt: "<",
|
|
236
|
+
gt: ">",
|
|
237
|
+
quot: '"',
|
|
238
|
+
apos: "'",
|
|
239
|
+
nbsp: " "
|
|
240
|
+
};
|
|
241
|
+
const lower = body.toLowerCase();
|
|
242
|
+
if (lower in named) return named[lower];
|
|
243
|
+
const code = Number.parseInt(
|
|
244
|
+
lower.slice(lower.startsWith("#x") ? 2 : 1),
|
|
245
|
+
lower.startsWith("#x") ? 16 : 10
|
|
246
|
+
);
|
|
247
|
+
return Number.isInteger(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : entity;
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function authoredClasses(node, tag, live) {
|
|
252
|
+
const classes = [...tag.classes];
|
|
253
|
+
if (classes.join(" ") === node.classes.join(" ")) return null;
|
|
254
|
+
if (!classes.every((c) => PLAIN_CLASS.test(c) && node.classes.includes(c))) return null;
|
|
255
|
+
const simple = `${node.tag}${classes.map((c) => `.${c}`).join("")}`;
|
|
256
|
+
const siblings = node.parent === null ? [node] : live[node.parent].children.map((i) => live[i]);
|
|
257
|
+
const siblingMatchCount = siblings.filter(
|
|
258
|
+
(other) => other.tag === node.tag && classes.every((c) => other.classes.includes(c))
|
|
259
|
+
).length;
|
|
260
|
+
const relative2 = node.relative.startsWith("#") ? node.relative : siblingMatchCount === 1 ? simple : `${node.tag}:nth-child(${node.position})`;
|
|
261
|
+
return { classes, groupSelector: simple, siblingMatchCount, relative: relative2 };
|
|
262
|
+
}
|
|
263
|
+
|
|
7
264
|
// src/elementMap.js
|
|
8
265
|
import { createHash } from "node:crypto";
|
|
9
266
|
function collectElementDom() {
|
|
@@ -63,6 +320,7 @@ function collectElementDom() {
|
|
|
63
320
|
text: [...element.childNodes].filter((child) => child.nodeType === 3).map((child) => child.textContent).join("").replace(/\s+/g, " ").trim(),
|
|
64
321
|
media: ["img", "video", "audio", "source", "iframe"].includes(tag) ? element.getAttribute("src") : null,
|
|
65
322
|
relative: element.id && document.querySelectorAll(`#${CSS.escape(element.id)}`).length === 1 ? `#${CSS.escape(element.id)}` : siblingCount(element, simple) === 1 ? simple : `${CSS.escape(tag)}:nth-child(${siblings.indexOf(element) + 1})`,
|
|
323
|
+
position: siblings.indexOf(element) + 1,
|
|
66
324
|
groupSelector: simple,
|
|
67
325
|
siblingMatchCount: siblingCount(element, simple),
|
|
68
326
|
children: []
|
|
@@ -106,10 +364,26 @@ function mediaIdentifier(source) {
|
|
|
106
364
|
if (source.startsWith("blob:")) return "blob:runtime";
|
|
107
365
|
return source;
|
|
108
366
|
}
|
|
109
|
-
function
|
|
367
|
+
function childPlaces(members) {
|
|
368
|
+
const runs = [];
|
|
369
|
+
let start = members[0].position;
|
|
370
|
+
let previous = start;
|
|
371
|
+
for (const member of members.slice(1)) {
|
|
372
|
+
if (member.position === previous + 1) {
|
|
373
|
+
previous = member.position;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
runs.push(start === previous ? `${start}` : `${start}-${previous}`);
|
|
377
|
+
start = previous = member.position;
|
|
378
|
+
}
|
|
379
|
+
runs.push(start === previous ? `${start}` : `${start}-${previous}`);
|
|
380
|
+
return runs.join(",");
|
|
381
|
+
}
|
|
382
|
+
function describe(node, selector, count, places = "") {
|
|
110
383
|
const fields = [selector];
|
|
111
384
|
if (!selector.startsWith(node.tag)) fields.push(node.tag);
|
|
112
|
-
if (count > 1) fields.push(`\xD7${count}`);
|
|
385
|
+
if (count > 1) fields.push(`\xD7${count}${places ? ` @${places}` : ""}`);
|
|
386
|
+
if (node.generated) fields.push("(generated)");
|
|
113
387
|
if (node.id && selector !== `#${node.id}`) fields.push(`id=${JSON.stringify(node.id)}`);
|
|
114
388
|
if (node.classes.length && !selector.includes("."))
|
|
115
389
|
fields.push(`class=${JSON.stringify(node.classes.join(" "))}`);
|
|
@@ -132,8 +406,8 @@ function formatElementMap(snapshot, { file = "index.html" } = {}) {
|
|
|
132
406
|
if (!node.id && previous?.key === key) previous.indices.push(index);
|
|
133
407
|
else groups.push({ key, indices: [index] });
|
|
134
408
|
}
|
|
135
|
-
for (const
|
|
136
|
-
const members =
|
|
409
|
+
for (const group2 of groups) {
|
|
410
|
+
const members = group2.indices.map((index) => nodes[index]);
|
|
137
411
|
const node = members[0];
|
|
138
412
|
const exactGroup = members.length > 1 && node.siblingMatchCount === members.length;
|
|
139
413
|
if (members.length > 1 && !exactGroup) {
|
|
@@ -144,7 +418,8 @@ function formatElementMap(snapshot, { file = "index.html" } = {}) {
|
|
|
144
418
|
function emit(members, depth, repeatedParents = false) {
|
|
145
419
|
const node = members[0];
|
|
146
420
|
const selector = members.length > 1 && !repeatedParents ? node.groupSelector : node.relative;
|
|
147
|
-
|
|
421
|
+
const places = members.length > 1 && !repeatedParents ? childPlaces(members) : "";
|
|
422
|
+
lines.push(`${" ".repeat(depth)}${describe(node, selector, members.length, places)}`);
|
|
148
423
|
entries.push({
|
|
149
424
|
line: lines.length,
|
|
150
425
|
selectors: members.map((member) => member.selector),
|
|
@@ -371,6 +646,36 @@ function rawTextElements(text, tag) {
|
|
|
371
646
|
}
|
|
372
647
|
return elements;
|
|
373
648
|
}
|
|
649
|
+
function htmlTagsOf(file, text) {
|
|
650
|
+
return [...text.matchAll(/<([a-z][\w-]*)\b[^>]*>/gi)].map((m) => {
|
|
651
|
+
const attr = (name) => {
|
|
652
|
+
const quoted = new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(m[0]);
|
|
653
|
+
if (quoted) return quoted[1];
|
|
654
|
+
return new RegExp(`\\s${name}\\s*=\\s*([^\\s"'=<>]+)`, "i").exec(m[0])?.[1];
|
|
655
|
+
};
|
|
656
|
+
return {
|
|
657
|
+
file,
|
|
658
|
+
start: m.index,
|
|
659
|
+
end: m.index + m[0].length,
|
|
660
|
+
tag: m[1].toLowerCase(),
|
|
661
|
+
id: attr("id"),
|
|
662
|
+
hfId: attr("data-hf-id"),
|
|
663
|
+
classes: (attr("class") ?? "").split(/\s+/).filter(Boolean)
|
|
664
|
+
};
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
function authoredHtml(project) {
|
|
668
|
+
const html = { files: /* @__PURE__ */ new Map(), htmlTags: [] };
|
|
669
|
+
for (const path of sourceFiles(project)) {
|
|
670
|
+
const file = relative(project, path);
|
|
671
|
+
if (!file.endsWith(".html") || /(?:^|\/)(?:vendor|lib)\//.test(file)) continue;
|
|
672
|
+
const text = readFileSync(path, "utf8");
|
|
673
|
+
if (text.length > 2e6) continue;
|
|
674
|
+
html.files.set(file, { text });
|
|
675
|
+
html.htmlTags.push(...htmlTagsOf(file, text));
|
|
676
|
+
}
|
|
677
|
+
return html;
|
|
678
|
+
}
|
|
374
679
|
var Sources = class {
|
|
375
680
|
constructor(project) {
|
|
376
681
|
this.project = project;
|
|
@@ -394,18 +699,7 @@ var Sources = class {
|
|
|
394
699
|
this.addChunk(file, script.content, script.contentStart);
|
|
395
700
|
for (const style of rawTextElements(text, "style"))
|
|
396
701
|
this.addCss(file, style.content, style.contentStart);
|
|
397
|
-
|
|
398
|
-
const attr = (name) => new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(m[0])?.[1];
|
|
399
|
-
this.htmlTags.push({
|
|
400
|
-
file,
|
|
401
|
-
start: m.index,
|
|
402
|
-
end: m.index + m[0].length,
|
|
403
|
-
tag: m[1].toLowerCase(),
|
|
404
|
-
id: attr("id"),
|
|
405
|
-
hfId: attr("data-hf-id"),
|
|
406
|
-
classes: (attr("class") ?? "").split(/\s+/).filter(Boolean)
|
|
407
|
-
});
|
|
408
|
-
}
|
|
702
|
+
this.htmlTags.push(...htmlTagsOf(file, text));
|
|
409
703
|
}
|
|
410
704
|
}
|
|
411
705
|
}
|
|
@@ -470,8 +764,10 @@ var Sources = class {
|
|
|
470
764
|
};
|
|
471
765
|
}
|
|
472
766
|
markup(element, target) {
|
|
473
|
-
const
|
|
474
|
-
|
|
767
|
+
const sameTag = this.htmlTags.filter((tag) => tag.tag === element.tag);
|
|
768
|
+
const byHfId = element.hfId ? sameTag.filter((tag) => tag.hfId === element.hfId) : [];
|
|
769
|
+
const tags = byHfId.length ? byHfId : sameTag.filter(
|
|
770
|
+
(tag) => element.id ? tag.id === element.id : element.classes.length > 0 && element.classes.every((c) => tag.classes.includes(c))
|
|
475
771
|
);
|
|
476
772
|
return tags.length === 1 ? this.slice(tags[0].file, tags[0].start, tags[0].end, "html opening tag", target) : null;
|
|
477
773
|
}
|
|
@@ -616,8 +912,8 @@ function mapInspection(raw, request, context) {
|
|
|
616
912
|
return {
|
|
617
913
|
target: index,
|
|
618
914
|
status: result.status,
|
|
619
|
-
candidates: result.candidates
|
|
620
|
-
candidatesOmitted:
|
|
915
|
+
candidates: result.candidates,
|
|
916
|
+
candidatesOmitted: 0,
|
|
621
917
|
animations: result.animations.length,
|
|
622
918
|
coTargets: [
|
|
623
919
|
...new Map(
|
|
@@ -656,22 +952,30 @@ function mapInspection(raw, request, context) {
|
|
|
656
952
|
const offset = request.offset ?? 0;
|
|
657
953
|
if (!Number.isInteger(offset) || offset < 0)
|
|
658
954
|
throw new Error("offset must be a non-negative integer");
|
|
955
|
+
const size = () => Buffer.byteLength(JSON.stringify(response));
|
|
956
|
+
const trimCandidates = (limit, keep) => {
|
|
957
|
+
while (size() > limit) {
|
|
958
|
+
const longest = response.results.reduce(
|
|
959
|
+
(a, b) => b.candidates.length > a.candidates.length ? b : a
|
|
960
|
+
);
|
|
961
|
+
if (longest.candidates.length <= keep) return;
|
|
962
|
+
longest.candidates.pop();
|
|
963
|
+
longest.candidatesOmitted++;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
trimCandidates(maxBytes / 2, 5);
|
|
659
967
|
for (let i = offset; i < selected.length; i++) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (Buffer.byteLength(JSON.stringify(response)) > maxBytes - 700) {
|
|
968
|
+
response.snippets.push(selected[i]);
|
|
969
|
+
if (size() > maxBytes - 700) {
|
|
663
970
|
response.snippets.pop();
|
|
664
971
|
response.omittedCount = selected.length - i;
|
|
665
972
|
response.omitted = selected.slice(i, i + 8).map((item) => ({ id: item.id, bytes: Buffer.byteLength(item.code) }));
|
|
666
|
-
response.
|
|
973
|
+
const nothingFit = response.snippets.length === 0;
|
|
974
|
+
response.nextOffset = nothingFit ? i + 1 < selected.length ? i + 1 : null : i;
|
|
667
975
|
break;
|
|
668
976
|
}
|
|
669
977
|
}
|
|
670
|
-
|
|
671
|
-
const r = response.results.find((r2) => r2.candidates.length);
|
|
672
|
-
r.candidates.pop();
|
|
673
|
-
r.candidatesOmitted++;
|
|
674
|
-
}
|
|
978
|
+
trimCandidates(maxBytes, 0);
|
|
675
979
|
if (Buffer.byteLength(JSON.stringify(response)) > maxBytes)
|
|
676
980
|
throw new Error("metadata_exceeds_budget: use fewer targets or increase maxBytes");
|
|
677
981
|
if (request.debug) return { ...response, debug: raw };
|
|
@@ -695,7 +999,8 @@ function timelinePage(request) {
|
|
|
695
999
|
const result = inspection.timeline(request);
|
|
696
1000
|
const following = [];
|
|
697
1001
|
const generation = inspected.__hfLiveInspection?.generation ?? null;
|
|
698
|
-
|
|
1002
|
+
const duration = inspected.__player?.getDuration();
|
|
1003
|
+
if (request.boundary === void 0) return { ...result, following, generation, duration };
|
|
699
1004
|
const boundary = request.boundary;
|
|
700
1005
|
const schedule = inspection.timeline({ ...request, boundary: void 0 });
|
|
701
1006
|
for (const crossing of result.events) {
|
|
@@ -707,7 +1012,7 @@ function timelinePage(request) {
|
|
|
707
1012
|
if (next && !following.includes(next)) following.push(next);
|
|
708
1013
|
}
|
|
709
1014
|
}
|
|
710
|
-
return { ...result, following, generation };
|
|
1015
|
+
return { ...result, following, generation, duration };
|
|
711
1016
|
}
|
|
712
1017
|
function liveGenerationPage() {
|
|
713
1018
|
return window.__hfLiveInspection?.generation ?? null;
|
|
@@ -722,13 +1027,13 @@ var COVERAGE_NOTE = "Observed DOM-target GSAP and explicit HF items only; callba
|
|
|
722
1027
|
var BOUNDARY_NOTE = "only actions crossing, starting, ending or instantaneous here. Later stagger members are excluded, not absent. Group spans do not imply simultaneous starts. Target labels T1, T2, ... identify the same element within this response, not DOM order.";
|
|
723
1028
|
function formatTimeline(raw, request, view) {
|
|
724
1029
|
const offset = request.offset ?? 0;
|
|
725
|
-
const allGroups = groupEvents(raw.events);
|
|
1030
|
+
const allGroups = byComponent(groupEvents(raw.events));
|
|
726
1031
|
const groups = allGroups.slice(offset, offset + GROUPS_PER_PAGE);
|
|
727
1032
|
const label = labeler();
|
|
728
1033
|
const events = groups.flat().map((event, index) => ({
|
|
729
1034
|
...event,
|
|
730
1035
|
stack: void 0,
|
|
731
|
-
sources: (event.stack ? view.map(event.stack, index) : []).map((s) => ({
|
|
1036
|
+
sources: onePerSourceLine(event.stack ? view.map(event.stack, index) : []).map((s) => ({
|
|
732
1037
|
file: s.file,
|
|
733
1038
|
line: s.startLine
|
|
734
1039
|
}))
|
|
@@ -737,11 +1042,14 @@ function formatTimeline(raw, request, view) {
|
|
|
737
1042
|
const lines = [
|
|
738
1043
|
`Timeline \xB7 global seconds \xB7 ${raw.status}`,
|
|
739
1044
|
`generation: ${view.generation}`,
|
|
740
|
-
...raw.status !== "found" ? raw.candidates.map(
|
|
1045
|
+
...raw.status !== "found" ? raw.candidates.map(
|
|
1046
|
+
(c) => `${c.ref} ${targetSummary([c])[0]} ${c.tag}${c.composition ? ` in ${c.composition}` : ""}`
|
|
1047
|
+
) : [],
|
|
1048
|
+
...pastTheEnd(request, raw.duration),
|
|
741
1049
|
...request.boundary !== void 0 ? [`Boundary ${request.boundary}s: ${BOUNDARY_NOTE}`] : [],
|
|
742
1050
|
...describeGroups(groups, request, view.map, label),
|
|
743
1051
|
...describeFollowing(raw, view.map, label),
|
|
744
|
-
...describeRelations(
|
|
1052
|
+
...describeRelations(groups),
|
|
745
1053
|
...nextOffset !== null ? [`More groups: repeat with offset=${nextOffset} and generation.`] : [],
|
|
746
1054
|
...raw.warnings.map((w) => `Coverage: ${w}`),
|
|
747
1055
|
...events.some((e) => e.values) ? [
|
|
@@ -777,6 +1085,21 @@ function groupEvents(events) {
|
|
|
777
1085
|
}
|
|
778
1086
|
return [...groups.values()];
|
|
779
1087
|
}
|
|
1088
|
+
var headingOf = (event) => event.component?.id ? `#${event.component.id}` : event.component?.composition ?? "unassigned";
|
|
1089
|
+
function byComponent(groups) {
|
|
1090
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1091
|
+
for (const group2 of groups) {
|
|
1092
|
+
const heading = headingOf(group2[0]);
|
|
1093
|
+
owners.set(heading, [...owners.get(heading) ?? [], group2]);
|
|
1094
|
+
}
|
|
1095
|
+
return [...owners.values()].flat();
|
|
1096
|
+
}
|
|
1097
|
+
function pastTheEnd(request, duration) {
|
|
1098
|
+
const from = request.range?.[0] ?? request.at ?? request.boundary;
|
|
1099
|
+
const filmLength = duration !== void 0 && duration > 0 ? duration : void 0;
|
|
1100
|
+
if (filmLength === void 0 || from === void 0 || from < filmLength) return [];
|
|
1101
|
+
return [`Requested time is past the end: the film is ${Number(filmLength.toFixed(4))}s long.`];
|
|
1102
|
+
}
|
|
780
1103
|
function labeler() {
|
|
781
1104
|
const labels = /* @__PURE__ */ new Map();
|
|
782
1105
|
return (ref) => {
|
|
@@ -785,10 +1108,15 @@ function labeler() {
|
|
|
785
1108
|
};
|
|
786
1109
|
}
|
|
787
1110
|
var at = (seconds) => seconds.toFixed(4);
|
|
788
|
-
|
|
1111
|
+
function onePerSourceLine(sources) {
|
|
1112
|
+
return [...new Map(sources.map((s) => [`${s.file}:${s.startLine}`, s])).values()];
|
|
1113
|
+
}
|
|
1114
|
+
var located = (sources) => onePerSourceLine(sources).map((s) => `${s.file}:${s.startLine}`).join(", ");
|
|
789
1115
|
function describeValue(value) {
|
|
790
1116
|
if (value.kind === "unknown") return "?";
|
|
791
|
-
|
|
1117
|
+
if (value.kind === "expression" && value.value === "custom function") return "custom function";
|
|
1118
|
+
const shown = typeof value.value === "number" ? Number(value.value.toFixed(4)) : value.value;
|
|
1119
|
+
return `${shown}${value.kind === "literal" ? "" : ` (${value.kind})`}`;
|
|
792
1120
|
}
|
|
793
1121
|
function describeValues(values) {
|
|
794
1122
|
return ` authored: ${values.properties.map((p) => `${p.property}: ${describeValue(p.from)} \u2192 ${describeValue(p.to)}`).join("; ")}; ease: ${describeValue(values.ease)}`;
|
|
@@ -805,7 +1133,7 @@ function describeGroups(groups, request, map, label) {
|
|
|
805
1133
|
targets: [...new Map(members.flatMap((e) => e.targets).map((t) => [t.ref, t])).values()]
|
|
806
1134
|
};
|
|
807
1135
|
const component = event.component;
|
|
808
|
-
const heading =
|
|
1136
|
+
const heading = headingOf(event);
|
|
809
1137
|
if (owner !== heading) {
|
|
810
1138
|
lines.push(`
|
|
811
1139
|
${heading}${component ? ` [${component.ref}]` : ""}`);
|
|
@@ -835,7 +1163,8 @@ ${heading}${component ? ` [${component.ref}]` : ""}`);
|
|
|
835
1163
|
function targetSummary(targets) {
|
|
836
1164
|
const counts = /* @__PURE__ */ new Map();
|
|
837
1165
|
for (const target of targets) {
|
|
838
|
-
const
|
|
1166
|
+
const own = target.id ? `#${target.id}` : target.hfId ?? (target.classes.length ? `.${target.classes.join(".")}` : target.tag);
|
|
1167
|
+
const name = !target.id && target.withinId ? `${own} in #${target.withinId}` : own;
|
|
839
1168
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
840
1169
|
}
|
|
841
1170
|
return [...counts].map(([name, count]) => count > 1 ? `${name} \xD7${count}` : name);
|
|
@@ -882,18 +1211,23 @@ function describeFollowing(raw, map, label) {
|
|
|
882
1211
|
lines.push(` ${raw.following.length - FOLLOWING_SHOWN} additional following actions omitted.`);
|
|
883
1212
|
return lines;
|
|
884
1213
|
}
|
|
885
|
-
function describeRelations(
|
|
1214
|
+
function describeRelations(groups) {
|
|
886
1215
|
const lines = [];
|
|
1216
|
+
const events = groups.flatMap((members, group2) => members.map((event) => ({ event, group: group2 })));
|
|
1217
|
+
const described = /* @__PURE__ */ new Set();
|
|
887
1218
|
for (let a = 0; a < events.length; a++)
|
|
888
1219
|
for (let b = a + 1; b < events.length; b++) {
|
|
889
|
-
const left = events[a];
|
|
890
|
-
const right = events[b];
|
|
1220
|
+
const left = events[a].event;
|
|
1221
|
+
const right = events[b].event;
|
|
1222
|
+
const pair = `${events[a].group}:${events[b].group}`;
|
|
1223
|
+
if (described.has(pair)) continue;
|
|
891
1224
|
if (left.kind !== "animation" || right.kind !== "animation") continue;
|
|
892
1225
|
if (left.end < right.start || right.end < left.start) continue;
|
|
893
1226
|
const shared = left.targets.some((t) => right.targets.some((r) => r.ref === t.ref));
|
|
894
1227
|
const properties = left.properties.filter((p) => right.properties.includes(p));
|
|
895
1228
|
if (!shared || !properties.length || lines.length >= RELATIONS_SHOWN) continue;
|
|
896
1229
|
const kind = left.start === left.end || right.start === right.end ? "instantaneous action touches or occurs inside the other interval" : Math.min(left.end, right.end) > Math.max(left.start, right.start) ? "intervals overlap" : "touch at an endpoint only, no positive-duration overlap";
|
|
1230
|
+
described.add(pair);
|
|
897
1231
|
lines.push(
|
|
898
1232
|
`Relation: ${at(left.start)}\u2013${at(left.end)} and ${at(right.start)}\u2013${at(right.end)} share targets and ${properties.join(",")}; ${kind}; not necessarily an error.`
|
|
899
1233
|
);
|
|
@@ -936,7 +1270,10 @@ async function inspectTimeline(runner, view, request, reused) {
|
|
|
936
1270
|
async function inspectElementMap(runner, view, file = "index.html") {
|
|
937
1271
|
const snapshot = await runner.run(collectElementDom, void 0);
|
|
938
1272
|
assertPageGeneration(view, snapshot.generation);
|
|
939
|
-
const
|
|
1273
|
+
const authored = view.authoredHtml?.();
|
|
1274
|
+
const map = formatElementMap(authored ? withAuthoredState(snapshot, authored) : snapshot, {
|
|
1275
|
+
file
|
|
1276
|
+
});
|
|
940
1277
|
await view.assertCurrent();
|
|
941
1278
|
return { ...map, sourceVersion: view.sourceVersion, generation: view.generation };
|
|
942
1279
|
}
|
|
@@ -1005,6 +1342,7 @@ ${runtime()}`.replace(/<\/script/gi, "<\\/script");
|
|
|
1005
1342
|
const prepared = html.replace(/<head\b[^>]*>/i, (head) => `${head}<script>${script}</script>`);
|
|
1006
1343
|
let document2 = prepared;
|
|
1007
1344
|
let context;
|
|
1345
|
+
let authored;
|
|
1008
1346
|
const catalog = /* @__PURE__ */ new Map();
|
|
1009
1347
|
const view = {
|
|
1010
1348
|
generation,
|
|
@@ -1017,6 +1355,9 @@ ${runtime()}`.replace(/<\/script/gi, "<\\/script");
|
|
|
1017
1355
|
}
|
|
1018
1356
|
return context;
|
|
1019
1357
|
},
|
|
1358
|
+
authoredHtml() {
|
|
1359
|
+
return authored ??= authoredHtml(project);
|
|
1360
|
+
},
|
|
1020
1361
|
async assertCurrent() {
|
|
1021
1362
|
if (fingerprint(project) !== sourceVersion)
|
|
1022
1363
|
throw new Error("stale_preview: the project changed; query again once the preview reloads");
|