@bendyline/squisq 2.13.11 → 2.13.13

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.
Files changed (38) hide show
  1. package/dist/{Doc-DqYt_b2b.d.ts → Doc-BhvotZx_.d.ts} +48 -2
  2. package/dist/{ImageEditDoc-CXXiXVJ-.d.ts → ImageEditDoc-D8hYio6b.d.ts} +1 -1
  3. package/dist/{chunk-S76W65BB.js → chunk-5DFFVW75.js} +1 -1
  4. package/dist/{chunk-DOOUVVOM.js → chunk-7MS26OXD.js} +4 -0
  5. package/dist/{chunk-XDOSE5M3.js → chunk-C4SB3NN4.js} +1 -1
  6. package/dist/{chunk-SKGMNBQX.js → chunk-CTT4XAGO.js} +2033 -375
  7. package/dist/{chunk-SG3HXPMF.js → chunk-DBVCSVNV.js} +2 -1
  8. package/dist/chunk-FCB6JLWB.js +125 -0
  9. package/dist/{chunk-WNEH3BZP.js → chunk-JNUNNQ74.js} +3 -3
  10. package/dist/chunk-L4AFSOT3.js +26 -0
  11. package/dist/{chunk-65WL5ILB.js → chunk-OIQ2GO2O.js} +1 -1
  12. package/dist/{chunk-3WUI2N6C.js → chunk-SGNWO7OF.js} +20 -1
  13. package/dist/{chunk-ESGKI6CD.js → chunk-TCGGJUKC.js} +25 -6
  14. package/dist/{chunk-GLWQTZ4C.js → chunk-XEQALK5B.js} +1 -1
  15. package/dist/{chunk-N7I6JP6Q.js → chunk-YQQWE4HG.js} +413 -216
  16. package/dist/doc/index.d.ts +225 -9
  17. package/dist/doc/index.js +32 -7
  18. package/dist/fence/index.d.ts +1 -1
  19. package/dist/generate/index.d.ts +1 -1
  20. package/dist/imageEdit/index.d.ts +3 -3
  21. package/dist/index.d.ts +7 -7
  22. package/dist/index.js +79 -11
  23. package/dist/jsonForm/index.d.ts +1 -1
  24. package/dist/jsonForm/index.js +3 -3
  25. package/dist/markdown/index.js +3 -3
  26. package/dist/{materializePageSection-S2hxZ_lO.d.ts → materializePageSection-CWxou4mX.d.ts} +62 -2
  27. package/dist/mediaEdit/index.d.ts +2 -2
  28. package/dist/narration/index.d.ts +1 -1
  29. package/dist/narration/index.js +5 -5
  30. package/dist/recommend/index.js +1 -1
  31. package/dist/schemas/index.d.ts +4 -4
  32. package/dist/schemas/index.js +2 -2
  33. package/dist/{themeLibrary-CFJbeItn.d.ts → themeLibrary-BIYE6aLw.d.ts} +1 -1
  34. package/dist/timing/index.d.ts +58 -2
  35. package/dist/timing/index.js +9 -1
  36. package/dist/transform/index.d.ts +173 -72
  37. package/dist/transform/index.js +43 -6
  38. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  buildNarrationScript,
3
+ buildSourceText,
3
4
  coerceTemplateParams,
5
+ conceptBuildStepTexts,
4
6
  flattenRenderableBlocks,
5
7
  getPinnedBlockMeta,
6
8
  markdownToDoc
