@bendyline/squisq 2.13.10 → 2.13.12

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 (35) 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-FXQ7C2FF.js → chunk-CQSCYJQU.js} +1 -1
  7. package/dist/{chunk-SG3HXPMF.js → chunk-DBVCSVNV.js} +2 -1
  8. package/dist/{chunk-WK6LEZ3U.js → chunk-IFZZI4XY.js} +132 -11
  9. package/dist/{chunk-WNEH3BZP.js → chunk-JNUNNQ74.js} +3 -3
  10. package/dist/{chunk-KRMAEYNM.js → chunk-OUSEEUEC.js} +389 -217
  11. package/dist/{chunk-LWNT6M4N.js → chunk-QJQQO3K4.js} +1857 -386
  12. package/dist/{chunk-DIY3NBIS.js → chunk-SGNWO7OF.js} +22 -1
  13. package/dist/{chunk-GLWQTZ4C.js → chunk-XEQALK5B.js} +1 -1
  14. package/dist/doc/index.d.ts +124 -9
  15. package/dist/doc/index.js +19 -7
  16. package/dist/fence/index.d.ts +1 -1
  17. package/dist/generate/index.d.ts +1 -1
  18. package/dist/imageEdit/index.d.ts +3 -3
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.js +23 -11
  21. package/dist/jsonForm/index.d.ts +1 -1
  22. package/dist/jsonForm/index.js +3 -3
  23. package/dist/markdown/index.js +3 -3
  24. package/dist/{materializePageSection-S2hxZ_lO.d.ts → materializePageSection-bLfqIy6j.d.ts} +1 -1
  25. package/dist/mediaEdit/index.d.ts +2 -2
  26. package/dist/narration/index.d.ts +1 -1
  27. package/dist/narration/index.js +5 -5
  28. package/dist/recommend/index.js +1 -1
  29. package/dist/schemas/index.d.ts +4 -4
  30. package/dist/schemas/index.js +2 -2
  31. package/dist/{themeLibrary-CFJbeItn.d.ts → themeLibrary-BIYE6aLw.d.ts} +1 -1
  32. package/dist/timing/index.d.ts +1 -1
  33. package/dist/transform/index.d.ts +23 -14
  34. package/dist/transform/index.js +6 -6
  35. 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-LWNT6M4N.js";
9
+ } from "./chunk-QJQQO3K4.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-DIY3NBIS.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,
@@ -67,7 +69,11 @@ var PRESENTATION_LAYOUTS = [
67
69
  "list",
68
70
  "comparison",
69
71
  "steps",
70
- "image"
72
+ "image",
73
+ "cards",
74
+ "process",
75
+ "cycle",
76
+ "layers"
71
77
  ];
72
78
  var AUTHORED_PRESENTATION_LAYOUT = "authored";
73
79
  var EMPHASIS_CHARACTERS = 60;
