@jokerized/decksmith 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -864,35 +864,35 @@ function elbow(sid, from, to, o) {
864
864
  }
865
865
  return path(d, { ...strokeAttrs(o), "marker-end": `url(#${headId(sid, o.stroke)})` });
866
866
  }
867
- function tracks(width, count, gap, x0 = 0) {
868
- const w = count > 1 ? (width - gap * (count - 1)) / count : width;
869
- return Array.from({ length: count }, (_, i) => ({ x: x0 + i * (w + gap), w }));
867
+ function tracks(width, count2, gap, x0 = 0) {
868
+ const w = count2 > 1 ? (width - gap * (count2 - 1)) / count2 : width;
869
+ return Array.from({ length: count2 }, (_, i) => ({ x: x0 + i * (w + gap), w }));
870
870
  }
871
871
  function fitBoxes(req) {
872
- const count = req.labels.length;
872
+ const count2 = req.labels.length;
873
873
  const minGap = req.minGap ?? 24;
874
874
  const padEm = req.padEm ?? 0.8;
875
875
  const weight = req.weight ?? 600;
876
876
  const x0 = req.x0 ?? 0;
877
877
  const unit = Math.max(1, ...req.labels.map((l) => textWidth(l, 1, weight) + 2 * padEm));
878
- const sizeAt = (gap) => count < 1 ? req.size : (req.width - gap * (count - 1)) / count / unit;
878
+ const sizeAt = (gap) => count2 < 1 ? req.size : (req.width - gap * (count2 - 1)) / count2 / unit;
879
879
  if (sizeAt(req.gap) >= req.size) {
880
- return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width, count, req.gap, x0) };
880
+ return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width, count2, req.gap, x0) };
881
881
  }
882
- if (count > 1 && sizeAt(minGap) >= req.size) {
883
- const gap = (req.width - count * req.size * unit) / (count - 1);
884
- return { ok: true, size: req.size, gap, boxes: tracks(req.width, count, gap, x0) };
882
+ if (count2 > 1 && sizeAt(minGap) >= req.size) {
883
+ const gap = (req.width - count2 * req.size * unit) / (count2 - 1);
884
+ return { ok: true, size: req.size, gap, boxes: tracks(req.width, count2, gap, x0) };
885
885
  }
886
886
  const size2 = sizeAt(minGap);
887
887
  if (size2 >= MIN_FONT) {
888
- return { ok: true, size: size2, gap: minGap, boxes: tracks(req.width, count, minGap, x0) };
888
+ return { ok: true, size: size2, gap: minGap, boxes: tracks(req.width, count2, minGap, x0) };
889
889
  }
890
890
  return {
891
891
  ok: false,
892
892
  size: MIN_FONT,
893
893
  gap: minGap,
894
- boxes: tracks(req.width, count, minGap, x0),
895
- needed: count * MIN_FONT * unit + minGap * (count - 1)
894
+ boxes: tracks(req.width, count2, minGap, x0),
895
+ needed: count2 * MIN_FONT * unit + minGap * (count2 - 1)
896
896
  };
897
897
  }
898
898
 
@@ -1225,8 +1225,8 @@ var MID_GAP = 56;
1225
1225
  var BAND_GAP = 34;
1226
1226
  var COL_GAP = 28;
1227
1227
  var MIN_COL = 250;