7
- } from "./chunk-SKGMNBQX.js";
9
+ } from "./chunk-CTT4XAGO.js";
8
10
  import {
9
11
  deriveTemplateInputs,
10
12
  extractEmbeddedVideos,
@@ -19,7 +21,7 @@ import {
19
21
  isStandaloneHtmlVideo,
20
22
  isTreeFence,
21
23
  matchNumberHighlight
22
- } from "./chunk-3WUI2N6C.js";
24
+ } from "./chunk-SGNWO7OF.js";
23
25
  import {
24
26
  extractContent,
25
27
  mapElementToBlock,
@@ -29,13 +31,13 @@ import {
29
31
  cloneAndFreezeData,
30
32
  cloneData,
31
33
  isTemplateBlock
32
- } from "./chunk-WNEH3BZP.js";
34
+ } from "./chunk-JNUNNQ74.js";
33
35
  import {
34
36
  parseMarkdown
35
- } from "./chunk-XDOSE5M3.js";
37
+ } from "./chunk-C4SB3NN4.js";
36
38
  import {
37
39
  isContainerTemplate
38
- } from "./chunk-DOOUVVOM.js";
40
+ } from "./chunk-7MS26OXD.js";
39
41
  import {
40
42
  extractPlainText,
41
43
  findBlockVideoEmbed,
@@ -46,6 +48,9 @@ import {
46
48
  import {
47
49
  parseBuildSpec
48
50
  } from "./chunk-P53Z4RYC.js";
51
+ import {
52
+ wordTimeAt
53
+ } from "./chunk-L4AFSOT3.js";
49
54
  import {
50
55
  estimateTimeFromText
51
56
  } from "./chunk-GHOPHGOK.js";
@@ -67,7 +72,11 @@ var PRESENTATION_LAYOUTS = [
67
72
  "list",
68
73
  "comparison",
69
74
  "steps",
70
- "image"
75
+ "image",
76
+ "cards",
77
+ "process",
78
+ "cycle",
79
+ "layers"
71
80
  ];
72
81
  var AUTHORED_PRESENTATION_LAYOUT = "authored";
73
82
  var EMPHASIS_CHARACTERS = 60;
@@ -83,7 +92,12 @@ function presentationLayoutFits(layout, headline, points, passage) {
83
92
  switch (layout) {
84
93
  case "list":
85
94
  case "steps":
95
+ case "cards":
96
+ case "process":
86
97
  return points.length >= 2;
98
+ case "cycle":
99
+ case "layers":
100
+ return points.length >= 3;
87
101
  case "comparison":
88
102
  return points.length === 2;
89
103
  case "definition":
@@ -289,96 +303,6 @@ function usesDynamicPresentation(doc) {
289
303
  return typeof style === "string" && style.trim().toLowerCase().replace(/\s+/g, "-") === DYNAMIC_PRESENTATION_STYLE;
290
304
  }
291
305
 
292
- // src/transform/blockAnalyzer.ts
293
- function contentsToPlainText(contents) {
294
- return contents.map((node) => extractPlainText(node)).join("\n");
295
- }
296
- var PROSE_NODE_TYPES = /* @__PURE__ */ new Set(["paragraph", "blockquote", "heading"]);
297
- function maskNonProse(stripped, contents) {
298
- let masked = stripped;
299
- let cursor = 0;
300
- for (const node of contents) {
301
- const text3 = stripMarkdown(extractPlainText(node));
302
- if (!text3) continue;
303
- const at = stripped.indexOf(text3, cursor);
304
- if (at < 0) continue;
305
- cursor = at + text3.length;
306
- if (PROSE_NODE_TYPES.has(node.type)) continue;
307
- masked = masked.slice(0, at) + text3.replace(/\S/g, " ") + masked.slice(cursor);
308
- }
309
- return masked;
310
- }
311
- function extractBlockImages(block, blockIndex) {
312
- return extractImages(block.contents).map(({ src, alt }) => ({
313
- src,
314
- alt,
315
- sourceBlockIndex: blockIndex
316
- }));
317
- }
318
- function extractDocImages(blocks) {
319
- const seen = /* @__PURE__ */ new Set();
320
- const images = [];
321
- function walk(blockList) {
322
- for (const block of blockList) {
323
- for (const img of extractImages(block.contents)) {
324
- if (!seen.has(img.src)) {
325
- seen.add(img.src);
326
- images.push({ src: img.src, alt: img.alt });
327
- }
328
- }
329
- if (block.children) {
330
- walk(block.children);
331
- }
332
- }
333
- }
334
- walk(blocks);
335
- return images;
336
- }
337
- function collectLeafBlocks(blocks) {
338
- const result = [];
339
- for (const block of blocks) {
340
- if (block.children && block.children.length > 0) {
341
- if (!block.contents || block.contents.length === 0) {
342
- result.push(block);
343
- }
344
- result.push(...collectLeafBlocks(block.children));
345
- } else {
346
- result.push(block);
347
- }
348
- }
349
- return result;
350
- }
351
- function analyzeBlocks(blocks, options) {
352
- return analyzeFlatBlocks(collectLeafBlocks(blocks), options);
353
- }
354
- function analyzeFlatBlocks(flat, options) {
355
- const results = [];
356
- for (const block of flat) {
357
- let plainText = "";
358
- if (block.contents && block.contents.length > 0) {
359
- plainText = contentsToPlainText(block.contents);
360
- } else if (block.title) {
361
- plainText = block.title;
362
- }
363
- const stripped = stripMarkdown(plainText);
364
- const bodyWordCount = stripped.split(/\s+/).filter((w) => w.length > 0).length;
365
- const extraction = extractContent(
366
- block.contents && block.contents.length > 0 ? maskNonProse(stripped, block.contents) : stripped,
367
- options
368
- );
369
- const images = extractBlockImages(block, results.length);
370
- results.push({
371
- block,
372
- plainText: stripped,
373
- extractions: extraction.elements,
374
- hasChildren: !!(block.children && block.children.length > 0),
375
- bodyWordCount,
376
- images
377
- });
378
- }
379
- return results;
380
- }
381
-
382
306
  // src/transform/styles/dynamicSlides.ts
383
307
  var dynamicSlidesStyle = {
384
308
  id: "dynamic-slides",
@@ -656,6 +580,96 @@ function getTransformStyleSummaries(registry) {
656
580
  });
657
581
  }
658
582
 
583
+ // src/transform/blockAnalyzer.ts
584
+ function contentsToPlainText(contents) {
585
+ return contents.map((node) => extractPlainText(node)).join("\n");
586
+ }
587
+ var PROSE_NODE_TYPES = /* @__PURE__ */ new Set(["paragraph", "blockquote", "heading"]);
588
+ function maskNonProse(stripped, contents) {
589
+ let masked = stripped;
590
+ let cursor = 0;
591
+ for (const node of contents) {
592
+ const text3 = stripMarkdown(extractPlainText(node));
593
+ if (!text3) continue;
594
+ const at = stripped.indexOf(text3, cursor);
595
+ if (at < 0) continue;
596
+ cursor = at + text3.length;
597
+ if (PROSE_NODE_TYPES.has(node.type)) continue;
598
+ masked = masked.slice(0, at) + text3.replace(/\S/g, " ") + masked.slice(cursor);
599
+ }
600
+ return masked;
601
+ }
602
+ function extractBlockImages(block, blockIndex) {
603
+ return extractImages(block.contents).map(({ src, alt }) => ({
604
+ src,
605
+ alt,
606
+ sourceBlockIndex: blockIndex
607
+ }));
608
+ }
609
+ function extractDocImages(blocks) {
610
+ const seen = /* @__PURE__ */ new Set();
611
+ const images = [];
612
+ function walk(blockList) {
613
+ for (const block of blockList) {
614
+ for (const img of extractImages(block.contents)) {
615
+ if (!seen.has(img.src)) {
616
+ seen.add(img.src);
617
+ images.push({ src: img.src, alt: img.alt });
618
+ }
619
+ }
620
+ if (block.children) {
621
+ walk(block.children);
622
+ }
623
+ }
624
+ }
625
+ walk(blocks);
626
+ return images;
627
+ }
628
+ function collectLeafBlocks(blocks) {
629
+ const result = [];
630
+ for (const block of blocks) {
631
+ if (block.children && block.children.length > 0) {
632
+ if (!block.contents || block.contents.length === 0) {
633
+ result.push(block);
634
+ }
635
+ result.push(...collectLeafBlocks(block.children));
636
+ } else {
637
+ result.push(block);
638
+ }
639
+ }
640
+ return result;
641
+ }
642
+ function analyzeBlocks(blocks, options) {
643
+ return analyzeFlatBlocks(collectLeafBlocks(blocks), options);
644
+ }
645
+ function analyzeFlatBlocks(flat, options) {
646
+ const results = [];
647
+ for (const block of flat) {
648
+ let plainText = "";
649
+ if (block.contents && block.contents.length > 0) {
650
+ plainText = contentsToPlainText(block.contents);
651
+ } else if (block.title) {
652
+ plainText = block.title;
653
+ }
654
+ const stripped = stripMarkdown(plainText);
655
+ const bodyWordCount = stripped.split(/\s+/).filter((w) => w.length > 0).length;
656
+ const extraction = extractContent(
657
+ block.contents && block.contents.length > 0 ? maskNonProse(stripped, block.contents) : stripped,
658
+ options
659
+ );
660
+ const images = extractBlockImages(block, results.length);
661
+ results.push({
662
+ block,
663
+ plainText: stripped,
664
+ extractions: extraction.elements,
665
+ hasChildren: !!(block.children && block.children.length > 0),
666
+ bodyWordCount,
667
+ images
668
+ });
669
+ }
670
+ return results;
671
+ }
672
+
659
673
  // src/transform/timingAllocator.ts
660
674
  var MIN_BLOCK_DURATION = 3;
661
675
  var MAX_BLOCK_DURATION = 20;
@@ -1708,7 +1722,23 @@ function videoBlock(video, index, segmentId, duration) {
1708
1722
  }
1709
1723
 
1710
1724
  // src/transform/presentationMedia.ts
1725
+ function presentationBlockImages(doc) {
1726
+ const own = /* @__PURE__ */ new Map();
1727
+ const seen = /* @__PURE__ */ new Set();
1728
+ const claim = (block) => [
1729
+ ...extractImages(block.contents).filter((image) => {
1730
+ if (seen.has(image.src)) return false;
1731
+ seen.add(image.src);
1732
+ return true;
1733
+ }),
1734
+ ...(block.children ?? []).flatMap(claim)
1735
+ ];
1736
+ for (const block of [...flattenRenderableBlocks(doc.blocks)].reverse())
1737
+ own.set(block.id, claim(block));
1738
+ return own;
1739
+ }
1711
1740
  function mediaKind(node) {
1741
+ if (node.type === "table") return { kind: "table", continuous: true };
1712
1742
  if (node.type === "code")
1713
1743
  return isPictureFence(node) || isExplicitDiagramLang(node.lang) || isExplicitTreeLang(node.lang) || isExplicitTimelineLang(node.lang) ? { kind: "diagram", continuous: true } : null;
1714
1744
  if (isStandaloneHtmlVideo(node)) return { kind: "video", continuous: true };
@@ -1719,17 +1749,39 @@ function mediaKind(node) {
1719
1749
  function collectPresentationMedia(doc, sectionIds) {
1720
1750
  const sections = new Set(sectionIds);
1721
1751
  const result = /* @__PURE__ */ new Map();
1752
+ const pictures = presentationBlockImages(doc);
1722
1753
  let owner = sectionIds[0];
1723
1754
  for (const block of flattenRenderableBlocks(doc.blocks)) {
1724
1755
  if (sections.has(block.id)) owner = block.id;
1725
1756
  if (!owner) continue;
1757
+ const items = result.get(owner) ?? [];
1758
+ const count = (kind) => items.filter((item) => item.kind === kind).length;
1726
1759
  for (const node of block.contents ?? []) {
1727
1760
  const kind = mediaKind(node);
1728
1761
  if (!kind) continue;
1729
- const items = result.get(owner) ?? [];
1730
- items.push({ ...kind, ref: `${owner}#${items.length}`, node });
1731
- result.set(owner, items);
1762
+ const ref = kind.kind === "table" ? `${owner}#table-${count("table")}` : `${owner}#${count("video") + count("diagram")}`;
1763
+ items.push({ ...kind, ref, node });
1732
1764
  }
1765
+ for (const { src, alt, width, height } of pictures.get(block.id) ?? [])
1766
+ items.push({
1767
+ kind: "image",
1768
+ continuous: true,
1769
+ ref: `${owner}#image-${count("image")}`,
1770
+ src,
1771
+ node: {
1772
+ type: "paragraph",
1773
+ children: [
1774
+ {
1775
+ type: "image",
1776
+ url: src,
1777
+ alt,
1778
+ ...width !== void 0 ? { width } : {},
1779
+ ...height !== void 0 ? { height } : {}
1780
+ }
1781
+ ]
1782
+ }
1783
+ });
1784
+ if (items.length) result.set(owner, items);
1733
1785
  }
1734
1786
  return result;
1735
1787
  }
@@ -1759,7 +1811,23 @@ function presentationMediaNode(item, elapsed) {
1759
1811
  // src/transform/presentationPlanner.ts
1760
1812
  var SENTENCE_END = /[.!?][”"')\]]?$/u;
1761
1813
  var SENTENCE_START = /^[\p{Lu}\p{N}"“‘'([]/u;
1762
- var SEQUENCE_CUE = /^(?:(?:first|second|third|fourth)(?:ly)?|then|next|after that|afterwards|finally|lastly)\b|^\d+[.)]\s/iu;
1814
+ var SEQUENCE_CUE = /^(?:(?:first|second|third|fourth)(?:ly)?|then|next|after\b|afterwards|at that point|eventually|finally|lastly)\b|^\d+[.)]\s/iu;
1815
+ var ORDINAL_OPENER = /^(?:(?:first|second|third|fourth)(?:ly)?|then|next|after that|afterwards|finally|lastly),?\s+/iu;
1816
+ function stagePoint(sentence) {
1817
+ const rest = sentence.replace(ORDINAL_OPENER, "");
1818
+ return pointText(rest === sentence ? sentence : `${rest[0].toUpperCase()}${rest.slice(1)}`);
1819
+ }
1820
+ var CYCLE_CUE = /\b(?:cycles?|cyclical|repeats?|over and over|again and again|loop)\b/iu;
1821
+ var DEFINES = /^(?:the |an? )?((?:[\p{Lu}][\p{L}'’-]*|[\p{Ll}][\p{L}'’-]*)(?: [\p{L}'’()-]+){0,3}?) (?:is|are|was|were) (?:an? |the )?(.{12,})$/u;
1822
+ var PRONOUN = /^(?:it|its|this|that|these|those|there|they|their|he|his|she|her|we|our|you|your|i|my|which|one|each|some|to|so|but|and|or)$/iu;
1823
+ function labelledPoint(sentence) {
1824
+ const match = sentence.replace(/\s+/gu, " ").trim().match(DEFINES);
1825
+ if (match && !PRONOUN.test(match[1].split(" ")[0])) {
1826
+ const label = match[1];
1827
+ return clip(`${label[0].toUpperCase()}${label.slice(1)}: ${match[2]}`, Infinity, 160);
1828
+ }
1829
+ return clip(sentence, Infinity, 160);
1830
+ }
1763
1831
  function clip(text3, words, characters) {
1764
1832
  const clean = text3.replace(/\s+/gu, " ").trim();
1765
1833
  const parts = clean.split(" ");
@@ -1774,6 +1842,17 @@ function clip(text3, words, characters) {
1774
1842
  var headlineText = (text3) => clip(text3, 12, 110);
1775
1843
  var pointText = (text3) => clip(text3, Infinity, 180);
1776
1844
  var captionText = (text3) => clip(text3, 18, 110);
1845
+ var FEATURE_POINTS = 2;
1846
+ var FEATURE_POINT_CHARACTERS = 100;
1847
+ function featurePointText(text3) {
1848
+ const clean = text3.replace(/\s+/gu, " ").trim();
1849
+ if (clean.length <= FEATURE_POINT_CHARACTERS) return clean;
1850
+ const head = clean.slice(0, FEATURE_POINT_CHARACTERS);
1851
+ const clause = Math.max(
1852
+ ...[",", ";", ":", " \u2013", " \u2014", " - "].map((mark) => head.lastIndexOf(mark))
1853
+ );
1854
+ return clause > FEATURE_POINT_CHARACTERS * 0.4 ? clean.slice(0, clause).replace(/[\s,;:–—-]+$/u, "") + "\u2026" : clip(clean, Infinity, FEATURE_POINT_CHARACTERS);
1855
+ }
1777
1856
  function sentencesIn(sourceText, start, end) {
1778
1857
  const passage = sourceText.slice(start, end);
1779
1858
  const sentences = [];
@@ -1816,24 +1895,31 @@ function chunkWords(words, limit) {
1816
1895
  }
1817
1896
  return chunks;
1818
1897
  }
1819
- function imagesBySection(doc, sectionIds) {
1820
- const flat = flattenRenderableBlocks(doc.blocks);
1821
- const own = /* @__PURE__ */ new Map();
1822
- const seen = /* @__PURE__ */ new Set();
1823
- for (const block of [...flat].reverse()) {
1824
- const images = extractDocImages([block]).filter((image) => !seen.has(image.src));
1825
- for (const image of images) seen.add(image.src);
1826
- own.set(block.id, images);
1827
- }
1828
- const sections = new Set(sectionIds);
1829
- const result = /* @__PURE__ */ new Map();
1830
- let owner = sectionIds[0];
1831
- for (const block of flat) {
1832
- if (sections.has(block.id)) owner = block.id;
1833
- if (!owner) continue;
1834
- result.set(owner, [...result.get(owner) ?? [], ...own.get(block.id) ?? []]);
1898
+ function chunkPassage(words, limit, feature) {
1899
+ const prose = words.flatMap((word, index) => feature(word) ? [] : [index]);
1900
+ if (!prose.length) return words.length ? [[0, words.length]] : [];
1901
+ const chunks = chunkWords(
1902
+ prose.map((index) => words[index]),
1903
+ limit
1904
+ ).map(([first, last]) => [prose[first], prose[last - 1] + 1]);
1905
+ chunks[0][0] = 0;
1906
+ chunks.forEach((chunk, index) => {
1907
+ chunk[1] = chunks[index + 1]?.[0] ?? words.length;
1908
+ });
1909
+ return chunks;
1910
+ }
1911
+ function featureSpans(sourceText, from, to, items) {
1912
+ const spans = [];
1913
+ let cursor = from;
1914
+ for (const item of items) {
1915
+ if (item.kind === "image") continue;
1916
+ const text3 = extractPlainText(item.node).trim();
1917
+ const at = text3 ? sourceText.indexOf(text3, cursor) : -1;
1918
+ if (at < 0 || at + text3.length > to) continue;
1919
+ spans.push([at, at + text3.length]);
1920
+ cursor = at + text3.length;
1835
1921
  }
1836
- return result;
1922
+ return spans;
1837
1923
  }
1838
1924
  function field(block, key) {
1839
1925
  if (!block || typeof block !== "object") return void 0;
@@ -1848,7 +1934,7 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1848
1934
  throw new Error(
1849
1935
  "This document is too long for one presentation. Split it into shorter documents."
1850
1936
  );
1851
- const sectionImages = imagesBySection(
1937
+ const sectionMedia = collectPresentationMedia(
1852
1938
  doc,
1853
1939
  script.blocks.map((block) => block.blockId)
1854
1940
  );
@@ -1881,8 +1967,22 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1881
1967
  continue;
1882
1968
  }
1883
1969
  const preferred = hint?.layout;
1884
- const chunks = chunkWords(words, wordLimit);
1885
- const images = sectionImages.get(block.blockId) ?? [];
1970
+ const media = sectionMedia.get(block.blockId) ?? [];
1971
+ const spans = featureSpans(script.sourceText, block.charStart, block.charEnd, media);
1972
+ const inFeature = (from, to) => spans.some(([s, e]) => from < e && to > s);
1973
+ const chunks = chunkPassage(
1974
+ words,
1975
+ wordLimit,
1976
+ (word) => inFeature(block.charStart + word.index, block.charStart + word.index + word[0].length)
1977
+ );
1978
+ for (let k = chunks.length - 1; k > 0; k--) {
1979
+ const opening = script.sourceText.slice(block.charStart + words[chunks[k][0]].index);
1980
+ if (SEQUENCE_CUE.test(opening) && chunks[k][1] - chunks[k - 1][0] <= wordLimit * 2) {
1981
+ chunks[k - 1][1] = chunks[k][1];
1982
+ chunks.splice(k, 1);
1983
+ }
1984
+ }
1985
+ const images = media.filter((item) => item.kind === "image");
1886
1986
  const opensDeck = beats.length === 0;
1887
1987
  for (const [chunk, [first, last]] of chunks.entries()) {
1888
1988
  const fromEnd = chunks.length - 1 - chunk;
@@ -1890,8 +1990,18 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1890
1990
  const start = block.charStart + words[first].index;
1891
1991
  const end = block.charStart + words[last - 1].index + words[last - 1][0].length;
1892
1992
  const passage = script.sourceText.slice(start, end);
1993
+ let prose = "";
1994
+ let cursor = start;
1995
+ for (const [s, e] of spans) {
1996
+ if (e <= cursor || s >= end) continue;
1997
+ prose += script.sourceText.slice(cursor, Math.max(cursor, s)) + "\n";
1998
+ cursor = Math.min(end, e);
1999
+ }
2000
+ prose = (prose + script.sourceText.slice(cursor, end)).trim() || passage;
1893
2001
  const index = beats.length;
1894
- const sentences = sentencesIn(script.sourceText, start, end);
2002
+ const sentences = sentencesIn(script.sourceText, start, end).filter(
2003
+ (sentence) => !inFeature(sentence.start, sentence.end)
2004
+ );
1895
2005
  const body = sentences.filter((sentence) => sentence.start >= headingEnd);
1896
2006
  let headline;
1897
2007
  let shown;
@@ -1899,12 +2009,12 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1899
2009
  headline = headlineText(heading);
1900
2010
  shown = body.filter((sentence) => comparable(sentence.text) !== comparable(heading));
1901
2011
  } else {
1902
- const suggested = transformNarratedSegment(
2012
+ const suggested2 = transformNarratedSegment(
1903
2013
  {
1904
2014
  articleId: doc.articleId,
1905
2015
  segmentId: `presentation-${index}`,
1906
2016
  title: "",
1907
- text: passage,
2017
+ text: prose,
1908
2018
  duration: Math.max(4, (last - first) / 2.5),
1909
2019
  audioSrc: "",
1910
2020
  includeTitleBlock: false
@@ -1912,23 +2022,24 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1912
2022
  { slidesPerMinute: 4, minConfidence: 0.25, style: "documentary" }
1913
2023
  ).doc.blocks[0];
1914
2024
  headline = headlineText(
1915
- field(suggested, "fact") ?? field(suggested, "description") ?? field(suggested, "quote") ?? sentences[0]?.text ?? passage
2025
+ field(suggested2, "fact") ?? field(suggested2, "description") ?? field(suggested2, "quote") ?? sentences[0]?.text ?? prose
1916
2026
  );
1917
- if (!headline) headline = headlineText(passage);
2027
+ if (!headline) headline = headlineText(prose);
1918
2028
  shown = body.filter((sentence) => !repeats(sentence.text, headline));
1919
2029
  }
1920
2030
  const steps = shown.filter((sentence) => SEQUENCE_CUE.test(sentence.text));
1921
2031
  const sequence = steps.length >= 2;
1922
2032
  const textPoints = shown.slice(0, 3).map((item) => pointText(item.text));
1923
- const stepPoints = sequence ? steps.slice(0, 3).map((item) => pointText(item.text)) : textPoints;
2033
+ const stepPoints = sequence ? steps.slice(0, 4).map((item) => stagePoint(item.text)) : textPoints;
2034
+ const cardPoints = shown.slice(0, 4).map((item) => labelledPoint(item.text));
1924
2035
  const custom = chunk === 0 ? wording : void 0;
1925
2036
  if (custom?.headline) headline = custom.headline;
1926
- const pointsFor = (layout2) => custom?.points ?? (layout2 === "steps" ? stepPoints : textPoints);
2037
+ const pointsFor = (layout2) => custom?.points ?? (["steps", "process", "cycle"].includes(layout2) ? stepPoints : layout2 === "cards" ? cardPoints : textPoints);
1927
2038
  const quote = custom?.headline ? void 0 : body.find((sentence) => sentence.text.length <= 110)?.text;
1928
2039
  const headlineFor = (layout2) => layout2 === "quote" && quote ? quote : headline;
1929
- let layout = index === 0 ? "title" : textPoints.length >= 2 && index % 3 === 1 ? "list" : "statement";
1930
- if (sequence) layout = "steps";
1931
- else if (textPoints.length === 2 && /\b(versus|whereas|compared|instead)\b/iu.test(passage))
2040
+ let layout = index === 0 ? "title" : textPoints.length >= 2 ? "cards" : "statement";
2041
+ if (sequence) layout = steps.length >= 3 && CYCLE_CUE.test(prose) ? "cycle" : "steps";
2042
+ else if (textPoints.length === 2 && /\b(versus|whereas|compared|instead)\b/iu.test(prose))
1932
2043
  layout = "comparison";
1933
2044
  if (image) layout = "image";
1934
2045
  if (preferred && (preferred === "image" ? !!image : presentationLayoutFits(
@@ -1945,7 +2056,7 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1945
2056
  layout,
1946
2057
  headline: headlineFor(layout),
1947
2058
  points: pointsFor(layout),
1948
- ...layout === "image" && image ? { imageSrc: image.src } : {}
2059
+ ...layout === "image" && image?.src ? { imageSrc: image.src } : {}
1949
2060
  };
1950
2061
  if (quote) quotes.set(beat.id, quote);
1951
2062
  let split;
@@ -1997,28 +2108,6 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1997
2108
  ))
1998
2109
  beat.hidden = true;
1999
2110
  }
2000
- const sectionMedia = collectPresentationMedia(
2001
- doc,
2002
- script.blocks.map((block) => block.blockId)
2003
- );
2004
- for (const block of script.blocks) {
2005
- const items = sectionMedia.get(block.blockId);
2006
- if (!items?.length) continue;
2007
- const own = beats.filter(
2008
- (beat) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd && beat.layout !== AUTHORED_PRESENTATION_LAYOUT && !beat.hidden
2009
- );
2010
- if (!own.length) continue;
2011
- const open = own.filter((beat) => beat !== beats[0] && beat.layout !== "image");
2012
- const slots = open.length ? open : own;
2013
- const shown = /* @__PURE__ */ new Set();
2014
- slots.forEach((beat, slot) => {
2015
- const from = Math.floor(slot * items.length / slots.length);
2016
- const to = Math.max(from + 1, Math.floor((slot + 1) * items.length / slots.length));
2017
- const refs = items.slice(from, to).filter((item) => item.continuous || !shown.has(item.ref)).slice(0, PRESENTATION_LIMITS.mediaPerBeat).map((item) => item.ref);
2018
- for (const ref of refs) shown.add(ref);
2019
- if (refs.length) beat.media = refs;
2020
- });
2021
- }
2022
2111
  const closing = beats[beats.length - 1];
2023
2112
  if (beats.length > 2 && closing?.layout === "statement" && !hints.blocks.some(
2024
2113
  (hint) => hint.layout && script.blocks.some(
@@ -2026,19 +2115,24 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
2026
2115
  )
2027
2116
  ))
2028
2117
  closing.layout = "title";
2118
+ const sectionOf = (beat) => script.blocks.find(
2119
+ (block) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd
2120
+ );
2121
+ const hintFor = (beat) => hints.blocks.find((item) => item.blockId === sectionOf(beat)?.blockId);
2122
+ const worded = /* @__PURE__ */ new Set();
2123
+ const suggested = /* @__PURE__ */ new Set();
2029
2124
  let origin = "automatic";
2030
2125
  for (const beat of beats) {
2031
2126
  if (beat.layout === AUTHORED_PRESENTATION_LAYOUT) continue;
2032
- const section = script.blocks.find(
2033
- (block) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd
2034
- );
2035
- const hint = hints.blocks.find((item) => item.blockId === section?.blockId);
2127
+ const section = sectionOf(beat);
2128
+ const hint = hintFor(beat);
2036
2129
  const passage = script.sourceText.slice(beat.sourceStart, beat.sourceEnd);
2037
2130
  const cached = hint?.ai?.find((item) => item.source === passage);
2038
2131
  if (cached && (cached.layout !== "image" || beat.imageSrc)) {
2039
2132
  beat.headline = cached.headline;
2040
2133
  beat.points = [...cached.points];
2041
2134
  beat.layout = cached.layout;
2135
+ suggested.add(beat.id);
2042
2136
  origin = "ai";
2043
2137
  }
2044
2138
  const preferred = hint?.layout === AUTHORED_PRESENTATION_LAYOUT ? void 0 : hint?.layout;
@@ -2052,15 +2146,101 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
2052
2146
  if (section && beat.sourceStart === section.charStart && hint?.wording?.source === script.sourceText.slice(section.charStart, section.charEnd)) {
2053
2147
  beat.headline = hint.wording.headline ?? beat.headline;
2054
2148
  beat.points = hint.wording.points ?? beat.points;
2149
+ if (hint.wording.points) worded.add(beat.id);
2055
2150
  }
2056
- if (beat.media && beat.layout === "steps" && preferred !== "steps") beat.layout = "list";
2057
2151
  if (beat.layout !== "image" && !presentationLayoutFits(beat.layout, beat.headline, beat.points, passage))
2058
2152
  beat.layout = "statement";
2059
2153
  if (beat.layout !== "image") delete beat.imageSrc;
2060
2154
  }
2155
+ const fullBleed = new Set(beats.flatMap((beat) => beat.imageSrc ? [beat.imageSrc] : []));
2156
+ for (const block of script.blocks) {
2157
+ const items = (sectionMedia.get(block.blockId) ?? []).filter(
2158
+ (item) => item.kind !== "image" || !fullBleed.has(item.src)
2159
+ );
2160
+ if (!items.length) continue;
2161
+ const own = beats.filter(
2162
+ (beat) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd && beat.layout !== AUTHORED_PRESENTATION_LAYOUT && !beat.hidden
2163
+ );
2164
+ if (!own.length) continue;
2165
+ const open = own.filter((beat) => beat !== beats[0] && beat.layout !== "image");
2166
+ const slots = open.length ? open : own;
2167
+ const shown = /* @__PURE__ */ new Set();
2168
+ slots.forEach((beat, slot) => {
2169
+ const from = Math.floor(slot * items.length / slots.length);
2170
+ const to = Math.max(from + 1, Math.floor((slot + 1) * items.length / slots.length));
2171
+ const refs = items.slice(from, to).filter((item) => item.continuous || !shown.has(item.ref)).slice(0, PRESENTATION_LIMITS.mediaPerBeat).map((item) => item.ref);
2172
+ for (const ref of refs) shown.add(ref);
2173
+ if (refs.length) beat.media = refs;
2174
+ });
2175
+ }
2176
+ for (const beat of beats) {
2177
+ if (!beat.media) continue;
2178
+ if (!worded.has(beat.id)) {
2179
+ const points = beat.points.slice(0, FEATURE_POINTS);
2180
+ beat.points = suggested.has(beat.id) ? points : points.map(featurePointText);
2181
+ }
2182
+ if (beat.layout === "steps" && hintFor(beat)?.layout !== "steps") beat.layout = "list";
2183
+ const passage = script.sourceText.slice(beat.sourceStart, beat.sourceEnd);
2184
+ if (!presentationLayoutFits(
2185
+ beat.layout,
2186
+ beat.headline,
2187
+ beat.points,
2188
+ passage
2189
+ ))
2190
+ beat.layout = "statement";
2191
+ }
2061
2192
  return { version: 1, sourceText: script.sourceText, origin, beats };
2062
2193
  }
2063
2194
 
2195
+ // src/transform/conceptReveal.ts
2196
+ var CONCEPT_TEMPLATES = ["cards", "process", "cycle", "layers"];
2197
+ function conceptTemplate(template) {
2198
+ return CONCEPT_TEMPLATES.find((name) => name === template);
2199
+ }
2200
+ function stems(text3) {
2201
+ return (text3.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}'-]*/gu) ?? []).filter((word) => word.length >= 4).map((word) => word.slice(0, 5));
2202
+ }
2203
+ function matchPointsToSentences(points, sourceText, start, end) {
2204
+ const passage = sourceText.slice(start, end);
2205
+ const sentences = [...passage.matchAll(/[^.!?\n]+[.!?]*/gu)].map((match) => ({
2206
+ offset: start + match.index,
2207
+ words: new Set(stems(match[0]))
2208
+ }));
2209
+ let last = -1;
2210
+ const offsets = [];
2211
+ for (const point of points) {
2212
+ const words = stems(point);
2213
+ let best;
2214
+ for (const sentence of sentences) {
2215
+ const shared = words.filter((word) => sentence.words.has(word)).length;
2216
+ if (shared > (best?.shared ?? 1)) best = { offset: sentence.offset, shared };
2217
+ }
2218
+ if (!best || best.offset < last) return void 0;
2219
+ offsets.push(last = best.offset);
2220
+ }
2221
+ return offsets;
2222
+ }
2223
+ function conceptBuildSpec() {
2224
+ return { mode: "items", effect: "fadeInUp", previous: "keep" };
2225
+ }
2226
+ function conceptRevealTiming(template, items, itemTimes, start, end) {
2227
+ const build = conceptBuildSpec();
2228
+ const steps = conceptBuildStepTexts(template, items);
2229
+ if (!itemTimes || itemTimes.length !== steps.length) return { build };
2230
+ const lead = Math.min(0.5, (end - start) / 8);
2231
+ const starts = itemTimes.map(
2232
+ (time) => Math.min(Math.max(0, end - start - 0.1), Math.max(lead, time - start))
2233
+ );
2234
+ starts[0] = Math.min(starts[0], Math.max(lead, Math.min(1, (end - start) / 6)));
2235
+ return {
2236
+ build,
2237
+ buildTiming: {
2238
+ sourceText: buildSourceText(steps.map((text3, index) => ({ id: `build-${index}`, text: text3 }))),
2239
+ starts
2240
+ }
2241
+ };
2242
+ }
2243
+
2064
2244
  // src/transform/presentationCompiler.ts
2065
2245
  function compilePresentationPlan(doc, plan) {
2066
2246
  const checked = parsePresentationPlan(plan);
@@ -2102,7 +2282,7 @@ function compilePresentationPlan(doc, plan) {
2102
2282
  );
2103
2283
  if (checked.beats.some((beat) => beat.media?.some((ref) => !media.has(ref))))
2104
2284
  throw new Error(
2105
- "A presentation video or diagram is no longer in the document. Make the presentation again."
2285
+ "A presentation table, picture, video or diagram is no longer in the document. Make the presentation again."
2106
2286
  );
2107
2287
  const duration = narration ? narration.startTime + narration.duration : Math.max(4, script.tokens.length / 2.5);
2108
2288
  const bookmarks = (narration?.bookmarks ?? []).filter(
@@ -2129,20 +2309,16 @@ function compilePresentationPlan(doc, plan) {
2129
2309
  const share = span > 0 ? (tokensBefore(offset) - tokensBefore(from.charOffset)) / span : 0;
2130
2310
  return from.time + share * Math.max(0, to.time - from.time);
2131
2311
  };
2312
+ const timeAt = (offset) => {
2313
+ const marked = markedTime(offset);
2314
+ if (marked !== void 0) return marked;
2315
+ if (narration && bookmarks.length) return wordTimeAt(bookmarks, offset, "next") ?? duration;
2316
+ return (narration?.startTime ?? 0) + tokensBefore(offset) / Math.max(1, script.tokens.length) * (narration?.duration ?? duration);
2317
+ };
2132
2318
  let previous = 0;
2133
2319
  const starts = checked.beats.map((beat, index) => {
2134
2320
  if (!index) return 0;
2135
- let time;
2136
- const marked = markedTime(beat.sourceStart);
2137
- if (marked !== void 0) {
2138
- time = marked;
2139
- } else if (narration && bookmarks.length) {
2140
- time = bookmarks.find((word) => word.charOffset >= beat.sourceStart)?.time ?? duration;
2141
- } else {
2142
- const before = script.tokens.filter((token) => token.charOffset < beat.sourceStart).length;
2143
- time = (narration?.startTime ?? 0) + before / Math.max(1, script.tokens.length) * (narration?.duration ?? duration);
2144
- }
2145
- previous = Math.min(duration, Math.max(previous, time));
2321
+ previous = Math.min(duration, Math.max(previous, timeAt(beat.sourceStart)));
2146
2322
  return previous;
2147
2323
  });
2148
2324
  const authoredBlocks = flattenRenderableBlocks(doc.blocks);
@@ -2229,8 +2405,16 @@ function compilePresentationPlan(doc, plan) {
2229
2405
  });
2230
2406
  continue;
2231
2407
  }
2232
- const block = visualBlock(beat, beat.layout, base, index);
2233
- blocks.push(block);
2408
+ const table = beat.layout !== "image" && (beat.media ?? []).some((ref) => media.get(ref).kind === "table");
2409
+ const block = table ? tableFeatureBlock(beat, beat.layout, base, contents) : visualBlock(beat, beat.layout, base, index);
2410
+ const concept = conceptTemplate(block.template);
2411
+ blocks.push({
2412
+ ...block,
2413
+ ...contents.length ? { summaryMode: "brief" } : {},
2414
+ // A concept slide reveals its ideas one at a time, each as the narration
2415
+ // reaches the sentence it summarizes (`conceptReveal`).
2416
+ ...concept && !build ? conceptReveal(concept, beat, start, end, script.sourceText, timeAt) : {}
2417
+ });
2234
2418
  }
2235
2419
  return {
2236
2420
  ...doc,
@@ -2242,6 +2426,34 @@ function compilePresentationPlan(doc, plan) {
2242
2426
  audio: doc.audio.segments.some((segment) => segment.src) ? doc.audio : { segments: [{ src: "", name: "presentation-clock", startTime: 0, duration }] }
2243
2427
  };
2244
2428
  }
2429
+ function conceptReveal(template, beat, start, end, sourceText, timeAt) {
2430
+ const steps = conceptBuildStepTexts(template, beat.points);
2431
+ const offsets = matchPointsToSentences(
2432
+ beat.points.slice(0, steps.length),
2433
+ sourceText,
2434
+ beat.sourceStart,
2435
+ beat.sourceEnd
2436
+ );
2437
+ return conceptRevealTiming(template, beat.points, offsets?.map(timeAt), start, end);
2438
+ }
2439
+ function tableFeatureBlock(beat, layout, base, features) {
2440
+ const paragraph = (text3) => ({
2441
+ type: "paragraph",
2442
+ children: [{ type: "text", value: text3 }]
2443
+ });
2444
+ const words = ["list", "steps", "comparison"].includes(layout) && beat.points.length > 1 ? [
2445
+ {
2446
+ type: "list",
2447
+ ordered: false,
2448
+ children: beat.points.map((point) => ({
2449
+ type: "listItem",
2450
+ children: [paragraph(point)]
2451
+ }))
2452
+ }
2453
+ ] : beat.points.map(paragraph);
2454
+ const block = { ...base, template: "bottomFeature", title: beat.headline };
2455
+ return Object.assign(block, { contents: [...words, ...features] });
2456
+ }
2245
2457
  function visualBlock(beat, layout, base, index) {
2246
2458
  const colorScheme = ["blue", "green", "orange"][index % 3];
2247
2459
  switch (layout) {
@@ -2250,52 +2462,40 @@ function visualBlock(beat, layout, base, index) {
2250
2462
  case "divider":
2251
2463
  return { ...base, template: "sectionHeader", title: beat.headline, colorScheme };
2252
2464
  case "emphasis":
2253
- return { ...base, template: "bigText", title: beat.headline };
2465
+ return { ...base, template: "cards", items: [beat.headline] };
2254
2466
  case "quote":
2255
2467
  return { ...base, template: "fullBleedQuote", text: beat.headline, colorScheme };
2256
2468
  case "stat": {
2257
2469
  const { match, line, index: at } = presentationStat(beat.headline, beat.points);
2258
- const detail = beat.points.find((_, i) => i !== at) ?? (at < 0 ? void 0 : beat.headline);
2470
+ const others = beat.points.filter((_, i) => i !== at);
2471
+ const [description, detail] = at < 0 ? others : [line, others[0]];
2259
2472
  return {
2260
2473
  ...base,
2261
2474
  template: "statHighlight",
2475
+ headline: beat.headline,
2262
2476
  stat: match.value,
2263
- description: line,
2264
- ...detail ? { detail } : {},
2265
- colorScheme
2477
+ description: description ?? "",
2478
+ ...detail ? { detail } : {}
2266
2479
  };
2267
2480
  }
2268
2481
  case "definition":
2269
2482
  return {
2270
2483
  ...base,
2271
- template: "definitionCard",
2272
- term: beat.headline,
2273
- definition: beat.points.join("\n\n"),
2274
- colorScheme
2484
+ template: "cards",
2485
+ title: beat.headline,
2486
+ items: beat.points.length ? [beat.points.map((point) => point.trim().replace(/([^.!?])$/u, "$1.")).join(" ")] : []
2275
2487
  };
2276
2488
  case "statement":
2277
- return {
2278
- ...base,
2279
- template: "factCard",
2280
- fact: beat.headline,
2281
- explanation: beat.points.join("\n\n")
2282
- };
2489
+ return beat.points.length ? { ...base, template: "cards", title: beat.headline, items: beat.points } : { ...base, template: "factCard", fact: beat.headline, explanation: "" };
2283
2490
  case "list":
2491
+ return { ...base, template: "cards", title: beat.headline, items: beat.points };
2492
+ case "comparison":
2284
2493
  return {
2285
2494
  ...base,
2286
- template: "list",
2495
+ template: "cards",
2287
2496
  title: beat.headline,
2288
2497
  items: beat.points,
2289
- ordered: false,
2290
- colorScheme
2291
- };
2292
- case "comparison":
2293
- return {
2294
- ...base,
2295
- template: "twoColumn",
2296
- header: beat.headline,
2297
- left: { label: beat.points[0] },
2298
- right: { label: beat.points[1] }
2498
+ ...beat.points.length === 2 ? { versus: true } : {}
2299
2499
  };
2300
2500
  case "image":
2301
2501
  return {
@@ -2308,22 +2508,14 @@ function visualBlock(beat, layout, base, index) {
2308
2508
  ambientMotion: "zoomIn"
2309
2509
  };
2310
2510
  case "steps":
2311
- return {
2312
- ...base,
2313
- template: "diagram",
2314
- background: "surface",
2315
- title: beat.headline,
2316
- colorScheme,
2317
- nodes: beat.points.map((point, i) => ({
2318
- id: `step-${i}`,
2319
- label: point,
2320
- x: i * 320,
2321
- y: 0,
2322
- w: 260,
2323
- h: 180
2324
- })),
2325
- edges: beat.points.slice(1).map((_, i) => ({ source: `step-${i}`, target: `step-${i + 1}`, directed: true }))
2326
- };
2511
+ case "process":
2512
+ return { ...base, template: "process", title: beat.headline, items: beat.points };
2513
+ case "cards":
2514
+ return { ...base, template: "cards", title: beat.headline, items: beat.points };
2515
+ case "cycle":
2516
+ return { ...base, template: "cycle", title: beat.headline, items: beat.points };
2517
+ case "layers":
2518
+ return { ...base, template: "layers", title: beat.headline, items: beat.points };
2327
2519
  }
2328
2520
  }
2329
2521
  function applySavedPresentation(doc) {
@@ -2371,18 +2563,23 @@ export {
2371
2563
  serializePresentationHints,
2372
2564
  presentationSections,
2373
2565
  usesDynamicPresentation,
2374
- extractDocImages,
2375
- analyzeBlocks,
2376
2566
  DEFAULT_TRANSFORM_STYLE_ID,
2377
2567
  createTransformStyleRegistry,
2378
2568
  resolveTransformStyle,
2379
2569
  getTransformStyleIds,
2380
2570
  getTransformStyleSummaries,
2571
+ extractDocImages,
2572
+ analyzeBlocks,
2381
2573
  allocateTiming,
2382
2574
  applyTransform,
2383
2575
  transformNarratedSegment,
2384
2576
  collectPresentationMedia,
2385
2577
  createPresentationPlan,
2578
+ CONCEPT_TEMPLATES,
2579
+ conceptTemplate,
2580
+ matchPointsToSentences,
2581
+ conceptBuildSpec,
2582
+ conceptRevealTiming,
2386
2583
  compilePresentationPlan,
2387
2584
  applySavedPresentation
2388
2585
  };