@@ -83,7 +89,12 @@ function presentationLayoutFits(layout, headline, points, passage) {
83
89
  switch (layout) {
84
90
  case "list":
85
91
  case "steps":
92
+ case "cards":
93
+ case "process":
86
94
  return points.length >= 2;
95
+ case "cycle":
96
+ case "layers":
97
+ return points.length >= 3;
87
98
  case "comparison":
88
99
  return points.length === 2;
89
100
  case "definition":
@@ -289,96 +300,6 @@ function usesDynamicPresentation(doc) {
289
300
  return typeof style === "string" && style.trim().toLowerCase().replace(/\s+/g, "-") === DYNAMIC_PRESENTATION_STYLE;
290
301
  }
291
302
 
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
303
  // src/transform/styles/dynamicSlides.ts
383
304
  var dynamicSlidesStyle = {
384
305
  id: "dynamic-slides",
@@ -656,6 +577,96 @@ function getTransformStyleSummaries(registry) {
656
577
  });
657
578
  }
658
579
 
580
+ // src/transform/blockAnalyzer.ts
581
+ function contentsToPlainText(contents) {
582
+ return contents.map((node) => extractPlainText(node)).join("\n");
583
+ }
584
+ var PROSE_NODE_TYPES = /* @__PURE__ */ new Set(["paragraph", "blockquote", "heading"]);
585
+ function maskNonProse(stripped, contents) {
586
+ let masked = stripped;
587
+ let cursor = 0;
588
+ for (const node of contents) {
589
+ const text3 = stripMarkdown(extractPlainText(node));
590
+ if (!text3) continue;
591
+ const at = stripped.indexOf(text3, cursor);
592
+ if (at < 0) continue;
593
+ cursor = at + text3.length;
594
+ if (PROSE_NODE_TYPES.has(node.type)) continue;
595
+ masked = masked.slice(0, at) + text3.replace(/\S/g, " ") + masked.slice(cursor);
596
+ }
597
+ return masked;
598
+ }
599
+ function extractBlockImages(block, blockIndex) {
600
+ return extractImages(block.contents).map(({ src, alt }) => ({
601
+ src,
602
+ alt,
603
+ sourceBlockIndex: blockIndex
604
+ }));
605
+ }
606
+ function extractDocImages(blocks) {
607
+ const seen = /* @__PURE__ */ new Set();
608
+ const images = [];
609
+ function walk(blockList) {
610
+ for (const block of blockList) {
611
+ for (const img of extractImages(block.contents)) {
612
+ if (!seen.has(img.src)) {
613
+ seen.add(img.src);
614
+ images.push({ src: img.src, alt: img.alt });
615
+ }
616
+ }
617
+ if (block.children) {
618
+ walk(block.children);
619
+ }
620
+ }
621
+ }
622
+ walk(blocks);
623
+ return images;
624
+ }
625
+ function collectLeafBlocks(blocks) {
626
+ const result = [];
627
+ for (const block of blocks) {
628
+ if (block.children && block.children.length > 0) {
629
+ if (!block.contents || block.contents.length === 0) {
630
+ result.push(block);
631
+ }
632
+ result.push(...collectLeafBlocks(block.children));
633
+ } else {
634
+ result.push(block);
635
+ }
636
+ }
637
+ return result;
638
+ }
639
+ function analyzeBlocks(blocks, options) {
640
+ return analyzeFlatBlocks(collectLeafBlocks(blocks), options);
641
+ }
642
+ function analyzeFlatBlocks(flat, options) {
643
+ const results = [];
644
+ for (const block of flat) {
645
+ let plainText = "";
646
+ if (block.contents && block.contents.length > 0) {
647
+ plainText = contentsToPlainText(block.contents);
648
+ } else if (block.title) {
649
+ plainText = block.title;
650
+ }
651
+ const stripped = stripMarkdown(plainText);
652
+ const bodyWordCount = stripped.split(/\s+/).filter((w) => w.length > 0).length;
653
+ const extraction = extractContent(
654
+ block.contents && block.contents.length > 0 ? maskNonProse(stripped, block.contents) : stripped,
655
+ options
656
+ );
657
+ const images = extractBlockImages(block, results.length);
658
+ results.push({
659
+ block,
660
+ plainText: stripped,
661
+ extractions: extraction.elements,
662
+ hasChildren: !!(block.children && block.children.length > 0),
663
+ bodyWordCount,
664
+ images
665
+ });
666
+ }
667
+ return results;
668
+ }
669
+
659
670
  // src/transform/timingAllocator.ts
660
671
  var MIN_BLOCK_DURATION = 3;
661
672
  var MAX_BLOCK_DURATION = 20;
@@ -1708,7 +1719,23 @@ function videoBlock(video, index, segmentId, duration) {
1708
1719
  }
1709
1720
 
1710
1721
  // src/transform/presentationMedia.ts
1722
+ function presentationBlockImages(doc) {
1723
+ const own = /* @__PURE__ */ new Map();
1724
+ const seen = /* @__PURE__ */ new Set();
1725
+ const claim = (block) => [
1726
+ ...extractImages(block.contents).filter((image) => {
1727
+ if (seen.has(image.src)) return false;
1728
+ seen.add(image.src);
1729
+ return true;
1730
+ }),
1731
+ ...(block.children ?? []).flatMap(claim)
1732
+ ];
1733
+ for (const block of [...flattenRenderableBlocks(doc.blocks)].reverse())
1734
+ own.set(block.id, claim(block));
1735
+ return own;
1736
+ }
1711
1737
  function mediaKind(node) {
1738
+ if (node.type === "table") return { kind: "table", continuous: true };
1712
1739
  if (node.type === "code")
1713
1740
  return isPictureFence(node) || isExplicitDiagramLang(node.lang) || isExplicitTreeLang(node.lang) || isExplicitTimelineLang(node.lang) ? { kind: "diagram", continuous: true } : null;
1714
1741
  if (isStandaloneHtmlVideo(node)) return { kind: "video", continuous: true };
@@ -1719,17 +1746,39 @@ function mediaKind(node) {
1719
1746
  function collectPresentationMedia(doc, sectionIds) {
1720
1747
  const sections = new Set(sectionIds);
1721
1748
  const result = /* @__PURE__ */ new Map();
1749
+ const pictures = presentationBlockImages(doc);
1722
1750
  let owner = sectionIds[0];
1723
1751
  for (const block of flattenRenderableBlocks(doc.blocks)) {
1724
1752
  if (sections.has(block.id)) owner = block.id;
1725
1753
  if (!owner) continue;
1754
+ const items = result.get(owner) ?? [];
1755
+ const count = (kind) => items.filter((item) => item.kind === kind).length;
1726
1756
  for (const node of block.contents ?? []) {
1727
1757
  const kind = mediaKind(node);
1728
1758
  if (!kind) continue;
1729
- const items = result.get(owner) ?? [];
1730
- items.push({ ...kind, ref: `${owner}#${items.length}`, node });
1731
- result.set(owner, items);
1759
+ const ref = kind.kind === "table" ? `${owner}#table-${count("table")}` : `${owner}#${count("video") + count("diagram")}`;
1760
+ items.push({ ...kind, ref, node });
1732
1761
  }
1762
+ for (const { src, alt, width, height } of pictures.get(block.id) ?? [])
1763
+ items.push({
1764
+ kind: "image",
1765
+ continuous: true,
1766
+ ref: `${owner}#image-${count("image")}`,
1767
+ src,
1768
+ node: {
1769
+ type: "paragraph",
1770
+ children: [
1771
+ {
1772
+ type: "image",
1773
+ url: src,
1774
+ alt,
1775
+ ...width !== void 0 ? { width } : {},
1776
+ ...height !== void 0 ? { height } : {}
1777
+ }
1778
+ ]
1779
+ }
1780
+ });
1781
+ if (items.length) result.set(owner, items);
1733
1782
  }
1734
1783
  return result;
1735
1784
  }
@@ -1759,7 +1808,23 @@ function presentationMediaNode(item, elapsed) {
1759
1808
  // src/transform/presentationPlanner.ts
1760
1809
  var SENTENCE_END = /[.!?][”"')\]]?$/u;
1761
1810
  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;
1811
+ var SEQUENCE_CUE = /^(?:(?:first|second|third|fourth)(?:ly)?|then|next|after\b|afterwards|at that point|eventually|finally|lastly)\b|^\d+[.)]\s/iu;
1812
+ var ORDINAL_OPENER = /^(?:(?:first|second|third|fourth)(?:ly)?|then|next|after that|afterwards|finally|lastly),?\s+/iu;
1813
+ function stagePoint(sentence) {
1814
+ const rest = sentence.replace(ORDINAL_OPENER, "");
1815
+ return pointText(rest === sentence ? sentence : `${rest[0].toUpperCase()}${rest.slice(1)}`);
1816
+ }
1817
+ var CYCLE_CUE = /\b(?:cycles?|cyclical|repeats?|over and over|again and again|loop)\b/iu;
1818
+ var DEFINES = /^(?:the |an? )?((?:[\p{Lu}][\p{L}'’-]*|[\p{Ll}][\p{L}'’-]*)(?: [\p{L}'’()-]+){0,3}?) (?:is|are|was|were) (?:an? |the )?(.{12,})$/u;
1819
+ 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;
1820
+ function labelledPoint(sentence) {
1821
+ const match = sentence.replace(/\s+/gu, " ").trim().match(DEFINES);
1822
+ if (match && !PRONOUN.test(match[1].split(" ")[0])) {
1823
+ const label = match[1];
1824
+ return clip(`${label[0].toUpperCase()}${label.slice(1)}: ${match[2]}`, Infinity, 160);
1825
+ }
1826
+ return clip(sentence, Infinity, 160);
1827
+ }
1763
1828
  function clip(text3, words, characters) {
1764
1829
  const clean = text3.replace(/\s+/gu, " ").trim();
1765
1830
  const parts = clean.split(" ");
@@ -1774,6 +1839,17 @@ function clip(text3, words, characters) {
1774
1839
  var headlineText = (text3) => clip(text3, 12, 110);
1775
1840
  var pointText = (text3) => clip(text3, Infinity, 180);
1776
1841
  var captionText = (text3) => clip(text3, 18, 110);
1842
+ var FEATURE_POINTS = 2;
1843
+ var FEATURE_POINT_CHARACTERS = 100;
1844
+ function featurePointText(text3) {
1845
+ const clean = text3.replace(/\s+/gu, " ").trim();
1846
+ if (clean.length <= FEATURE_POINT_CHARACTERS) return clean;
1847
+ const head = clean.slice(0, FEATURE_POINT_CHARACTERS);
1848
+ const clause = Math.max(
1849
+ ...[",", ";", ":", " \u2013", " \u2014", " - "].map((mark) => head.lastIndexOf(mark))
1850
+ );
1851
+ return clause > FEATURE_POINT_CHARACTERS * 0.4 ? clean.slice(0, clause).replace(/[\s,;:–—-]+$/u, "") + "\u2026" : clip(clean, Infinity, FEATURE_POINT_CHARACTERS);
1852
+ }
1777
1853
  function sentencesIn(sourceText, start, end) {
1778
1854
  const passage = sourceText.slice(start, end);
1779
1855
  const sentences = [];
@@ -1816,24 +1892,31 @@ function chunkWords(words, limit) {
1816
1892
  }
1817
1893
  return chunks;
1818
1894
  }
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) ?? []]);
1895
+ function chunkPassage(words, limit, feature) {
1896
+ const prose = words.flatMap((word, index) => feature(word) ? [] : [index]);
1897
+ if (!prose.length) return words.length ? [[0, words.length]] : [];
1898
+ const chunks = chunkWords(
1899
+ prose.map((index) => words[index]),
1900
+ limit
1901
+ ).map(([first, last]) => [prose[first], prose[last - 1] + 1]);
1902
+ chunks[0][0] = 0;
1903
+ chunks.forEach((chunk, index) => {
1904
+ chunk[1] = chunks[index + 1]?.[0] ?? words.length;
1905
+ });
1906
+ return chunks;
1907
+ }
1908
+ function featureSpans(sourceText, from, to, items) {
1909
+ const spans = [];
1910
+ let cursor = from;
1911
+ for (const item of items) {
1912
+ if (item.kind === "image") continue;
1913
+ const text3 = extractPlainText(item.node).trim();
1914
+ const at = text3 ? sourceText.indexOf(text3, cursor) : -1;
1915
+ if (at < 0 || at + text3.length > to) continue;
1916
+ spans.push([at, at + text3.length]);
1917
+ cursor = at + text3.length;
1835
1918
  }
1836
- return result;
1919
+ return spans;
1837
1920
  }
1838
1921
  function field(block, key) {
1839
1922
  if (!block || typeof block !== "object") return void 0;
@@ -1848,7 +1931,7 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1848
1931
  throw new Error(
1849
1932
  "This document is too long for one presentation. Split it into shorter documents."
1850
1933
  );
1851
- const sectionImages = imagesBySection(
1934
+ const sectionMedia = collectPresentationMedia(
1852
1935
  doc,
1853
1936
  script.blocks.map((block) => block.blockId)
1854
1937
  );
@@ -1881,8 +1964,22 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1881
1964
  continue;
1882
1965
  }
1883
1966
  const preferred = hint?.layout;
1884
- const chunks = chunkWords(words, wordLimit);
1885
- const images = sectionImages.get(block.blockId) ?? [];
1967
+ const media = sectionMedia.get(block.blockId) ?? [];
1968
+ const spans = featureSpans(script.sourceText, block.charStart, block.charEnd, media);
1969
+ const inFeature = (from, to) => spans.some(([s, e]) => from < e && to > s);
1970
+ const chunks = chunkPassage(
1971
+ words,
1972
+ wordLimit,
1973
+ (word) => inFeature(block.charStart + word.index, block.charStart + word.index + word[0].length)
1974
+ );
1975
+ for (let k = chunks.length - 1; k > 0; k--) {
1976
+ const opening = script.sourceText.slice(block.charStart + words[chunks[k][0]].index);
1977
+ if (SEQUENCE_CUE.test(opening) && chunks[k][1] - chunks[k - 1][0] <= wordLimit * 2) {
1978
+ chunks[k - 1][1] = chunks[k][1];
1979
+ chunks.splice(k, 1);
1980
+ }
1981
+ }
1982
+ const images = media.filter((item) => item.kind === "image");
1886
1983
  const opensDeck = beats.length === 0;
1887
1984
  for (const [chunk, [first, last]] of chunks.entries()) {
1888
1985
  const fromEnd = chunks.length - 1 - chunk;
@@ -1890,8 +1987,18 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1890
1987
  const start = block.charStart + words[first].index;
1891
1988
  const end = block.charStart + words[last - 1].index + words[last - 1][0].length;
1892
1989
  const passage = script.sourceText.slice(start, end);
1990
+ let prose = "";
1991
+ let cursor = start;
1992
+ for (const [s, e] of spans) {
1993
+ if (e <= cursor || s >= end) continue;
1994
+ prose += script.sourceText.slice(cursor, Math.max(cursor, s)) + "\n";
1995
+ cursor = Math.min(end, e);
1996
+ }
1997
+ prose = (prose + script.sourceText.slice(cursor, end)).trim() || passage;
1893
1998
  const index = beats.length;
1894
- const sentences = sentencesIn(script.sourceText, start, end);
1999
+ const sentences = sentencesIn(script.sourceText, start, end).filter(
2000
+ (sentence) => !inFeature(sentence.start, sentence.end)
2001
+ );
1895
2002
  const body = sentences.filter((sentence) => sentence.start >= headingEnd);
1896
2003
  let headline;
1897
2004
  let shown;
@@ -1899,12 +2006,12 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1899
2006
  headline = headlineText(heading);
1900
2007
  shown = body.filter((sentence) => comparable(sentence.text) !== comparable(heading));
1901
2008
  } else {
1902
- const suggested = transformNarratedSegment(
2009
+ const suggested2 = transformNarratedSegment(
1903
2010
  {
1904
2011
  articleId: doc.articleId,
1905
2012
  segmentId: `presentation-${index}`,
1906
2013
  title: "",
1907
- text: passage,
2014
+ text: prose,
1908
2015
  duration: Math.max(4, (last - first) / 2.5),
1909
2016
  audioSrc: "",
1910
2017
  includeTitleBlock: false
@@ -1912,23 +2019,24 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1912
2019
  { slidesPerMinute: 4, minConfidence: 0.25, style: "documentary" }
1913
2020
  ).doc.blocks[0];
1914
2021
  headline = headlineText(
1915
- field(suggested, "fact") ?? field(suggested, "description") ?? field(suggested, "quote") ?? sentences[0]?.text ?? passage
2022
+ field(suggested2, "fact") ?? field(suggested2, "description") ?? field(suggested2, "quote") ?? sentences[0]?.text ?? prose
1916
2023
  );
1917
- if (!headline) headline = headlineText(passage);
2024
+ if (!headline) headline = headlineText(prose);
1918
2025
  shown = body.filter((sentence) => !repeats(sentence.text, headline));
1919
2026
  }
1920
2027
  const steps = shown.filter((sentence) => SEQUENCE_CUE.test(sentence.text));
1921
2028
  const sequence = steps.length >= 2;
1922
2029
  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;
2030
+ const stepPoints = sequence ? steps.slice(0, 4).map((item) => stagePoint(item.text)) : textPoints;
2031
+ const cardPoints = shown.slice(0, 4).map((item) => labelledPoint(item.text));
1924
2032
  const custom = chunk === 0 ? wording : void 0;
1925
2033
  if (custom?.headline) headline = custom.headline;
1926
- const pointsFor = (layout2) => custom?.points ?? (layout2 === "steps" ? stepPoints : textPoints);
2034
+ const pointsFor = (layout2) => custom?.points ?? (["steps", "process", "cycle"].includes(layout2) ? stepPoints : layout2 === "cards" ? cardPoints : textPoints);
1927
2035
  const quote = custom?.headline ? void 0 : body.find((sentence) => sentence.text.length <= 110)?.text;
1928
2036
  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))
2037
+ let layout = index === 0 ? "title" : textPoints.length >= 2 ? "cards" : "statement";
2038
+ if (sequence) layout = steps.length >= 3 && CYCLE_CUE.test(prose) ? "cycle" : "steps";
2039
+ else if (textPoints.length === 2 && /\b(versus|whereas|compared|instead)\b/iu.test(prose))
1932
2040
  layout = "comparison";
1933
2041
  if (image) layout = "image";
1934
2042
  if (preferred && (preferred === "image" ? !!image : presentationLayoutFits(
@@ -1945,7 +2053,7 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1945
2053
  layout,
1946
2054
  headline: headlineFor(layout),
1947
2055
  points: pointsFor(layout),
1948
- ...layout === "image" && image ? { imageSrc: image.src } : {}
2056
+ ...layout === "image" && image?.src ? { imageSrc: image.src } : {}
1949
2057
  };
1950
2058
  if (quote) quotes.set(beat.id, quote);
1951
2059
  let split;
@@ -1997,28 +2105,6 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1997
2105
  ))
1998
2106
  beat.hidden = true;
1999
2107
  }
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
2108
  const closing = beats[beats.length - 1];
2023
2109
  if (beats.length > 2 && closing?.layout === "statement" && !hints.blocks.some(
2024
2110
  (hint) => hint.layout && script.blocks.some(
@@ -2026,19 +2112,24 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
2026
2112
  )
2027
2113
  ))
2028
2114
  closing.layout = "title";
2115
+ const sectionOf = (beat) => script.blocks.find(
2116
+ (block) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd
2117
+ );
2118
+ const hintFor = (beat) => hints.blocks.find((item) => item.blockId === sectionOf(beat)?.blockId);
2119
+ const worded = /* @__PURE__ */ new Set();
2120
+ const suggested = /* @__PURE__ */ new Set();
2029
2121
  let origin = "automatic";
2030
2122
  for (const beat of beats) {
2031
2123
  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);
2124
+ const section = sectionOf(beat);
2125
+ const hint = hintFor(beat);
2036
2126
  const passage = script.sourceText.slice(beat.sourceStart, beat.sourceEnd);
2037
2127
  const cached = hint?.ai?.find((item) => item.source === passage);
2038
2128
  if (cached && (cached.layout !== "image" || beat.imageSrc)) {
2039
2129
  beat.headline = cached.headline;
2040
2130
  beat.points = [...cached.points];
2041
2131
  beat.layout = cached.layout;
2132
+ suggested.add(beat.id);
2042
2133
  origin = "ai";
2043
2134
  }
2044
2135
  const preferred = hint?.layout === AUTHORED_PRESENTATION_LAYOUT ? void 0 : hint?.layout;
@@ -2052,12 +2143,49 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
2052
2143
  if (section && beat.sourceStart === section.charStart && hint?.wording?.source === script.sourceText.slice(section.charStart, section.charEnd)) {
2053
2144
  beat.headline = hint.wording.headline ?? beat.headline;
2054
2145
  beat.points = hint.wording.points ?? beat.points;
2146
+ if (hint.wording.points) worded.add(beat.id);
2055
2147
  }
2056
- if (beat.media && beat.layout === "steps" && preferred !== "steps") beat.layout = "list";
2057
2148
  if (beat.layout !== "image" && !presentationLayoutFits(beat.layout, beat.headline, beat.points, passage))
2058
2149
  beat.layout = "statement";
2059
2150
  if (beat.layout !== "image") delete beat.imageSrc;
2060
2151
  }
2152
+ const fullBleed = new Set(beats.flatMap((beat) => beat.imageSrc ? [beat.imageSrc] : []));
2153
+ for (const block of script.blocks) {
2154
+ const items = (sectionMedia.get(block.blockId) ?? []).filter(
2155
+ (item) => item.kind !== "image" || !fullBleed.has(item.src)
2156
+ );
2157
+ if (!items.length) continue;
2158
+ const own = beats.filter(
2159
+ (beat) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd && beat.layout !== AUTHORED_PRESENTATION_LAYOUT && !beat.hidden
2160
+ );
2161
+ if (!own.length) continue;
2162
+ const open = own.filter((beat) => beat !== beats[0] && beat.layout !== "image");
2163
+ const slots = open.length ? open : own;
2164
+ const shown = /* @__PURE__ */ new Set();
2165
+ slots.forEach((beat, slot) => {
2166
+ const from = Math.floor(slot * items.length / slots.length);
2167
+ const to = Math.max(from + 1, Math.floor((slot + 1) * items.length / slots.length));
2168
+ const refs = items.slice(from, to).filter((item) => item.continuous || !shown.has(item.ref)).slice(0, PRESENTATION_LIMITS.mediaPerBeat).map((item) => item.ref);
2169
+ for (const ref of refs) shown.add(ref);
2170
+ if (refs.length) beat.media = refs;
2171
+ });
2172
+ }
2173
+ for (const beat of beats) {
2174
+ if (!beat.media) continue;
2175
+ if (!worded.has(beat.id)) {
2176
+ const points = beat.points.slice(0, FEATURE_POINTS);
2177
+ beat.points = suggested.has(beat.id) ? points : points.map(featurePointText);
2178
+ }
2179
+ if (beat.layout === "steps" && hintFor(beat)?.layout !== "steps") beat.layout = "list";
2180
+ const passage = script.sourceText.slice(beat.sourceStart, beat.sourceEnd);
2181
+ if (!presentationLayoutFits(
2182
+ beat.layout,
2183
+ beat.headline,
2184
+ beat.points,
2185
+ passage
2186
+ ))
2187
+ beat.layout = "statement";
2188
+ }
2061
2189
  return { version: 1, sourceText: script.sourceText, origin, beats };
2062
2190
  }
2063
2191
 
@@ -2078,7 +2206,7 @@ function compilePresentationPlan(doc, plan) {
2078
2206
  );
2079
2207
  if (narration && narration.sourceText !== checked.sourceText)
2080
2208
  throw new Error(
2081
- "The narration no longer matches the document. Generate narration again before presenting."
2209
+ "The narration no longer matches the document. Record or generate narration again before presenting."
2082
2210
  );
2083
2211
  if (narration && flattenRenderableBlocks(doc.blocks).some((block) => {
2084
2212
  const pin = getPinnedBlockMeta(block);
@@ -2102,7 +2230,7 @@ function compilePresentationPlan(doc, plan) {
2102
2230
  );
2103
2231
  if (checked.beats.some((beat) => beat.media?.some((ref) => !media.has(ref))))
2104
2232
  throw new Error(
2105
- "A presentation video or diagram is no longer in the document. Make the presentation again."
2233
+ "A presentation table, picture, video or diagram is no longer in the document. Make the presentation again."
2106
2234
  );
2107
2235
  const duration = narration ? narration.startTime + narration.duration : Math.max(4, script.tokens.length / 2.5);
2108
2236
  const bookmarks = (narration?.bookmarks ?? []).filter(
@@ -2129,20 +2257,17 @@ function compilePresentationPlan(doc, plan) {
2129
2257
  const share = span > 0 ? (tokensBefore(offset) - tokensBefore(from.charOffset)) / span : 0;
2130
2258
  return from.time + share * Math.max(0, to.time - from.time);
2131
2259
  };
2260
+ const timeAt = (offset) => {
2261
+ const marked = markedTime(offset);
2262
+ if (marked !== void 0) return marked;
2263
+ if (narration && bookmarks.length)
2264
+ return bookmarks.find((word) => word.charOffset >= offset)?.time ?? duration;
2265
+ return (narration?.startTime ?? 0) + tokensBefore(offset) / Math.max(1, script.tokens.length) * (narration?.duration ?? duration);
2266
+ };
2132
2267
  let previous = 0;
2133
2268
  const starts = checked.beats.map((beat, index) => {
2134
2269
  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));
2270
+ previous = Math.min(duration, Math.max(previous, timeAt(beat.sourceStart)));
2146
2271
  return previous;
2147
2272
  });
2148
2273
  const authoredBlocks = flattenRenderableBlocks(doc.blocks);
@@ -2229,8 +2354,16 @@ function compilePresentationPlan(doc, plan) {
2229
2354
  });
2230
2355
  continue;
2231
2356
  }
2232
- const block = visualBlock(beat, beat.layout, base, index);
2233
- blocks.push(block);
2357
+ const table = beat.layout !== "image" && (beat.media ?? []).some((ref) => media.get(ref).kind === "table");
2358
+ const block = table ? tableFeatureBlock(beat, beat.layout, base, contents) : visualBlock(beat, beat.layout, base, index);
2359
+ const concept = conceptTemplate(block.template);
2360
+ blocks.push({
2361
+ ...block,
2362
+ ...contents.length ? { summaryMode: "brief" } : {},
2363
+ // A concept slide reveals its ideas one at a time, each as the narration
2364
+ // reaches the sentence it summarizes (`conceptReveal`).
2365
+ ...concept && !build ? conceptReveal(concept, beat, start, end, script.sourceText, timeAt) : {}
2366
+ });
2234
2367
  }
2235
2368
  return {
2236
2369
  ...doc,
@@ -2242,6 +2375,65 @@ function compilePresentationPlan(doc, plan) {
2242
2375
  audio: doc.audio.segments.some((segment) => segment.src) ? doc.audio : { segments: [{ src: "", name: "presentation-clock", startTime: 0, duration }] }
2243
2376
  };
2244
2377
  }
2378
+ var CONCEPT_TEMPLATES = ["cards", "process", "cycle", "layers"];
2379
+ function conceptTemplate(template) {
2380
+ return CONCEPT_TEMPLATES.find((name) => name === template);
2381
+ }
2382
+ function stems(text3) {
2383
+ return (text3.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}'-]*/gu) ?? []).filter((word) => word.length >= 4).map((word) => word.slice(0, 5));
2384
+ }
2385
+ function conceptReveal(template, beat, start, end, sourceText, timeAt) {
2386
+ const build = { mode: "items", effect: "fadeInUp", previous: "keep" };
2387
+ const steps = conceptBuildStepTexts(template, beat.points);
2388
+ const passage = sourceText.slice(beat.sourceStart, beat.sourceEnd);
2389
+ const sentences = [...passage.matchAll(/[^.!?\n]+[.!?]*/gu)].map((match) => ({
2390
+ offset: beat.sourceStart + match.index,
2391
+ words: new Set(stems(match[0]))
2392
+ }));
2393
+ let last = -1;
2394
+ const offsets = [];
2395
+ for (const point of beat.points.slice(0, steps.length)) {
2396
+ const words = stems(point);
2397
+ let best;
2398
+ for (const sentence of sentences) {
2399
+ const shared = words.filter((word) => sentence.words.has(word)).length;
2400
+ if (shared > (best?.shared ?? 1)) best = { offset: sentence.offset, shared };
2401
+ }
2402
+ if (!best || best.offset < last) return { build };
2403
+ offsets.push(last = best.offset);
2404
+ }
2405
+ if (offsets.length !== steps.length) return { build };
2406
+ const lead = Math.min(0.5, (end - start) / 8);
2407
+ const starts = offsets.map(
2408
+ (offset) => Math.min(Math.max(0, end - start - 0.1), Math.max(lead, timeAt(offset) - start))
2409
+ );
2410
+ starts[0] = Math.min(starts[0], Math.max(lead, Math.min(1, (end - start) / 6)));
2411
+ return {
2412
+ build,
2413
+ buildTiming: {
2414
+ sourceText: buildSourceText(steps.map((text3, index) => ({ id: `build-${index}`, text: text3 }))),
2415
+ starts
2416
+ }
2417
+ };
2418
+ }
2419
+ function tableFeatureBlock(beat, layout, base, features) {
2420
+ const paragraph = (text3) => ({
2421
+ type: "paragraph",
2422
+ children: [{ type: "text", value: text3 }]
2423
+ });
2424
+ const words = ["list", "steps", "comparison"].includes(layout) && beat.points.length > 1 ? [
2425
+ {
2426
+ type: "list",
2427
+ ordered: false,
2428
+ children: beat.points.map((point) => ({
2429
+ type: "listItem",
2430
+ children: [paragraph(point)]
2431
+ }))
2432
+ }
2433
+ ] : beat.points.map(paragraph);
2434
+ const block = { ...base, template: "bottomFeature", title: beat.headline };
2435
+ return Object.assign(block, { contents: [...words, ...features] });
2436
+ }
2245
2437
  function visualBlock(beat, layout, base, index) {
2246
2438
  const colorScheme = ["blue", "green", "orange"][index % 3];
2247
2439
  switch (layout) {
@@ -2250,52 +2442,40 @@ function visualBlock(beat, layout, base, index) {
2250
2442
  case "divider":
2251
2443
  return { ...base, template: "sectionHeader", title: beat.headline, colorScheme };
2252
2444
  case "emphasis":
2253
- return { ...base, template: "bigText", title: beat.headline };
2445
+ return { ...base, template: "cards", items: [beat.headline] };
2254
2446
  case "quote":
2255
2447
  return { ...base, template: "fullBleedQuote", text: beat.headline, colorScheme };
2256
2448
  case "stat": {
2257
2449
  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);
2450
+ const others = beat.points.filter((_, i) => i !== at);
2451
+ const [description, detail] = at < 0 ? others : [line, others[0]];
2259
2452
  return {
2260
2453
  ...base,
2261
2454
  template: "statHighlight",
2455
+ headline: beat.headline,
2262
2456
  stat: match.value,
2263
- description: line,
2264
- ...detail ? { detail } : {},
2265
- colorScheme
2457
+ description: description ?? "",
2458
+ ...detail ? { detail } : {}
2266
2459
  };
2267
2460
  }
2268
2461
  case "definition":
2269
2462
  return {
2270
2463
  ...base,
2271
- template: "definitionCard",
2272
- term: beat.headline,
2273
- definition: beat.points.join("\n\n"),
2274
- colorScheme
2464
+ template: "cards",
2465
+ title: beat.headline,
2466
+ items: beat.points.length ? [beat.points.map((point) => point.trim().replace(/([^.!?])$/u, "$1.")).join(" ")] : []
2275
2467
  };
2276
2468
  case "statement":
2277
- return {
2278
- ...base,
2279
- template: "factCard",
2280
- fact: beat.headline,
2281
- explanation: beat.points.join("\n\n")
2282
- };
2469
+ return beat.points.length ? { ...base, template: "cards", title: beat.headline, items: beat.points } : { ...base, template: "factCard", fact: beat.headline, explanation: "" };
2283
2470
  case "list":
2471
+ return { ...base, template: "cards", title: beat.headline, items: beat.points };
2472
+ case "comparison":
2284
2473
  return {
2285
2474
  ...base,
2286
- template: "list",
2475
+ template: "cards",
2287
2476
  title: beat.headline,
2288
2477
  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] }
2478
+ ...beat.points.length === 2 ? { versus: true } : {}
2299
2479
  };
2300
2480
  case "image":
2301
2481
  return {
@@ -2308,22 +2488,14 @@ function visualBlock(beat, layout, base, index) {
2308
2488
  ambientMotion: "zoomIn"
2309
2489
  };
2310
2490
  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
- };
2491
+ case "process":
2492
+ return { ...base, template: "process", title: beat.headline, items: beat.points };
2493
+ case "cards":
2494
+ return { ...base, template: "cards", title: beat.headline, items: beat.points };
2495
+ case "cycle":
2496
+ return { ...base, template: "cycle", title: beat.headline, items: beat.points };
2497
+ case "layers":
2498
+ return { ...base, template: "layers", title: beat.headline, items: beat.points };
2327
2499
  }
2328
2500
  }
2329
2501
  function applySavedPresentation(doc) {
@@ -2371,13 +2543,13 @@ export {
2371
2543
  serializePresentationHints,
2372
2544
  presentationSections,
2373
2545
  usesDynamicPresentation,
2374
- extractDocImages,
2375
- analyzeBlocks,
2376
2546
  DEFAULT_TRANSFORM_STYLE_ID,
2377
2547
  createTransformStyleRegistry,
2378
2548
  resolveTransformStyle,
2379
2549
  getTransformStyleIds,
2380
2550
  getTransformStyleSummaries,
2551
+ extractDocImages,
2552
+ analyzeBlocks,
2381
2553
  allocateTiming,
2382
2554
  applyTransform,
2383
2555
  transformNarratedSegment,