@bendyline/squisq 2.13.9 → 2.13.10

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.
@@ -4,7 +4,7 @@ import {
4
4
  flattenRenderableBlocks,
5
5
  getPinnedBlockMeta,
6
6
  markdownToDoc
7
- } from "./chunk-L3VQQ3WT.js";
7
+ } from "./chunk-LWNT6M4N.js";
8
8
  import {
9
9
  deriveTemplateInputs,
10
10
  extractEmbeddedVideos,
@@ -17,8 +17,9 @@ import {
17
17
  isPictureFence,
18
18
  isPictureParagraph,
19
19
  isStandaloneHtmlVideo,
20
- isTreeFence
21
- } from "./chunk-R2J7CQJZ.js";
20
+ isTreeFence,
21
+ matchNumberHighlight
22
+ } from "./chunk-DIY3NBIS.js";
22
23
  import {
23
24
  extractContent,
24
25
  mapElementToBlock,
@@ -57,12 +58,46 @@ import {
57
58
  var PRESENTATION_KEY = "squisq-presentation";
58
59
  var PRESENTATION_LAYOUTS = [
59
60
  "title",
61
+ "divider",
60
62
  "statement",
63
+ "emphasis",
64
+ "quote",
65
+ "stat",
66
+ "definition",
61
67
  "list",
62
68
  "comparison",
63
69
  "steps",
64
70
  "image"
65
71
  ];
72
+ var AUTHORED_PRESENTATION_LAYOUT = "authored";
73
+ var EMPHASIS_CHARACTERS = 60;
74
+ var spaced = (text3) => text3.replace(/\s+/gu, " ").trim();
75
+ function presentationStat(headline, points) {
76
+ for (const [index, line] of [headline, ...points].entries()) {
77
+ const match = matchNumberHighlight(line);
78
+ if (match) return { match, line, index: index - 1 };
79
+ }
80
+ return null;
81
+ }
82
+ function presentationLayoutFits(layout, headline, points, passage) {
83
+ switch (layout) {
84
+ case "list":
85
+ case "steps":
86
+ return points.length >= 2;
87
+ case "comparison":
88
+ return points.length === 2;
89
+ case "definition":
90
+ return points.length >= 1;
91
+ case "stat":
92
+ return presentationStat(headline, points) !== null;
93
+ case "emphasis":
94
+ return headline.length <= EMPHASIS_CHARACTERS;
95
+ case "quote":
96
+ return spaced(passage).includes(spaced(headline));
97
+ default:
98
+ return true;
99
+ }
100
+ }
66
101
  var PRESENTATION_LIMITS = {
67
102
  sourceCharacters: 4e4,
68
103
  beats: 80,
@@ -100,12 +135,18 @@ function parsePresentationPlan(input) {
100
135
  if (!record(beat) || !keys(
101
136
  beat,
102
137
  ["id", "sourceStart", "sourceEnd", "layout", "headline", "points"],
103
- ["imageSrc", "media"]
104
- ) || !text(beat.id, 80) || ids.has(beat.id) || !Number.isInteger(beat.sourceStart) || !Number.isInteger(beat.sourceEnd) || typeof beat.sourceStart !== "number" || typeof beat.sourceEnd !== "number" || beat.sourceStart < end || beat.sourceEnd <= beat.sourceStart || beat.sourceEnd > value.sourceText.length || value.sourceText.slice(end, beat.sourceStart).trim() || !PRESENTATION_LAYOUTS.includes(beat.layout) || !text(beat.headline, 110) || !Array.isArray(beat.points) || beat.points.length > 4 || !beat.points.every((point) => text(point, 180)))
138
+ ["imageSrc", "media", "hidden"]
139
+ ) || "hidden" in beat && beat.hidden !== true || !text(beat.id, 80) || ids.has(beat.id) || !Number.isInteger(beat.sourceStart) || !Number.isInteger(beat.sourceEnd) || typeof beat.sourceStart !== "number" || typeof beat.sourceEnd !== "number" || beat.sourceStart < end || beat.sourceEnd <= beat.sourceStart || beat.sourceEnd > value.sourceText.length || value.sourceText.slice(end, beat.sourceStart).trim() || !text(beat.headline, 110) || !Array.isArray(beat.points) || beat.points.length > 4 || !beat.points.every((point) => text(point, 180)))
105
140
  return null;
106
- if (["comparison", "steps", "list"].includes(String(beat.layout)) && beat.points.length < 2)
141
+ if (beat.layout === AUTHORED_PRESENTATION_LAYOUT) {
142
+ if (beat.points.length || "media" in beat) return null;
143
+ } else if (!PRESENTATION_LAYOUTS.includes(beat.layout) || !presentationLayoutFits(
144
+ beat.layout,
145
+ beat.headline,
146
+ beat.points,
147
+ value.sourceText.slice(beat.sourceStart, beat.sourceEnd)
148
+ ))
107
149
  return null;
108
- if (beat.layout === "comparison" && beat.points.length !== 2) return null;
109
150
  if (beat.layout === "image" ? !text(beat.imageSrc, 2048) : "imageSrc" in beat) return null;
110
151
  if ("media" in beat && (!Array.isArray(beat.media) || !beat.media.length || beat.media.length > PRESENTATION_LIMITS.mediaPerBeat || new Set(beat.media).size !== beat.media.length || !beat.media.every((ref) => text(ref, 1100))))
111
152
  return null;
@@ -119,7 +160,8 @@ function parsePresentationPlan(input) {
119
160
  headline: beat.headline,
120
161
  points: [...beat.points],
121
162
  ...beat.layout === "image" ? { imageSrc: beat.imageSrc } : {},
122
- ..."media" in beat ? { media: [...beat.media] } : {}
163
+ ..."media" in beat ? { media: [...beat.media] } : {},
164
+ ...beat.hidden === true ? { hidden: true } : {}
123
165
  });
124
166
  }