1228
- function lineBudget(count, stageH) {
1229
- const perSide = Math.max(1, Math.ceil(count / 2));
1228
+ function lineBudget(count2, stageH) {
1229
+ const perSide = Math.max(1, Math.ceil(count2 / 2));
1230
1230
  const room = (stageH - (perSide - 1) * LAB_GAP) / perSide;
1231
1231
  const lines = Math.floor((room - ASCENT - RULE_LIFT - RULE_W) / (LAB * LAB_LH)) + 1;
1232
1232
  return Math.min(LINE_CEIL, Math.max(1, lines));
@@ -1637,7 +1637,7 @@ var HEAD_GAP = 14;
1637
1637
  var barCompare = (beat, ctx) => {
1638
1638
  const { sid, theme } = ctx;
1639
1639
  const p = beat.params;
1640
- const count = p.bars.length;
1640
+ const count2 = p.bars.length;
1641
1641
  const W = contentW(ctx.format);
1642
1642
  const face = faceOf(ctx.theme.fontStack);
1643
1643
  const tall = isPortrait(ctx.format);
@@ -1651,7 +1651,7 @@ var barCompare = (beat, ctx) => {
1651
1651
  0,
1652
1652
  face
1653
1653
  );
1654
- let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (count + GAP_RATIO * (count - 1)));
1654
+ let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (count2 + GAP_RATIO * (count2 - 1)));
1655
1655
  let pitch = bar * (1 + GAP_RATIO);
1656
1656
  const gutterMax = tall ? 0 : share(GUTTER_MAX, W);
1657
1657
  const gutterInner = tall ? W : gutterMax - GUTTER_PAD;
@@ -1673,7 +1673,7 @@ var barCompare = (beat, ctx) => {
1673
1673
  if (tall) {
1674
1674
  bar = Math.min(
1675
1675
  BAR_MAX,
1676
- (avail - unitBand - FOOT - count * head) / (count + GAP_RATIO * (count - 1))
1676
+ (avail - unitBand - FOOT - count2 * head) / (count2 + GAP_RATIO * (count2 - 1))
1677
1677
  );
1678
1678
  pitch = head + bar * (1 + GAP_RATIO);
1679
1679
  } else {
@@ -1684,7 +1684,7 @@ var barCompare = (beat, ctx) => {
1684
1684
  }
1685
1685
  }
1686
1686
  const valueSize = Math.max(MIN_FONT, Math.min(VALUE_MAX, bar * 0.9));
1687
- const barsH = (count - 1) * pitch + head + bar;
1687
+ const barsH = (count2 - 1) * pitch + head + bar;
1688
1688
  const foot = Math.max(
1689
1689
  0,
1690
1690
  Math.ceil(
@@ -1697,7 +1697,7 @@ var barCompare = (beat, ctx) => {
1697
1697
  const H = barsH + unitBand + foot;
1698
1698
  if (H > avail) {
1699
1699
  throw new Error(
1700
- `bar-compare ${beat.id}: ${count} bars with labels this long need ${Math.ceil(H)}px of the ${Math.floor(avail)}px this slide has. Shorten the labels or split the beat.`
1700
+ `bar-compare ${beat.id}: ${count2} bars with labels this long need ${Math.ceil(H)}px of the ${Math.floor(avail)}px this slide has. Shorten the labels or split the beat.`
1701
1701
  );
1702
1702
  }
1703
1703
  const gutter = tall ? 0 : Math.min(
@@ -1850,7 +1850,7 @@ ${svg(id(sid, "chart"), W, H, body)}
1850
1850
  </div>${note2}`;
1851
1851
  const railsAt = 0.6;
1852
1852
  const barsAt = 0.95;
1853
- const step = Math.min(0.4, 2.4 / count);
1853
+ const step = Math.min(0.4, 2.4 / count2);
1854
1854
  const grow = 0.85;
1855
1855
  const tl = [
1856
1856
  ...chromeIn(sid, p.eyebrow !== void 0),
@@ -1900,11 +1900,11 @@ ${svg(id(sid, "chart"), W, H, body)}
1900
1900
  );
1901
1901
  }
1902
1902
  }
1903
- const settled = barsAt + (count - 1) * step + grow + 0.05;
1903
+ const settled = barsAt + (count2 - 1) * step + grow + 0.05;
1904
1904
  const holds = [settled + 0.2];
1905
1905
  const focal = rows.reduce((best, r) => Math.abs(r.value) > Math.abs(best.value) ? r : best);
1906
1906
  const spot = spotlighter(sid, ".bc-bar");
1907
- if (count > 1) tl.push(...spot.lit(`#${id(sid, "bar", focal.i)}`, settled));
1907
+ if (count2 > 1) tl.push(...spot.lit(`#${id(sid, "bar", focal.i)}`, settled));
1908
1908
  const tailAt = settled + 0.3;
1909
1909
  if (p.unit) {
1910
1910
  tl.push(tween(`#${id(sid, "unit")}`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, tailAt));
@@ -2397,6 +2397,90 @@ var dataTable = (beat, ctx) => {
2397
2397
  };
2398
2398
  };
2399
2399
 
2400
+ // src/emit/tex.ts
2401
+ import katex from "katex";
2402
+ var UNFIT_ATTR = "data-ds-unfit";
2403
+ var trust = (c) => c.command === "\\htmlClass";
2404
+ function texError(tex, display) {
2405
+ try {
2406
+ katex.renderToString(tex, {
2407
+ displayMode: display,
2408
+ throwOnError: true,
2409
+ strict: false,
2410
+ trust,
2411
+ output: "html"
2412
+ });
2413
+ return null;
2414
+ } catch (err) {
2415
+ return err instanceof Error ? err.message : String(err);
2416
+ }
2417
+ }
2418
+ var LEFT = /\\left(?![a-zA-Z])/g;
2419
+ var RIGHT = /\\right(?![a-zA-Z])/g;
2420
+ var NO_INK = /\\label\{[^{}]*\}|\\nonumber(?![a-zA-Z])|\\notag(?![a-zA-Z])/g;
2421
+ function braceBalance(tex) {
2422
+ let depth = 0;
2423
+ for (let i = 0; i < tex.length; i++) {
2424
+ const c = tex[i];
2425
+ if (c === "\\") {
2426
+ i++;
2427
+ continue;
2428
+ }
2429
+ if (c === "{") depth++;
2430
+ else if (c === "}") depth--;
2431
+ }
2432
+ return depth;
2433
+ }
2434
+ var count = (tex, re) => (tex.match(re) ?? []).length;
2435
+ function repairTex(input, display) {
2436
+ const repairs = [];
2437
+ let tex = input;
2438
+ let error = texError(tex, display);
2439
+ if (error === null) return { tex, repairs, error };
2440
+ const gone = tex.match(NO_INK) ?? [];
2441
+ if (gone.length) {
2442
+ tex = tex.replace(NO_INK, "").trim();
2443
+ repairs.push(`removed ${gone.join(", ")}, which draw nothing`);
2444
+ error = texError(tex, display);
2445
+ }
2446
+ const lefts = count(tex, LEFT);
2447
+ const rights = count(tex, RIGHT);
2448
+ if (error !== null && lefts !== rights) {
2449
+ const n3 = Math.abs(lefts - rights);
2450
+ tex = lefts > rights ? `${tex} ${"\\right.".repeat(n3)}` : `${"\\left.".repeat(n3)} ${tex}`;
2451
+ repairs.push(
2452
+ `${lefts} \\left against ${rights} \\right: added ${n3} invisible ${lefts > rights ? "\\right." : "\\left."} to balance them`
2453
+ );
2454
+ error = texError(tex, display);
2455
+ }
2456
+ const depth = braceBalance(tex);
2457
+ if (error !== null && depth !== 0) {
2458
+ const n3 = Math.abs(depth);
2459
+ tex = depth > 0 ? `${tex}${"}".repeat(n3)}` : `${"{".repeat(n3)}${tex}`;
2460
+ repairs.push(
2461
+ `${n3} unclosed brace group(s): ${depth > 0 ? "closed at the end" : "opened at the start"}`
2462
+ );
2463
+ error = texError(tex, display);
2464
+ }
2465
+ const undefinedNames = [];
2466
+ for (let round7 = 0; error !== null && round7 < 12; round7++) {
2467
+ const m = /Undefined control sequence: \\([a-zA-Z]+)/.exec(error);
2468
+ if (!m) break;
2469
+ const name = m[1];
2470
+ const next = tex.replace(new RegExp(`\\\\${name}(?![a-zA-Z])`, "g"), `\\operatorname{${name}}`);
2471
+ if (next === tex) break;
2472
+ tex = next;
2473
+ undefinedNames.push(`\\${name}`);
2474
+ error = texError(tex, display);
2475
+ }
2476
+ if (undefinedNames.length) {
2477
+ repairs.push(
2478
+ `${undefinedNames.join(", ")} ${undefinedNames.length === 1 ? "is" : "are"} never defined in the source \u2014 drawn as ${undefinedNames.length === 1 ? "its name" : "their names"} in upright type`
2479
+ );
2480
+ }
2481
+ return { tex, repairs, error };
2482
+ }
2483
+
2400
2484
  // src/emit/archetypes/equation-walk.ts
2401
2485
  var TRUST = 'function (c) { return c.command === "\\\\htmlClass"; }';
2402
2486
  var OPTS = `{ displayMode: true, trust: ${TRUST}, strict: false, output: "html" }`;
@@ -2411,6 +2495,9 @@ var SYNONYMS = [
2411
2495
  function normalise(tex) {
2412
2496
  let out = "";
2413
2497
  const map = [];
2498
+ const ends = [];
2499
+ const lead = [];
2500
+ let hint = null;
2414
2501
  for (let i = 0; i < tex.length; ) {
2415
2502
  if (/\s/.test(tex[i])) {
2416
2503
  i++;
@@ -2422,19 +2509,19 @@ function normalise(tex) {
2422
2509
  const m = new RegExp(`^(?:${re.source})`).exec(rest);
2423
2510
  return m ? { len: m[0].length, to } : null;
2424
2511
  }).find(Boolean);
2425
- if (hit) {
2426
- for (const ch of hit.to) {
2427
- out += ch;
2428
- map.push(i);
2429
- }
2430
- i += hit.len;
2431
- continue;
2512
+ const len = hit ? hit.len : 1;
2513
+ const chars = hit ? hit.to : tex[i];
2514
+ if (chars === "") hint ??= i;
2515
+ for (const ch of chars) {
2516
+ out += ch;
2517
+ map.push(i);
2518
+ ends.push(i + len);
2519
+ lead.push(hint ?? i);
2520
+ hint = null;
2432
2521
  }
2433
- out += tex[i];
2434
- map.push(i);
2435
- i++;
2522
+ i += len;
2436
2523
  }
2437
- return { text: out, map };
2524
+ return { text: out, map, ends, lead };
2438
2525
  }
2439
2526
  function locate(tex, term) {
2440
2527
  const hay = normalise(tex);
@@ -2442,58 +2529,131 @@ function locate(tex, term) {
2442
2529
  if (needle === "") return null;
2443
2530
  const at = hay.text.indexOf(needle);
2444
2531
  if (at < 0) return null;
2445
- const start = hay.map[at];
2446
- const lastNorm = at + needle.length - 1;
2447
- const lastOrig = hay.map[lastNorm];
2448
- let end = lastOrig + 1;
2449
- while (end < tex.length && (hay.map[lastNorm + 1] ?? tex.length) > end && /\s/.test(tex[end])) {
2450
- end++;
2451
- }
2452
- end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
2532
+ const start = hay.lead[at];
2533
+ const end = hay.ends[at + needle.length - 1];
2453
2534
  return { start, end: Math.min(end, tex.length) };
2454
2535
  }
2455
2536
  function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
2537
+ const checked = texError(tex, true) === null;
2456
2538
  let parts = [{ text: tex, raw: true }];
2457
2539
  const used = [];
2458
2540
  const missing = [];
2541
+ const broken = [];
2542
+ const joined = (ps) => ps.map((p) => p.text).join("");
2459
2543
  for (const term of terms) {
2460
2544
  let placed = false;
2545
+ let found = false;
2461
2546
  for (let i = 0; i < parts.length && !placed; i++) {
2462
2547
  const part = parts[i];
2463
2548
  if (!part.raw) continue;
2464
2549
  const at = locate(part.text, term.tex);
2465
2550
  if (!at) continue;
2466
- parts = parts.toSpliced(
2467
- i,
2468
- 1,
2469
- { text: part.text.slice(0, at.start), raw: true },
2470
- {
2471
- text: `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`,
2472
- raw: false
2473
- },
2474
- { text: part.text.slice(at.end), raw: true }
2475
- );
2476
- used.push(term);
2477
- placed = true;
2551
+ found = true;
2552
+ const body = `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`;
2553
+ for (const text2 of [body, `{${body}}`]) {
2554
+ const next = parts.toSpliced(
2555
+ i,
2556
+ 1,
2557
+ { text: part.text.slice(0, at.start), raw: true },
2558
+ { text: text2, raw: false },
2559
+ { text: part.text.slice(at.end), raw: true }
2560
+ );
2561
+ if (checked && texError(joined(next), true) !== null) continue;
2562
+ parts = next;
2563
+ used.push(term);
2564
+ placed = true;
2565
+ break;
2566
+ }
2567
+ }
2568
+ if (!placed) {
2569
+ missing.push(term);
2570
+ if (found) broken.push(term);
2478
2571
  }
2479
- if (!placed) missing.push(term);
2480
2572
  }
2481
2573
  if (used.length === 0) {
2482
2574
  throw new Error(
2483
- `equation-walk ${beatId}: none of its ${terms.length} term(s) occur in the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(tex)}`
2575
+ `equation-walk ${beatId}: none of its ${terms.length} term(s) ${broken.length ? "can be highlighted in" : "occur in"} the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(tex)}`
2576
+ );
2577
+ }
2578
+ return { tex: joined(parts), used, missing, broken };
2579
+ }
2580
+ function droppedTerms(eqId, missing, broken) {
2581
+ return missing.map(
2582
+ (t2) => broken.includes(t2) ? `term ${JSON.stringify(t2.tex)} is in ${eqId} but no span around it parses, so it is not highlighted and its legend row ("${t2.label}") is left out` : `term ${JSON.stringify(t2.tex)} does not occur in ${eqId}, so its legend row ("${t2.label}") is left out`
2583
+ );
2584
+ }
2585
+ function prepareWalk(eq, terms, beatId) {
2586
+ const fixed = repairTex(eq.tex, true);
2587
+ const notes = fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`);
2588
+ const repairedTerms = terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
2589
+ if (fixed.error !== null) {
2590
+ notes.push(
2591
+ `${eq.id} does not parse even after repair (${fixed.error}), so it is shown as its TeX source in plain text \u2014 ${JSON.stringify(eq.tex)}`
2484
2592
  );
2593
+ const parts = segment(eq.tex, [...terms]);
2594
+ if (parts.used.length === 0) {
2595
+ throw new Error(
2596
+ `equation-walk ${beatId}: none of its ${terms.length} term(s) occur in the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(eq.tex)}`
2597
+ );
2598
+ }
2599
+ notes.push(...droppedTerms(eq.id, parts.missing, []));
2600
+ return {
2601
+ raw: eq.tex,
2602
+ plain: { error: fixed.error },
2603
+ used: parts.used,
2604
+ missing: parts.missing,
2605
+ notes
2606
+ };
2485
2607
  }
2486
- return { tex: parts.map((part) => part.text).join(""), used, missing };
2608
+ const walk = wrapTerms(fixed.tex, repairedTerms, beatId);
2609
+ const back = (ts) => ts.map((t2) => terms[repairedTerms.indexOf(t2)]);
2610
+ const used = back(walk.used);
2611
+ const missing = back(walk.missing);
2612
+ notes.push(...droppedTerms(eq.id, missing, back(walk.broken)));
2613
+ return { tex: walk.tex, raw: fixed.tex, used, missing, notes };
2614
+ }
2615
+ function segment(tex, terms) {
2616
+ let parts = [{ text: tex }];
2617
+ const used = [];
2618
+ const missing = [];
2619
+ for (const term of terms) {
2620
+ const i = parts.findIndex((p) => !p.term && locate(p.text, term.tex));
2621
+ const part = parts[i];
2622
+ const at = part && locate(part.text, term.tex);
2623
+ if (!part || !at) {
2624
+ missing.push(term);
2625
+ continue;
2626
+ }
2627
+ parts = parts.toSpliced(
2628
+ i,
2629
+ 1,
2630
+ { text: part.text.slice(0, at.start) },
2631
+ { text: part.text.slice(at.start, at.end), term },
2632
+ { text: part.text.slice(at.end) }
2633
+ );
2634
+ used.push(term);
2635
+ }
2636
+ return { parts, used, missing };
2637
+ }
2638
+ function plainHtml(tex, terms) {
2639
+ return segment(tex, terms).parts.map(
2640
+ (p) => p.term ? `<span class="term t-${p.term.tone}">${esc(p.text)}</span>` : esc(p.text)
2641
+ ).join("");
2642
+ }
2643
+ function chipSetup(sid, term) {
2644
+ const fixed = repairTex(term.tex, false);
2645
+ const el = `document.getElementById("${sid}-chip-${term.tone}")`;
2646
+ return fixed.error === null ? `katex.render('${js(fixed.tex)}', ${el}, ${INLINE_OPTS});` : `${el}.textContent = '${js(term.tex)}';`;
2487
2647
  }
2488
2648
  function equationSize(tex) {
2489
2649
  return tex.length > 120 ? 68 : tex.length > 90 ? 80 : tex.length > 55 ? 92 : 108;
2490
2650
  }
2491
- var NO_INK = /\\(math[a-z]+|text[a-z]*|bm|boldsymbol|displaystyle|textstyle|scriptstyle|left|right|operatorname|limits|nolimits|htmlClass|hspace|phantom|[,;:!])/g;
2651
+ var NO_INK2 = /\\(math[a-z]+|text[a-z]*|bm|boldsymbol|displaystyle|textstyle|scriptstyle|left|right|operatorname|limits|nolimits|htmlClass|hspace|phantom|[,;:!])/g;
2492
2652
  var GLYPH_EM = 0.9;
2493
2653
  function texUnits(tex) {
2494
2654
  let ems = 0;
2495
2655
  for (const m of tex.matchAll(/\\qquad|\\quad/g)) ems += m[0] === "\\qquad" ? 2 : 1;
2496
- const glyphs = tex.replace(/\\q?quad/g, "").replace(NO_INK, "").replace(/[{}\s_^]/g, "");
2656
+ const glyphs = tex.replace(/\\q?quad/g, "").replace(NO_INK2, "").replace(/[{}\s_^]/g, "");
2497
2657
  return ems + glyphs.length * GLYPH_EM;
2498
2658
  }
2499
2659
  function statements(tex, stacked) {
@@ -2501,6 +2661,100 @@ function statements(tex, stacked) {
2501
2661
  const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
2502
2662
  return parts.length > 0 ? parts : [tex];
2503
2663
  }
2664
+ var PLAIN_FONT = 48;
2665
+ function mathFit(sid, targets, shown, raw2, want, floor) {
2666
+ return [
2667
+ `var dsEqFit = (function () {
2668
+ var box = document.getElementById("${sid}-eq");
2669
+ var slide = box && box.parentNode;
2670
+ var bw = box ? box.getBoundingClientRect().width : 0;
2671
+ if (!(bw > 0) || !slide) return "unmeasured";
2672
+ var targets = [${targets.map((id2) => `"${id2}"`).join(", ")}].map(function (id) { return document.getElementById(id); });
2673
+ var TEX = [${shown.map((s) => `'${js(s)}'`).join(", ")}];
2674
+ var spill0 = slide.scrollHeight - slide.clientHeight;
2675
+ function extent(each) {
2676
+ var w = 0;
2677
+ targets.forEach(function (t) {
2678
+ var lo = Infinity, hi = -Infinity;
2679
+ Array.prototype.forEach.call(t.querySelectorAll(".katex-html > .base"), function (b) {
2680
+ var r = b.getBoundingClientRect();
2681
+ if (each) w = Math.max(w, r.width);
2682
+ else { lo = Math.min(lo, r.left); hi = Math.max(hi, r.right); }
2683
+ });
2684
+ if (!each && hi > lo) w = Math.max(w, hi - lo);
2685
+ });
2686
+ return w;
2687
+ }
2688
+ function fits(each) {
2689
+ return extent(each) <= bw + 0.5 && slide.scrollHeight - slide.clientHeight <= Math.max(1, spill0 + 1);
2690
+ }
2691
+ function settle(each, size) {
2692
+ size = Math.max(${floor}, Math.min(${want}, Math.floor(size)));
2693
+ box.style.fontSize = size + "px";
2694
+ while (!fits(each) && size > ${floor}) {
2695
+ size = Math.max(${floor}, size - 2);
2696
+ box.style.fontSize = size + "px";
2697
+ }
2698
+ return fits(each);
2699
+ }
2700
+ if (fits(false)) return "fits";
2701
+ var size = parseFloat(box.style.fontSize);
2702
+ if (settle(false, (size * bw) / extent(false))) return "scaled";
2703
+ targets.forEach(function (t, i) { katex.render("\\\\displaystyle " + TEX[i], t, ${INLINE_OPTS}); });
2704
+ box.className += " eq-broken";
2705
+ box.style.fontSize = "${want}px";
2706
+ var wb = extent(true);
2707
+ if (settle(true, wb > bw ? (${want} * bw) / wb : ${want})) return "broken";
2708
+ box.setAttribute("${UNFIT_ATTR}", '${js(raw2)}');
2709
+ return "unfit";
2710
+ })()`
2711
+ ];
2712
+ }
2713
+ function chipFit(sid, terms) {
2714
+ if (terms.length === 0) return [];
2715
+ return [
2716
+ `[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
2717
+ var chip = document.getElementById("${sid}-chip-" + tone);
2718
+ var r = chip && chip.getBoundingClientRect();
2719
+ if (!r || !(r.height > 0)) return;
2720
+ var k = chip.offsetHeight / r.height;
2721
+ var top = r.top, bottom = r.bottom;
2722
+ Array.prototype.forEach.call(chip.querySelectorAll("span"), function (el) {
2723
+ for (var n = el.firstChild; n; n = n.nextSibling) {
2724
+ if (n.nodeType === 3 && n.nodeValue.trim()) {
2725
+ var q = el.getBoundingClientRect();
2726
+ top = Math.min(top, q.top);
2727
+ bottom = Math.max(bottom, q.bottom);
2728
+ return;
2729
+ }
2730
+ }
2731
+ });
2732
+ var cs = getComputedStyle(chip);
2733
+ if (top < r.top) chip.style.paddingTop = parseFloat(cs.paddingTop) + Math.ceil((r.top - top) * k) + "px";
2734
+ if (bottom > r.bottom) chip.style.paddingBottom = parseFloat(cs.paddingBottom) + Math.ceil((bottom - r.bottom) * k) + "px";
2735
+ })`
2736
+ ];
2737
+ }
2738
+ var SWELL = 1.16;
2739
+ var SWELL_EM = 0.06;
2740
+ function swellFit(sid, terms) {
2741
+ return [
2742
+ `var dsSwell = (function () {
2743
+ var out = {};
2744
+ var box = document.getElementById("${sid}-eq");
2745
+ var k = box && box.offsetWidth ? box.getBoundingClientRect().width / box.offsetWidth : 1;
2746
+ var em = box ? parseFloat(getComputedStyle(box).fontSize) : 0;
2747
+ [${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
2748
+ var w = 0;
2749
+ Array.prototype.forEach.call(document.querySelectorAll("#${sid} .t-" + tone), function (el) {
2750
+ w = Math.max(w, el.getBoundingClientRect().width / k);
2751
+ });
2752
+ out[tone] = w > 0 && em > 0 ? Math.min(${SWELL}, Math.round((1 + (2 * ${SWELL_EM} * em) / w) * 1000) / 1000) : ${SWELL};
2753
+ });
2754
+ return out;
2755
+ })()`
2756
+ ];
2757
+ }
2504
2758
  function legendRows(sid, terms, theme) {
2505
2759
  return terms.map(
2506
2760
  (t2) => `<div class="leg" id="${sid}-leg-${t2.tone}"><span class="chip" id="${sid}-chip-${t2.tone}" style="color:${theme.tones[t2.tone]}"></span><span>${esc(t2.label)}</span></div>`
@@ -2516,7 +2770,10 @@ function legendCss(theme) {
2516
2770
  `.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
2517
2771
  // A common chip width, so the labels share a spine too — the glyphs inside
2518
2772
  // are one symbol each and their natural widths differ by a few pixels.
2519
- `.chip{display:inline-block;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`
2773
+ // `flex:none`: beside a long label the chip used to SHRINK below its own
2774
+ // nowrap TeX, which then ran out of the painted box into the label — on a
2775
+ // HypePaper deck, "pixelcoord" 91px past the chip's right edge.
2776
+ `.chip{display:inline-block;flex:none;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`
2520
2777
  ].join("\n");
2521
2778
  }
2522
2779
  var equationWalk = (beat, ctx) => {
@@ -2528,37 +2785,53 @@ var equationWalk = (beat, ctx) => {
2528
2785
  `equation-walk ${beat.id}: no equation "${p.equationId}" in source ${ctx.source.id}`
2529
2786
  );
2530
2787
  }
2531
- const walk = wrapTerms(eq.tex, p.terms, beat.id);
2788
+ const walk = prepareWalk(eq, p.terms, beat.id);
2532
2789
  const terms = walk.used;
2533
2790
  const legend = legendRows(sid, terms, theme);
2534
- const stacked = isPortrait(ctx.format);
2535
- const raw2 = statements(eq.tex, stacked);
2536
- const shown = statements(walk.tex, stacked);
2537
- const longest = raw2.reduce((a, b) => b.length > a.length ? b : a);
2538
- const size2 = Math.max(
2539
- MIN_FONT,
2540
- Math.min(
2541
- equationSize(longest),
2542
- Math.floor(contentW(ctx.format) / Math.max(...raw2.map(texUnits)))
2543
- )
2544
- );
2545
- const body = shown.length === 1 ? "" : shown.map((_, i) => `<div id="${sid}-eq${i}"></div>`).join("\n ");
2791
+ const chips = terms.map((t2) => chipSetup(sid, t2));
2792
+ let eqHtml;
2793
+ let setup;
2794
+ let measure4;
2795
+ if (walk.plain) {
2796
+ eqHtml = `<div class="eq eq-plain" id="${sid}-eq" style="font-size:${PLAIN_FONT}px">${plainHtml(eq.tex, terms)}</div>`;
2797
+ setup = chips;
2798
+ measure4 = [...chipFit(sid, terms), ...swellFit(sid, terms)];
2799
+ } else {
2800
+ const wrapped = walk.tex;
2801
+ const split = statements(wrapped, isPortrait(ctx.format));
2802
+ const stacked = split.length > 1 && split.every((s) => texError(s, true) === null);
2803
+ const raw2 = statements(walk.raw, stacked);
2804
+ const shown = stacked ? split : [wrapped];
2805
+ const longest = raw2.reduce((a, b) => b.length > a.length ? b : a);
2806
+ const want = equationSize(longest);
2807
+ const size2 = Math.max(
2808
+ MIN_FONT,
2809
+ Math.min(want, Math.floor(contentW(ctx.format) / Math.max(...raw2.map(texUnits))))
2810
+ );
2811
+ const body = shown.length === 1 ? "" : shown.map((_, i) => `<div id="${sid}-eq${i}"></div>`).join("\n ");
2812
+ eqHtml = `<div class="eq${shown.length === 1 ? "" : " eqstack"}" id="${sid}-eq" style="font-size:${size2}px">${body}</div>`;
2813
+ const targets = shown.map((_, i) => `${sid}-eq${shown.length === 1 ? "" : i}`);
2814
+ setup = [
2815
+ `var OPTS = ${OPTS};`,
2816
+ ...shown.map(
2817
+ (part, i) => `katex.render('${js(part)}', document.getElementById("${targets[i]}"), OPTS);`
2818
+ ),
2819
+ ...chips
2820
+ ];
2821
+ measure4 = [
2822
+ ...chipFit(sid, terms),
2823
+ ...mathFit(sid, targets, shown, walk.raw, want, MIN_FONT),
2824
+ // Last: it measures each term at the size the fit settled on.
2825
+ ...swellFit(sid, terms)
2826
+ ];
2827
+ }
2546
2828
  const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
2547
2829
  <div class="eqslide">
2548
- <div class="eq${shown.length === 1 ? "" : " eqstack"}" id="${sid}-eq" style="font-size:${size2}px">${body}</div>
2830
+ ${eqHtml}
2549
2831
  <div class="legend">
2550
2832
  ${legend}
2551
2833
  </div>
2552
2834
  </div>`;
2553
- const setup = [
2554
- `var OPTS = ${OPTS};`,
2555
- ...shown.map(
2556
- (part, i) => `katex.render('${js(part)}', document.getElementById("${sid}-eq${shown.length === 1 ? "" : i}"), OPTS);`
2557
- ),
2558
- ...terms.map(
2559
- (t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
2560
- )
2561
- ];
2562
2835
  const tl = [
2563
2836
  ...chromeIn(sid, p.eyebrow !== void 0),
2564
2837
  tween(`#${sid}-eq`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8)
@@ -2570,6 +2843,7 @@ var equationWalk = (beat, ctx) => {
2570
2843
  );
2571
2844
  const holds = [];
2572
2845
  const spot = spotlighter(sid, ".term");
2846
+ const swell = (t2) => raw(`dsSwell.${t2.tone}`);
2573
2847
  terms.forEach((term, i) => {
2574
2848
  const at = first + i * step;
2575
2849
  const colour = theme.tones[term.tone];
@@ -2586,13 +2860,15 @@ var equationWalk = (beat, ctx) => {
2586
2860
  tween(
2587
2861
  `#${sid} .t-${term.tone}`,
2588
2862
  { color: theme.fg, scale: 1 },
2589
- { color: colour, scale: 1.16, duration: 0.5 },
2863
+ { color: colour, scale: swell(term), duration: 0.5 },
2590
2864
  at
2591
2865
  )
2592
2866
  );
2593
2867
  const prev = terms[i - 1];
2594
2868
  if (prev) {
2595
- tl.push(tween(`#${sid} .t-${prev.tone}`, { scale: 1.16 }, { scale: 1, duration: 0.4 }, at));
2869
+ tl.push(
2870
+ tween(`#${sid} .t-${prev.tone}`, { scale: swell(prev) }, { scale: 1, duration: 0.4 }, at)
2871
+ );
2596
2872
  }
2597
2873
  holds.push(at + 0.6);
2598
2874
  });
@@ -2600,7 +2876,7 @@ var equationWalk = (beat, ctx) => {
2600
2876
  if (last) {
2601
2877
  const at = first + terms.length * step;
2602
2878
  tl.push(
2603
- tween(`#${sid} .t-${last.tone}`, { scale: 1.16 }, { scale: 1, duration: 0.4 }, at),
2879
+ tween(`#${sid} .t-${last.tone}`, { scale: swell(last) }, { scale: 1, duration: 0.4 }, at),
2604
2880
  ...spot.restore(at)
2605
2881
  );
2606
2882
  }
@@ -2608,6 +2884,8 @@ var equationWalk = (beat, ctx) => {
2608
2884
  html,
2609
2885
  tl,
2610
2886
  setup,
2887
+ measure: measure4,
2888
+ ...walk.notes.length ? { warnings: walk.notes } : {},
2611
2889
  holds: holdsWithin(holds, beat.seconds),
2612
2890
  css: [
2613
2891
  chromeCss(theme),
@@ -2627,6 +2905,15 @@ var equationWalk = (beat, ctx) => {
2627
2905
  // has its margin zeroed above, so without an explicit gap the two lines
2628
2906
  // would touch and read as one equation broken mid-expression.
2629
2907
  ".eqstack{display:flex;flex-direction:column;gap:32px}",
2908
+ // Set by `mathFit` when one line will not fit at the floor: the display is
2909
+ // re-rendered inline, where KaTeX breaks after a top-level relation or
2910
+ // operator, and the lines it breaks into are centred and spaced as lines.
2911
+ ".eq-broken{line-height:1.5}",
2912
+ // An equation KaTeX refuses, shown as its source. Wraps anywhere, because
2913
+ // TeX source has few spaces and must not run off the slide either. The
2914
+ // deck's own face, not a monospace one: a family the bundle does not
2915
+ // declare falls back silently (invariant 9).
2916
+ ".eq-plain{white-space:pre-wrap;overflow-wrap:anywhere}",
2630
2917
  // Transforms do not apply to inline boxes, and KaTeX spans are inline.
2631
2918
  ".term{display:inline-block}",
2632
2919
  legendCss(theme),
@@ -2641,26 +2928,67 @@ var equationWalk = (beat, ctx) => {
2641
2928
 
2642
2929
  // src/emit/archetypes/equation-morph.ts
2643
2930
  var MORPH_SECONDS = 1.6;
2644
- var equationMorph = (beat, ctx) => {
2645
- const { sid, theme } = ctx;
2931
+ function prepareMorph(beat, source) {
2646
2932
  const p = beat.params;
2647
2933
  const find3 = (id2) => {
2648
- const eq = ctx.source.equations.find((e) => e.id === id2);
2934
+ const eq = source.equations.find((e) => e.id === id2);
2649
2935
  if (!eq)
2650
- throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${ctx.source.id}`);
2936
+ throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${source.id}`);
2651
2937
  return eq;
2652
2938
  };
2653
- const a = find3(p.fromId);
2654
- const b = find3(p.toId);
2655
- const both = p.terms.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
2656
- if (both.length === 0) {
2939
+ const notes = [];
2940
+ const drawable = (eq) => {
2941
+ const fixed = repairTex(eq.tex, true);
2942
+ if (fixed.error !== null) {
2943
+ throw new Error(
2944
+ `equation-morph ${beat.id}: ${eq.id} does not parse even after repair (${fixed.error}), and a morph cannot be drawn from plain text. Formula: ${JSON.stringify(eq.tex)}`
2945
+ );
2946
+ }
2947
+ notes.push(...fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`));
2948
+ return { id: eq.id, tex: fixed.tex };
2949
+ };
2950
+ const a = drawable(find3(p.fromId));
2951
+ const b = drawable(find3(p.toId));
2952
+ const repaired = p.terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
2953
+ const original = (t2) => p.terms[repaired.indexOf(t2)];
2954
+ let keyed = repaired.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
2955
+ if (keyed.length === 0) {
2657
2956
  throw new Error(
2658
2957
  `equation-morph ${beat.id}: none of its ${p.terms.length} term(s) occur in both equations. Terms: ${p.terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. From: ${JSON.stringify(a.tex)}. To: ${JSON.stringify(b.tex)}`
2659
2958
  );
2660
2959
  }
2960
+ notes.push(
2961
+ ...repaired.filter((t2) => !keyed.includes(t2)).map(
2962
+ (t2) => `term ${JSON.stringify(original(t2).tex)} does not occur in both ${a.id} and ${b.id}, so it does not travel and its legend row ("${t2.label}") is left out`
2963
+ )
2964
+ );
2661
2965
  const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
2662
- const wa = wrapTerms(a.tex, both, beat.id, cls).tex;
2663
- const wb = wrapTerms(b.tex, both, beat.id, cls).tex;
2966
+ let wa = wrapTerms(a.tex, keyed, beat.id, cls);
2967
+ let wb = wrapTerms(b.tex, keyed, beat.id, cls);
2968
+ const paired = keyed.filter((t2) => wa.used.includes(t2) && wb.used.includes(t2));
2969
+ if (paired.length < keyed.length) {
2970
+ const unpaired = keyed.filter((t2) => !paired.includes(t2));
2971
+ notes.push(
2972
+ ...unpaired.map(
2973
+ (t2) => `term ${JSON.stringify(original(t2).tex)} cannot be marked on both lines without breaking one, so it does not travel and its legend row ("${t2.label}") is left out`
2974
+ )
2975
+ );
2976
+ if (paired.length === 0) {
2977
+ throw new Error(
2978
+ `equation-morph ${beat.id}: no term can be marked on both lines without breaking one. Terms: ${p.terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}`
2979
+ );
2980
+ }
2981
+ keyed = paired;
2982
+ wa = wrapTerms(a.tex, keyed, beat.id, cls);
2983
+ wb = wrapTerms(b.tex, keyed, beat.id, cls);
2984
+ }
2985
+ const both = keyed.map(original);
2986
+ return { a, b, wa: wa.tex, wb: wb.tex, both, notes };
2987
+ }
2988
+ var equationMorph = (beat, ctx) => {
2989
+ const { sid, theme } = ctx;
2990
+ const p = beat.params;
2991
+ const { a, b, wa, wb, both, notes } = prepareMorph(beat, ctx.source);
2664
2992
  const size2 = Math.max(
2665
2993
  MIN_FONT,
2666
2994
  Math.min(
@@ -2682,9 +3010,7 @@ var equationMorph = (beat, ctx) => {
2682
3010
  `var OPTS = ${OPTS};`,
2683
3011
  `katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
2684
3012
  `katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
2685
- ...both.map(
2686
- (t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
2687
- )
3013
+ ...both.map((t2) => chipSetup(sid, t2))
2688
3014
  ];
2689
3015
  const first = 1.8;
2690
3016
  const at = Math.round(
@@ -2717,8 +3043,11 @@ var equationMorph = (beat, ctx) => {
2717
3043
  setup,
2718
3044
  // SEAM B: the plan is browser geometry after fonts, so it is built inside
2719
3045
  // the ready gate, and the plugin tween above finds it on the host.
2720
- measure: [`DSMorph.build(document.getElementById("${sid}-morph"));`],
3046
+ // The chips are fitted before the plan is built: growing one moves the
3047
+ // legend, and nothing the morph measures may move after it has measured.
3048
+ measure: [...chipFit(sid, both), `DSMorph.build(document.getElementById("${sid}-morph"));`],
2721
3049
  plugins: ["dsMorph"],
3050
+ ...notes.length ? { warnings: notes } : {},
2722
3051
  holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
2723
3052
  css: [
2724
3053
  chromeCss(theme),
@@ -3299,16 +3628,16 @@ var lineChart = (beat, ctx) => {
3299
3628
  </svg>${readout}
3300
3629
  </div>`;
3301
3630
  const draw2 = 0.8;
3302
- const count = p.points.length;
3303
- const idealStep = Math.min(0.45, DRAW_SECONDS / count);
3631
+ const count2 = p.points.length;
3632
+ const idealStep = Math.min(0.45, DRAW_SECONDS / count2);
3304
3633
  const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
3305
- const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count - 1) : 0;
3634
+ const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count2 - 1) : 0;
3306
3635
  const tailAfter = (s) => Math.max(
3307
- s * count + 0.4,
3636
+ s * count2 + 0.4,
3308
3637
  // the ring: its walk, then the 0.4s it takes to leave
3309
- 0.3 + s * (count - 1),
3638
+ 0.3 + s * (count2 - 1),
3310
3639
  // the dots
3311
- 0.5 + s * (count - 1),
3640
+ 0.5 + s * (count2 - 1),
3312
3641
  ...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
3313
3642
  );
3314
3643
  const room = beat.seconds - spine;
@@ -3371,8 +3700,8 @@ var lineChart = (beat, ctx) => {
3371
3700
  )
3372
3701
  );
3373
3702
  }
3374
- const walk = cmp ? sec(step * count) : DRAW_SECONDS;
3375
- if (count > 1) {
3703
+ const walk = cmp ? sec(step * count2) : DRAW_SECONDS;
3704
+ if (count2 > 1) {
3376
3705
  const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
3377
3706
  tl.push(
3378
3707
  tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
@@ -3387,7 +3716,7 @@ var lineChart = (beat, ctx) => {
3387
3716
  )
3388
3717
  );
3389
3718
  }
3390
- const drawn = settled + (cmp ? tailAfter(step) : step * count + 0.4);
3719
+ const drawn = settled + (cmp ? tailAfter(step) : step * count2 + 0.4);
3391
3720
  const holds = cmp ? [lift2, drawn] : [drawn];
3392
3721
  if (p.readout) {
3393
3722
  tl.push(
@@ -3403,7 +3732,7 @@ var lineChart = (beat, ctx) => {
3403
3732
  return {
3404
3733
  ...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
3405
3734
  warnings: [
3406
- `line-chart ${beat.id}: a comparison against "${cmp.label}" over ${count} points needs ${need}s and the beat is ${beat.seconds}s, so the chart was drawn without it. Lengthen the beat to keep the comparison.`
3735
+ `line-chart ${beat.id}: a comparison against "${cmp.label}" over ${count2} points needs ${need}s and the beat is ${beat.seconds}s, so the chart was drawn without it. Lengthen the beat to keep the comparison.`
3407
3736
  ]
3408
3737
  };
3409
3738
  }
@@ -3483,7 +3812,7 @@ var V_BOX_H = 268;
3483
3812
  var V_LOOP_SIDE = 56;
3484
3813
  var V_LOOP_PAD = 18;
3485
3814
  function columnLayout(stageW, stageH, stages, face, loop) {
3486
- const count = stages.length;
3815
+ const count2 = stages.length;
3487
3816
  const loopW = loop ? Math.max(240, Math.round(stageW * 0.32)) : 0;
3488
3817
  const boxX = M;
3489
3818
  const boxW = stageW - 2 * M - (loop ? V_LOOP_SIDE + V_LOOP_PAD + loopW : 0);
@@ -3500,11 +3829,11 @@ function columnLayout(stageW, stageH, stages, face, loop) {
3500
3829
  return Math.ceil(2 * PAD_Y2 + label + (note2 > 0 ? NOTE_TOP + note2 * NOTE * NOTE_LH : 0));
3501
3830
  })
3502
3831
  );
3503
- const room = (stageH - 2 * M - (count - 1) * V_GAP) / count;
3832
+ const room = (stageH - 2 * M - (count2 - 1) * V_GAP) / count2;
3504
3833
  const boxH = Math.max(need, Math.min(room, V_BOX_H));
3505
3834
  return {
3506
3835
  size: size2,
3507
- boxes: Array.from({ length: count }, (_, i) => ({ x: M + i * (boxH + V_GAP), w: boxH })),
3836
+ boxes: Array.from({ length: count2 }, (_, i) => ({ x: M + i * (boxH + V_GAP), w: boxH })),
3508
3837
  boxH,
3509
3838
  boxX,
3510
3839
  boxW,
@@ -3516,7 +3845,7 @@ function columnLayout(stageW, stageH, stages, face, loop) {
3516
3845
  loopLines: loop ? wrap(loop.label, NOTE, loopW, 500, 0, face) : [],
3517
3846
  loopX: boxX + boxW + V_LOOP_SIDE,
3518
3847
  loopW,
3519
- svgH: 2 * M + count * boxH + (count - 1) * V_GAP
3848
+ svgH: 2 * M + count2 * boxH + (count2 - 1) * V_GAP
3520
3849
  };
3521
3850
  }
3522
3851
  function balance(label, k, face) {
@@ -4234,8 +4563,8 @@ function stackLayout(p, format, face = "latin") {
4234
4563
  if (inline2.fits) return inline2;
4235
4564
  return inline2.wide && inline2.blockH < stacked.blockH ? inline2 : stacked;
4236
4565
  }
4237
- function labelWeight(i, count) {
4238
- return i === count - 1 ? 700 : 600;
4566
+ function labelWeight(i, count2) {
4567
+ return i === count2 - 1 ? 700 : 600;
4239
4568
  }
4240
4569
  function floorFor(p, format) {
4241
4570
  if (!p.tilt) return MIN_FONT;
@@ -4245,7 +4574,7 @@ function solve2(p, format, inline2, face) {
4245
4574
  const width = contentW(format);
4246
4575
  const boxH = contentH(format);
4247
4576
  const floor = floorFor(p, format);
4248
- const count = p.layers.length;
4577
+ const count2 = p.layers.length;
4249
4578
  const k = isPortrait(format) ? "tall" : "wide";
4250
4579
  const riseMax = RISE_MAX[k];
4251
4580
  const syMax = SY_MAX[k];
@@ -4253,7 +4582,7 @@ function solve2(p, format, inline2, face) {
4253
4582
  const noteW = (l) => inline2 && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
4254
4583
  const want = Math.max(
4255
4584
  ...p.layers.map(
4256
- (l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count), 0, false, face) + noteW(l)
4585
+ (l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count2), 0, false, face) + noteW(l)
4257
4586
  )
4258
4587
  );
4259
4588
  const colCap = inline2 ? width * 0.56 : width * 0.5;
@@ -4262,7 +4591,7 @@ function solve2(p, format, inline2, face) {
4262
4591
  const labelX = width - colW;
4263
4592
  const labelRoom = Math.min(
4264
4593
  ...p.layers.map(
4265
- (l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count), 0, false, face))
4594
+ (l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count2), 0, false, face))
4266
4595
  )
4267
4596
  );
4268
4597
  const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
@@ -4270,7 +4599,7 @@ function solve2(p, format, inline2, face) {
4270
4599
  const nw = noteW(l);
4271
4600
  const labelMaxW = Math.max(labelSize, colW - nw);
4272
4601
  return {
4273
- label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count), 0, face),
4602
+ label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count2), 0, face),
4274
4603
  // Inline notes stay on one line by contract — the schema calls a note "one
4275
4604
  // short line" — and wrapping one would put its second line under the label.
4276
4605
  note: l.note === void 0 ? [] : inline2 ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
@@ -4288,13 +4617,13 @@ function solve2(p, format, inline2, face) {
4288
4617
  const noteH = noteHeight(p.note, noteWidth(format), 28, face);
4289
4618
  const free = boxH - chromeH - 20 - noteH;
4290
4619
  const avail = Math.max(300, free);
4291
- const room = (avail - 2 * pad - syMax - tMax) / Math.max(1, count - 1);
4620
+ const room = (avail - 2 * pad - syMax - tMax) / Math.max(1, count2 - 1);
4292
4621
  const rise = clamp(room, RISE_MIN, riseMax);
4293
4622
  const sy = clamp(rise * 0.62, 46, syMax);
4294
4623
  const t2 = clamp(rise * 0.14, 10, tMax);
4295
4624
  const span = width - NUM_W - GAP3 - colW;
4296
4625
  const sx = clamp(span * 0.2, 110, 220);
4297
- const height = 2 * pad + (count - 1) * rise + sy + t2;
4626
+ const height = 2 * pad + (count2 - 1) * rise + sy + t2;
4298
4627
  return {
4299
4628
  // A rise shorter than a label block prints the note through the label below.
4300
4629
  // BOTH directions. A layout that fits the height and not the width is not a
@@ -4319,7 +4648,7 @@ function solve2(p, format, inline2, face) {
4319
4648
  labelSize,
4320
4649
  blockH,
4321
4650
  pad,
4322
- yBase: pad + (count - 1) * rise + sy,
4651
+ yBase: pad + (count2 - 1) * rise + sy,
4323
4652
  lines
4324
4653
  };
4325
4654
  }
@@ -4336,11 +4665,11 @@ var stack = (beat, ctx) => {
4336
4665
  const p = beat.params;
4337
4666
  const face = faceOf(ctx.theme.fontStack);
4338
4667
  const L = stackLayout(p, ctx.format, face);
4339
- const count = p.layers.length;
4340
- const last = count - 1;
4668
+ const count2 = p.layers.length;
4669
+ const last = count2 - 1;
4341
4670
  if (!L.fits) {
4342
4671
  throw new Error(
4343
- `stack ${beat.id}: ${count} layers with ${p.layers.filter((l) => l.note).length} note(s) need ${Math.round(L.blockH)}px of label block against ${Math.round(L.avail)}px of room, at the ${Math.round(L.floor)}px floor and with the notes already moved beside their labels. Nothing here may be set smaller, so the lever is upstream of this beat: drop a layer, or shorten the notes.`
4672
+ `stack ${beat.id}: ${count2} layers with ${p.layers.filter((l) => l.note).length} note(s) need ${Math.round(L.blockH)}px of label block against ${Math.round(L.avail)}px of room, at the ${Math.round(L.floor)}px floor and with the notes already moved beside their labels. Nothing here may be set smaller, so the lever is upstream of this beat: drop a layer, or shorten the notes.`
4344
4673
  );
4345
4674
  }
4346
4675
  const parts = {};
@@ -4368,7 +4697,7 @@ var stack = (beat, ctx) => {
4368
4697
  { x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
4369
4698
  {
4370
4699
  size: L.labelSize,
4371
- weight: labelWeight(i, count),
4700
+ weight: labelWeight(i, count2),
4372
4701
  fill: top ? theme.tones.b : theme.fg,
4373
4702
  maxWidth: block.labelMaxW,
4374
4703
  lineHeight: 1.16,
@@ -4422,10 +4751,10 @@ var stack = (beat, ctx) => {
4422
4751
  <div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
4423
4752
  const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
4424
4753
  <div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe2)}</div>${noteHtml}`;
4425
- const centre2 = (count - 1) / 2;
4754
+ const centre2 = (count2 - 1) / 2;
4426
4755
  const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
4427
4756
  const first = 0.9;
4428
- const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count));
4757
+ const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count2));
4429
4758
  const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
4430
4759
  const holds = [];
4431
4760
  const spot = spotlighter(sid);
@@ -4457,11 +4786,11 @@ var stack = (beat, ctx) => {
4457
4786
  );
4458
4787
  }
4459
4788
  if (p.note) {
4460
- const at = first + count * step;
4789
+ const at = first + count2 * step;
4461
4790
  tl.push(tween(`#${sid}-note`, { opacity: 0 }, { opacity: 1, duration: 0.6 }, at));
4462
4791
  holds.push(at + 0.7);
4463
4792
  }
4464
- if (count > 1) tl.push(...spot.restore(first + count * step));
4793
+ if (count2 > 1) tl.push(...spot.restore(first + count2 * step));
4465
4794
  return {
4466
4795
  html,
4467
4796
  parts,
@@ -5802,8 +6131,8 @@ async function narrate(storyboard, source, prefs, opts) {
5802
6131
  const text2 = beat.narration?.trim();
5803
6132
  if (!text2) continue;
5804
6133
  const segments2 = [];
5805
- const count = stopsFor(beat, source, format, `s${i + 1}`, theme);
5806
- const plan = planSegments(text2, Math.min(count, paced.speakingStops));
6134
+ const count2 = stopsFor(beat, source, format, `s${i + 1}`, theme);
6135
+ const plan = planSegments(text2, Math.min(count2, paced.speakingStops));
5807
6136
  for (const [stop, line2] of plan.entries()) {
5808
6137
  if (!line2) continue;
5809
6138
  const speech = await synthesize(line2, {
@@ -5825,7 +6154,7 @@ async function narrate(storyboard, source, prefs, opts) {
5825
6154
  }
5826
6155
  if (segments2.length > 0) {
5827
6156
  beats[beat.id] = segments2;
5828
- staged[beat.id] = count;
6157
+ staged[beat.id] = count2;
5829
6158
  }
5830
6159
  }
5831
6160
  return {
@@ -6715,11 +7044,11 @@ function stageScene(scene, speed) {
6715
7044
  function speechPlan(open, holds, segments2) {
6716
7045
  const starts = [];
6717
7046
  let at = open;
6718
- for (const [i, segment] of segments2.entries()) {
6719
- const hold = holds[Math.min(segment.stop, holds.length - 1)] ?? 0;
7047
+ for (const [i, segment2] of segments2.entries()) {
7048
+ const hold = holds[Math.min(segment2.stop, holds.length - 1)] ?? 0;
6720
7049
  if (i > 0) at = Math.max(at, hold);
6721
7050
  starts.push(at);
6722
- at += segment.seconds;
7051
+ at += segment2.seconds;
6723
7052
  }
6724
7053
  return { starts, end: at };
6725
7054
  }
@@ -6947,10 +7276,10 @@ function pack(words2, width) {
6947
7276
  function splitCue(cue, max = CUE_MAX_CHARS) {
6948
7277
  if (cue.text.length <= max) return [cue];
6949
7278
  const words2 = cue.text.split(/\s+/).filter(Boolean);
6950
- const count = pack(words2, max).length;
7279
+ const count2 = pack(words2, max).length;
6951
7280
  let width = max;
6952
- for (let w = Math.ceil(cue.text.length / count); w < max; w++) {
6953
- if (pack(words2, w).length <= count) {
7281
+ for (let w = Math.ceil(cue.text.length / count2); w < max; w++) {
7282
+ if (pack(words2, w).length <= count2) {
6954
7283
  width = w;
6955
7284
  break;
6956
7285
  }
@@ -7042,17 +7371,17 @@ function place(scenes, spoken) {
7042
7371
  );
7043
7372
  }
7044
7373
  const { starts } = speechPlan(scene.open, scene.holds, segments2);
7045
- for (const [i, segment] of segments2.entries()) {
7046
- const index = Math.min(segment.stop, scene.holds.length - 1);
7374
+ for (const [i, segment2] of segments2.entries()) {
7375
+ const index = Math.min(segment2.stop, scene.holds.length - 1);
7047
7376
  out.push({
7048
- id: `${scene.id}.${segment.stop}`,
7377
+ id: `${scene.id}.${segment2.stop}`,
7049
7378
  scene: scene.id,
7050
- stop: segment.stop,
7051
- audio: segment.audio,
7379
+ stop: segment2.stop,
7380
+ audio: segment2.audio,
7052
7381
  hold: round5(scene.start + scene.holds[index]),
7053
7382
  start: round5(scene.start + starts[i]),
7054
- duration: segment.seconds,
7055
- cues: segment.cues.flatMap((c) => splitCue(c, cueMax(c.text)))
7383
+ duration: segment2.seconds,
7384
+ cues: segment2.cues.flatMap((c) => splitCue(c, cueMax(c.text)))
7056
7385
  });
7057
7386
  }
7058
7387
  }
@@ -7129,6 +7458,15 @@ function planTiming(input) {
7129
7458
  // `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
7130
7459
  // disagree with the box the archetypes actually drew into.
7131
7460
  captionReserve: format.captionReserve ?? 0,
7461
+ // An unbudgeted format's limit is Infinity, which JSON writes as `null`.
7462
+ // Left out instead: recorded as a format with no limit, never with a limit
7463
+ // of null or zero.
7464
+ format: {
7465
+ id: format.id,
7466
+ minWeight: format.minWeight,
7467
+ ...Number.isFinite(format.maxSeconds) && { maxSeconds: format.maxSeconds },
7468
+ ...format.warnSeconds !== void 0 && { warnSeconds: format.warnSeconds }
7469
+ },
7132
7470
  scenes,
7133
7471
  segments: segments2
7134
7472
  };
@@ -7155,15 +7493,15 @@ function framePlan(timing, fps) {
7155
7493
  );
7156
7494
  }
7157
7495
  pieces.push({ from: first, motion: last - first, freeze: 0 });
7158
- for (const segment of segments2) {
7159
- const at = f(segment.start);
7496
+ for (const segment2 of segments2) {
7497
+ const at = f(segment2.start);
7160
7498
  audio.push({
7161
- id: segment.id,
7162
- audio: segment.audio,
7499
+ id: segment2.id,
7500
+ audio: segment2.audio,
7163
7501
  startFrame: at,
7164
7502
  delayMs: Math.round(at / fps * 1e3)
7165
7503
  });
7166
- for (const cue of segment.cues) {
7504
+ for (const cue of segment2.cues) {
7167
7505
  const start = at / fps + cue.start;
7168
7506
  const end = at / fps + cue.end;
7169
7507
  if (end > start) cues.push({ start, end, text: cue.text });
@@ -7737,6 +8075,22 @@ async function resolveChrome(need = "open the deck with") {
7737
8075
  async function chromePath(need = "open the deck with") {
7738
8076
  return (await resolveChrome(need)).path;
7739
8077
  }
8078
+ function sandboxMissing(err) {
8079
+ return /No usable sandbox/.test(err instanceof Error ? err.message : String(err));
8080
+ }
8081
+ var SANDBOX_FIX = "`sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`, or give Chrome an AppArmor profile";
8082
+ async function launchOwnPage(launch, args, what) {
8083
+ try {
8084
+ return await launch(args);
8085
+ } catch (err) {
8086
+ if (!sandboxMissing(err)) throw err;
8087
+ process.stderr.write(
8088
+ `chrome: this host gives Chrome no sandbox, so ${what} runs without one, as hyperframes' render always does. To keep the sandbox: ${SANDBOX_FIX}.
8089
+ `
8090
+ );
8091
+ return launch([...args, "--no-sandbox"]);
8092
+ }
8093
+ }
7740
8094
  function runtimePath() {
7741
8095
  return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
7742
8096
  }
@@ -7754,10 +8108,9 @@ async function openDeck(dir, opts = {}) {
7754
8108
  const timeout = opts.timeoutMs ?? 6e4;
7755
8109
  const { default: puppeteer } = await import("puppeteer-core");
7756
8110
  const chrome2 = await resolveChrome();
7757
- const browser = await puppeteer.launch({
7758
- executablePath: chrome2.path,
7759
- headless: true,
7760
- args: [
8111
+ const browser = await launchOwnPage(
8112
+ (args) => puppeteer.launch({ executablePath: chrome2.path, headless: true, args }),
8113
+ [
7761
8114
  // A retina host would otherwise hand back a 2x frame, whose clip is not the
7762
8115
  // renderer's.
7763
8116
  "--force-device-scale-factor=1",
@@ -7770,8 +8123,9 @@ async function openDeck(dir, opts = {}) {
7770
8123
  "--use-gl=angle",
7771
8124
  "--use-angle=swiftshader",
7772
8125
  "--enable-unsafe-swiftshader"
7773
- ]
7774
- });
8126
+ ],
8127
+ "the gates' page"
8128
+ );
7775
8129
  try {
7776
8130
  const page = await browser.newPage();
7777
8131
  await page.evaluateOnNewDocument("self.__name = self.__name || ((fn) => fn);");
@@ -8792,6 +9146,14 @@ async function readInBrowser(html, timeoutMs) {
8792
9146
  // happens to be that run.
8793
9147
  "--host-resolver-rules=MAP * ~NOTFOUND"
8794
9148
  ]
9149
+ // No `--no-sandbox` fallback here, unlike `launchOwnPage`: this is a page
9150
+ // from the web, and a renderer bug it triggers is exactly what the sandbox
9151
+ // contains. Refused, with the fix, rather than Chrome's raw FATAL.
9152
+ }).catch((err) => {
9153
+ if (!sandboxMissing(err)) throw err;
9154
+ throw new Error(
9155
+ `Chrome has no usable sandbox on this host, and a page from the web is not opened without one. ${SANDBOX_FIX}, then ingest again \u2014 or save the page and ingest the file.`
9156
+ );
8795
9157
  });
8796
9158
  try {
8797
9159
  const page = await browser.newPage();
@@ -10250,8 +10612,13 @@ function renderSource(source) {
10250
10612
  }
10251
10613
  }
10252
10614
  out.push("", "== EQUATIONS ==");
10253
- for (const e of source.equations)
10254
- out.push(`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${e.tex}`);
10615
+ for (const e of source.equations) {
10616
+ const fixed = repairTex(e.tex, true);
10617
+ const note2 = fixed.error === null ? "" : " (KaTeX cannot draw this one: a walk shows it as plain source, a morph cannot use it)";
10618
+ out.push(
10619
+ `[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${fixed.error === null ? fixed.tex : e.tex}${note2}`
10620
+ );
10621
+ }
10255
10622
  if (!source.equations.length)
10256
10623
  out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
10257
10624
  out.push("", "== TABLES ==");
@@ -10825,10 +11192,10 @@ function drawSvg(req) {
10825
11192
  const seed = createHash7("sha256").update([req.prompt, req.style, req.aspect].join("|")).digest();
10826
11193
  const rand = mulberry32(seed.readUInt32BE(0));
10827
11194
  const accent = ACCENTS[Math.floor(rand() * ACCENTS.length)];
10828
- const count = 6 + Math.floor(rand() * 5);
11195
+ const count2 = 6 + Math.floor(rand() * 5);
10829
11196
  const unit = Math.min(W, H);
10830
11197
  const parts = [`<rect width="${W}" height="${H}" fill="${PAPER}"/>`];
10831
- for (let i = 0; i < count; i++) {
11198
+ for (let i = 0; i < count2; i++) {
10832
11199
  const kind = Math.floor(rand() * 3);
10833
11200
  const cx = Math.round(W * (0.15 + 0.7 * rand()));
10834
11201
  const cy = Math.round(H * (0.15 + 0.7 * rand()));
@@ -11051,10 +11418,10 @@ function readCanvas(html) {
11051
11418
  function profilesFor(width, height, formats = FORMATS) {
11052
11419
  return Object.values(formats).filter((f) => f.width === width && f.height === height);
11053
11420
  }
11054
- function scanBudget(html, storyboard, kept, formats = FORMATS) {
11421
+ function scanBudget(html, storyboard, kept, formats = FORMATS, recorded) {
11055
11422
  const canvas = readCanvas(html);
11056
11423
  if (!canvas) return [];
11057
- const profiles = profilesFor(canvas.width, canvas.height, formats);
11424
+ const profiles = recorded && recorded.width === canvas.width && recorded.height === canvas.height ? [recorded] : profilesFor(canvas.width, canvas.height, formats);
11058
11425
  if (profiles.length === 0) {
11059
11426
  return [
11060
11427
  {
@@ -11782,6 +12149,20 @@ function captionBottom(sid, selector, fallbackPx) {
11782
12149
  }
11783
12150
  return bottom > 0 ? bottom : fallbackPx;
11784
12151
  }
12152
+ function collectUnfit(attr) {
12153
+ return Array.from(document.querySelectorAll(`[${attr}]`)).map((el) => ({
12154
+ sid: el.closest("[data-composition-id]")?.getAttribute("data-composition-id") ?? "?",
12155
+ tex: el.getAttribute(attr) ?? ""
12156
+ }));
12157
+ }
12158
+ function gradeUnfit(rows) {
12159
+ return rows.map((r) => ({
12160
+ severity: "error",
12161
+ gate: "fidelity",
12162
+ rule: "math_unfit",
12163
+ message: `${r.sid}: the equation does not fit its box even at the ${TYPE_FLOOR_PX}px floor broken across lines \u2014 ${JSON.stringify(r.tex)}. Shorten it in the source, or walk it across two beats.`
12164
+ }));
12165
+ }
11785
12166
  async function fidelity(dir, opts = {}) {
11786
12167
  const started = Date.now();
11787
12168
  const floor = opts.floor ?? INK_FLOOR;
@@ -11805,6 +12186,10 @@ async function fidelity(dir, opts = {}) {
11805
12186
  try {
11806
12187
  deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
11807
12188
  const { page, height } = deck;
12189
+ await page.waitForFunction("window.__hfTimelinesBuilding === false", {
12190
+ timeout: opts.timeoutMs ?? 6e4
12191
+ });
12192
+ const unfit = await page.evaluate(collectUnfit, UNFIT_ATTR);
11808
12193
  const measured = [];
11809
12194
  const collided = [];
11810
12195
  const apparent = [];
@@ -11848,7 +12233,8 @@ async function fidelity(dir, opts = {}) {
11848
12233
  ...gradeFidelity(measured, floor),
11849
12234
  ...gradeReserve(measured),
11850
12235
  ...gradeOverprint(collided),
11851
- ...gradeApparent(apparent)
12236
+ ...gradeApparent(apparent),
12237
+ ...gradeUnfit(unfit)
11852
12238
  ],
11853
12239
  elapsedMs: Date.now() - started
11854
12240
  };
@@ -12195,9 +12581,13 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
12195
12581
  await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => ""),
12196
12582
  await listFiles(dir)
12197
12583
  );
12198
- const budget2 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
12584
+ const timing = await readTiming(dir);
12585
+ const recorded = typeof timing?.format?.id === "string" ? { ...timing.format, width: timing.width, height: timing.height } : void 0;
12586
+ const budget2 = html.flatMap(
12587
+ ([, text2]) => scanBudget(text2, storyboard, kept, void 0, recorded)
12588
+ );
12199
12589
  const type = html.flatMap(([file, text2]) => scanTypeFloor(text2, file));
12200
- const lead = kept ? await readTiming(dir).then((t2) => t2 ? scanNarrationLead(kept, t2) : []) : [];
12590
+ const lead = kept && timing ? scanNarrationLead(kept, timing) : [];
12201
12591
  const ours = [...determinism, ...narration, ...budget2, ...type, ...lead];
12202
12592
  const stops = await declaredStops(dir);
12203
12593
  const [verdict, frames2] = await Promise.all([
@@ -12386,6 +12776,31 @@ function scanUnusedFigures(storyboard, source) {
12386
12776
  }
12387
12777
  ];
12388
12778
  }
12779
+ function scanMath(storyboard, source) {
12780
+ const out = [];
12781
+ const say = (rule, message2) => {
12782
+ out.push({ severity: "warning", gate: "storyboard", rule, message: message2 });
12783
+ };
12784
+ for (const beat of storyboard.beats) {
12785
+ try {
12786
+ let notes = [];
12787
+ if (beat.archetype === "equation-walk") {
12788
+ const params = beat.params;
12789
+ const eq = source.equations.find((e) => e.id === params.equationId);
12790
+ if (eq) notes = prepareWalk(eq, params.terms, beat.id).notes;
12791
+ } else if (beat.archetype === "equation-morph") {
12792
+ notes = prepareMorph(beat, source).notes;
12793
+ }
12794
+ for (const n3 of notes) say("tex_repaired", `${beat.id}: ${n3}`);
12795
+ } catch (err) {
12796
+ say(
12797
+ "tex_unrenderable",
12798
+ `${beat.id}: ${err instanceof Error ? err.message : String(err)} \u2014 build will leave this beat out`
12799
+ );
12800
+ }
12801
+ }
12802
+ return out;
12803
+ }
12389
12804
  function figuresShown2(params) {
12390
12805
  const sides = ["left", "right"].map(
12391
12806
  (side) => params[side]?.figureId
@@ -12467,14 +12882,14 @@ function scanNarrationLead(beats, timing) {
12467
12882
  return findings;
12468
12883
  }
12469
12884
  function firstMention(segments2, label) {
12470
- for (const segment of segments2) {
12471
- for (const cue of segment.cues) {
12885
+ for (const segment2 of segments2) {
12886
+ for (const cue of segment2.cues) {
12472
12887
  const words2 = cue.text.split(/\s+/);
12473
12888
  let at = 0;
12474
12889
  for (const word of words2) {
12475
12890
  if (mentions(word, label)) {
12476
12891
  const share2 = cue.text.length > 0 ? at / cue.text.length : 0;
12477
- return segment.start + cue.start + share2 * (cue.end - cue.start);
12892
+ return segment2.start + cue.start + share2 * (cue.end - cue.start);
12478
12893
  }
12479
12894
  at += word.length + 1;
12480
12895
  }
@@ -12609,7 +13024,7 @@ ${bands}
12609
13024
  </div>
12610
13025
  `;
12611
13026
  }
12612
- function measure3(count) {
13027
+ function measure3(count2) {
12613
13028
  const doc = document;
12614
13029
  const probe2 = doc.getElementById("c0");
12615
13030
  if (probe2) {
@@ -12622,7 +13037,7 @@ function measure3(count) {
12622
13037
  probe2.classList.remove("on");
12623
13038
  }
12624
13039
  const rects = [];
12625
- for (let i = 0; i < count; i++) {
13040
+ for (let i = 0; i < count2; i++) {
12626
13041
  const el = doc.getElementById(`c${i}`);
12627
13042
  if (!el) continue;
12628
13043
  el.classList.add("on");
@@ -12650,11 +13065,12 @@ async function renderCaptions(cues, style, deck, work) {
12650
13065
  await mkdir8(work, { recursive: true });
12651
13066
  await writeFile10(page, captionPage(cues, style, href));
12652
13067
  const { default: puppeteer } = await import("puppeteer-core");
12653
- const browser = await puppeteer.launch({
12654
- executablePath: await chromePath(CAPTION_NEED),
12655
- headless: true,
12656
- args: ["--force-device-scale-factor=1", "--hide-scrollbars"]
12657
- });
13068
+ const executablePath = await chromePath(CAPTION_NEED);
13069
+ const browser = await launchOwnPage(
13070
+ (args) => puppeteer.launch({ executablePath, headless: true, args }),
13071
+ ["--force-device-scale-factor=1", "--hide-scrollbars"],
13072
+ "the caption band"
13073
+ );
12658
13074
  try {
12659
13075
  const tab = await browser.newPage();
12660
13076
  await tab.setViewport({ width: style.width, height: style.height, deviceScaleFactor: 1 });
@@ -13314,6 +13730,7 @@ export {
13314
13730
  sampleTimes,
13315
13731
  scanBeatCount,
13316
13732
  scanHeadlines,
13733
+ scanMath,
13317
13734
  scanNarrationDrift,
13318
13735
  scanNarrationLead,
13319
13736
  scanPaperArc,