@bendyline/squisq 2.11.16 → 2.11.18

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 (37) hide show
  1. package/dist/{Doc-Bd2kdzuy.d.ts → Doc-B6TYHYGc.d.ts} +41 -2
  2. package/dist/{ImageEditDoc-C3HOltpW.d.ts → ImageEditDoc-CN_3Ysj5.d.ts} +1 -1
  3. package/dist/chunk-3XEABCT3.js +114 -0
  4. package/dist/{chunk-WMXQDP2C.js → chunk-7IGKCHDP.js} +352 -17
  5. package/dist/{chunk-C3NAY5YP.js → chunk-A5QJJZ5S.js} +8 -5
  6. package/dist/{chunk-HVJ6VFDX.js → chunk-GB7OFYUK.js} +4 -0
  7. package/dist/{chunk-NC6LD6D3.js → chunk-J4ACZBO4.js} +50 -8
  8. package/dist/{chunk-ANH2OZ2M.js → chunk-LJZHC6GN.js} +146 -11
  9. package/dist/{chunk-IFJPPI63.js → chunk-NM747LUO.js} +829 -298
  10. package/dist/{chunk-GDHXT5Q7.js → chunk-PGWYC3S2.js} +2 -2
  11. package/dist/{chunk-X7BNDUNI.js → chunk-Q6ELOAOQ.js} +383 -1
  12. package/dist/{chunk-IRVXZUVU.js → chunk-RYO4QW6R.js} +2 -4
  13. package/dist/{chunk-TMOHAFYE.js → chunk-WSFXV5BT.js} +17 -0
  14. package/dist/doc/index.d.ts +35 -24
  15. package/dist/doc/index.js +13 -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 +7 -7
  20. package/dist/index.js +88 -34
  21. package/dist/jsonForm/index.d.ts +1 -1
  22. package/dist/jsonForm/index.js +1 -1
  23. package/dist/markdown/index.d.ts +170 -4
  24. package/dist/markdown/index.js +24 -4
  25. package/dist/{materializePageSection-FRv9whYy.d.ts → materializePageSection-IYXU3tDN.d.ts} +6 -1
  26. package/dist/mediaEdit/index.d.ts +2 -2
  27. package/dist/narration/index.d.ts +1 -1
  28. package/dist/narration/index.js +10 -9
  29. package/dist/recommend/index.js +2 -2
  30. package/dist/schemas/index.d.ts +4 -4
  31. package/dist/schemas/index.js +1 -1
  32. package/dist/{themeLibrary-BbF3EKWM.d.ts → themeLibrary-_46saKYO.d.ts} +1 -1
  33. package/dist/timing/index.d.ts +1 -1
  34. package/dist/transform/index.d.ts +84 -4
  35. package/dist/transform/index.js +38 -8
  36. package/package.json +1 -1
  37. package/dist/chunk-5W7F46OO.js +0 -337
@@ -13,7 +13,7 @@ import {
13
13
  parseAsciiTimeline,
14
14
  pickAutoTemplate,
15
15
  profileBlockContents
16
- } from "./chunk-C3NAY5YP.js";
16
+ } from "./chunk-A5QJJZ5S.js";
17
17
  import {
18
18
  anchorPoint,
19
19
  base64ToUtf8,
@@ -61,17 +61,22 @@ import {
61
61
  sanitizeUrl,
62
62
  splitKeyValueToken,
63
63
  tokenizeAttrTokens
64
- } from "./chunk-TMOHAFYE.js";
64
+ } from "./chunk-WSFXV5BT.js";
65
65
  import {
66
66
  extractPlainText,
67
+ findBlockVideoEmbed,
68
+ isVideoEmbedUrl,
69
+ mediaKindForUrl,
70
+ parseVideoEmbedUrl,
67
71
  readFrontmatterThemeId,
68
72
  walkMarkdownTree
69
- } from "./chunk-X7BNDUNI.js";
73
+ } from "./chunk-Q6ELOAOQ.js";
70
74
  import {
71
75
  resolveBlockTransition
72
76
  } from "./chunk-4VOD55SX.js";
73
77
  import {
74
- estimateReadingTime
78
+ estimateReadingTime,
79
+ estimateSpokenWordCount
75
80
  } from "./chunk-GHOPHGOK.js";
