@hyperframes/inspector 0.8.156 → 0.8.157

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/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 describe(node, selector, count) {
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 group of groups) {
136
- const members = group.indices.map((index) => nodes[index]);
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
- lines.push(`${" ".repeat(depth)}${describe(node, selector, members.length)}`);
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
- for (const m of text.matchAll(/<([a-z][\w-]*)\b[^>]*>/gi)) {
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 tags = this.htmlTags.filter(
474
- (tag) => tag.tag === element.tag && (element.hfId ? tag.hfId === element.hfId : element.id ? tag.id === element.id : element.classes.length > 0 && element.classes.every((c) => tag.classes.includes(c)))
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.slice(0, 5),
620
- candidatesOmitted: Math.max(0, result.candidates.length - 5),
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
- const snippet = selected[i];
661
- response.snippets.push(snippet);
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.nextOffset = i;
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
- while (Buffer.byteLength(JSON.stringify(response)) > maxBytes && response.results.some((r) => r.candidates.length)) {
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
- if (request.boundary === void 0) return { ...result, following, generation };
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((c) => `${c.ref} #${c.id} ${c.tag}`) : [],
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(events),
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
- var located = (sources) => sources.map((s) => `${s.file}:${s.startLine}`).join(", ");
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
- return `${value.value}${value.kind === "literal" ? "" : ` (${value.kind})`}`;
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 = component?.id ? `#${component.id}` : component?.composition ?? "unassigned";
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 name = target.id ? `#${target.id}` : target.hfId ?? (target.classes.length ? `.${target.classes.join(".")}` : target.tag);
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(events) {
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 map = formatElementMap(snapshot, { file });
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");