@bendyline/squisq 2.13.1 → 2.13.3

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-C7iCmmmF.d.ts → Doc-BPhdJFS_.d.ts} +53 -2
  2. package/dist/{ImageEditDoc-CxRHe41J.d.ts → ImageEditDoc-DDx-sw_S.d.ts} +1 -1
  3. package/dist/{chunk-ITLJQG7Z.js → chunk-5FUBK3DK.js} +527 -266
  4. package/dist/{chunk-HE3HQZWU.js → chunk-6NWVLLMI.js} +104 -1
  5. package/dist/{chunk-3A6YM375.js → chunk-D3JQMZUT.js} +8 -8
  6. package/dist/chunk-DOOUVVOM.js +78 -0
  7. package/dist/{chunk-2LTALYXX.js → chunk-F3LRTRST.js} +15 -10
  8. package/dist/{chunk-5SVFLLOD.js → chunk-ISVJ42I3.js} +3 -0
  9. package/dist/{chunk-7ZXCS3UU.js → chunk-JFJZ4NS6.js} +2 -2
  10. package/dist/{chunk-V4USYYYO.js → chunk-JOBMPMY5.js} +1 -1
  11. package/dist/{chunk-HGCIIXHP.js → chunk-POZCKPCO.js} +235 -4
  12. package/dist/{chunk-DCO5GYVW.js → chunk-VX7MPZOK.js} +5 -5
  13. package/dist/doc/index.d.ts +100 -15
  14. package/dist/doc/index.js +15 -7
  15. package/dist/fence/index.d.ts +1 -1
  16. package/dist/generate/index.d.ts +1 -1
  17. package/dist/imageEdit/index.d.ts +3 -3
  18. package/dist/index.d.ts +5 -5
  19. package/dist/index.js +26 -18
  20. package/dist/jsonForm/index.d.ts +1 -1
  21. package/dist/jsonForm/index.js +1 -1
  22. package/dist/markdown/index.js +11 -12
  23. package/dist/{materializePageSection-_5uQrEYP.d.ts → materializePageSection-DnQi-0TA.d.ts} +1 -1
  24. package/dist/mediaEdit/index.d.ts +2 -2
  25. package/dist/narration/index.d.ts +1 -1
  26. package/dist/narration/index.js +5 -5
  27. package/dist/recommend/index.js +3 -2
  28. package/dist/schemas/index.d.ts +4 -4
  29. package/dist/schemas/index.js +1 -1
  30. package/dist/{themeLibrary-DsntZLoa.d.ts → themeLibrary-DM7eSK1w.d.ts} +1 -1
  31. package/dist/timing/index.d.ts +1 -1
  32. package/dist/transform/index.d.ts +2 -2
  33. package/dist/transform/index.js +6 -6
  34. package/package.json +1 -1
  35. package/dist/chunk-Y25EJEZN.js +0 -178
@@ -10,6 +10,7 @@ import {
10
10
  extractEmbeddedVideos,
11
11
  extractHostedVideos,
12
12
  extractImages,
13
+ extractMediaFromContents,
13
14
  extractRichListItems,
14
15
  extractTableFromContents,
15
16
  isEligibleAsciiFenceLang,
@@ -19,15 +20,19 @@ import {
19
20
  isExplicitTimelineLang,
20
21
  isExplicitTreeLang,
21
22
  isMaterializedMediaNode,
23
+ isMediaAnnotationName,
22
24
  isPictureFence,
25
+ isPictureParagraph,
23
26
  markdownBlockSeparatorLines,
27
+ parseAnnotationInner,
24
28
  parseAsciiDiagram,
25
29
  parseAsciiTimeline,
30
+ parseStandaloneAnnotation,
26
31
  pickAutoTemplate,
27
32
  profileBlockContents,
28
33
  renderMarkdownBlocksHtml,
29
34
  treeToTemplateData
30
- } from "./chunk-HGCIIXHP.js";
35
+ } from "./chunk-POZCKPCO.js";
31
36
  import {
32
37
  anchorPoint,
33
38
  base64ToUtf8,
@@ -58,33 +63,25 @@ import {
58
63
  validateTheme,
59
64
  withAlpha
60
65
  } from "./chunk-WNEH3BZP.js";
61
- import {
62
- mediaEditValuesFromAttributes,
63
- mediaEditValuesFromParams,
64
- parseMediaEdits
65
- } from "./chunk-L7QZORLB.js";
66
66
  import {
67
67
  TABLE_FED_TEMPLATES,
68
68
  isContainerTemplate,
69
- matchTrailingTemplateAnnotation,
70
- resolveTemplateName,
71
- splitKeyValueToken,
72
- tokenizeAttrTokens
73
- } from "./chunk-Y25EJEZN.js";
69
+ resolveTemplateName
70
+ } from "./chunk-DOOUVVOM.js";
74
71
  import {
75
72
  KNOWN_BLOCK_META_KEYS,
76
73
  coerceAnnotationValues,
77
74
  extractPlainText,
78
75
  isSvgIslandLang,
79
76
  isVideoEmbedUrl,
77
+ matchTrailingTemplateAnnotation,
80
78
  mediaKindForUrl,
81
79
  parseNumber,
82
80
  parseSvgIsland,
83
- parseTimeSeconds,
84
81
  parseVideoEmbedUrl,
85
82
  readFrontmatterThemeId,
86
83
  walkMarkdownTree
87
- } from "./chunk-HE3HQZWU.js";
84
+ } from "./chunk-6NWVLLMI.js";
88
85
  import {
89
86
  resolveBlockTransition
90
87
  } from "./chunk-P53Z4RYC.js";