76
81
  import {
77
82
  SeededRandom,
@@ -1574,9 +1579,29 @@ function findFirstList(contents) {
1574
1579
 
1575
1580
  // src/doc/templateInputs.ts
1576
1581
  var VIDEO_FILE_RE = /\.(?:webm|mp4|mov|m4v|ogv)(?:[?#].*)?$/i;
1582
+ function extractHostedVideos(contents, limit = Infinity) {
1583
+ const found = [];
1584
+ const seen = /* @__PURE__ */ new Set();
1585
+ for (const node of contents ?? []) {
1586
+ if (found.length >= limit) break;
1587
+ const video = findBlockVideoEmbed(node);
1588
+ if (!video || seen.has(video.embed.watchUrl)) continue;
1589
+ seen.add(video.embed.watchUrl);
1590
+ found.push({
1591
+ url: video.embed.watchUrl,
1592
+ title: video.title,
1593
+ aspectRatio: video.embed.aspectRatio
1594
+ });
1595
+ }
1596
+ return found;
1597
+ }
1598
+ function isMaterializedMediaNode(node) {
1599
+ if (node.type === "code") return node.lang?.trim().toLowerCase() === "mermaid";
1600
+ return findBlockVideoEmbed(node) !== null;
1601
+ }
1577
1602
  function extractBodyPlainText(contents) {
1578
1603
  if (!contents || contents.length === 0) return "";
1579
- return contents.filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((n) => extractPlainText(n)).join("\n").trim();
1604
+ return contents.filter((node) => !isMaterializedMediaNode(node)).map((n) => extractPlainText(n)).join("\n").trim();
1580
1605
  }
1581
1606
  function extractListItems(contents) {
1582
1607
  return extractRichListItems(contents).map((item) => item.text);
@@ -1750,7 +1775,9 @@ function extractImages(contents, limit = Infinity) {
1750
1775
  if (!node || typeof node !== "object") return;
1751
1776
  const n = node;
1752
1777
  if (n.type === "image" && typeof n.url === "string" && n.url) {
1753
- found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
1778
+ if (!mediaKindForUrl(n.url)) {
1779
+ found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
1780
+ }
1754
1781
  return;
1755
1782
  }
1756
1783
  if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
@@ -1951,7 +1978,7 @@ function autoTemplatePreservesContent(templateName, contents) {
1951
1978
  }
1952
1979
  case "videoWithCaption": {
1953
1980
  const videos = extractEmbeddedVideos(nodes);
1954
- if (videos.length !== 1) return false;
1981
+ if (videos.length + extractHostedVideos(nodes).length !== 1) return false;
1955
1982
  const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
1956
1983
  return bodyText3 === normalizeCoverageText(videos[0]?.alt ?? "");
1957
1984
  }
@@ -2097,7 +2124,17 @@ function deriveTemplateInputs(templateName, headingText, contents, options = {})
2097
2124
  }
2098
2125
  case "videoWithCaption": {
2099
2126
  const video = extractFirstEmbeddedVideo(contents);
2100
- if (!video) return placeholders ? { caption: headingText } : null;
2127
+ if (!video) {
2128
+ const hosted = extractHostedVideos(contents, 1)[0];
2129
+ if (hosted) {
2130
+ return {
2131
+ videoSrc: hosted.url,
2132
+ videoAlt: hosted.title || headingText,
2133
+ caption: headingText
2134
+ };
2135
+ }
2136
+ return placeholders ? { caption: headingText } : null;
2137
+ }
2101
2138
  return {
2102
2139
  videoSrc: video.src,
2103
2140
  posterSrc: video.posterSrc,
@@ -2160,10 +2197,10 @@ function deriveTemplateInputs(templateName, headingText, contents, options = {})
2160
2197
  function bodyNodes(context) {
2161
2198
  if (context.block?.summaryMode === "headings-and-features") return [];
2162
2199
  return (context.block?.contents ?? []).filter(
2163
- // Mermaid source is already materialized as a visual layer. Repeating the
2164
- // fence text as prose makes the loss-averse default noisy without
2165
- // preserving any additional authored meaning.
2166
- (node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")
2200
+ // Mermaid source and hosted-video links are already materialized as
2201
+ // visual layers (a diagram, a player). Repeating them as prose makes the
2202
+ // loss-averse default noisy without preserving any additional meaning.
2203
+ (node) => !isMaterializedMediaNode(node)
2167
2204
  );
2168
2205
  }
2169
2206
  function plainListText(list, depth = 0) {
@@ -4579,6 +4616,37 @@ function videoWithCaption(input, context) {
4579
4616
  const resolvedClipEnd = Math.max(clipStart, clipEnd ?? clipStart + Math.max(0, input.duration));
4580
4617
  const captionFontSize = themedFontSize(30, context, false);
4581
4618
  const creditFontSize = themedFontSize(16, context, false);
4619
+ if (isVideoEmbedUrl(videoSrc)) {
4620
+ const layers2 = [
4621
+ createBackgroundLayer("bg", theme.colors.background),
4622
+ {
4623
+ type: "videoEmbed",
4624
+ id: "bg-video",
4625
+ content: { url: videoSrc, ...videoAlt ? { title: videoAlt } : {} },
4626
+ position: caption ? { x: "4%", y: "4%", width: "92%", height: "78%" } : { x: "4%", y: "4%", width: "92%", height: "92%" }
4627
+ }
4628
+ ];
4629
+ if (caption) {
4630
+ layers2.push({
4631
+ type: "text",
4632
+ id: "caption",
4633
+ content: {
4634
+ text: caption,
4635
+ style: {
4636
+ fontSize: captionFontSize,
4637
+ fontFamily: getThemeFont(context, "body"),
4638
+ color: theme.colors.text,
4639
+ textAlign: "center",
4640
+ lineHeight: 1.18,
4641
+ maxLines: 2
4642
+ }
4643
+ },
4644
+ position: { x: "50%", y: "90%", anchor: "center", width: "84%" },
4645
+ animation: { type: "fadeIn", duration: 1.5, delay: 0.5 }
4646
+ });
4647
+ }
4648
+ return layers2;
4649
+ }
4582
4650
  const layers = [
4583
4651
  // Background video clip
4584
4652
  {
@@ -5668,7 +5736,7 @@ function resolveSupplementalMediaLayout(layers, template, viewport, mediaCount,
5668
5736
  const policy = getBlockMediaLayoutPolicy(template);
5669
5737
  const shape = mediaShape(mediaCount, aspectRatios);
5670
5738
  const alreadyOwnsMedia = layers.some(
5671
- (layer) => layer.type === "image" || layer.type === "video" || layer.type === "map" || layer.type === "mermaid"
5739
+ (layer) => layer.type === "image" || layer.type === "video" || layer.type === "videoEmbed" || layer.type === "map" || layer.type === "mermaid"
5672
5740
  );
5673
5741
  if (!alreadyOwnsMedia && (policy?.nativeLayout === "feature-left" || policy?.nativeLayout === "feature-right")) {
5674
5742
  const { position } = featureMediaSlot(
@@ -5981,9 +6049,11 @@ function nodesFromTemplateData(input) {
5981
6049
  function diagramBlock(input, context) {
5982
6050
  const { theme, viewport, children = [] } = context;
5983
6051
  const colors = input.colorScheme ? resolveColorScheme(context, input.colorScheme) : void 0;
6052
+ const background = input.background === "surface" ? [createBackgroundLayer("diagram-bg", themedSurfaceGradient(context, 170))] : [];
5984
6053
  const resolved = children.length > 0 ? nodesFromChildren(context) : input.nodes && input.nodes.length > 0 ? nodesFromTemplateData(input) : { nodes: [], edges: [] };
5985
6054
  if (resolved.nodes.length === 0) {
5986
6055
  return [
6056
+ ...background,
5987
6057
  {
5988
6058
  type: "text",
5989
6059
  id: "diagram-empty",
@@ -6024,7 +6094,7 @@ function diagramBlock(input, context) {
6024
6094
  w: n.w * scale,
6025
6095
  h: n.h * scale
6026
6096
  });
6027
- const layers = [];
6097
+ const layers = [...background];
6028
6098
  if (input.title) {
6029
6099
  const titleLayer = canvasTitleLayer("diagram-title", input.title, frame, placement, context);
6030
6100
  if (titleLayer) layers.push(titleLayer);
@@ -9921,6 +9991,9 @@ function collectRichMediaItems(layers, block) {
9921
9991
  const existingMermaid = new Set(
9922
9992
  layers.filter((layer) => layer.type === "mermaid").map((layer) => layer.content.source)
9923
9993
  );
9994
+ const existingEmbeds = new Set(
9995
+ layers.filter((layer) => layer.type === "videoEmbed").map((layer) => parseVideoEmbedUrl(layer.content.url)?.watchUrl ?? layer.content.url)
9996
+ );
9924
9997
  const embeddedVideos = extractEmbeddedVideos(block.contents);
9925
9998
  const videos = embeddedVideos.filter((video) => !existingVideos.has(video.src));
9926
9999
  const videoSources = new Set(embeddedVideos.map((video) => video.src));
@@ -9931,6 +10004,9 @@ function collectRichMediaItems(layers, block) {
9931
10004
  return true;
9932
10005
  });
9933
10006
  const sources = mermaidSources(block).filter((source) => !existingMermaid.has(source));
10007
+ const hosted = extractHostedVideos(block.contents).filter(
10008
+ (video) => !existingEmbeds.has(video.url)
10009
+ );
9934
10010
  return [
9935
10011
  ...images.map(
9936
10012
  (image) => ({
@@ -9952,6 +10028,14 @@ function collectRichMediaItems(layers, block) {
9952
10028
  ...video.clipEnd != null ? { clipEnd: video.clipEnd } : {}
9953
10029
  })
9954
10030
  ),
10031
+ ...hosted.map(
10032
+ (video) => ({
10033
+ kind: "videoEmbed",
10034
+ url: video.url,
10035
+ title: video.title,
10036
+ aspectRatio: video.aspectRatio
10037
+ })
10038
+ ),
9955
10039
  ...sources.map((source) => ({ kind: "mermaid", source, aspectRatio: 16 / 9 })),
9956
10040
  ...embeddedSpatialMedia(block)
9957
10041
  ];
@@ -10023,6 +10107,17 @@ function richMediaLayers(item, position, block, theme, kindIndex) {
10023
10107
  }
10024
10108
  ];
10025
10109
  }
10110
+ if (item.kind === "videoEmbed") {
10111
+ const title = item.title || block.title;
10112
+ return [
10113
+ {
10114
+ id: `${block.id}-video-embed-${kindIndex}`,
10115
+ type: "videoEmbed",
10116
+ position,
10117
+ content: { url: item.url, ...title ? { title } : {} }
10118
+ }
10119
+ ];
10120
+ }
10026
10121
  if (item.kind === "mermaid") {
10027
10122
  return [
10028
10123
  {
@@ -10107,6 +10202,7 @@ function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRati
10107
10202
  const counters = {
10108
10203
  image: 0,
10109
10204
  video: 0,
10205
+ videoEmbed: 0,
10110
10206
  mermaid: 0,
10111
10207
  spatial: 0
10112
10208
  };
@@ -10912,6 +11008,28 @@ function expandDocBlocks(blocks, options = {}) {
10912
11008
  const splitParts = /* @__PURE__ */ new Map();
10913
11009
  for (const [segmentIndex, segmentBlocks] of blocksBySegment) {
10914
11010
  const audioSegment = audioSegments[segmentIndex];
11011
+ if (segmentBlocks.every(({ block }) => block.timelineLocked === true)) {
11012
+ for (const { block, originalIndex } of segmentBlocks) {
11013
+ const expanded = materializeScheduledBlock(
11014
+ block,
11015
+ originalIndex,
11016
+ totalBlocks,
11017
+ theme,
11018
+ viewport,
11019
+ effectivePersistentLayers,
11020
+ expandedPersistentLayers,
11021
+ registry,
11022
+ failureMode,
11023
+ onDiagnostic,
11024
+ motion,
11025
+ opts.mediaAspectRatios
11026
+ );
11027
+ expanded.startTime = block.sourceStartTime ?? 0;
11028
+ expanded.duration = block.duration;
11029
+ expandedBlocks[originalIndex] = expanded;
11030
+ }
11031
+ continue;
11032
+ }
10915
11033
  if (!audioSegment) {
10916
11034
  let offsetTime = 0;
10917
11035
  for (const { block, originalIndex } of segmentBlocks) {
@@ -12272,7 +12390,7 @@ function getBlockBodyText2(block) {
12272
12390
  if (!block.contents || block.contents.length === 0) return "";
12273
12391
  const tmpl = resolveTemplateName(block.template ?? "");
12274
12392
  const consumedFence = tmpl === "diagram" && block.templateData !== void 0 && "nodes" in block.templateData || tmpl === "timeline" && block.templateData !== void 0 && "tracks" in block.templateData || tmpl === "tree" && block.templateData !== void 0 && "items" in block.templateData;
12275
- return block.contents.filter((node) => !(consumedFence && node.type === "code")).map((node) => extractPlainText(node)).join("\n").trim();
12393
+ return block.contents.filter((node) => !(consumedFence && node.type === "code")).filter((node) => findBlockVideoEmbed(node) === null).map((node) => extractPlainText(node)).join("\n").trim();
12276
12394
  }
12277
12395
  var MAX_PHRASE_WORDS = 12;
12278
12396
  function splitIntoSentences(text) {
@@ -12334,7 +12452,7 @@ function findFirstHtmlImage(nodes) {
12334
12452
  function findFirstImage(node) {
12335
12453
  if (node.type === "image") {
12336
12454
  const img = node;
12337
- return { url: img.url, alt: img.alt };
12455
+ return mediaKindForUrl(img.url) ? void 0 : { url: img.url, alt: img.alt };
12338
12456
  }
12339
12457
  if (node.type === "htmlBlock" || node.type === "htmlInline") {
12340
12458
  const html = node;
@@ -12392,6 +12510,215 @@ function buildStartBlock(markdownDoc, rootBlocks, fileName) {
12392
12510
  return config;
12393
12511
  }
12394
12512
 
12513
+ // src/narration/syllables.ts
12514
+ var BOUNDARY_PUNCT = /^[.,!?;:'"()[\]{}]+|[.,!?;:'"()[\]{}]+$/g;
12515
+ var SYLLABLES_PER_SPOKEN_WORD = 1.4;
12516
+ function estimateSyllables(token) {
12517
+ const stripped = token.replace(BOUNDARY_PUNCT, "");
12518
+ if (!stripped) return 1;
12519
+ if (/^[-+]?[\d.,]+$/.test(stripped)) {
12520
+ const num = parseFloat(stripped.replace(/,/g, ""));
12521
+ if (!Number.isNaN(num) && Number.isFinite(num)) {
12522
+ return Math.max(1, Math.round(estimateSpokenWordCount(stripped) * SYLLABLES_PER_SPOKEN_WORD));
12523
+ }
12524
+ }
12525
+ if (/^[A-Z]{2,5}$/.test(stripped) && !/[AEIOUY]/.test(stripped)) {
12526
+ return stripped.length;
12527
+ }
12528
+ const parts = stripped.toLowerCase().split(/[^a-z']+/).filter((p) => p.length > 0);
12529
+ if (parts.length === 0) return 1;
12530
+ let total = 0;
12531
+ for (const part of parts) {
12532
+ total += countAlphaSyllables(part.replace(/'/g, ""));
12533
+ }
12534
+ return Math.max(1, total);
12535
+ }
12536
+ function countAlphaSyllables(word) {
12537
+ if (!word) return 0;
12538
+ const groups = word.match(/[aeiouy]+/g);
12539
+ let count = groups ? groups.length : 0;
12540
+ if (count === 0) return 1;
12541
+ if (count > 1 && /[^aeiouy]e$/.test(word)) {
12542
+ count -= 1;
12543
+ }
12544
+ if (count > 1 && /[^aeiouy]ed$/.test(word) && !/[td]ed$/.test(word)) {
12545
+ count -= 1;
12546
+ }
12547
+ if (count > 1 && /[^aeiouy]es$/.test(word) && !/(?:[sxz]|[cs]h)es$/.test(word)) {
12548
+ count -= 1;
12549
+ }
12550
+ if (/[^aeiouy]le$/.test(word)) {
12551
+ count += 1;
12552
+ }
12553
+ return Math.max(1, count);
12554
+ }
12555
+
12556
+ // src/narration/script.ts
12557
+ function blockSpokenText(block, includeTitles) {
12558
+ const title = includeTitles && block.title ? block.title.trim() : "";
12559
+ const body = getBlockBodyText2(block);
12560
+ if (title && body) return `${title}
12561
+ ${body}`;
12562
+ return title || body;
12563
+ }
12564
+ function buildNarrationScript(doc, options) {
12565
+ const includeTitles = options?.includeTitles ?? true;
12566
+ const separator = options?.blockSeparator ?? "\n\n";
12567
+ const pieces = [];
12568
+ for (const block of flattenRenderableBlocks(doc.blocks)) {
12569
+ const text = blockSpokenText(block, includeTitles);
12570
+ if (!text) continue;
12571
+ pieces.push({
12572
+ blockId: block.id,
12573
+ heading: block.title || void 0,
12574
+ text
12575
+ });
12576
+ }
12577
+ let sourceText = "";
12578
+ const blocks = [];
12579
+ for (let i = 0; i < pieces.length; i++) {
12580
+ if (i > 0) sourceText += separator;
12581
+ const charStart = sourceText.length;
12582
+ sourceText += pieces[i].text;
12583
+ blocks.push({
12584
+ blockId: pieces[i].blockId,
12585
+ ...pieces[i].heading !== void 0 ? { heading: pieces[i].heading } : {},
12586
+ tokenStart: 0,
12587
+ tokenEnd: 0,
12588
+ charStart,
12589
+ charEnd: sourceText.length
12590
+ });
12591
+ }
12592
+ const tokens = [];
12593
+ const tokenRe = /\S+/g;
12594
+ let blockIndex = 0;
12595
+ let match;
12596
+ while ((match = tokenRe.exec(sourceText)) !== null) {
12597
+ const charOffset = match.index;
12598
+ const charEnd = charOffset + match[0].length;
12599
+ while (blockIndex < blocks.length - 1 && charOffset >= blocks[blockIndex].charEnd) {
12600
+ blockIndex++;
12601
+ }
12602
+ const spoken = hasSpokenContent(match[0]);
12603
+ tokens.push({
12604
+ text: match[0],
12605
+ charOffset,
12606
+ charEnd,
12607
+ blockId: blocks[blockIndex]?.blockId ?? "",
12608
+ blockIndex,
12609
+ syllables: spoken ? estimateSyllables(match[0]) : 0,
12610
+ spokenWordEquiv: spoken ? estimateSpokenWordCount(stripBoundaryPunct(match[0])) : 0,
12611
+ pauseAfter: 0,
12612
+ spoken
12613
+ });
12614
+ }
12615
+ for (const range of blocks) {
12616
+ range.tokenStart = tokens.length;
12617
+ range.tokenEnd = 0;
12618
+ }
12619
+ for (let i = 0; i < tokens.length; i++) {
12620
+ const range = blocks[tokens[i].blockIndex];
12621
+ if (range) {
12622
+ if (i < range.tokenStart) range.tokenStart = i;
12623
+ range.tokenEnd = i + 1;
12624
+ }
12625
+ }
12626
+ for (const range of blocks) {
12627
+ if (range.tokenStart > range.tokenEnd) {
12628
+ range.tokenStart = range.tokenEnd;
12629
+ }
12630
+ }
12631
+ for (let i = 0; i < tokens.length; i++) {
12632
+ const token = tokens[i];
12633
+ const next = tokens[i + 1];
12634
+ if (!next || next.blockIndex !== token.blockIndex) {
12635
+ token.pauseAfter = 3;
12636
+ } else if (sourceText.slice(token.charEnd, next.charOffset).includes("\n")) {
12637
+ token.pauseAfter = 2;
12638
+ } else if (endsWithClausePunctuation(token.text)) {
12639
+ token.pauseAfter = 1;
12640
+ }
12641
+ }
12642
+ const cumulativeSyllables = new Array(tokens.length + 1);
12643
+ cumulativeSyllables[0] = 0;
12644
+ for (let i = 0; i < tokens.length; i++) {
12645
+ cumulativeSyllables[i + 1] = cumulativeSyllables[i] + tokens[i].syllables;
12646
+ }
12647
+ return {
12648
+ sourceText,
12649
+ tokens,
12650
+ blocks,
12651
+ totalSyllables: cumulativeSyllables[tokens.length],
12652
+ cumulativeSyllables
12653
+ };
12654
+ }
12655
+ var TRAILING_CLOSERS = /["')\]}»”’]+$/;
12656
+ function hasSpokenContent(token) {
12657
+ return /[\p{L}\p{N}]/u.test(token);
12658
+ }
12659
+ function stripBoundaryPunct(token) {
12660
+ return token.replace(/^[.,!?;:'"()[\]{}]+|[.,!?;:'"()[\]{}]+$/g, "");
12661
+ }
12662
+ function endsWithClausePunctuation(token) {
12663
+ const bare = token.replace(TRAILING_CLOSERS, "");
12664
+ const last = bare.charAt(bare.length - 1);
12665
+ if (!last) return false;
12666
+ if (last === ",") {
12667
+ return true;
12668
+ }
12669
+ return last === "." || last === ";" || last === ":" || last === "!" || last === "?";
12670
+ }
12671
+ function clamp2(value, min, max) {
12672
+ return value < min ? min : value > max ? max : value;
12673
+ }
12674
+ function expectedSyllablesAt(script, wordPos) {
12675
+ const n = script.tokens.length;
12676
+ if (n === 0) return 0;
12677
+ const pos = clamp2(wordPos, 0, n);
12678
+ const idx = Math.floor(pos);
12679
+ if (idx >= n) return script.totalSyllables;
12680
+ return script.cumulativeSyllables[idx] + (pos - idx) * script.tokens[idx].syllables;
12681
+ }
12682
+ function wordPosAtExpectedSyllables(script, syllables) {
12683
+ const n = script.tokens.length;
12684
+ if (n === 0) return 0;
12685
+ const target = clamp2(syllables, 0, script.totalSyllables);
12686
+ let lo = 0;
12687
+ let hi = n;
12688
+ while (lo < hi) {
12689
+ const mid = lo + hi + 1 >> 1;
12690
+ if (script.cumulativeSyllables[mid] <= target) lo = mid;
12691
+ else hi = mid - 1;
12692
+ }
12693
+ if (lo >= n) return n;
12694
+ const within = target - script.cumulativeSyllables[lo];
12695
+ const sylls = script.tokens[lo].syllables;
12696
+ return lo + (sylls > 0 ? within / sylls : 0);
12697
+ }
12698
+ function wordIndexAtChar(script, charOffset) {
12699
+ const tokens = script.tokens;
12700
+ if (tokens.length === 0) return 0;
12701
+ let lo = 0;
12702
+ let hi = tokens.length - 1;
12703
+ while (lo < hi) {
12704
+ const mid = lo + hi >> 1;
12705
+ if (tokens[mid].charEnd <= charOffset) lo = mid + 1;
12706
+ else hi = mid;
12707
+ }
12708
+ return lo;
12709
+ }
12710
+ function wordIndexAtTime(words, tSec) {
12711
+ if (words.length === 0) return 0;
12712
+ let lo = 0;
12713
+ let hi = words.length - 1;
12714
+ while (lo < hi) {
12715
+ const mid = lo + hi + 1 >> 1;
12716
+ if (words[mid].tSec <= tSec) lo = mid;
12717
+ else hi = mid - 1;
12718
+ }
12719
+ return words[lo].tSec <= tSec ? lo : 0;
12720
+ }
12721
+
12395
12722
  export {
12396
12723
  expandPersistentLayers,
12397
12724
  getPersistentLayersFromTheme,
@@ -12434,6 +12761,8 @@ export {
12434
12761
  treeFromTemplateData,
12435
12762
  treeFromMarkdownList,
12436
12763
  findFirstList,
12764
+ extractHostedVideos,
12765
+ isMaterializedMediaNode,
12437
12766
  extractBodyPlainText,
12438
12767
  extractListItems,
12439
12768
  renderMarkdownBlocksHtml,
@@ -12527,5 +12856,11 @@ export {
12527
12856
  flattenRenderableBlocks,
12528
12857
  countBlocks,
12529
12858
  getBlockDepth,
12530
- getBlockBodyText2 as getBlockBodyText
12859
+ getBlockBodyText2 as getBlockBodyText,
12860
+ estimateSyllables,
12861
+ buildNarrationScript,
12862
+ expectedSyllablesAt,
12863
+ wordPosAtExpectedSyllables,
12864
+ wordIndexAtChar,
12865
+ wordIndexAtTime
12531
12866
  };
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  extractPlainText,
3
3
  findNodesByType,
4
+ isVideoEmbedUrl,
5
+ mediaKindForUrl,
4
6
  walkMarkdownTree
5
- } from "./chunk-X7BNDUNI.js";
7
+ } from "./chunk-Q6ELOAOQ.js";
6
8
 
7
9
  // src/doc/asciiDiagram/chars.ts
8
10
  function toCells(s) {
@@ -2483,7 +2485,6 @@ var DATE_PATTERNS = [
2483
2485
  /\b\d{4}s\b/,
2484
2486
  /\b\d{1,2}(st|nd|rd|th)\s+century\b/i
2485
2487
  ];
2486
- var VIDEO_HOST_RE = /(youtube\.com|youtu\.be|vimeo\.com|wistia\.|loom\.com)/i;
2487
2488
  function htmlElementsByTag(root, tagNames) {
2488
2489
  const blocks = findNodesByType(root, "htmlBlock");
2489
2490
  const inline = findNodesByType(root, "htmlInline");
@@ -2508,7 +2509,9 @@ function profileBlockContents(nodes) {
2508
2509
  let hasTable = false;
2509
2510
  const plainParts = [];
2510
2511
  for (const node of nodes) {
2511
- imageCount += findNodesByType(node, "image").length;
2512
+ imageCount += findNodesByType(node, "image").filter(
2513
+ (img) => !mediaKindForUrl(img.url ?? "")
2514
+ ).length;
2512
2515
  imageCount += findNodesByType(node, "imageReference").length;
2513
2516
  if (findNodesByType(node, "blockquote").length > 0) hasBlockquote = true;
2514
2517
  if (findNodesByType(node, "list").length > 0) hasList = true;
@@ -2526,7 +2529,7 @@ function profileBlockContents(nodes) {
2526
2529
  break;
2527
2530
  }
2528
2531
  const src = el.attributes.src || el.attributes.href || "";
2529
- if (VIDEO_HOST_RE.test(src)) {
2532
+ if (isVideoEmbedUrl(src)) {
2530
2533
  hasVideo = true;
2531
2534
  break;
2532
2535
  }
@@ -2534,7 +2537,7 @@ function profileBlockContents(nodes) {
2534
2537
  if (!hasVideo) {
2535
2538
  const links = findNodesByType(root, "link");
2536
2539
  for (const link of links) {
2537
- if (link.url && VIDEO_HOST_RE.test(link.url)) {
2540
+ if (isVideoEmbedUrl(link.url)) {
2538
2541
  hasVideo = true;
2539
2542
  break;
2540
2543
  }
@@ -57,6 +57,7 @@ var LAYER_TYPES = /* @__PURE__ */ new Set([
57
57
  "path",
58
58
  "map",
59
59
  "video",
60
+ "videoEmbed",
60
61
  "table",
61
62
  "tree",
62
63
  "mermaid"
@@ -199,6 +200,9 @@ function validateLayerContent(type, content, path, issues) {
199
200
  requiredFiniteNumber(content, "clipStart", `${path}.clipStart`, issues, { min: 0 });
200
201
  requiredFiniteNumber(content, "clipEnd", `${path}.clipEnd`, issues, { min: 0 });
201
202
  break;
203
+ case "videoEmbed":
204
+ requiredString(content, "url", `${path}.url`, issues);
205
+ break;
202
206
  case "table":
203
207
  requiredStringArray(content, "headers", `${path}.headers`, issues);
204
208
  validateStringRows(content.rows, `${path}.rows`, issues);