@bendyline/squisq 2.13.12 → 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.
@@ -5900,6 +5900,12 @@ function formatChartValue(value, unit) {
5900
5900
  }
5901
5901
  return unit ? `${text} ${unit}` : text;
5902
5902
  }
5903
+ function formatChartDataValue(value, unit) {
5904
+ if (!Number.isFinite(value) || Math.abs(value) >= 1e4) return formatChartValue(value, unit);
5905
+ const sign = value < 0 ? "-" : "";
5906
+ const text = `${sign}${Math.abs(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5907
+ return unit ? `${text} ${unit}` : text;
5908
+ }
5903
5909
  function trimTrailingZero(text) {
5904
5910
  return text.replace(/\.0$/, "");
5905
5911
  }
@@ -6425,7 +6431,7 @@ function paintBars(paint) {
6425
6431
  } = paint;
6426
6432
  const scale = valueTicks("bar", data, input);
6427
6433
  const labelGap = axisFontSize * 0.6;
6428
- const labelWidthPx = (value) => formatChartValue(value, input.unit).length * axisFontSize * CHART_CHAR_WIDTH_EM;
6434
+ const labelWidthPx = (value) => formatChartDataValue(value, input.unit).length * axisFontSize * CHART_CHAR_WIDTH_EM;
6429
6435
  const showValueLabels = input.showValues === true && !(input.stacked === true);
6430
6436
  const reservedForLabels = showValueLabels ? data.series.flatMap((series) => series.values).filter((v) => v !== null).reduce((max, v) => Math.max(max, labelWidthPx(v)), 0) + labelGap : 0;
6431
6437
  const axisWidth = Math.max(20, frame2.plot.width - reservedForLabels);
@@ -6768,7 +6774,7 @@ function valueLabel(paint, id, value, x, y, animation) {
6768
6774
  type: "text",
6769
6775
  id,
6770
6776
  content: {
6771
- text: formatChartValue(value, paint.input.unit),
6777
+ text: formatChartDataValue(value, paint.input.unit),
6772
6778
  style: {
6773
6779
  fontSize: paint.axisFontSize,
6774
6780
  fontFamily: paint.bodyFont,
@@ -11954,6 +11960,177 @@ var TEMPLATE_AUTHORING_METADATA = {
11954
11960
  }
11955
11961
  };
11956
11962
 
11963
+ // src/doc/templates/authoringGuide.ts
11964
+ var CHART = {
11965
+ fields: "title, headers [label, value], rows [[label, value], \u2026] 2\u20136 rows, showValues: true",
11966
+ use: "only values the {source} states"
11967
+ };
11968
+ var CONCEPT_BUILD = 'build: {mode: "items"} to reveal them as they are spoken';
11969
+ var TEMPLATE_GUIDES = {
11970
+ imageWithCaption: {
11971
+ fields: "imageSrc, imageAlt, caption \u2264 90 chars, ambientMotion: zoomIn|zoomOut|panLeft|panRight",
11972
+ use: "a photograph with the narration's idea as caption"
11973
+ },
11974
+ pullQuote: {
11975
+ fields: "text, backgroundImage {src, alt}, ambientMotion",
11976
+ use: "a VERBATIM sentence or clause from the {source} over a photograph"
11977
+ },
11978
+ fullBleedQuote: {
11979
+ fields: "text",
11980
+ use: "a VERBATIM short sentence from the {source}, text only"
11981
+ },
11982
+ factCard: {
11983
+ fields: "fact \u2264 4 words, explanation \u2264 160 chars, accentImage {src, alt, position: left-strip|right-strip}"
11984
+ },
11985
+ dateEvent: {
11986
+ fields: "date, description \u2264 160 chars, mood: neutral|somber|celebratory, accentImage optional",
11987
+ use: "a year or period the {source} states"
11988
+ },
11989
+ statHighlight: {
11990
+ fields: 'stat like "98%" or "36", description \u2264 60 chars, detail \u2264 80 chars, accentImage optional',
11991
+ use: "counts up; the number must be in the {source}"
11992
+ },
11993
+ comparisonBar: {
11994
+ fields: "leftLabel, leftValue, rightLabel, rightValue, unit optional",
11995
+ use: "two quantities the {source} gives in the same unit"
11996
+ },
11997
+ columnChart: CHART,
11998
+ barChart: CHART,
11999
+ pieChart: CHART,
12000
+ timeline: {
12001
+ fields: "title, tracks: [{ id, label, events: [{ id, label: year, position: 0..1 in date order, description \u2264 40 chars, marker: 'diamond' optional }] }]",
12002
+ use: "3\u20137 dated events the {source} states"
12003
+ },
12004
+ diagram: {
12005
+ fields: "title, nodeShape: 'pill', nodes: [{ id, label \u2264 3 words, x, y }] 3\u20136 nodes with x in {0, 280, 560} and y in {0, 160, 320}, edges: [{ source, target, label \u2264 3 words }]",
12006
+ use: "relationships the {source} describes"
12007
+ },
12008
+ list: {
12009
+ fields: "title, items: 2\u20135 short items taken from the {source}'s own words"
12010
+ },
12011
+ photoGrid: {
12012
+ fields: "images: [{src, alt}] \xD7 2\u20134, caption \u2264 90 chars",
12013
+ use: "several related photographs"
12014
+ },
12015
+ map: {
12016
+ fields: "center: {lat, lng}, zoom, mapStyle: 'terrain', title, ambientMotion",
12017
+ use: "where the place is, at its first mention"
12018
+ },
12019
+ sectionHeader: {
12020
+ fields: "title = the section title, imageSrc, imageAlt, ambientMotion",
12021
+ use: "opens a section on a photograph"
12022
+ },
12023
+ cards: {
12024
+ fields: `title, items: ["Label: detail", \u2026] 2\u20134, versus: true for exactly two set against each other, ${CONCEPT_BUILD}`,
12025
+ use: "parallel ideas side by side, such as kinds, causes or features"
12026
+ },
12027
+ process: {
12028
+ fields: `title, items: ["Label: detail", \u2026] 2\u20134 in order, ${CONCEPT_BUILD}`,
12029
+ use: "ordered stages or dated events, drawn as numbered steps joined by arrows"
12030
+ },
12031
+ cycle: {
12032
+ fields: `title, items: ["Label: detail", \u2026] 3\u20135, ${CONCEPT_BUILD}`,
12033
+ use: "stages that repeat in a loop, drawn on a ring"
12034
+ },
12035
+ layers: {
12036
+ fields: `title, items: ["Label: detail", \u2026] 3\u20135 innermost first, ${CONCEPT_BUILD}`,
12037
+ use: "parts nested inside one another, drawn as concentric rings"
12038
+ }
12039
+ };
12040
+ function guideText(text, wording = {}) {
12041
+ return text.replace(/\{source\}/gu, wording.source ?? "source");
12042
+ }
12043
+ function templateCatalog(entries, wording = {}) {
12044
+ return entries.map((entry) => {
12045
+ const [name, guide] = typeof entry === "string" ? [entry, TEMPLATE_GUIDES[entry]] : "templates" in entry ? [entry.templates.join(" | "), TEMPLATE_GUIDES[entry.templates[0]]] : [entry.template, entry];
12046
+ const use = guide.use ? ` \u2014 ${guide.use}` : "";
12047
+ return guideText(`- ${name} (${guide.fields})${use}`, wording);
12048
+ });
12049
+ }
12050
+ var PRESENTATION_LAYOUT_GUIDE = {
12051
+ title: "an opening",
12052
+ divider: "a new topic begins; only the headline shows",
12053
+ statement: "one idea",
12054
+ emphasis: "one striking takeaway; only the headline shows, so keep it under 60 characters",
12055
+ quote: "the headline is one sentence copied word for word from the passage, without quotation marks; only the headline shows",
12056
+ stat: "the passage\u2019s key figure, such as 40% or $3 million, stated in the headline or a point",
12057
+ definition: "the headline is a term and the points define it",
12058
+ list: "2\u20133 points",
12059
+ comparison: "exactly 2 genuinely contrasting points",
12060
+ steps: "2\u20133 ordered actions",
12061
+ image: 'only when the current layout is "image"; the headline is then its caption',
12062
+ cards: "2\u20134 parallel ideas side by side, such as kinds, causes or features",
12063
+ process: "2\u20134 ordered stages or dated events, drawn as numbered steps joined by arrows",
12064
+ cycle: "3\u20135 stages that repeat in a loop, drawn on a ring",
12065
+ layers: "3\u20135 parts nested inside one another, innermost first, such as a core and the layers around it, drawn as concentric rings"
12066
+ };
12067
+ var DRAWN = ["cards", "process", "cycle", "layers"];
12068
+ var NUMBERS = ["statHighlight", "comparisonBar", "columnChart", "barChart", "pieChart"];
12069
+ var ACCENTED = [
12070
+ "factCard",
12071
+ "statHighlight",
12072
+ "dateEvent",
12073
+ "quote",
12074
+ "list",
12075
+ "definitionCard"
12076
+ ];
12077
+ var AUTHORING_TRICKS = [
12078
+ {
12079
+ id: "one-idea",
12080
+ text: "One idea per slide: each slide shows what the sentence being spoken is about. Captions and labels restate its idea briefly; they never add a fact."
12081
+ },
12082
+ {
12083
+ id: "numbers-as-numbers",
12084
+ text: "Draw numbers as numbers: one striking quantity \u2192 statHighlight (it counts up); two quantities in one unit \u2192 comparisonBar; three or more values in one unit \u2192 a chart; three to seven dated events \u2192 timeline.",
12085
+ templates: [...NUMBERS, "timeline"]
12086
+ },
12087
+ {
12088
+ id: "ranges",
12089
+ text: 'A range ("3 to 7 miles deep") is not one value: never chart it as a single bar or count up to one end. Write it as the {source} gives it, on a card or in a list.',
12090
+ templates: NUMBERS
12091
+ },
12092
+ {
12093
+ id: "draw-ideas",
12094
+ text: `Slides that draw their ideas explain best: several parallel ideas \u2192 cards, ordered stages \u2192 process, a loop that comes back to its start \u2192 cycle, parts nested inside one another \u2192 layers; prefer them to a list. Write each item as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters, in the {source}'s own words. For dated events, start each label with its date: a process of dated stages draws as a timeline.`,
12095
+ summary: `Slides that draw their ideas explain best: when the passage has several parallel ideas, ordered stages, a repeating loop or nested parts, choose ${DRAWN.map((layout) => `"${layout}"`).join(", ")} rather than "statement" or "list", and write each of their points (up to 4) as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters. For dated events, start each label with its date.`,
12096
+ templates: DRAWN
12097
+ },
12098
+ {
12099
+ id: "reveal-as-spoken",
12100
+ text: 'A drawn slide can reveal its items one at a time as the narration says them (build {mode: "items"}). Put the items in the order they are spoken and let each share its key words with the sentence that says it, so every reveal lands on its words.',
12101
+ templates: DRAWN
12102
+ },
12103
+ {
12104
+ id: "chain-not-diagram",
12105
+ text: "A chain \u2014 one thing leads to the next, which leads to the next \u2014 is a process, not a diagram. Keep diagrams for relationships that branch or meet: causes on the left and effects on the right, every edge labelled with the verb the {source} uses, and no edges that cross.",
12106
+ templates: ["diagram", "process"]
12107
+ },
12108
+ {
12109
+ id: "no-empty-frames",
12110
+ text: "A text card on its own is small type on an empty field. Give each factCard, statHighlight, dateEvent, quote, list and definitionCard an accentImage showing what its sentence is about, and alternate the side from card to card.",
12111
+ templates: ACCENTED
12112
+ },
12113
+ {
12114
+ id: "motion-follows-picture",
12115
+ text: "Motion comes from the theme's motion profile; never hand-author animations. On a photograph, choose the ambientMotion the picture suggests \u2014 zoomIn toward a detail, panLeft or panRight along a skyline or coast \u2014 and alternate directions between neighbouring slides.",
12116
+ templates: ["imageWithCaption", "pullQuote", "sectionHeader", "map"]
12117
+ },
12118
+ {
12119
+ id: "vary",
12120
+ text: "Vary the slides: when two templates fit equally well, choose the one not just shown. A long run of photo-and-caption slides is where a narrated story feels static."
12121
+ }
12122
+ ];
12123
+ function authoringTricks(options = {}) {
12124
+ const offered = options.templates ? new Set(options.templates) : void 0;
12125
+ const selected = options.ids ? options.ids.flatMap((id) => AUTHORING_TRICKS.filter((trick) => trick.id === id)) : AUTHORING_TRICKS;
12126
+ return selected.filter(
12127
+ (trick) => !offered || !trick.templates || trick.templates.some((template) => offered.has(template))
12128
+ ).flatMap((trick) => {
12129
+ const text = options.surface === "summary" ? trick.summary : trick.text;
12130
+ return text ? [guideText(text, options.wording)] : [];
12131
+ });
12132
+ }
12133
+
11957
12134
  // src/doc/templates/coverBlock.ts