125
167
  if (value.sourceText.slice(end).trim()) return null;
@@ -142,6 +184,9 @@ function serializePresentationPlan(plan) {
142
184
  // src/transform/presentationHints.ts
143
185
  var PRESENTATION_HINTS_KEY = "squisq-presentation-hints";
144
186
  var DYNAMIC_PRESENTATION_STYLE = "dynamic-slides";
187
+ function presentationPassageKey(passage) {
188
+ return `${hashString(passage).toString(36)}-${passage.length.toString(36)}`;
189
+ }
145
190
  function defaultPresentationHints() {
146
191
  return { version: 1, density: "balanced", inbetweens: true, blocks: [] };
147
192
  }
@@ -164,12 +209,14 @@ function parsePresentationHints(input) {
164
209
  return null;
165
210
  }
166
211
  }
167
- if (!record2(value) || !exact(value, ["version", "density", "inbetweens", "blocks"]) || value.version !== 1 || !["concise", "balanced", "detailed"].includes(String(value.density)) || typeof value.inbetweens !== "boolean" || !Array.isArray(value.blocks) || value.blocks.length > 80)
212
+ if (!record2(value) || !exact(value, ["version", "density", "inbetweens", "blocks"]) || value.version !== 1 || !["concise", "balanced", "detailed", "section"].includes(String(value.density)) || typeof value.inbetweens !== "boolean" || !Array.isArray(value.blocks) || value.blocks.length > 80)
168
213
  return null;
169
214
  const ids = /* @__PURE__ */ new Set();
170
215
  const blocks = [];