@@ -1791,12 +1788,12 @@ function statHighlight(input, context) {
1791
1788
  const statTop = groupTop;
1792
1789
  const descTop = statTop + statBoxH + gap;
1793
1790
  const detailTop = descTop + descBoxH + detailGap;
1794
- const pct = (px) => `${Number((px / frameH * 100).toFixed(3))}%`;
1791
+ const pct2 = (px) => `${Number((px / frameH * 100).toFixed(3))}%`;
1795
1792
  const box = (top, height) => ({
1796
1793
  x: accentLayout.textCenterX,
1797
- y: pct(top + height / 2),
1794
+ y: pct2(top + height / 2),
1798
1795
  width: accentLayout.textWidth,
1799
- height: pct(height),
1796
+ height: pct2(height),
1800
1797
  anchor: "center"
1801
1798
  });
1802
1799
  layers.push({
@@ -1880,7 +1877,7 @@ function quoteBlock(input, context) {
1880
1877
  const accentImage = accentForViewport(input.accentImage, viewport);
1881
1878
  const H = viewport.height;
1882
1879
  const W = viewport.width;
1883
- const pct = (px) => `${Number((px / H * 100).toFixed(3))}%`;
1880
+ const pct2 = (px) => `${Number((px / H * 100).toFixed(3))}%`;
1884
1881
  const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1885
1882
  const yShiftPx = accentLayout.adjustY ? accentLayout.textYAdjust / 100 * H : 0;
1886
1883
  const colWidthPct = parseFloat(accentLayout.textWidth);
@@ -1938,9 +1935,9 @@ function quoteBlock(input, context) {
1938
1935
  },
1939
1936
  position: {
1940
1937
  x: colLeft,
1941
- y: pct(bandTopPx),
1938
+ y: pct2(bandTopPx),
1942
1939
  width: accentLayout.textWidth,
1943
- height: pct(titleFit.heightPx),
1940
+ height: pct2(titleFit.heightPx),
1944
1941
  anchor: "top-left"
1945
1942
  },
1946
1943
  animation: { type: "fadeIn", duration: 0.8 }
@@ -1994,7 +1991,7 @@ function quoteBlock(input, context) {
1994
1991
  },
1995
1992
  position: {
1996
1993
  x: accentLayout.textCenterX,
1997
- y: pct(quoteTopPx - decoMarginPx - decoFontSize * 0.6),
1994
+ y: pct2(quoteTopPx - decoMarginPx - decoFontSize * 0.6),
1998
1995
  anchor: "center"
1999
1996
  }
2000
1997
  });
@@ -2020,9 +2017,9 @@ function quoteBlock(input, context) {
2020
2017
  },
2021
2018
  position: {
2022
2019
  x: colLeft,
2023
- y: pct(quoteTopPx),
2020
+ y: pct2(quoteTopPx),
2024
2021
  width: accentLayout.textWidth,
2025
- height: pct(quoteFit.heightPx),
2022
+ height: pct2(quoteFit.heightPx),
2026
2023
  anchor: "top-left"
2027
2024
  },
2028
2025
  animation: motionEntrance(context, { type: "fadeIn", duration: 2 })
@@ -2047,9 +2044,9 @@ function quoteBlock(input, context) {
2047
2044
  },
2048
2045
  position: {
2049
2046
  x: colLeft,
2050
- y: pct(quoteBottomPx + attrGapPx),
2047
+ y: pct2(quoteBottomPx + attrGapPx),
2051
2048
  width: accentLayout.textWidth,
2052
- height: pct(attrFit.heightPx),
2049
+ height: pct2(attrFit.heightPx),
2053
2050
  anchor: "top-left"
2054
2051
  },
2055
2052
  animation: { type: "fadeIn", duration: 1, delay: 1.5 }
@@ -2133,12 +2130,12 @@ function factCard(input, context) {
2133
2130
  const factTop = groupTop;
2134
2131
  const explainTop = factTop + factBoxH + gap;
2135
2132
  const sourceTop = explainTop + explainBoxH + sourceGap;
2136
- const pct = (px) => `${Number((px / frameH * 100).toFixed(3))}%`;
2133
+ const pct2 = (px) => `${Number((px / frameH * 100).toFixed(3))}%`;
2137
2134
  const box = (top, height) => ({
2138
2135
  x: accentLayout.textCenterX,
2139
- y: pct(top + height / 2),
2136
+ y: pct2(top + height / 2),
2140
2137
  width: accentLayout.textWidth,
2141
- height: pct(height),
2138
+ height: pct2(height),
2142
2139
  anchor: "center"
2143
2140
  });
2144
2141
  layers.push({
@@ -2965,6 +2962,17 @@ function rightFeature(input, context) {
2965
2962
  return buildFeatureLayers(input, context, "right");
2966
2963
  }
2967
2964
 
2965
+ // src/doc/templates/mediaFeatureBlock.ts
2966
+ function topFeature(input, context) {
2967
+ return contentBlock({ ...input, template: "content" }, context, { omitPictures: true });
2968
+ }
2969
+ function bottomFeature(input, context) {
2970
+ return contentBlock({ ...input, template: "content" }, context, { omitPictures: true });
2971
+ }
2972
+ function mediaOnly(input, context) {
2973
+ return contentBlock({ ...input, template: "content" }, context);
2974
+ }
2975
+
2968
2976
  // src/doc/templates/mapBlock.ts
2969
2977
  function mapBlock(input, context) {
2970
2978
  const {
@@ -3090,7 +3098,7 @@ function fullBleedQuote(input, context) {
3090
3098
  const colors = resolveColorScheme(context, colorScheme);
3091
3099
  const H = viewport.height;
3092
3100
  const W = viewport.width;
3093
- const pct = (px) => `${Number((px / H * 100).toFixed(3))}%`;
3101
+ const pct2 = (px) => `${Number((px / H * 100).toFixed(3))}%`;
3094
3102
  const impactFontSize = themedFontSize(120, context, true);
3095
3103
  const baseFontSize = Math.round(impactFontSize * lengthFactor(text));
3096
3104
  const bandHeightPx = (BAND_BOTTOM3 - BAND_TOP3) * H;
@@ -3141,9 +3149,9 @@ function fullBleedQuote(input, context) {
3141
3149
  },
3142
3150
  position: {
3143
3151
  x: `${COLUMN_LEFT * 100}%`,
3144
- y: pct(topPx),
3152
+ y: pct2(topPx),
3145
3153
  width: `${COLUMN_WIDTH * 100}%`,
3146
- height: pct(fit.heightPx),
3154
+ height: pct2(fit.heightPx),
3147
3155
  anchor: "top-left"
3148
3156
  },
3149
3157
  animation: motionEntrance(
@@ -3605,12 +3613,12 @@ function definitionCard(input, context) {
3605
3613
  const ruleTop = termTop + termH + ruleGap;
3606
3614
  const defTop = ruleTop + RULE_HEIGHT_PX + defGap;
3607
3615
  const originTop = defTop + defH + originGap;
3608
- const pct = (px) => `${(px / viewport.height * 100).toFixed(3)}%`;
3616
+ const pct2 = (px) => `${(px / viewport.height * 100).toFixed(3)}%`;
3609
3617
  const box = (topPx, heightPx) => {
3610
3618
  const height = Math.round(heightPx);
3611
3619
  return {
3612
3620
  x: accentLayout.textCenterX,
3613
- y: pct(topPx + height / 2),
3621
+ y: pct2(topPx + height / 2),
3614
3622
  width: accentLayout.textWidth,
3615
3623
  height,
3616
3624
  anchor: "center"
@@ -3657,7 +3665,7 @@ function definitionCard(input, context) {
3657
3665
  },
3658
3666
  position: {
3659
3667
  x: columnLeft,
3660
- y: pct(ruleTop),
3668
+ y: pct2(ruleTop),
3661
3669
  width: "30%",
3662
3670
  height: `${RULE_HEIGHT_PX}px`,
3663
3671
  anchor: "top-left"
@@ -3883,7 +3891,7 @@ var ATTR_LINE_HEIGHT = 1.4;
3883
3891
  function pullQuoteLockup(options, context) {
3884
3892
  const { text, attribution, overMedia } = options;
3885
3893
  const { theme, viewport } = context;
3886
- const pct = (px) => px / viewport.height * 100;
3894
+ const pct2 = (px) => px / viewport.height * 100;
3887
3895
  const quoteBaseSize = themedFontSize(52, context, true);
3888
3896
  const quoteMinSize = Math.max(18, themedFontSize(24, context, true));
3889
3897
  const attrFontSize = themedFontSize(26, context, false);
@@ -3910,12 +3918,12 @@ function pullQuoteLockup(options, context) {
3910
3918
  const gapPx = attribution ? Math.max(16, fit.fontSize * 0.6) : 0;
3911
3919
  const stackHeightPx = fit.heightPx + gapPx + attrHeightPx;
3912
3920
  const stackTopPx = Math.max(frameTopPx, viewport.height / 2 - stackHeightPx / 2);
3913
- const quoteTopPct = pct(stackTopPx);
3914
- const attrTopPct = pct(stackTopPx + fit.heightPx + gapPx);
3921
+ const quoteTopPct = pct2(stackTopPx);
3922
+ const attrTopPct = pct2(stackTopPx + fit.heightPx + gapPx);
3915
3923
  const decoHeadroomPx = stackTopPx - 0.03 * viewport.height;
3916
3924
  const decoFontSize = Math.min(decoBaseSize, Math.floor(decoHeadroomPx / 0.6));
3917
3925
  const showDeco = decoFontSize >= decoMinSize;
3918
- const decoYPct = quoteTopPct - pct(decoFontSize * 0.55);
3926
+ const decoYPct = quoteTopPct - pct2(decoFontSize * 0.55);
3919
3927
  const shadow = overMedia ? true : shouldUseShadow(context);
3920
3928
  const quoteColor = overMedia ? "#ffffff" : theme.colors.text;
3921
3929
  const attrColor = overMedia ? "rgba(255, 255, 255, 0.85)" : theme.colors.textMuted;
@@ -5524,6 +5532,30 @@ var BLOCK_MEDIA_LAYOUT_POLICIES = {
5524
5532
  additionalMediaLayout: "overlay-inset",
5525
5533
  unconsumedMedia: "retain-native-layout"
5526
5534
  },
5535
+ topFeature: {
5536
+ summary: "Body media spans the full width across the top; the heading and complete body take a band below sized to the text. Without media it is a content slide.",
5537
+ noMedia: "template-default",
5538
+ ownership: "native-slot",
5539
+ nativeLayout: "feature-top",
5540
+ additionalMediaLayout: "overlay-inset",
5541
+ unconsumedMedia: "retain-native-layout"
5542
+ },
5543
+ bottomFeature: {
5544
+ summary: "The heading and complete body take a band across the top sized to the text; body media spans the full width below. Without media it is a content slide.",
5545
+ noMedia: "template-default",
5546
+ ownership: "native-slot",
5547
+ nativeLayout: "feature-bottom",
5548
+ additionalMediaLayout: "overlay-inset",
5549
+ unconsumedMedia: "retain-native-layout"
5550
+ },
5551
+ mediaOnly: {
5552
+ summary: "Body media fills the whole slide with no text; without media the heading and body render as a content slide.",
5553
+ noMedia: "template-default",
5554
+ ownership: "native-slot",
5555
+ nativeLayout: "full-frame",
5556
+ additionalMediaLayout: "overlay-inset",
5557
+ unconsumedMedia: "retain-native-layout"
5558
+ },
5527
5559
  map: {
5528
5560
  summary: "The map canvas is the primary visual; other media must not reflow its geography.",
5529
5561
  noMedia: "intrinsic-visual",
@@ -6198,12 +6230,144 @@ function leastObstructedMediaRect(layers, viewport) {
6198
6230
  return score < bestScore ? candidate : best;
6199
6231
  });
6200
6232
  }
6233
+ var MEDIA_BANDS = {
6234
+ "feature-top": "top",
6235
+ "feature-bottom": "bottom",
6236
+ "full-frame": "full"
6237
+ };
6238
+ var BLOCK_GAP_EM = 0.55;
6239
+ function textLayer(layers, id) {
6240
+ const layer = layers.find((candidate) => candidate.id === id);
6241
+ return layer?.type === "text" ? layer : void 0;
6242
+ }
6243
+ function mediaBandLayout(layers, viewport, band, aspect) {
6244
+ const backdrop = layers.filter((layer) => isFullViewportBackground(layer, viewport));
6245
+ const title = textLayer(layers, "title");
6246
+ const body = textLayer(layers, "body");
6247
+ if (band === "full" || !title && !body) {
6248
+ return { layers: backdrop, mediaRect: rect(viewport, 0, 0, 1, 1), framed: false };
6249
+ }
6250
+ const { width, height } = viewport;
6251
+ const unit = Math.min(width, height);
6252
+ const margin = unit * 0.045;
6253
+ const gap = unit * 0.03;
6254
+ const innerGap = title && body ? unit * 0.02 : 0;
6255
+ const textWidth = width - margin * 2;
6256
+ const maxBandHeight = height * (body ? 0.4 : 0.2);
6257
+ const titleLineHeight = title?.content.style.lineHeight ?? 1.15;
6258
+ const titleBaseSize = Math.max(
6259
+ title?.content.style.fontSize ?? 0,
6260
+ Math.round((body?.content.style.fontSize ?? 0) * 1.2)
6261
+ );
6262
+ const titleWrapWidth = title ? Math.min(
6263
+ textWidth * 0.86,
6264
+ fitWrapWidth(
6265
+ title.content.text,
6266
+ titleBaseSize,
6267
+ textWidth,
6268
+ title.content.style.fontFamily ?? ""
6269
+ )
6270
+ ) : textWidth;
6271
+ const titleFit = title ? fitProse({
6272
+ text: title.content.text,
6273
+ baseFontSize: titleBaseSize,
6274
+ minFontSize: Math.min(titleBaseSize, unit * 0.03),
6275
+ maxWidthPx: titleWrapWidth,
6276
+ maxHeightPx: body ? maxBandHeight * 0.35 : maxBandHeight,
6277
+ lineHeight: titleLineHeight
6278
+ }) : void 0;
6279
+ const titleHeight = titleFit?.heightPx ?? 0;
6280
+ const bodyRoom = maxBandHeight - titleHeight - innerGap;
6281
+ const bodyLineHeight = body?.content.style.lineHeight ?? 1.35;
6282
+ const blockGaps = body ? (body.content.text.match(/\n[^\S\n]*\n/g) ?? []).length : 0;
6283
+ const bodyBaseSize = body?.content.style.fontSize ?? 0;
6284
+ const headroomLines = 1;
6285
+ const bodyText3 = body?.content.text.replace(/\n[^\S\n]*\n/g, "\n") ?? "";
6286
+ const bodyFit = body ? fitProse({
6287
+ text: bodyText3,
6288
+ baseFontSize: bodyBaseSize,
6289
+ minFontSize: Math.min(bodyBaseSize, unit * 0.016),
6290
+ // HTML wraps on real glyphs; wide ones (CJK, emoji) fill lines in
6291
+ // half the characters the estimate assumes, so count lines narrower.
6292
+ maxWidthPx: fitWrapWidth(
6293
+ bodyText3,
6294
+ bodyBaseSize,
6295
+ textWidth,
6296
+ body.content.style.fontFamily ?? ""
6297
+ ),
6298
+ maxHeightPx: bodyRoom - (blockGaps * BLOCK_GAP_EM + headroomLines * bodyLineHeight) * bodyBaseSize,
6299
+ lineHeight: bodyLineHeight
6300
+ }) : void 0;
6301
+ const bodyHeight = bodyFit ? Math.min(
6302
+ bodyRoom,
6303
+ bodyFit.heightPx + (blockGaps * BLOCK_GAP_EM + headroomLines * bodyLineHeight) * bodyFit.fontSize
6304
+ ) : 0;
6305
+ const bandHeight = titleHeight + innerGap + bodyHeight;
6306
+ const roomForMedia = height - margin - gap - bandHeight;
6307
+ const mediaHeight = aspect && Number.isFinite(aspect) && aspect > 0 ? Math.min(roomForMedia, width / aspect) : roomForMedia;
6308
+ const offset = (roomForMedia - mediaHeight) / 2;
6309
+ const mediaTop = band === "top" ? offset : margin + offset + bandHeight + gap;
6310
+ const bandTop = band === "top" ? mediaTop + mediaHeight + gap : margin + offset;
6311
+ const placed = [...backdrop];
6312
+ if (title && titleFit) {
6313
+ placed.push({
6314
+ ...title,
6315
+ position: {
6316
+ x: margin,
6317
+ y: bandTop,
6318
+ width: titleWrapWidth,
6319
+ height: titleHeight,
6320
+ anchor: "top-left"
6321
+ },
6322
+ content: {
6323
+ ...title.content,
6324
+ style: {
6325
+ ...title.content.style,
6326
+ fontSize: titleFit.fontSize,
6327
+ lineHeight: titleLineHeight,
6328
+ ...titleFit.maxLines ? { maxLines: titleFit.maxLines } : {}
6329
+ }
6330
+ }
6331
+ });
6332
+ }
6333
+ if (body && bodyFit) {
6334
+ placed.push({
6335
+ ...body,
6336
+ position: {
6337
+ x: margin,
6338
+ y: bandTop + titleHeight + innerGap,
6339
+ width: textWidth,
6340
+ height: bodyHeight,
6341
+ anchor: "top-left"
6342
+ },
6343
+ content: {
6344
+ ...body.content,
6345
+ style: {
6346
+ ...body.content.style,
6347
+ fontSize: bodyFit.fontSize,
6348
+ lineHeight: bodyLineHeight,
6349
+ ...bodyFit.maxLines ? { maxLines: bodyFit.maxLines } : {}
6350
+ }
6351
+ }
6352
+ });
6353
+ }
6354
+ return {
6355
+ layers: placed,
6356
+ mediaRect: { x: 0, y: mediaTop, width, height: mediaHeight },
6357
+ framed: false
6358
+ };
6359
+ }
6201
6360
  function resolveSupplementalMediaLayout(layers, template, viewport, mediaCount, aspectRatios, featureOptions = {}) {
6202
6361
  const policy = getBlockMediaLayoutPolicy(template);
6203
6362
  const shape = mediaShape(mediaCount, aspectRatios);
6204
6363
  const alreadyOwnsMedia = layers.some(
6205
6364
  (layer) => layer.type === "image" || layer.type === "video" || layer.type === "videoEmbed" || layer.type === "map" || layer.type === "mermaid" || layer.type === "svg"
6206
6365
  );
6366
+ const band = policy?.nativeLayout ? MEDIA_BANDS[policy.nativeLayout] : void 0;
6367
+ if (band && !alreadyOwnsMedia) {
6368
+ const aspect = mediaCount === 1 ? featureOptions.knownAspectRatios?.[0] : void 0;
6369
+ return mediaBandLayout(layers, viewport, band, aspect);
6370
+ }
6207
6371
  if (!alreadyOwnsMedia && (policy?.nativeLayout === "feature-left" || policy?.nativeLayout === "feature-right")) {
6208
6372
  const { position } = featureMediaSlot(
6209
6373
  policy.nativeLayout === "feature-left" ? "left" : "right",
@@ -6884,6 +7048,35 @@ var BOLD_CHAR_WIDTH_EM = 0.66;
6884
7048
  function estimateTextWidth(text, fontSize, charWidthEm = AVERAGE_CHAR_WIDTH_EM) {
6885
7049
  return text.length * fontSize * charWidthEm;
6886
7050
  }
7051
+ var DISPLAY_CHAR_EM = {
7052
+ space: 0.27,
7053
+ narrowLower: 0.36,
7054
+ wideLower: 0.86,
7055
+ lower: 0.57,
7056
+ narrowUpper: 0.4,
7057
+ wideUpper: 0.95,
7058
+ upper: 0.7,
7059
+ digit: 0.57,
7060
+ punct: 0.34,
7061
+ other: 0.62
7062
+ };
7063
+ function displayCharEm(ch) {
7064
+ if (ch === " ") return DISPLAY_CHAR_EM.space;
7065
+ if ("fijlrt".includes(ch)) return DISPLAY_CHAR_EM.narrowLower;
7066
+ if (ch === "m" || ch === "w") return DISPLAY_CHAR_EM.wideLower;
7067
+ if (ch >= "a" && ch <= "z") return DISPLAY_CHAR_EM.lower;
7068
+ if (ch === "I" || ch === "J") return DISPLAY_CHAR_EM.narrowUpper;
7069
+ if (ch === "M" || ch === "W") return DISPLAY_CHAR_EM.wideUpper;
7070
+ if (ch >= "A" && ch <= "Z") return DISPLAY_CHAR_EM.upper;
7071
+ if (ch >= "0" && ch <= "9") return DISPLAY_CHAR_EM.digit;
7072
+ if (".,:;-/'\u2019!?()".includes(ch)) return DISPLAY_CHAR_EM.punct;
7073
+ return DISPLAY_CHAR_EM.other;
7074
+ }
7075
+ function estimateDisplayTextWidth(text, fontSize) {
7076
+ let em = 0;
7077
+ for (const ch of text.normalize("NFD").replace(/\p{M}/gu, "")) em += displayCharEm(ch);
7078
+ return em * fontSize;
7079
+ }
6887
7080
 
6888
7081
  // src/doc/templates/timelineBlock.ts
6889
7082
  var clamp01 = (value) => Math.max(0, Math.min(1, value));
@@ -7773,7 +7966,7 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
7773
7966
  const fill = s.fill ?? defaultFill;
7774
7967
  if (s.kind === "text") {
7775
7968
  out.push(
7776
- textLayer(
7969
+ textLayer2(
7777
7970
  `shape-${s.id}`,
7778
7971
  s.text ?? s.label ?? "",
7779
7972
  x + w / 2,
@@ -7832,7 +8025,7 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
7832
8025
  }
7833
8026
  if (s.label) {
7834
8027
  out.push(
7835
- textLayer(
8028
+ textLayer2(
7836
8029
  `shape-label-${s.id}`,
7837
8030
  s.label,
7838
8031
  x + w / 2,
@@ -7847,7 +8040,7 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
7847
8040
  }
7848
8041
  if (s.sublabel) {
7849
8042
  out.push(
7850
- textLayer(
8043
+ textLayer2(
7851
8044
  `shape-sublabel-${s.id}`,
7852
8045
  s.sublabel,
7853
8046
  x + w / 2,
@@ -7862,7 +8055,7 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
7862
8055
  }
7863
8056
  return out;
7864
8057
  }
7865
- function textLayer(id, text, cx, cy, width, context, color, bold, fontAdj) {
8058
+ function textLayer2(id, text, cx, cy, width, context, color, bold, fontAdj) {
7866
8059
  return {
7867
8060
  type: "text",
7868
8061
  id,
@@ -8407,6 +8600,9 @@ var templateRegistry = {
8407
8600
  imageWithCaption,
8408
8601
  leftFeature,
8409
8602
  rightFeature,
8603
+ topFeature,
8604
+ bottomFeature,
8605
+ mediaOnly,
8410
8606
  map: mapBlock,
8411
8607
  fullBleedQuote,
8412
8608
  list: listBlock,
@@ -8725,6 +8921,9 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
8725
8921
  ],
8726
8922
  leftFeature: FEATURE_DESCRIPTORS(),
8727
8923
  rightFeature: FEATURE_DESCRIPTORS(),
8924
+ topFeature: [{ key: "title", description: "Heading shown with the body under the media" }],
8925
+ bottomFeature: [{ key: "title", description: "Heading shown with the body above the media" }],
8926
+ mediaOnly: [{ key: "title", description: "Heading, shown only when the block has no media" }],
8728
8927
  map: [
8729
8928
  {
8730
8929
  key: "center",
@@ -9421,11 +9620,13 @@ function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRati
9421
9620
  const timedLayers = applyEmbeddedVideoTiming(layers, block);
9422
9621
  const items = collectRichMediaItems(timedLayers, block);
9423
9622
  if (items.length === 0) return timedLayers;
9424
- const aspects = items.map((item) => {
9623
+ const knownAspects = items.map((item) => {
9425
9624
  const key = item.kind === "image" || item.kind === "video" ? item.src : item.kind === "mermaid" ? item.source : void 0;
9426
9625
  const intrinsic = key ? mediaAspectRatios?.[key] : void 0;
9427
- return intrinsic && Number.isFinite(intrinsic) && intrinsic > 0 ? intrinsic : item.aspectRatio;
9626
+ if (intrinsic && Number.isFinite(intrinsic) && intrinsic > 0) return intrinsic;
9627
+ return item.kind === "video" ? void 0 : item.aspectRatio;
9428
9628
  });
9629
+ const aspects = items.map((item, index) => knownAspects[index] ?? item.aspectRatio);
9429
9630
  const layout = block.summaryLayout === "feature" ? resolveFeatureSummaryLayout(timedLayers, viewport, aspects) : resolveSupplementalMediaLayout(
9430
9631
  timedLayers,
9431
9632
  block.template ? resolveTemplateName(block.template) : void 0,
@@ -9434,7 +9635,8 @@ function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRati
9434
9635
  aspects,
9435
9636
  {
9436
9637
  summaryFeature: block.summaryMode === "headings-and-features" && !timedLayers.some((layer) => layer.id === "feature-body"),
9437
- stackColumns: createTemplateContext(theme, 0, 1, viewport).layout.stackColumns
9638
+ stackColumns: createTemplateContext(theme, 0, 1, viewport).layout.stackColumns,
9639
+ knownAspectRatios: knownAspects
9438
9640
  }
9439
9641
  );
9440
9642
  const cells = gridCells(layout.mediaRect, items.length, viewport, layout.framed);
@@ -9596,6 +9798,18 @@ var TEMPLATE_METADATA = {
9596
9798
  label: "Right Feature",
9597
9799
  description: "Image on the right, title and body text stacked on the left."
9598
9800
  },
9801
+ topFeature: {
9802
+ label: "Top Feature",
9803
+ description: "Image or video across the full width at the top, title and body text below."
9804
+ },
9805
+ bottomFeature: {
9806
+ label: "Bottom Feature",
9807
+ description: "Title and body text at the top, image or video across the full width below."
9808
+ },
9809
+ mediaOnly: {
9810
+ label: "Media Only",
9811
+ description: "The image or video fills the slide, with no title, text or caption."
9812
+ },
9599
9813
  map: {
9600
9814
  label: "Map",
9601
9815
  description: "Embeds an interactive map centered on a geographic location."
@@ -9761,6 +9975,24 @@ var TEMPLATE_AUTHORING_METADATA = {
9761
9975
  safeForContentFirst: false,
9762
9976
  placement: "heading"
9763
9977
  },
9978
+ topFeature: {
9979
+ role: "media",
9980
+ bodyPolicy: "complete",
9981
+ safeForContentFirst: false,
9982
+ placement: "heading"
9983
+ },
9984
+ bottomFeature: {
9985
+ role: "media",
9986
+ bodyPolicy: "complete",
9987
+ safeForContentFirst: false,
9988
+ placement: "heading"
9989
+ },
9990
+ mediaOnly: {
9991
+ role: "media",
9992
+ bodyPolicy: "ignored",
9993
+ safeForContentFirst: false,
9994
+ placement: "heading"
9995
+ },
9764
9996
  map: {
9765
9997
  role: "spatial",
9766
9998
  bodyPolicy: "structured",
@@ -9921,13 +10153,227 @@ function coverScrimGradient(background) {
9921
10153
  const stops = COVER_SCRIM_STOPS.map((s) => `${withAlpha(background, s.alpha)} ${s.pos}%`);
9922
10154
  return `linear-gradient(0deg, ${stops.join(", ")})`;
9923
10155
  }
10156
+ var FROSTED_LINE_HEIGHT = 1.12;
10157
+ var FROSTED_MAX_LINES = 3;
10158
+ var FROSTED_SQUARE_SHARE = 0.9;
10159
+ var FROSTED_PAD_X_EM = 0.45;
10160
+ var FROSTED_PAD_Y_EM = 0.3;
10161
+ var FROSTED_RADIUS_EM = 0.3;
10162
+ var FROSTED_BLUR_SHARE = 0.0167;
10163
+ var FROSTED_BOTTOM_MARGIN_PCT = { wide: 7, tall: 17 };
10164
+ var FROSTED_TINT_UNDER_LIGHT_TEXT = "rgba(12, 12, 14, 0.26)";
10165
+ var FROSTED_TINT_UNDER_DARK_TEXT = "rgba(250, 250, 250, 0.5)";
10166
+ var FROSTED_BACKDROP_CONTRAST = 0.65;
10167
+ var COVER_SUBTITLE_BASE_PX = 40;
10168
+ var INK_EM = { xHeight: 0.45, ascent: 0.71, accentedCapital: 0.8, descent: 0.165 };
10169
+ function opticalCentreShiftEm(lines) {
10170
+ const first = (lines[0] ?? "").normalize("NFD");
10171
+ const last = lines[lines.length - 1] ?? "";
10172
+ const top = /[A-Z]\p{M}/u.test(first) ? INK_EM.accentedCapital : /[A-Zbdfhklt0-9]|[a-z]\p{M}/u.test(first) ? INK_EM.ascent : INK_EM.xHeight;
10173
+ const bottom = /[gjpqyQ,;]/.test(last) ? INK_EM.descent : 0;
10174
+ return Math.round(((top - bottom) / 2 - INK_EM.xHeight / 2) * 1e3) / 1e3;
10175
+ }
10176
+ function wrapDisplayLine(line, fontSize, maxWidth) {
10177
+ const lines = [];
10178
+ let current = "";
10179
+ for (const word of line.trim().split(/\s+/).filter(Boolean)) {
10180
+ const candidate = current ? `${current} ${word}` : word;
10181
+ if (!current || estimateDisplayTextWidth(candidate, fontSize) <= maxWidth) {
10182
+ current = candidate;
10183
+ } else {
10184
+ lines.push(current);
10185
+ current = word;
10186
+ }
10187
+ }
10188
+ if (current) lines.push(current);
10189
+ return lines.length > 0 ? lines : [""];
10190
+ }
10191
+ function balancedDisplayLines(line, fontSize, maxWidth) {
10192
+ const greedy = wrapDisplayLine(line, fontSize, maxWidth);
10193
+ if (greedy.length < 2) return greedy;
10194
+ let lo = Math.max(
10195
+ ...line.split(/\s+/).filter(Boolean).map((word) => estimateDisplayTextWidth(word, fontSize))
10196
+ );
10197
+ let hi = maxWidth;
10198
+ if (lo >= hi) return greedy;
10199
+ while (hi - lo > 0.5) {
10200
+ const mid = (lo + hi) / 2;
10201
+ if (wrapDisplayLine(line, fontSize, mid).length <= greedy.length) hi = mid;
10202
+ else lo = mid;
10203
+ }
10204
+ return wrapDisplayLine(line, fontSize, hi);
10205
+ }
10206
+ function frostedCoverLayout(title, subtitle, context) {
10207
+ const { width: frameW, height: frameH } = context.viewport;
10208
+ const shortSide = Math.min(frameW, frameH);
10209
+ const maxPlateWidth = FROSTED_SQUARE_SHARE * shortSide;
10210
+ const minSize = themedFontSize(COVER_TITLE_MIN_BASE_PX, context, true);
10211
+ const wrapAt = (size) => {
10212
+ const column2 = maxPlateWidth - 2 * FROSTED_PAD_X_EM * size;
10213
+ const lines2 = title.split("\n").flatMap((line) => balancedDisplayLines(line, size, column2));
10214
+ const width = Math.max(...lines2.map((line) => estimateDisplayTextWidth(line, size)));
10215
+ return { lines: lines2, width, column: column2 };
10216
+ };
10217
+ let fontSize = themedFontSize(fitCoverTitleSize(title), context, true);
10218
+ let wrapped = wrapAt(fontSize);
10219
+ while ((wrapped.lines.length > FROSTED_MAX_LINES || wrapped.width > wrapped.column) && fontSize > minSize) {
10220
+ fontSize = Math.max(minSize, Math.floor(fontSize * 0.95));
10221
+ wrapped = wrapAt(fontSize);
10222
+ }
10223
+ const { lines, column } = wrapped;
10224
+ const textWidth = Math.min(wrapped.width, column);
10225
+ const textHeight = lines.length * fontSize * FROSTED_LINE_HEIGHT;
10226
+ const padX = FROSTED_PAD_X_EM * fontSize;
10227
+ const padY = FROSTED_PAD_Y_EM * fontSize;
10228
+ let sub;
10229
+ if (subtitle?.trim()) {
10230
+ const subSize = themedFontSize(COVER_SUBTITLE_BASE_PX, context, false);
10231
+ const subLines = subtitle.split("\n").flatMap((line) => wrapDisplayLine(line, subSize, column));
10232
+ sub = {
10233
+ lines: subLines,
10234
+ width: Math.min(
10235
+ column,
10236
+ Math.max(...subLines.map((l) => estimateDisplayTextWidth(l, subSize)))
10237
+ ),
10238
+ height: subLines.length * subSize * 1.3,
10239
+ fontSize: subSize,
10240
+ gap: 0.3 * fontSize
10241
+ };
10242
+ }
10243
+ const plateWidth = Math.max(textWidth, sub?.width ?? 0) + 2 * padX;
10244
+ const plateHeight = textHeight + (sub ? sub.gap + sub.height : 0) + 2 * padY;
10245
+ const marginPct = frameH > frameW ? FROSTED_BOTTOM_MARGIN_PCT.tall : FROSTED_BOTTOM_MARGIN_PCT.wide;
10246
+ const plateBottom = frameH * (1 - marginPct / 100);
10247
+ const plateY = plateBottom - plateHeight;
10248
+ const cx = frameW / 2;
10249
+ const longestChars = Math.max(...lines.map((line) => line.length));
10250
+ const titleBoxWidth = Math.max(textWidth, longestChars * fontSize * 0.5 + 1);
10251
+ return {
10252
+ fontSize,
10253
+ lines,
10254
+ plate: {
10255
+ x: cx - plateWidth / 2,
10256
+ y: plateY,
10257
+ width: plateWidth,
10258
+ height: plateHeight,
10259
+ radius: FROSTED_RADIUS_EM * fontSize
10260
+ },
10261
+ // The box spans the plate's padding too: shrink-to-fit measures the font's whole
10262
+ // ascent and descent (about 1.3 em), which overflows a box of 1.12 em lines and
10263
+ // would scale every title down by up to 14%.
10264
+ title: {
10265
+ cx,
10266
+ cy: plateY + padY + textHeight / 2,
10267
+ width: titleBoxWidth,
10268
+ height: textHeight + 2 * padY
10269
+ },
10270
+ ...sub ? {
10271
+ subtitle: {
10272
+ cx,
10273
+ cy: plateY + padY + textHeight + sub.gap + sub.height / 2,
10274
+ width: Math.max(
10275
+ sub.width,
10276
+ Math.max(...sub.lines.map((l) => l.length)) * sub.fontSize * 0.5 + 1
10277
+ ),
10278
+ height: sub.height,
10279
+ fontSize: sub.fontSize,
10280
+ lines: sub.lines
10281
+ }
10282
+ } : {},
10283
+ baselineShift: opticalCentreShiftEm(lines),
10284
+ blur: Math.round(FROSTED_BLUR_SHARE * shortSide)
10285
+ };
10286
+ }
10287
+ function pct(value, total) {
10288
+ return `${Math.round(value / total * 1e4) / 100}%`;
10289
+ }
10290
+ function frostedCoverLayers(input, context) {
10291
+ const { theme, viewport } = context;
10292
+ const layout = frostedCoverLayout(input.title, input.subtitle, context);
10293
+ const lightText = relativeLuminance(theme.colors.text) > 0.5;
10294
+ const { plate } = layout;
10295
+ const layers = [
10296
+ {
10297
+ type: "shape",
10298
+ id: "cover-plate",
10299
+ content: {
10300
+ shape: "rect",
10301
+ fill: lightText ? FROSTED_TINT_UNDER_LIGHT_TEXT : FROSTED_TINT_UNDER_DARK_TEXT,
10302
+ borderRadius: Math.round(plate.radius),
10303
+ backdropBlur: layout.blur,
10304
+ ...lightText ? { backdropContrast: FROSTED_BACKDROP_CONTRAST } : {}
10305
+ },
10306
+ position: {
10307
+ x: pct(plate.x, viewport.width),
10308
+ y: pct(plate.y, viewport.height),
10309
+ width: pct(plate.width, viewport.width),
10310
+ height: pct(plate.height, viewport.height)
10311
+ }
10312
+ },
10313
+ {
10314
+ type: "text",
10315
+ id: "cover-title",
10316
+ content: {
10317
+ text: layout.lines.join("\n"),
10318
+ style: {
10319
+ fontSize: layout.fontSize,
10320
+ fontFamily: getThemeFont(context, "title"),
10321
+ fontWeight: "bold",
10322
+ color: theme.colors.text,
10323
+ textAlign: "center",
10324
+ verticalAlign: "middle",
10325
+ lineHeight: FROSTED_LINE_HEIGHT,
10326
+ baselineShift: layout.baselineShift,
10327
+ shadow: lightText,
10328
+ shrinkToFit: true
10329
+ }
10330
+ },
10331
+ position: {
10332
+ x: "50%",
10333
+ y: pct(layout.title.cy, viewport.height),
10334
+ anchor: "center",
10335
+ width: pct(layout.title.width, viewport.width),
10336
+ height: pct(layout.title.height, viewport.height)
10337
+ },
10338
+ animation: getMotionProfile(context).coverWordReveal ? { type: "wordReveal", duration: 0.5, delay: 0.1, stagger: 0.12 } : { type: "fadeIn", duration: 0.8 }
10339
+ }
10340
+ ];
10341
+ if (layout.subtitle) {
10342
+ layers.push({
10343
+ type: "text",
10344
+ id: "cover-subtitle",
10345
+ content: {
10346
+ text: layout.subtitle.lines.join("\n"),
10347
+ style: {
10348
+ fontSize: layout.subtitle.fontSize,
10349
+ fontFamily: getThemeFont(context, "body"),
10350
+ fontWeight: "normal",
10351
+ color: lightText ? withAlpha(theme.colors.text, 0.86) : theme.colors.textMuted,
10352
+ textAlign: "center",
10353
+ verticalAlign: "middle",
10354
+ lineHeight: 1.3,
10355
+ shrinkToFit: true
10356
+ }
10357
+ },
10358
+ position: {
10359
+ x: "50%",
10360
+ y: pct(layout.subtitle.cy, viewport.height),
10361
+ anchor: "center",
10362
+ width: pct(layout.subtitle.width, viewport.width),
10363
+ height: pct(layout.subtitle.height, viewport.height)
10364
+ },
10365
+ animation: motionEntrance(context, { type: "fadeIn", duration: 0.8, delay: 0.2 })
10366
+ });
10367
+ }
10368
+ return layers;
10369
+ }
9924
10370
  function coverBlock(input, context) {
9925
10371
  const treatment = themedImageTreatment(context, input.imageTreatment);
9926
10372
  const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
9927
10373
  const { theme, layout, viewport } = context;
9928
10374
  const standardLightCover = !heroSrc && isStandardLightCover(context);
9929
10375
  const titleFontSize = themedFontSize(fitCoverTitleSize(title), context, true);
9930
- const subtitleFontSize = themedFontSize(40, context, false);
10376
+ const subtitleFontSize = themedFontSize(COVER_SUBTITLE_BASE_PX, context, false);
9931
10377
  const titleYPct = subtitle ? parseFloat(layout.primaryY) : 50;
9932
10378
  const maxWidthPx = parseFloat(layout.maxTextWidth) / 100 * viewport.width;
9933
10379
  const titleH = Math.max(
@@ -9951,31 +10397,30 @@ function coverBlock(input, context) {
9951
10397
  const layers = [];
9952
10398
  if (heroSrc) {
9953
10399
  const imageAnimation = mapAmbientMotion(ambientMotion);
9954
- layers.push(
9955
- {
9956
- type: "image",
9957
- id: "cover-hero",
9958
- content: {
9959
- src: heroSrc,
9960
- alt: heroAlt || title,
9961
- fit: "cover",
9962
- credit: heroCredit,
9963
- license: heroLicense,
9964
- ...treatment ? { treatment } : {}
9965
- },
9966
- position: { x: 0, y: 0, width: "100%", height: "100%" },
9967
- animation: imageAnimation
10400
+ layers.push({
10401
+ type: "image",
10402
+ id: "cover-hero",
10403
+ content: {
10404
+ src: heroSrc,
10405
+ alt: heroAlt || title,
10406
+ fit: "cover",
10407
+ credit: heroCredit,
10408
+ license: heroLicense,
10409
+ ...treatment ? { treatment } : {}
9968
10410
  },
9969
- {
9970
- type: "shape",
9971
- id: "cover-gradient",
9972
- content: {
9973
- shape: "rect",
9974
- fill: coverScrimGradient(theme.colors.background)
9975
- },
9976
- position: { x: 0, y: 0, width: "100%", height: "100%" }
9977
- }
9978
- );
10411
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
10412
+ animation: imageAnimation
10413
+ });
10414
+ if (input.coverStyle === "frosted") return [...layers, ...frostedCoverLayers(input, context)];
10415
+ layers.push({
10416
+ type: "shape",
10417
+ id: "cover-gradient",
10418
+ content: {
10419
+ shape: "rect",
10420
+ fill: coverScrimGradient(theme.colors.background)
10421
+ },
10422
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
10423
+ });
9979
10424
  } else {
9980
10425
  if (standardLightCover) {
9981
10426
  layers.push(
@@ -10084,7 +10529,8 @@ function startBlockToCoverInput(config) {
10084
10529
  subtitle: config.subtitle,
10085
10530
  ambientMotion: config.ambientMotion,
10086
10531
  heroCredit: config.heroCredit,
10087
- heroLicense: config.heroLicense
10532
+ heroLicense: config.heroLicense,
10533
+ ...config.coverStyle ? { coverStyle: config.coverStyle } : {}
10088
10534
  };
10089
10535
  }
10090
10536
  function expandCoverBlock(config, context, template = "cover") {
@@ -10653,193 +11099,6 @@ function extractTableData(table) {
10653
11099
  };
10654
11100
  }
10655
11101
 
10656
- // src/doc/standaloneAnnotation.ts
10657
- function parseAnnotationInner(inner) {
10658
- const tokens = tokenizeAttrTokens(inner.trim());
10659
- const firstIsParam = tokens.length > 0 && tokens[0].indexOf("=") > 0;
10660
- const template = firstIsParam || tokens.length === 0 ? void 0 : tokens[0];
10661
- const startIdx = firstIsParam ? 0 : 1;
10662
- const params = {};
10663
- for (let i = startIdx; i < tokens.length; i++) {
10664
- const kv = splitKeyValueToken(tokens[i]);
10665
- if (kv) params[kv.key] = kv.value;
10666
- }
10667
- return { template, params };
10668
- }
10669
- function parseStandaloneAnnotation(node) {
10670
- if (node.type !== "paragraph") return null;
10671
- const children = node.children ?? [];
10672
- if (children.length !== 1 || children[0].type !== "text") return null;
10673
- const text = children[0].value.trim();
10674
- const match = matchTrailingTemplateAnnotation(text);
10675
- if (!match || match.index !== 0) return null;
10676
- return parseAnnotationInner(match.inner);
10677
- }
10678
-
10679
- // src/doc/mediaAnnotations.ts
10680
- var MEDIA_TEMPLATES = /* @__PURE__ */ new Set(["audio", "video", "media"]);
10681
- function isMediaAnnotationName(name) {
10682
- return name != null && MEDIA_TEMPLATES.has(name);
10683
- }
10684
- function time(raw) {
10685
- if (raw == null) return null;
10686
- return parseTimeSeconds(raw);
10687
- }
10688
- function videoPlacement(params) {
10689
- const explicit = params.placement ?? params.presentation;
10690
- if (explicit === "content") return "content";
10691
- if (explicit === "pip" || explicit === "picture-in-picture") return "picture-in-picture";
10692
- if (explicit === "overlay" || explicit === "full-frame") return "overlay";
10693
- if (params.pip === "" || params.pip === "true") return "picture-in-picture";
10694
- if (params.overlay === "" || params.overlay === "true") return "overlay";
10695
- return void 0;
10696
- }
10697
- function lockToBlock(raw) {
10698
- return raw !== "false" && raw !== "0";
10699
- }
10700
- function videoPipSize(raw) {
10701
- return raw === "small" || raw === "large" ? raw : void 0;
10702
- }
10703
- function videoPipShape(raw) {
10704
- return raw === "square" || raw === "wide" ? raw : void 0;
10705
- }
10706
- function videoPipPosition(raw) {
10707
- return raw === "top-left" || raw === "top-right" || raw === "bottom-left" || raw === "bottom-right" ? raw : void 0;
10708
- }
10709
- function toMediaClip(parsed, id) {
10710
- const { template, params } = parsed;
10711
- if (!template || !MEDIA_TEMPLATES.has(template)) return null;
10712
- const src = params.src;
10713
- if (!src) return null;
10714
- const kind = template === "video" ? "video" : "audio";
10715
- const anchor = params.anchor === "document" || params.span === "document" ? "document" : "block";
10716
- const clip = {
10717
- id,
10718
- src,
10719
- kind,
10720
- startAt: time(params.startAt ?? params.startTime) ?? 0,
10721
- anchor
10722
- };
10723
- const placement = kind === "video" ? videoPlacement(params) : void 0;
10724
- if (placement && placement !== "content") clip.placement = placement;
10725
- const pipSize = videoPipSize(params.pipSize);
10726
- const pipShape = videoPipShape(params.pipShape);
10727
- const pipPosition = videoPipPosition(params.pipPosition);
10728
- if (pipSize) clip.pipSize = pipSize;
10729
- if (pipShape) clip.pipShape = pipShape;
10730
- if (pipPosition) clip.pipPosition = pipPosition;
10731
- if (kind === "video" && placement && placement !== "content" && params.lockToBlock != null) {
10732
- const locked = lockToBlock(params.lockToBlock);
10733
- clip.lockToBlock = locked;
10734
- clip.anchor = locked ? "block" : "document";
10735
- if (locked) clip.startAt = 0;
10736
- }
10737
- const clipStart = time(params.clipStart);
10738
- const clipEnd = time(params.clipEnd);
10739
- if (clipStart != null) clip.clipStart = clipStart;
10740
- if (clipEnd != null) clip.clipEnd = clipEnd;
10741
- if (clip.lockToBlock === true) delete clip.clipEnd;
10742
- if (params.spillover === "true" || params.spillover === "") clip.spillover = true;
10743
- const edits = parseMediaEdits(mediaEditValuesFromParams(params));
10744
- if (edits) clip.edits = edits;
10745
- return clip;
10746
- }
10747
- function firstVideoElement(children) {
10748
- const visit = (nodes) => {
10749
- for (const child of nodes) {
10750
- if (child.type !== "htmlElement") continue;
10751
- if (child.tagName === "video") return child;
10752
- const nested = visit(child.children);
10753
- if (nested) return nested;
10754
- }
10755
- return null;
10756
- };
10757
- return visit(children);
10758
- }
10759
- function standaloneHtmlVideo(node) {
10760
- if (node.type === "htmlBlock") {
10761
- if (!/^\s*<video\b/i.test(node.rawHtml)) return null;
10762
- const element = firstVideoElement(node.htmlChildren);
10763
- return element ? { element, raw: node.rawHtml.trim() } : null;
10764
- }
10765
- if (node.type !== "paragraph" || !node.children.every((child) => child.type === "htmlInline")) {
10766
- return null;
10767
- }
10768
- const raw = node.children.map((child) => child.type === "htmlInline" ? child.rawHtml : "").join("");
10769
- if (!/^\s*<video\b[\s\S]*<\/video>\s*$/i.test(raw)) return null;
10770
- for (const child of node.children) {
10771
- if (child.type !== "htmlInline") continue;
10772
- const element = firstVideoElement(child.htmlChildren);
10773
- if (element) return { element, raw: raw.trim() };
10774
- }
10775
- return null;
10776
- }
10777
- function htmlVideoClip(node, id) {
10778
- const authored = standaloneHtmlVideo(node);
10779
- if (!authored) return null;
10780
- const { element } = authored;
10781
- const placement = videoPlacement({
10782
- placement: element.attributes["data-squisq-video-placement"] ?? ""
10783
- });
10784
- if (!placement || placement === "content") return null;
10785
- const src = element.attributes.src;
10786
- if (!src) return null;
10787
- const locked = lockToBlock(element.attributes["data-squisq-video-lock-to-block"]);
10788
- const clip = {
10789
- id,
10790
- src,
10791
- kind: "video",
10792
- placement,
10793
- lockToBlock: locked,
10794
- startAt: locked ? 0 : time(element.attributes["data-squisq-video-start-at"]) ?? 0,
10795
- anchor: locked ? "block" : "document"
10796
- };
10797
- const pipSize = videoPipSize(element.attributes["data-squisq-video-pip-size"]);
10798
- const pipShape = videoPipShape(element.attributes["data-squisq-video-pip-shape"]);
10799
- const pipPosition = videoPipPosition(element.attributes["data-squisq-video-pip-position"]);
10800
- if (pipSize) clip.pipSize = pipSize;
10801
- if (pipShape) clip.pipShape = pipShape;
10802
- if (pipPosition) clip.pipPosition = pipPosition;
10803
- const clipStart = time(element.attributes["data-squisq-video-clip-start"]);
10804
- const clipEnd = time(element.attributes["data-squisq-video-clip-end"]);
10805
- if (clipStart != null) clip.clipStart = clipStart;
10806
- if (!locked && clipEnd != null) clip.clipEnd = clipEnd;
10807
- const edits = parseMediaEdits(mediaEditValuesFromAttributes("video", element.attributes));
10808
- if (edits) clip.edits = edits;
10809
- return clip;
10810
- }
10811
- function extractMediaFromContents(contents, makeId, blockId) {
10812
- const media = [];
10813
- const documentMedia = [];
10814
- const remaining = [];
10815
- let index = 0;
10816
- let originalIndex = 0;
10817
- for (const node of contents ?? []) {
10818
- const parsed = parseStandaloneAnnotation(node);
10819
- const clip = parsed ? toMediaClip(parsed, "") : htmlVideoClip(node, "");
10820
- if (clip) {
10821
- clip.id = makeId(clip.src, index++);
10822
- const line = node.position?.start.line;
10823
- if (line != null) clip.sourceLine = line;
10824
- if (blockId !== void 0) {
10825
- const authoredHtml = standaloneHtmlVideo(node);
10826
- const rawText = node.type === "paragraph" && node.children?.[0]?.type === "text" ? node.children[0].value.trim() : authoredHtml?.raw;
10827
- clip.origin = {
10828
- blockId,
10829
- index: originalIndex,
10830
- ...rawText !== void 0 ? { raw: rawText } : {},
10831
- ...authoredHtml ? { format: "html" } : {}
10832
- };
10833
- }
10834
- (clip.anchor === "document" ? documentMedia : media).push(clip);
10835
- } else {
10836
- remaining.push(node);
10837
- }
10838
- originalIndex++;
10839
- }
10840
- return { media, documentMedia, remaining };
10841
- }
10842
-
10843
11102
  // src/doc/annotationBlocks.ts
10844
11103
  function applyBlockMeta(block, m) {
10845
11104
  if (m.build) block.build = m.build;
@@ -11841,7 +12100,7 @@ function resolveBuildTimeline(steps, spec, duration, timing) {
11841
12100
  let consumed = 0;
11842
12101
  let previous = 0;
11843
12102
  const matched = timing?.sourceText === buildSourceText(steps) && timing.starts.length === steps.length && timing.starts.every(
11844
- (time2, i) => Number.isFinite(time2) && time2 >= 0 && (i === 0 || time2 >= timing.starts[i - 1])
12103
+ (time, i) => Number.isFinite(time) && time >= 0 && (i === 0 || time >= timing.starts[i - 1])
11845
12104
  );
11846
12105
  const starts = steps.map((_, i) => {
11847
12106
  const automatic = intro + Math.max(0, latest - intro) * consumed / Math.max(1, total);
@@ -11858,15 +12117,15 @@ function resolveBuildTimeline(steps, spec, duration, timing) {
11858
12117
  );
11859
12118
  return { starts, entrance, compressed: steps.length > 0 && end < steps.length * 0.8 };
11860
12119
  }
11861
- function buildCursorAtTime(timeline, time2) {
12120
+ function buildCursorAtTime(timeline, time) {
11862
12121
  let revealed = 0;
11863
12122
  for (const start of timeline.starts) {
11864
- if (time2 < start) break;
12123
+ if (time < start) break;
11865
12124
  revealed++;
11866
12125
  }
11867
12126
  return {
11868
12127
  revealed,
11869
- progress: revealed === 0 ? 0 : timeline.entrance === 0 ? 1 : Math.min(1, Math.max(0, (time2 - timeline.starts[revealed - 1]) / timeline.entrance))
12128
+ progress: revealed === 0 ? 0 : timeline.entrance === 0 ? 1 : Math.min(1, Math.max(0, (time - timeline.starts[revealed - 1]) / timeline.entrance))
11870
12129
  };
11871
12130
  }
11872
12131
  function buildTargetState(index, steps, cursor, spec, motion = true) {
@@ -11937,13 +12196,13 @@ function resolveDocBuildTiming(doc, blocks) {
11937
12196
  }
11938
12197
 
11939
12198
  // src/doc/templates/contentBlock.ts
11940
- function bodyNodes(context) {
12199
+ function bodyNodes(context, omitPictures) {
11941
12200
  if (context.block?.summaryMode === "headings-and-features") return [];
11942
12201
  return (context.block?.contents ?? []).filter(
11943
12202
  // Mermaid source and hosted-video links are already materialized as
11944
12203
  // visual layers (a diagram, a player). Repeating them as prose makes the
11945
12204
  // loss-averse default noisy without preserving any additional meaning.
11946
- (node) => !isMaterializedMediaNode(node)
12205
+ (node) => !isMaterializedMediaNode(node) && !(omitPictures && isPictureParagraph(node))
11947
12206
  );
11948
12207
  }
11949
12208
  function listParagraphs(list, level = 0) {
@@ -11988,10 +12247,10 @@ function bodyFontSize(body, context) {
11988
12247
  const base = body.length > 1800 ? 18 : body.length > 1100 ? 21 : body.length > 650 ? 24 : 28;
11989
12248
  return themedFontSize(base, context, false);
11990
12249
  }
11991
- function contentBlock(input, context) {
12250
+ function contentBlock(input, context, options = {}) {
11992
12251
  const { theme } = context;
11993
12252
  const title = input.title ?? context.block?.title ?? "";
11994
- const markdown = bodyNodes(context);
12253
+ const markdown = bodyNodes(context, options.omitPictures === true);
11995
12254
  const paragraphs = completeBodyParagraphs(markdown);
11996
12255
  const body = paragraphs.map(plainParagraphText).join("\n");
11997
12256
  const builds = planContentBuilds(markdown, input.build ?? context.block?.build);
@@ -12093,6 +12352,9 @@ export {
12093
12352
  imageWithCaption,
12094
12353
  leftFeature,
12095
12354
  rightFeature,
12355
+ topFeature,
12356
+ bottomFeature,
12357
+ mediaOnly,
12096
12358
  mapBlock,
12097
12359
  fullBleedQuote,
12098
12360
  listBlock,
@@ -12149,7 +12411,6 @@ export {
12149
12411
  parseYamlSubset,
12150
12412
  findFirstTable,
12151
12413
  extractTableData,
12152
- parseStandaloneAnnotation,
12153
12414
  getPinnedBlockMeta,
12154
12415
  markdownToDoc,
12155
12416
  flattenBlocks,