11958
12135
  var COVER_TITLE_BASE_PX = 120;
11959
12136
  var COVER_TITLE_COMFORTABLE_CHARS = 26;
@@ -12566,9 +12743,9 @@ function expandDocBlocks(blocks, options = {}) {
12566
12743
  return expandedBlock;
12567
12744
  });
12568
12745
  }
12569
- const containsBuilds = blocks.some(hasBlockBuilds);
12570
- const splitLongBlocks = requestedSplit && !containsBuilds;
12571
- const mergeShortBlocks = requestedMerge && !containsBuilds;
12746
+ const buildSegments = new Set(
12747
+ blocks.filter(hasBlockBuilds).map((block) => block.audioSegment ?? 0)
12748
+ );
12572
12749
  const blocksBySegment = /* @__PURE__ */ new Map();
12573
12750
  blocks.forEach((block, index) => {
12574
12751
  const segmentIndex = block.audioSegment ?? 0;
@@ -12581,6 +12758,8 @@ function expandDocBlocks(blocks, options = {}) {
12581
12758
  const splitParts = /* @__PURE__ */ new Map();
12582
12759
  for (const [segmentIndex, segmentBlocks] of blocksBySegment) {
12583
12760
  const audioSegment = audioSegments[segmentIndex];
12761
+ const splitLongBlocks = requestedSplit && !buildSegments.has(segmentIndex);
12762
+ const mergeShortBlocks = requestedMerge && !buildSegments.has(segmentIndex);
12584
12763
  if (segmentBlocks.every(({ block }) => block.timelineLocked === true)) {
12585
12764
  for (const { block, originalIndex } of segmentBlocks) {
12586
12765
  const expanded = materializeScheduledBlock(
@@ -12797,7 +12976,9 @@ function expandDocBlocks(blocks, options = {}) {
12797
12976
  }
12798
12977
  expandedBlocks.splice(0, expandedBlocks.length, ...sequenced);
12799
12978
  }
12800
- return mergeShortBlocks ? expandedBlocks.filter((block) => block && block.duration > 0) : expandedBlocks.filter(Boolean);
12979
+ return requestedMerge ? expandedBlocks.filter(
12980
+ (block) => block && (block.duration > 0 || buildSegments.has(block.audioSegment ?? 0))
12981
+ ) : expandedBlocks.filter(Boolean);
12801
12982
  }
12802
12983
  function getAvailableTemplates() {
12803
12984
  return Object.keys(templateRegistry);
@@ -14133,6 +14314,12 @@ export {
14133
14314
  materializeBlockLayersWithRuntime,
14134
14315
  TEMPLATE_METADATA,
14135
14316
  TEMPLATE_AUTHORING_METADATA,
14317
+ TEMPLATE_GUIDES,
14318
+ guideText,
14319
+ templateCatalog,
14320
+ PRESENTATION_LAYOUT_GUIDE,
14321
+ AUTHORING_TRICKS,
14322
+ authoringTricks,
14136
14323
  coverBlock,
14137
14324
  expandCoverBlock,
14138
14325
  expandDocBlocks,
@@ -0,0 +1,125 @@
1
+ // src/transform/grounding.ts
2
+ var NUMBER = /\d[\d,]*(?:\.\d+)?/gu;
3
+ function normalizeGroundingText(text) {
4
+ return text.toLowerCase().replace(/[‘’ʼ`]/gu, "'").replace(/[“”]/gu, '"').replace(/[–—]/gu, "-").replace(/[^a-z0-9'%\s-]/gu, " ").replace(/\s+/gu, " ").trim();
5
+ }
6
+ function numbersIn(value) {
7
+ if (typeof value === "number") return Number.isFinite(value) ? [String(value)] : [];
8
+ return (value.match(NUMBER) ?? []).map((number) => number.replace(/,/gu, ""));
9
+ }
10
+ var UNIT_WORDS = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine"];
11
+ var TEEN_WORDS = [
12
+ "ten",
13
+ "eleven",
14
+ "twelve",
15
+ "thirteen",
16
+ "fourteen",
17
+ "fifteen",
18
+ "sixteen",
19
+ "seventeen",
20
+ "eighteen",
21
+ "nineteen"
22
+ ];
23
+ var TENS_WORDS = ["twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
24
+ var NUMBER_WORD = new RegExp(
25
+ `\\b(?:(${TENS_WORDS.join("|")})(?:[-\\s](${UNIT_WORDS.join("|")}))?|(${[...UNIT_WORDS.slice(1), ...TEEN_WORDS].join("|")}))\\b`,
26
+ "giu"
27
+ );
28
+ function spelledNumbers(source) {
29
+ return [...source.matchAll(NUMBER_WORD)].map((match) => {
30
+ const [, tens, unit, small] = match;
31
+ const value = small ? [...UNIT_WORDS, ...TEEN_WORDS].indexOf(small.toLowerCase()) + 1 : (TENS_WORDS.indexOf(tens.toLowerCase()) + 2) * 10 + (unit ? UNIT_WORDS.indexOf(unit.toLowerCase()) + 1 : 0);
32
+ return { value: String(value), index: match.index };
33
+ });
34
+ }
35
+ function sourceNumbers(source) {
36
+ return /* @__PURE__ */ new Set([...numbersIn(source), ...spelledNumbers(source).map(({ value }) => value)]);
37
+ }
38
+ function ungroundedNumber(values, source) {
39
+ const stated = typeof source === "string" ? sourceNumbers(source) : source;
40
+ const list = Array.isArray(values) ? values : [values];
41
+ for (const value of list) {
42
+ const missing = numbersIn(value).find((number) => !stated.has(number));
43
+ if (missing !== void 0) return missing;
44
+ }
45
+ return void 0;
46
+ }
47
+ function groundingSentences(text) {
48
+ return (text.match(/[^.!?\n]+[.!?]*/gu) ?? []).map((sentence) => sentence.trim()).filter(Boolean);
49
+ }
50
+ function isVerbatimQuote(quote, source, options = {}) {
51
+ const q = normalizeGroundingText(quote);
52
+ if (!q) return false;
53
+ if (normalizeGroundingText(source).includes(q)) return true;
54
+ const words = q.split(" ").filter((word) => word.length > 1);
55
+ if (words.length === 0) return false;
56
+ const minShare = options.minShare ?? 0.9;
57
+ for (const sentence of options.sentences ?? groundingSentences(source)) {
58
+ const present = new Set(normalizeGroundingText(sentence).split(" "));
59
+ const hits = words.filter((word) => present.has(word)).length;
60
+ if (hits / words.length >= minShare) return true;
61
+ }
62
+ return false;
63
+ }
64
+ var COMMON = new Set(
65
+ "about after also because been before being both could does each from have into just like more most much only other over same should some such than that their them then there these they this those through very what when where which while will with would your ours".split(
66
+ " "
67
+ )
68
+ );
69
+ function distinctiveStems(text) {
70
+ return (text.toLowerCase().replace(/[‘’]/gu, "'").replace(/[“”]/gu, '"').replace(/[‐‑]/gu, "-").replace(/[‒–—―]/gu, " ").match(/[\p{L}\p{N}][\p{L}\p{N}'-]*/gu) ?? []).filter((word) => word.length >= 4 && !COMMON.has(word)).map((word) => word.slice(0, 4));
71
+ }
72
+ function isGroundedParaphrase(text, source, options = {}) {
73
+ if (ungroundedNumber(text, source) !== void 0) return false;
74
+ const stems = new Set(distinctiveStems(source));
75
+ const words = distinctiveStems(text);
76
+ const minShare = options.minShare ?? 2 / 3;
77
+ return words.filter((word) => stems.has(word)).length >= Math.ceil(words.length * minShare);
78
+ }
79
+ function restatesHeadline(point, headline) {
80
+ const title = distinctiveStems(headline);
81
+ const words = distinctiveStems(point);
82
+ const shared = words.filter((word) => title.includes(word)).length;
83
+ return words.length > 0 && shared >= words.length * 0.6 || title.length >= 3 && title.every((word) => words.includes(word));
84
+ }
85
+ var RANGE = /(?:\bbetween\s+(\d[\d,]*(?:\.\d+)?)\s+and\s+(\d[\d,]*(?:\.\d+)?))|(?:(\d[\d,]*(?:\.\d+)?)\s*(?:[-–—]|\bto\b)\s*(\d[\d,]*(?:\.\d+)?))/giu;
86
+ function statedRanges(source) {
87
+ return [...source.matchAll(RANGE)].map((match) => ({
88
+ low: (match[1] ?? match[3]).replace(/,/gu, ""),
89
+ high: (match[2] ?? match[4]).replace(/,/gu, ""),
90
+ text: match[0],
91
+ index: match.index
92
+ }));
93
+ }
94
+ function rangeEndOnly(value, source) {
95
+ const [number] = numbersIn(value);
96
+ if (number === void 0) return void 0;
97
+ const ranges = statedRanges(source);
98
+ if (ranges.length === 0) return void 0;
99
+ if (spelledNumbers(source).some((spelled) => spelled.value === number)) return void 0;
100
+ let first;
101
+ for (const match of source.matchAll(NUMBER)) {
102
+ if (match[0].replace(/,/gu, "") !== number) continue;
103
+ const range = ranges.find(
104
+ (candidate) => match.index >= candidate.index && match.index < candidate.index + candidate.text.length
105
+ );
106
+ if (!range) return void 0;
107
+ first ?? (first = range);
108
+ }
109
+ return first;
110
+ }
111
+
112
+ export {
113
+ normalizeGroundingText,
114
+ numbersIn,
115
+ spelledNumbers,
116
+ sourceNumbers,
117
+ ungroundedNumber,
118
+ groundingSentences,
119
+ isVerbatimQuote,
120
+ distinctiveStems,
121
+ isGroundedParaphrase,
122
+ restatesHeadline,
123
+ statedRanges,
124
+ rangeEndOnly
125
+ };
@@ -0,0 +1,26 @@
1
+ // src/timing/wordTiming.ts
2
+ function wordTimeAt(bookmarks, offset, snap = "containing") {
3
+ if (snap === "next") return bookmarks.find((word) => word.charOffset >= offset)?.time;
4
+ let best = bookmarks[0];
5
+ for (const bookmark of bookmarks) {
6
+ if (bookmark.charOffset <= offset) best = bookmark;
7
+ else break;
8
+ }
9
+ return best?.time;
10
+ }
11
+ function phraseOffset(timeline, phrase, from = 0) {
12
+ return phrase ? timeline.sourceText.indexOf(phrase, from) : -1;
13
+ }
14
+ function phraseStartTime(timeline, phrase, options = {}) {
15
+ const offset = phraseOffset(timeline, phrase, options.from ?? 0);
16
+ if (offset < 0) return null;
17
+ const time = wordTimeAt(timeline.bookmarks, offset, options.snap);
18
+ if (time === void 0) return null;
19
+ return Math.max(0, time - (options.lead ?? 0));
20
+ }
21
+
22
+ export {
23
+ wordTimeAt,
24
+ phraseOffset,
25
+ phraseStartTime
26
+ };
@@ -9,7 +9,7 @@ import {
9
9
  expectedSyllablesAt,
10
10
  flattenRenderableBlocks,
11
11
  wordPosAtExpectedSyllables
12
- } from "./chunk-QJQQO3K4.js";
12
+ } from "./chunk-CTT4XAGO.js";
13
13
 
14
14
  // src/narration/types.ts
15
15
  var DEFAULT_FEATURE_CONFIG = Object.freeze({
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-HKHOFTGM.js";
4
4
  import {
5
5
  applySavedPresentation
6
- } from "./chunk-OUSEEUEC.js";
6
+ } from "./chunk-YQQWE4HG.js";
7
7
  import {
8
8
  SHAPE_NAMES,
9
9
  buildChartData,
@@ -32,7 +32,7 @@ import {
32
32
  templateRegistry,
33
33
  writeCustomTemplatesToFrontmatter,
34
34
  writeCustomThemesToFrontmatter
35
- } from "./chunk-QJQQO3K4.js";
35
+ } from "./chunk-CTT4XAGO.js";
36
36
  import {
37
37
  ASCII_TREE_VOCAB,
38
38
  ASCII_VOCAB,
@@ -6,7 +6,7 @@ import {
6
6
  flattenRenderableBlocks,
7
7
  getPinnedBlockMeta,
8
8
  markdownToDoc
9
- } from "./chunk-QJQQO3K4.js";
9
+ } from "./chunk-CTT4XAGO.js";
10
10
  import {
11
11
  deriveTemplateInputs,
12
12
  extractEmbeddedVideos,
@@ -48,6 +48,9 @@ import {
48
48
  import {
49
49
  parseBuildSpec
50
50
  } from "./chunk-P53Z4RYC.js";
51
+ import {
52
+ wordTimeAt
53
+ } from "./chunk-L4AFSOT3.js";
51
54
  import {
52
55
  estimateTimeFromText
53
56
  } from "./chunk-GHOPHGOK.js";
@@ -2189,6 +2192,55 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
2189
2192
  return { version: 1, sourceText: script.sourceText, origin, beats };
2190
2193
  }
2191
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
+
2192
2244
  // src/transform/presentationCompiler.ts
2193
2245
  function compilePresentationPlan(doc, plan) {
2194
2246
  const checked = parsePresentationPlan(plan);
@@ -2260,8 +2312,7 @@ function compilePresentationPlan(doc, plan) {
2260
2312
  const timeAt = (offset) => {
2261
2313
  const marked = markedTime(offset);
2262
2314
  if (marked !== void 0) return marked;
2263
- if (narration && bookmarks.length)
2264
- return bookmarks.find((word) => word.charOffset >= offset)?.time ?? duration;
2315
+ if (narration && bookmarks.length) return wordTimeAt(bookmarks, offset, "next") ?? duration;
2265
2316
  return (narration?.startTime ?? 0) + tokensBefore(offset) / Math.max(1, script.tokens.length) * (narration?.duration ?? duration);
2266
2317
  };
2267
2318
  let previous = 0;
@@ -2375,46 +2426,15 @@ function compilePresentationPlan(doc, plan) {
2375
2426
  audio: doc.audio.segments.some((segment) => segment.src) ? doc.audio : { segments: [{ src: "", name: "presentation-clock", startTime: 0, duration }] }
2376
2427
  };
2377
2428
  }
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
2429
  function conceptReveal(template, beat, start, end, sourceText, timeAt) {
2386
- const build = { mode: "items", effect: "fadeInUp", previous: "keep" };
2387
2430
  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))
2431
+ const offsets = matchPointsToSentences(
2432
+ beat.points.slice(0, steps.length),
2433
+ sourceText,
2434
+ beat.sourceStart,
2435
+ beat.sourceEnd
2409
2436
  );
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
- };
2437
+ return conceptRevealTiming(template, beat.points, offsets?.map(timeAt), start, end);
2418
2438
  }
2419
2439
  function tableFeatureBlock(beat, layout, base, features) {
2420
2440
  const paragraph = (text3) => ({
@@ -2555,6 +2575,11 @@ export {
2555
2575
  transformNarratedSegment,
2556
2576
  collectPresentationMedia,
2557
2577
  createPresentationPlan,
2578
+ CONCEPT_TEMPLATES,
2579
+ conceptTemplate,
2580
+ matchPointsToSentences,
2581
+ conceptBuildSpec,
2582
+ conceptRevealTiming,
2558
2583
  compilePresentationPlan,
2559
2584
  applySavedPresentation
2560
2585
  };