171
216
  for (const entry of value.blocks) {
172
- if (!record2(entry) || !exact(entry, ["blockId"], ["layout", "inbetweens", "wording", "ai", "build"]) || !text2(entry.blockId, 1024) || ids.has(entry.blockId) || entry.layout !== void 0 && !PRESENTATION_LAYOUTS.includes(entry.layout) || entry.inbetweens !== void 0 && typeof entry.inbetweens !== "boolean")
217
+ if (!record2(entry) || !exact(entry, ["blockId"], ["layout", "inbetweens", "wording", "ai", "build", "hidden"]) || !text2(entry.blockId, 1024) || ids.has(entry.blockId) || entry.layout !== void 0 && entry.layout !== AUTHORED_PRESENTATION_LAYOUT && !PRESENTATION_LAYOUTS.includes(entry.layout) || entry.inbetweens !== void 0 && typeof entry.inbetweens !== "boolean" || entry.hidden !== void 0 && (!Array.isArray(entry.hidden) || !entry.hidden.length || entry.hidden.length > 80 || new Set(entry.hidden).size !== entry.hidden.length || !entry.hidden.every(
218
+ (key) => typeof key === "string" && /^[0-9a-z]{1,7}-[0-9a-z]{1,4}$/u.test(key)
219
+ )))
173
220
  return null;
174
221
  const hint = { blockId: entry.blockId };
175
222
  if (entry.build !== void 0) {
@@ -179,11 +226,17 @@ function parsePresentationHints(input) {
179
226
  }
180
227
  if (entry.layout !== void 0) hint.layout = entry.layout;
181
228
  if (entry.inbetweens !== void 0) hint.inbetweens = entry.inbetweens;
229
+ if (entry.hidden !== void 0) hint.hidden = [...entry.hidden];
182
230
  if (entry.ai !== void 0) {
183
231
  if (!Array.isArray(entry.ai) || entry.ai.length > 80) return null;
184
232
  hint.ai = [];
185
233
  for (const item of entry.ai) {
186
- if (!record2(item) || !exact(item, ["source", "layout", "headline", "points"]) || !text2(item.source, 4e3) || !PRESENTATION_LAYOUTS.includes(item.layout) || !text2(item.headline, 110) || !Array.isArray(item.points) || item.points.length > 4 || !item.points.every((point) => text2(point, 180)) || ["list", "steps"].includes(String(item.layout)) && item.points.length < 2 || item.layout === "comparison" && item.points.length !== 2)
234
+ if (!record2(item) || !exact(item, ["source", "layout", "headline", "points"]) || !text2(item.source, 4e3) || !PRESENTATION_LAYOUTS.includes(item.layout) || !text2(item.headline, 110) || !Array.isArray(item.points) || item.points.length > 4 || !item.points.every((point) => text2(point, 180)) || !presentationLayoutFits(
235
+ item.layout,
236
+ item.headline,
237
+ item.points,
238
+ item.source
239
+ ))
187
240
  return null;
188
241
  hint.ai.push({
189
242
  source: item.source,
@@ -895,15 +948,17 @@ function extractFeatureContents(contents, keepData = false) {
895
948
  function withoutProse(record3) {
896
949
  return record3 && Object.fromEntries(Object.entries(record3).filter(([key]) => !PROSE_KEYS.has(key)));
897
950
  }
898
- function typedMediaContents(block) {
951
+ function typedMediaContents(block, bodyMedia) {
899
952
  const record3 = {
900
953
  ...block,
901
954
  ...block.templateData,
902
955
  ...coerceTemplateParams(block.template ?? "", block.templateOverrides ?? {}).input
903
956
  };
904
957
  const contents = [];
958
+ const seen = new Set(extractImages([...bodyMedia]).map((image) => image.src));
905
959
  const addImage = (src, alt) => {
906
- if (typeof src === "string" && src) {
960
+ if (typeof src === "string" && src && !seen.has(src)) {
961
+ seen.add(src);
907
962
  contents.push({
908
963
  type: "paragraph",
909
964
  children: [{ type: "image", url: src, alt: typeof alt === "string" ? alt : "" }]
@@ -957,7 +1012,7 @@ function summarizeBlock(block, mode) {
957
1012
  const contents = [
958
1013
  ...brief && !nativeFeature && !tableInputs ? [{ type: "paragraph", children: [{ type: "text", value: brief }] }] : [],
959
1014
  ...featureContents,
960
- ...typedMediaContents(block)
1015
+ ...typedMediaContents(block, featureContents)
961
1016
  ];
962
1017
  const base = {
963
1018
  id: block.id,
@@ -1004,10 +1059,11 @@ function summarizeBlock(block, mode) {
1004
1059
  };
1005
1060
  }
1006
1061
  function summarizeHighlight(block, source) {
1062
+ const features = extractFeatureContents(source.contents);
1007
1063
  const summary = {
1008
1064
  ...block,
1009
1065
  summaryMode: "brief",
1010
- contents: [...extractFeatureContents(source.contents), ...typedMediaContents(source)],
1066
+ contents: [...features, ...typedMediaContents(source, features)],
1011
1067
  media: source.media
1012
1068
  };
1013
1069
  if (summary.template === "quote") return { ...summary, quote: briefText(summary.quote) };
@@ -1798,15 +1854,33 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1798
1854
  );
1799
1855
  const beats = [];
1800
1856
  const supportingOpeners = /* @__PURE__ */ new Set();
1801
- const wordLimit = { concise: 28, balanced: 42, detailed: 64 }[hints.density];
1857
+ const quotes = /* @__PURE__ */ new Map();
1858
+ const perSection = hints.density === "section";
1859
+ const wordLimit = { concise: 28, balanced: 42, detailed: 64, section: Infinity }[hints.density];
1802
1860
  for (const block of script.blocks) {
1803
1861
  const source = script.sourceText.slice(block.charStart, block.charEnd);
1804
1862
  const words = [...source.matchAll(/\S+/gu)];
1805
1863
  const hint = hints.blocks.find((item) => item.blockId === block.blockId);
1806
1864
  const wording = hint?.wording?.source === source ? hint.wording : void 0;
1807
- const inbetweens = hint?.inbetweens ?? hints.inbetweens;
1865
+ const inbetweens = !perSection && (hint?.inbetweens ?? hints.inbetweens);
1808
1866
  const heading = block.heading?.trim();
1809
1867
  const headingEnd = block.charStart + (heading?.length ?? 0);
1868
+ if (hint?.layout === AUTHORED_PRESENTATION_LAYOUT) {
1869
+ if (!words.length) continue;
1870
+ const start = block.charStart + words[0].index;
1871
+ const last = words[words.length - 1];
1872
+ const end = block.charStart + last.index + last[0].length;
1873
+ beats.push({
1874
+ id: `beat-${beats.length + 1}`,
1875
+ sourceStart: start,
1876
+ sourceEnd: end,
1877
+ layout: AUTHORED_PRESENTATION_LAYOUT,
1878
+ headline: headlineText(heading || sentencesIn(script.sourceText, start, end)[0].text),
1879
+ points: []
1880
+ });
1881
+ continue;
1882
+ }
1883
+ const preferred = hint?.layout;
1810
1884
  const chunks = chunkWords(words, wordLimit);
1811
1885
  const images = sectionImages.get(block.blockId) ?? [];
1812
1886
  const opensDeck = beats.length === 0;
@@ -1850,25 +1924,30 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1850
1924
  const custom = chunk === 0 ? wording : void 0;
1851
1925
  if (custom?.headline) headline = custom.headline;
1852
1926
  const pointsFor = (layout2) => custom?.points ?? (layout2 === "steps" ? stepPoints : textPoints);
1927
+ const quote = custom?.headline ? void 0 : body.find((sentence) => sentence.text.length <= 110)?.text;
1928
+ const headlineFor = (layout2) => layout2 === "quote" && quote ? quote : headline;
1853
1929
  let layout = index === 0 ? "title" : textPoints.length >= 2 && index % 3 === 1 ? "list" : "statement";
1854
1930
  if (sequence) layout = "steps";
1855
1931
  else if (textPoints.length === 2 && /\b(versus|whereas|compared|instead)\b/iu.test(passage))
1856
1932
  layout = "comparison";
1857
1933
  if (image) layout = "image";
1858
- if (hint?.layout) {
1859
- const count = pointsFor(hint.layout).length;
1860
- const eligible = hint.layout === "image" ? !!image : hint.layout === "comparison" ? count === 2 : ["steps", "list"].includes(hint.layout) ? count >= 2 : true;
1861
- if (eligible) layout = hint.layout;
1862
- }
1934
+ if (preferred && (preferred === "image" ? !!image : presentationLayoutFits(
1935
+ preferred,
1936
+ headlineFor(preferred),
1937
+ pointsFor(preferred),
1938
+ passage
1939
+ )))
1940
+ layout = preferred;
1863
1941
  const beat = {
1864
1942
  id: `beat-${index + 1}`,
1865
1943
  sourceStart: start,
1866
1944
  sourceEnd: end,
1867
1945
  layout,
1868
- headline,
1946
+ headline: headlineFor(layout),
1869
1947
  points: pointsFor(layout),
1870
1948
  ...layout === "image" && image ? { imageSrc: image.src } : {}
1871
1949
  };
1950
+ if (quote) quotes.set(beat.id, quote);
1872
1951
  let split;
1873
1952
  if (inbetweens && !custom?.points && layout === "steps" && sequence) {
1874
1953
  if (shown.some((sentence) => sentence.end <= steps[0].start)) split = steps[0].start;
@@ -1909,6 +1988,15 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1909
1988
  throw new Error(
1910
1989
  "This document has too many sections for one presentation. Split it into shorter documents."
1911
1990
  );
1991
+ for (const block of script.blocks) {
1992
+ const hidden = hints.blocks.find((item) => item.blockId === block.blockId)?.hidden;
1993
+ if (!hidden) continue;
1994
+ for (const beat of beats)
1995
+ if (beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd && hidden.includes(
1996
+ presentationPassageKey(script.sourceText.slice(beat.sourceStart, beat.sourceEnd))
1997
+ ))
1998
+ beat.hidden = true;
1999
+ }
1912
2000
  const sectionMedia = collectPresentationMedia(
1913
2001
  doc,
1914
2002
  script.blocks.map((block) => block.blockId)
@@ -1917,8 +2005,9 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1917
2005
  const items = sectionMedia.get(block.blockId);
1918
2006
  if (!items?.length) continue;
1919
2007
  const own = beats.filter(
1920
- (beat) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd
2008
+ (beat) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd && beat.layout !== AUTHORED_PRESENTATION_LAYOUT && !beat.hidden
1921
2009
  );
2010
+ if (!own.length) continue;
1922
2011
  const open = own.filter((beat) => beat !== beats[0] && beat.layout !== "image");
1923
2012
  const slots = open.length ? open : own;
1924
2013
  const shown = /* @__PURE__ */ new Set();
@@ -1939,27 +2028,33 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1939
2028
  closing.layout = "title";
1940
2029
  let origin = "automatic";
1941
2030
  for (const beat of beats) {
2031
+ if (beat.layout === AUTHORED_PRESENTATION_LAYOUT) continue;
1942
2032
  const section = script.blocks.find(
1943
2033
  (block) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd
1944
2034
  );
1945
2035
  const hint = hints.blocks.find((item) => item.blockId === section?.blockId);
1946
- const cached = hint?.ai?.find(
1947
- (item) => item.source === script.sourceText.slice(beat.sourceStart, beat.sourceEnd)
1948
- );
2036
+ const passage = script.sourceText.slice(beat.sourceStart, beat.sourceEnd);
2037
+ const cached = hint?.ai?.find((item) => item.source === passage);
1949
2038
  if (cached && (cached.layout !== "image" || beat.imageSrc)) {
1950
2039
  beat.headline = cached.headline;
1951
2040
  beat.points = [...cached.points];
1952
2041
  beat.layout = cached.layout;
1953
2042
  origin = "ai";
1954
2043
  }
1955
- if (hint?.layout && !supportingOpeners.has(beat.id) && (hint.layout !== "image" || beat.imageSrc) && (hint.layout !== "comparison" || beat.points.length === 2) && (!["steps", "list"].includes(hint.layout) || beat.points.length >= 2))
1956
- beat.layout = hint.layout;
2044
+ const preferred = hint?.layout === AUTHORED_PRESENTATION_LAYOUT ? void 0 : hint?.layout;
2045
+ if (preferred && !supportingOpeners.has(beat.id)) {
2046
+ const headline = preferred === "quote" && quotes.get(beat.id) || beat.headline;
2047
+ if (preferred === "image" ? !!beat.imageSrc : presentationLayoutFits(preferred, headline, beat.points, passage)) {
2048
+ beat.layout = preferred;
2049
+ beat.headline = headline;
2050
+ }
2051
+ }
1957
2052
  if (section && beat.sourceStart === section.charStart && hint?.wording?.source === script.sourceText.slice(section.charStart, section.charEnd)) {
1958
2053
  beat.headline = hint.wording.headline ?? beat.headline;
1959
2054
  beat.points = hint.wording.points ?? beat.points;
1960
2055
  }
1961
- if (beat.media && beat.layout === "steps" && hint?.layout !== "steps") beat.layout = "list";
1962
- if (["steps", "list", "comparison"].includes(beat.layout) && (beat.points.length < 2 || beat.layout === "comparison" && beat.points.length !== 2))
2056
+ if (beat.media && beat.layout === "steps" && preferred !== "steps") beat.layout = "list";
2057
+ if (beat.layout !== "image" && !presentationLayoutFits(beat.layout, beat.headline, beat.points, passage))
1963
2058
  beat.layout = "statement";
1964
2059
  if (beat.layout !== "image") delete beat.imageSrc;
1965
2060
  }
@@ -2013,11 +2108,35 @@ function compilePresentationPlan(doc, plan) {
2013
2108
  const bookmarks = (narration?.bookmarks ?? []).filter(
2014
2109
  (word) => Number.isFinite(word.time) && word.time >= 0 && word.time <= duration && Number.isInteger(word.charOffset) && word.charOffset >= 0 && word.charOffset < script.sourceText.length
2015
2110
  ).sort((a, b) => a.charOffset - b.charOffset);
2111
+ const marks = (narration?.slides ?? []).filter(
2112
+ (mark) => Number.isFinite(mark.time) && mark.time >= 0 && mark.time <= duration && Number.isInteger(mark.charOffset) && mark.charOffset >= 0 && mark.charOffset < script.sourceText.length
2113
+ );
2114
+ const tokensBefore = (offset) => script.tokens.filter((token) => token.charOffset < offset).length;
2115
+ const markedTime = (offset) => {
2116
+ if (!narration || !marks.length) return void 0;
2117
+ const exact2 = marks.find((mark) => mark.charOffset === offset);
2118
+ if (exact2) return exact2.time;
2119
+ if (bookmarks.length) return void 0;
2120
+ const anchors = [
2121
+ { charOffset: 0, time: narration.startTime },
2122
+ ...[...marks].sort((a, b) => a.charOffset - b.charOffset),
2123
+ { charOffset: script.sourceText.length, time: duration }
2124
+ ];
2125
+ const after = anchors.findIndex((anchor) => anchor.charOffset > offset);
2126
+ const from = anchors[Math.max(0, after - 1)];
2127
+ const to = anchors[after] ?? from;
2128
+ const span = tokensBefore(to.charOffset) - tokensBefore(from.charOffset);
2129
+ const share = span > 0 ? (tokensBefore(offset) - tokensBefore(from.charOffset)) / span : 0;
2130
+ return from.time + share * Math.max(0, to.time - from.time);
2131
+ };
2016
2132
  let previous = 0;
2017
2133
  const starts = checked.beats.map((beat, index) => {
2018
2134
  if (!index) return 0;
2019
2135
  let time;
2020
- if (narration && bookmarks.length) {
2136
+ const marked = markedTime(beat.sourceStart);
2137
+ if (marked !== void 0) {
2138
+ time = marked;
2139
+ } else if (narration && bookmarks.length) {
2021
2140
  time = bookmarks.find((word) => word.charOffset >= beat.sourceStart)?.time ?? duration;
2022
2141
  } else {
2023
2142
  const before = script.tokens.filter((token) => token.charOffset < beat.sourceStart).length;
@@ -2036,13 +2155,44 @@ function compilePresentationPlan(doc, plan) {
2036
2155
  const section = sourceOf(beat)?.blockId;
2037
2156
  if (section) sectionEnds.set(section, starts[index + 1] ?? duration);
2038
2157
  });
2158
+ const shown = checked.beats.flatMap((beat, index) => beat.hidden ? [] : [index]);
2159
+ if (!shown.length) throw new Error("Every slide is hidden. Show at least one slide.");
2160
+ const windows = shown.map((index, position) => ({
2161
+ index,
2162
+ start: position ? starts[index] : 0,
2163
+ end: position + 1 < shown.length ? starts[shown[position + 1]] : duration
2164
+ })).filter((window) => window.end > window.start);
2165
+ const onScreen = (time) => windows.reduce((found, window) => window.start <= time ? window : found, windows[0]);
2039
2166
  const clipsPlaced = /* @__PURE__ */ new Set();
2040
- const mediaStarts = /* @__PURE__ */ new Map();
2041
- const blocks = [];
2167
+ const sectionClips = /* @__PURE__ */ new Map();
2042
2168
  checked.beats.forEach((beat, index) => {
2043
2169
  const start = starts[index];
2044
2170
  const end = starts[index + 1] ?? duration;
2045
- if (end <= start) return;
2171
+ const source = sourceOf(beat);
2172
+ if (end <= start || !source || clipsPlaced.has(source.blockId) || !windows.length) return;
2173
+ clipsPlaced.add(source.blockId);
2174
+ const sectionEnd = sectionEnds.get(source.blockId) ?? end;
2175
+ const host = onScreen(start);
2176
+ const shift = (clip2) => start === host.start ? clip2 : { ...clip2, startAt: clip2.startAt + start - host.start };
2177
+ const clips = (authoredBlocks.find((block) => block.id === source.blockId)?.media ?? []).flatMap((clip2) => {
2178
+ if (clip2.spillover) return [shift(clip2)];
2179
+ const room = sectionEnd - start - clip2.startAt;
2180
+ if (room <= 0) return [];
2181
+ const clipStart = clip2.clipStart ?? 0;
2182
+ return [
2183
+ shift({
2184
+ ...clip2,
2185
+ spillover: true,
2186
+ clipEnd: Math.min(clip2.clipEnd ?? Infinity, clipStart + room)
2187
+ })
2188
+ ];
2189
+ });
2190
+ sectionClips.set(host.index, [...sectionClips.get(host.index) ?? [], ...clips]);
2191
+ });
2192
+ const mediaStarts = /* @__PURE__ */ new Map();
2193
+ const blocks = [];
2194
+ for (const { index, start, end } of windows) {
2195
+ const beat = checked.beats[index];
2046
2196
  const source = sourceOf(beat);
2047
2197
  const build = buildHints?.blocks.find((hint) => hint.blockId === source?.blockId)?.build ?? authoredBlocks.find((block2) => block2.id === source?.blockId)?.build;
2048
2198
  const contents = (beat.media ?? []).flatMap((ref) => {
@@ -2051,26 +2201,8 @@ function compilePresentationPlan(doc, plan) {
2051
2201
  const node = presentationMediaNode(media.get(ref), start - first);
2052
2202
  return node ? [node] : [];
2053
2203
  });
2054
- let clips = [];
2055
- if (source && !clipsPlaced.has(source.blockId)) {
2056
- clipsPlaced.add(source.blockId);
2057
- const sectionEnd = sectionEnds.get(source.blockId) ?? end;
2058
- clips = (authoredBlocks.find((block2) => block2.id === source.blockId)?.media ?? []).flatMap(
2059
- (clip2) => {
2060
- if (clip2.spillover) return [clip2];
2061
- const room = sectionEnd - start - clip2.startAt;
2062
- if (room <= 0) return [];
2063
- const clipStart = clip2.clipStart ?? 0;
2064
- return [
2065
- {
2066
- ...clip2,
2067
- spillover: true,
2068
- clipEnd: Math.min(clip2.clipEnd ?? Infinity, clipStart + room)
2069
- }
2070
- ];
2071
- }
2072
- );
2073
- }
2204
+ const authored = authoredBlocks.find((block2) => block2.id === source?.blockId);
2205
+ const clips = sectionClips.get(index) ?? [];
2074
2206
  const base = {
2075
2207
  ...build ? { build } : {},
2076
2208
  ...contents.length ? { contents } : {},
@@ -2086,9 +2218,20 @@ function compilePresentationPlan(doc, plan) {
2086
2218
  sourceCharOffset: beat.sourceStart,
2087
2219
  transition: { type: "fade", duration: Math.min(0.3, (end - start) / 5) }
2088
2220
  };
2089
- const block = visualBlock(beat, base, index);
2221
+ if (beat.layout === AUTHORED_PRESENTATION_LAYOUT) {
2222
+ if (!authored) throw new Error("A section shown as authored is no longer in the document.");
2223
+ const { children, media: _media, ...own } = authored;
2224
+ blocks.push({
2225
+ ...own,
2226
+ ...base,
2227
+ ...children && isContainerTemplate(authored.template) ? { children } : {},
2228
+ transition: authored.transition ?? base.transition
2229
+ });
2230
+ continue;
2231
+ }
2232
+ const block = visualBlock(beat, beat.layout, base, index);
2090
2233
  blocks.push(block);
2091
- });
2234
+ }
2092
2235
  return {
2093
2236
  ...doc,
2094
2237
  blocks,
@@ -2099,11 +2242,37 @@ function compilePresentationPlan(doc, plan) {
2099
2242
  audio: doc.audio.segments.some((segment) => segment.src) ? doc.audio : { segments: [{ src: "", name: "presentation-clock", startTime: 0, duration }] }
2100
2243
  };
2101
2244
  }
2102
- function visualBlock(beat, base, index) {
2245
+ function visualBlock(beat, layout, base, index) {
2103
2246
  const colorScheme = ["blue", "green", "orange"][index % 3];
2104
- switch (beat.layout) {
2247
+ switch (layout) {
2105
2248
  case "title":
2106
2249
  return { ...base, template: "title", title: beat.headline, subtitle: beat.points[0] ?? "" };
2250
+ case "divider":
2251
+ return { ...base, template: "sectionHeader", title: beat.headline, colorScheme };
2252
+ case "emphasis":
2253
+ return { ...base, template: "bigText", title: beat.headline };
2254
+ case "quote":
2255
+ return { ...base, template: "fullBleedQuote", text: beat.headline, colorScheme };
2256
+ case "stat": {
2257
+ 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);
2259
+ return {
2260
+ ...base,
2261
+ template: "statHighlight",
2262
+ stat: match.value,
2263
+ description: line,
2264
+ ...detail ? { detail } : {},
2265
+ colorScheme
2266
+ };
2267
+ }
2268
+ case "definition":
2269
+ return {
2270
+ ...base,
2271
+ template: "definitionCard",
2272
+ term: beat.headline,
2273
+ definition: beat.points.join("\n\n"),
2274
+ colorScheme
2275
+ };
2107
2276
  case "statement":
2108
2277
  return {
2109
2278
  ...base,
@@ -2188,11 +2357,15 @@ function applySavedPresentation(doc) {
2188
2357
  export {
2189
2358
  PRESENTATION_KEY,
2190
2359
  PRESENTATION_LAYOUTS,
2360
+ AUTHORED_PRESENTATION_LAYOUT,
2361
+ presentationStat,
2362
+ presentationLayoutFits,
2191
2363
  PRESENTATION_LIMITS,
2192
2364
  parsePresentationPlan,
2193
2365
  serializePresentationPlan,
2194
2366
  PRESENTATION_HINTS_KEY,
2195
2367
  DYNAMIC_PRESENTATION_STYLE,
2368
+ presentationPassageKey,
2196
2369
  defaultPresentationHints,
2197
2370
  parsePresentationHints,
2198
2371
  serializePresentationHints,
@@ -32,7 +32,7 @@ import {
32
32
  profileBlockContents,
33
33
  renderMarkdownBlocksHtml,
34
34
  treeToTemplateData
35
- } from "./chunk-R2J7CQJZ.js";
35
+ } from "./chunk-DIY3NBIS.js";
36
36
  import {
37
37
  anchorPoint,
38
38
  base64ToUtf8,
@@ -3181,10 +3181,10 @@ function rightFeature(input, context) {
3181
3181
 
3182
3182
  // src/doc/templates/mediaFeatureBlock.ts
3183
3183
  function topFeature(input, context) {
3184
- return contentBlock({ ...input, template: "content" }, context, { omitPictures: true });
3184
+ return contentBlock({ ...input, template: "content" }, context);
3185
3185
  }
3186
3186
  function bottomFeature(input, context) {
3187
- return contentBlock({ ...input, template: "content" }, context, { omitPictures: true });
3187
+ return contentBlock({ ...input, template: "content" }, context);
3188
3188
  }
3189
3189
  function mediaOnly(input, context) {
3190
3190
  return contentBlock({ ...input, template: "content" }, context);
@@ -4444,11 +4444,14 @@ function dataTable(input, context) {
4444
4444
  const titleFontSize = themedFontSize(48, context, true);
4445
4445
  const tableFontSize = themedFontSize(28, context, false);
4446
4446
  const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
4447
- const rowCount = Math.max(
4448
- 1,
4449
- Math.min(resolvedRows.length, MAX_VISIBLE_ROWS) + (resolvedHeaders.length > 0 ? 1 : 0)
4447
+ const visibleRows = Math.min(resolvedRows.length, MAX_VISIBLE_ROWS);
4448
+ const hasHeader = resolvedHeaders.length > 0;
4449
+ const rowCount = Math.max(1, visibleRows + (hasHeader ? 1 : 0));
4450
+ const ruleHeight = (hasHeader ? 2 : 0) + Math.max(0, visibleRows - 1) + 2 + 1;
4451
+ const naturalTableHPct = Math.min(
4452
+ 74,
4453
+ (rowCount * tableFontSize * 2.4 + ruleHeight) / viewport.height * 100
4450
4454
  );
4451
- const naturalTableHPct = Math.min(74, rowCount * tableFontSize * 2.4 / viewport.height * 100);
4452
4455
  const titleBandPct = title ? titleFontSize * 2.2 * 100 / viewport.height : 0;
4453
4456
  const groupTopPct = Math.max(8, (100 - titleBandPct - naturalTableHPct) / 2);
4454
4457
  if (title) {
@@ -12445,13 +12448,15 @@ function resolveDocBuildTiming(doc, blocks) {
12445
12448
  }
12446
12449
 
12447
12450
  // src/doc/templates/contentBlock.ts
12448
- function bodyNodes(context, omitPictures) {
12451
+ function bodyNodes(context) {
12449
12452
  if (context.block?.summaryMode === "headings-and-features") return [];
12450
12453
  return (context.block?.contents ?? []).filter(
12451
- // Mermaid source and hosted-video links are already materialized as
12452
- // visual layers (a diagram, a player). Repeating them as prose makes the
12453
- // loss-averse default noisy without preserving any additional meaning.
12454
- (node) => !isMaterializedMediaNode(node) && !(omitPictures && isPictureParagraph(node))
12454
+ // Mermaid source, hosted-video links and picture-only paragraphs are
12455
+ // already materialized as visual layers (a diagram, a player, the picture
12456
+ // itself). Repeating them as prose makes the loss-averse default noisy
12457
+ // without preserving any additional meaning — a pasted picture's alt text
12458
+ // is just its generated file name.
12459
+ (node) => !isMaterializedMediaNode(node) && !isPictureParagraph(node)
12455
12460
  );
12456
12461
  }
12457
12462
  function listParagraphs(list, level = 0) {
@@ -12496,10 +12501,10 @@ function bodyFontSize(body, context) {
12496
12501
  const base = body.length > 1800 ? 18 : body.length > 1100 ? 21 : body.length > 650 ? 24 : 28;
12497
12502
  return themedFontSize(base, context, false);
12498
12503
  }
12499
- function contentBlock(input, context, options = {}) {
12504
+ function contentBlock(input, context) {
12500
12505
  const { theme } = context;
12501
12506
  const title = input.title ?? context.block?.title ?? "";
12502
- const markdown = bodyNodes(context, options.omitPictures === true);
12507
+ const markdown = bodyNodes(context);
12503
12508
  const paragraphs = completeBodyParagraphs(markdown);
12504
12509
  const body = paragraphs.map(plainParagraphText).join("\n");
12505
12510
  const builds = planContentBuilds(markdown, input.build ?? context.block?.build);