@bendyline/squisq 2.2.0 → 2.3.0

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/NOTICE.md +3 -3
  2. package/README.md +8 -8
  3. package/dist/{Doc-BubIpWsp.d.ts → Doc-COe-rPQB.d.ts} +6 -4
  4. package/dist/{ImageEditDoc-CGxk5CHc.d.ts → ImageEditDoc-dtKahVyD.d.ts} +1 -1
  5. package/dist/{chunk-DYPMOPIW.js → chunk-EK446BUE.js} +1 -1
  6. package/dist/{chunk-7ARJZYQY.js → chunk-GIIOY6XR.js} +2048 -1200
  7. package/dist/{chunk-I2O6W7DX.js → chunk-QNTQIEIV.js} +16 -5
  8. package/dist/{chunk-FDYDYL77.js → chunk-QWCFK5FN.js} +1 -0
  9. package/dist/{chunk-CB45YGAM.js → chunk-SPTY4C6F.js} +134 -2
  10. package/dist/doc/index.d.ts +289 -6
  11. package/dist/doc/index.js +13 -4
  12. package/dist/generate/index.d.ts +1 -1
  13. package/dist/generate/index.js +0 -1
  14. package/dist/icons/index.js +0 -1
  15. package/dist/imageEdit/index.d.ts +3 -3
  16. package/dist/index.d.ts +6 -6
  17. package/dist/index.js +17 -10
  18. package/dist/jsonForm/index.d.ts +1 -1
  19. package/dist/jsonForm/index.js +1 -1
  20. package/dist/{materializePageSection-wSLA2A1g.d.ts → materializePageSection-BSMOocve.d.ts} +1 -1
  21. package/dist/narration/index.d.ts +1 -1
  22. package/dist/narration/index.js +3 -4
  23. package/dist/random/index.js +0 -1
  24. package/dist/recommend/index.js +1 -2
  25. package/dist/schemas/index.d.ts +17 -5
  26. package/dist/schemas/index.js +3 -1
  27. package/dist/{themeLibrary-DU9mUmVl.d.ts → themeLibrary-BUTWjufL.d.ts} +1 -1
  28. package/dist/timing/index.js +0 -1
  29. package/dist/transform/index.d.ts +2 -2
  30. package/package.json +5 -2
  31. package/dist/chunk-2L55443L.js +0 -0
  32. package/dist/chunk-QKPXHDLR.js +0 -0
  33. package/dist/chunk-WV5ARNDA.js +0 -0
  34. package/dist/chunk-XWNNA3KE.js +0 -0
  35. package/dist/chunk-YZNGMV3G.js +0 -0
@@ -20,7 +20,7 @@ import {
20
20
  parseAsciiTimeline,
21
21
  pickAutoTemplate,
22
22
  profileBlockContents
23
- } from "./chunk-FDYDYL77.js";
23
+ } from "./chunk-QWCFK5FN.js";
24
24
  import {
25
25
  DEFAULT_THEME,
26
26
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
@@ -37,6 +37,7 @@ import {
37
37
  cloneData,
38
38
  createTemplateContext,
39
39
  getTwoColumnPositions,
40
+ getViewportOrientation,
40
41
  isPersistentLayerTemplate,
41
42
  isTemplateBlock,
42
43
  scaledFontSize2 as scaledFontSize
@@ -1034,10 +1035,11 @@ function statHighlight(input, context) {
1034
1035
 
1035
1036
  // src/doc/templates/quoteBlock.ts
1036
1037
  function quoteBlock(input, context) {
1037
- const { quote, attribution, accentImage } = input;
1038
+ const { title, quote, attribution, accentImage } = input;
1038
1039
  const { theme, viewport } = context;
1039
1040
  const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1040
1041
  const quoteFontSize = themedFontSize(48, context, true);
1042
+ const titleFontSize = themedFontSize(34, context, true);
1041
1043
  const attrFontSize = themedFontSize(26, context, false);
1042
1044
  const quoteLineHeight = Math.max(theme.typography.titleLineHeight ?? 1.4, 1.25);
1043
1045
  const decorativeQuoteFontSize = themedFontSize(280, context, true);
@@ -1051,6 +1053,30 @@ function quoteBlock(input, context) {
1051
1053
  )
1052
1054
  );
1053
1055
  }
1056
+ if (title) {
1057
+ layers.push({
1058
+ type: "text",
1059
+ id: "quote-title",
1060
+ content: {
1061
+ text: title,
1062
+ style: {
1063
+ fontSize: titleFontSize,
1064
+ fontFamily: getThemeFont(context, "body"),
1065
+ fontWeight: "bold",
1066
+ color: theme.colors.textMuted,
1067
+ textAlign: "center",
1068
+ shadow: shouldUseShadow(context)
1069
+ }
1070
+ },
1071
+ position: {
1072
+ x: accentLayout.textCenterX,
1073
+ y: adjustY("14%", accentLayout),
1074
+ width: accentLayout.textWidth,
1075
+ anchor: "center"
1076
+ },
1077
+ animation: { type: "fadeIn", duration: 0.8 }
1078
+ });
1079
+ }
1054
1080
  layers.push({
1055
1081
  type: "text",
1056
1082
  id: "deco-quote",
@@ -1069,7 +1095,7 @@ function quoteBlock(input, context) {
1069
1095
  anchor: "center"
1070
1096
  }
1071
1097
  });
1072
- const quoteYPct = attribution ? 45 : 50;
1098
+ const quoteYPct = attribution ? 45 : title ? 52 : 50;
1073
1099
  layers.push({
1074
1100
  type: "text",
1075
1101
  id: "quote",
@@ -2576,7 +2602,7 @@ function videoWithCaption(input, context) {
2576
2602
  videoSrc,
2577
2603
  posterSrc,
2578
2604
  videoAlt,
2579
- clipStart,
2605
+ clipStart = 0,
2580
2606
  clipEnd,
2581
2607
  sourceDuration,
2582
2608
  caption: rawCaption,
@@ -2586,6 +2612,7 @@ function videoWithCaption(input, context) {
2586
2612
  } = input;
2587
2613
  const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
2588
2614
  const { theme } = context;
2615
+ const resolvedClipEnd = Math.max(clipStart, clipEnd ?? clipStart + Math.max(0, input.duration));
2589
2616
  const captionFontSize = themedFontSize(30, context, false);
2590
2617
  const creditFontSize = themedFontSize(16, context, false);
2591
2618
  const layers = [
@@ -2599,7 +2626,7 @@ function videoWithCaption(input, context) {
2599
2626
  alt: videoAlt,
2600
2627
  fit: "cover",
2601
2628
  clipStart,
2602
- clipEnd,
2629
+ clipEnd: resolvedClipEnd,
2603
2630
  sourceDuration,
2604
2631
  credit: videoCredit,
2605
2632
  license: videoLicense
@@ -4943,6 +4970,7 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
4943
4970
  { key: "colorScheme", description: "Color scheme for the stat", valueHint: COLOR_SCHEME_HINT }
4944
4971
  ],
4945
4972
  quote: [
4973
+ { key: "title", description: "Optional heading above the quote" },
4946
4974
  { key: "quote", description: "The quote text" },
4947
4975
  { key: "attribution", description: "Attribution (author, source)" }
4948
4976
  ],
@@ -5265,1298 +5293,2111 @@ function extractBodyText(block) {
5265
5293
  return `${text.slice(0, MAX_BODY_CHARS).trimEnd()}\u2026`;
5266
5294
  }
5267
5295
 
5268
- // src/doc/materializeBlockLayers.ts
5269
- function materializeBlockLayers(block, options = {}) {
5270
- const registry = options.customTemplates && options.customTemplates.length > 0 ? buildRegistry(options.customTemplates) : templateRegistry;
5271
- return materializeBlockLayersWithRuntime(block, options, { registry });
5296
+ // src/doc/asciiDiagram/types.ts
5297
+ var ASCII_CHAR_W = 14;
5298
+ var ASCII_CHAR_H = 28;
5299
+
5300
+ // src/doc/asciiDiagram/mapping.ts
5301
+ function asciiCellToCanvas(col, row) {
5302
+ return { x: col * ASCII_CHAR_W, y: row * ASCII_CHAR_H };
5272
5303
  }
5273
- function materializeBlockLayersWithRuntime(block, options, runtime) {
5274
- const theme = options.theme ?? DEFAULT_THEME;
5275
- const viewport = options.viewport ?? VIEWPORT_PRESETS.landscape;
5276
- const blockIndex = options.blockIndex ?? 0;
5277
- const totalBlocks = options.totalBlocks ?? 1;
5278
- const effectivePersistentLayers = options.persistentLayers === false ? void 0 : options.persistentLayers ?? theme.persistentLayers;
5279
- const existingLayers = block.layers;
5280
- if (existingLayers && existingLayers.length > 0 && !isTemplateBlock(block)) {
5281
- const layers = appendMermaidLayers(cloneData(existingLayers), block, theme, viewport);
5282
- return {
5283
- layers: injectPersistentLayers(
5284
- layers,
5285
- block,
5286
- theme,
5287
- effectivePersistentLayers,
5288
- runtime.expandedPersistentLayers
5289
- ),
5290
- source: "authored"
5291
- };
5292
- }
5293
- if (!isTemplateBlock(block)) {
5294
- const layers = appendMermaidLayers([], block, theme, viewport);
5295
- return {
5296
- layers: injectPersistentLayers(
5297
- layers,
5298
- block,
5299
- theme,
5300
- effectivePersistentLayers,
5301
- runtime.expandedPersistentLayers
5302
- ),
5303
- source: layers.length > 0 ? "rich-content" : "empty"
5304
- };
5305
- }
5306
- const context = runtime.templateContext ? cloneData(runtime.templateContext) : createTemplateContext(theme, blockIndex, totalBlocks, viewport);
5307
- context.block = cloneData(block);
5308
- const execution = executeTemplateMaterialization(block, context, runtime.registry);
5309
- if (execution.status === "ok") {
5310
- const styledLayers = runtime.applyRenderStyle !== false ? applyRenderStyleToLayers(execution.layers, block, theme) : execution.layers;
5311
- const layers = appendMermaidLayers(styledLayers, block, theme, viewport);
5312
- return {
5313
- layers: injectPersistentLayers(
5314
- layers,
5315
- block,
5316
- theme,
5317
- effectivePersistentLayers,
5318
- runtime.expandedPersistentLayers
5319
- ),
5320
- source: "template"
5321
- };
5322
- }
5323
- const diagnostic = toLayerMaterializationDiagnostic(block.template, execution);
5324
- if ((options.failureMode ?? "fallback") === "empty") {
5325
- const layers = appendMermaidLayers([], block, theme, viewport);
5326
- return {
5327
- layers: injectPersistentLayers(
5328
- layers,
5329
- block,
5330
- theme,
5331
- effectivePersistentLayers,
5332
- runtime.expandedPersistentLayers
5333
- ),
5334
- source: layers.length > 0 ? "rich-content" : "empty",
5335
- diagnostic
5336
- };
5337
- }
5338
- const fallbackLayers = appendMermaidLayers(
5339
- fallbackBlockLayers(block, context, diagnostic.message),
5340
- block,
5341
- theme,
5342
- viewport
5343
- );
5304
+ function canvasToAsciiCell(x, y) {
5344
5305
  return {
5345
- layers: injectPersistentLayers(
5346
- fallbackLayers,
5347
- block,
5348
- theme,
5349
- effectivePersistentLayers,
5350
- runtime.expandedPersistentLayers
5351
- ),
5352
- source: "fallback",
5353
- diagnostic
5306
+ col: Math.max(0, Math.round(x / ASCII_CHAR_W)),
5307
+ row: Math.max(0, Math.round(y / ASCII_CHAR_H))
5354
5308
  };
5355
5309
  }
5356
- function mermaidSources(block) {
5357
- return (block.contents ?? []).filter(
5358
- (node) => node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid"
5359
- ).map((node) => node.value).filter((source) => source.trim().length > 0);
5360
- }
5361
- function resolvePositionValue(value, dimension) {
5362
- if (value === void 0) return 0;
5363
- if (typeof value === "number") return value;
5364
- const percent = value.match(/^\s*(-?\d+(?:\.\d+)?)%\s*$/);
5365
- if (percent) return Number(percent[1]) / 100 * dimension;
5366
- const numeric = Number.parseFloat(value);
5367
- return Number.isFinite(numeric) ? numeric : 0;
5368
- }
5369
- function positionRect(position, viewport, fallbackWidth, fallbackHeight) {
5370
- const width = position.width ? resolvePositionValue(position.width, viewport.width) : fallbackWidth;
5371
- const height = position.height ? resolvePositionValue(position.height, viewport.height) : fallbackHeight;
5372
- let x = resolvePositionValue(position.x, viewport.width);
5373
- let y = resolvePositionValue(position.y, viewport.height);
5374
- const anchor = position.anchor ?? "top-left";
5375
- if (anchor === "center") {
5376
- x -= width / 2;
5377
- y -= height / 2;
5378
- } else {
5379
- if (anchor.endsWith("right")) x -= width;
5380
- if (anchor.startsWith("bottom")) y -= height;
5381
- }
5382
- return { x, y, width, height };
5383
- }
5384
- function overlapArea(a, b) {
5385
- const width = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
5386
- const height = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
5387
- return width * height;
5388
- }
5389
- function mermaidRegion(layers, viewport) {
5390
- const occupied = layers.filter((layer) => layer.type !== "shape" && layer.type !== "path" && layer.type !== "mermaid").map(
5391
- (layer) => positionRect(
5392
- layer.position,
5393
- viewport,
5394
- layer.type === "text" ? viewport.width * 0.45 : viewport.width * 0.5,
5395
- layer.type === "text" ? viewport.height * 0.2 : viewport.height * 0.55
5396
- )
5397
- );
5398
- if (occupied.length === 0) {
5399
- return {
5400
- x: viewport.width * 0.06,
5401
- y: viewport.height * 0.08,
5402
- width: viewport.width * 0.88,
5403
- height: viewport.height * 0.84
5404
- };
5405
- }
5406
- const candidates = [
5407
- {
5408
- x: viewport.width * 0.52,
5409
- y: viewport.height * 0.12,
5410
- width: viewport.width * 0.44,
5411
- height: viewport.height * 0.76
5412
- },
5413
- {
5414
- x: viewport.width * 0.04,
5415
- y: viewport.height * 0.12,
5416
- width: viewport.width * 0.44,
5417
- height: viewport.height * 0.76
5418
- },
5419
- {
5420
- x: viewport.width * 0.15,
5421
- y: viewport.height * 0.53,
5422
- width: viewport.width * 0.7,
5423
- height: viewport.height * 0.41
5424
- }
5425
- ];
5426
- return candidates.reduce((best, candidate) => {
5427
- const score = occupied.reduce((sum, rect) => sum + overlapArea(candidate, rect), 0);
5428
- const bestScore = occupied.reduce((sum, rect) => sum + overlapArea(best, rect), 0);
5429
- return score < bestScore ? candidate : best;
5430
- });
5431
- }
5432
- function appendMermaidLayers(layers, block, theme, viewport) {
5433
- const sources = mermaidSources(block);
5434
- if (sources.length === 0 || layers.some((layer) => layer.type === "mermaid")) return layers;
5435
- const region = mermaidRegion(layers, viewport);
5436
- const columns = sources.length === 1 ? 1 : 2;
5437
- const rows = Math.ceil(sources.length / columns);
5438
- const gap = Math.min(viewport.width, viewport.height) * 0.018;
5439
- const width = (region.width - gap * (columns - 1)) / columns;
5440
- const height = (region.height - gap * (rows - 1)) / rows;
5441
- const mermaidLayers = sources.map((source, index) => ({
5442
- id: `${block.id}-mermaid-${index + 1}`,
5443
- type: "mermaid",
5444
- position: {
5445
- x: region.x + index % columns * (width + gap),
5446
- y: region.y + Math.floor(index / columns) * (height + gap),
5447
- width,
5448
- height
5449
- },
5450
- content: {
5451
- source,
5452
- background: theme.colors.backgroundLight,
5453
- foreground: theme.colors.text,
5454
- padding: Math.max(12, Math.round(Math.min(width, height) * 0.025))
5455
- }
5310
+ function asciiDiagramToTemplateData(diagram) {
5311
+ const nodes = diagram.nodes.map((n) => ({
5312
+ id: n.id,
5313
+ label: n.label,
5314
+ ...asciiCellToCanvas(n.col, n.row),
5315
+ w: n.wCols * ASCII_CHAR_W,
5316
+ h: n.hRows * ASCII_CHAR_H,
5317
+ ...n.containerId ? { container: n.containerId } : {}
5318
+ }));
5319
+ const anchors = inferEdgeAnchors(nodes, diagram.edges);
5320
+ const edges = diagram.edges.map((e, index) => ({
5321
+ source: e.source,
5322
+ target: e.target,
5323
+ ...e.label ? { label: e.label } : {},
5324
+ ...e.directed ? {} : { directed: false },
5325
+ ...anchors[index],
5326
+ routing: "orthogonal"
5456
5327
  }));
5457
- return [...layers, ...mermaidLayers];
5328
+ return { nodes, edges };
5458
5329
  }
5459
- function executeTemplateMaterialization(templateBlock, context, registry) {
5460
- const safeTemplateBlock = cloneData(templateBlock);
5461
- const safeContext = cloneData(context);
5462
- const maybeChildren = safeTemplateBlock.children;
5463
- if ((!safeContext.children || safeContext.children.length === 0) && maybeChildren?.length) {
5464
- safeContext.children = maybeChildren;
5465
- }
5466
- const templateFn = registry[resolveTemplateName(safeTemplateBlock.template)];
5467
- if (!templateFn) return { status: "unknown-template", layers: [] };
5468
- const { templateData, templateOverrides } = safeTemplateBlock;
5469
- const input = templateData || templateOverrides ? {
5470
- ...safeTemplateBlock,
5471
- ...templateData,
5472
- ...coerceTemplateParams(safeTemplateBlock.template, templateOverrides ?? {}).input
5473
- } : safeTemplateBlock;
5474
- try {
5475
- const renderedLayers = templateFn(input, safeContext);
5476
- if (!Array.isArray(renderedLayers)) {
5330
+ function inferEdgeAnchors(nodes, edges) {
5331
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
5332
+ const pairs = edges.map((edge) => {
5333
+ const source = nodeById.get(edge.source);
5334
+ const target = nodeById.get(edge.target);
5335
+ if (!source || !target) {
5477
5336
  return {
5478
- status: "invalid-result",
5479
- layers: [],
5480
- receivedType: typeof renderedLayers
5337
+ sourceAnchor: { side: "right", offset: 0.5 },
5338
+ targetAnchor: { side: "left", offset: 0.5 }
5481
5339
  };
5482
5340
  }
5483
- return { status: "ok", layers: cloneData(renderedLayers) };
5484
- } catch (error) {
5485
- return { status: "error", layers: [], error };
5486
- }
5487
- }
5488
- function toLayerMaterializationDiagnostic(template, execution) {
5489
- if (execution.status === "unknown-template") {
5490
- return {
5491
- code: "unknown-template",
5492
- template,
5493
- message: `Unknown template "${template}"`
5341
+ const sourceW = source.w ?? ASCII_CHAR_W * 12;
5342
+ const sourceH = source.h ?? ASCII_CHAR_H * 3;
5343
+ const targetW = target.w ?? ASCII_CHAR_W * 12;
5344
+ const targetH = target.h ?? ASCII_CHAR_H * 3;
5345
+ const dx = target.x + targetW / 2 - (source.x + sourceW / 2);
5346
+ const dy = target.y + targetH / 2 - (source.y + sourceH / 2);
5347
+ const horizontalDistance = Math.abs(dx) / Math.max(1, (sourceW + targetW) / 2);
5348
+ const verticalDistance = Math.abs(dy) / Math.max(1, (sourceH + targetH) / 2);
5349
+ if (verticalDistance >= horizontalDistance) {
5350
+ return dy >= 0 ? {
5351
+ sourceAnchor: { side: "bottom", offset: 0.5 },
5352
+ targetAnchor: { side: "top", offset: 0.5 }
5353
+ } : {
5354
+ sourceAnchor: { side: "top", offset: 0.5 },
5355
+ targetAnchor: { side: "bottom", offset: 0.5 }
5356
+ };
5357
+ }
5358
+ return dx >= 0 ? {
5359
+ sourceAnchor: { side: "right", offset: 0.5 },
5360
+ targetAnchor: { side: "left", offset: 0.5 }
5361
+ } : {
5362
+ sourceAnchor: { side: "left", offset: 0.5 },
5363
+ targetAnchor: { side: "right", offset: 0.5 }
5494
5364
  };
5495
- }
5496
- if (execution.status === "invalid-result") {
5365
+ });
5366
+ distributeOffsets(pairs, edges, nodes, "source");
5367
+ distributeOffsets(pairs, edges, nodes, "target");
5368
+ return pairs;
5369
+ }
5370
+ function distributeOffsets(pairs, edges, nodes, endpoint) {
5371
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
5372
+ const groups = /* @__PURE__ */ new Map();
5373
+ for (let index = 0; index < edges.length; index++) {
5374
+ const edge = edges[index];
5375
+ const anchor = endpoint === "source" ? pairs[index].sourceAnchor : pairs[index].targetAnchor;
5376
+ const nodeId = endpoint === "source" ? edge.source : edge.target;
5377
+ const key = `${nodeId}:${anchor.side}`;
5378
+ const group = groups.get(key) ?? [];
5379
+ group.push(index);
5380
+ groups.set(key, group);
5381
+ }
5382
+ for (const indexes of groups.values()) {
5383
+ if (indexes.length < 2) continue;
5384
+ const side = endpoint === "source" ? pairs[indexes[0]].sourceAnchor.side : pairs[indexes[0]].targetAnchor.side;
5385
+ indexes.sort((a, b) => {
5386
+ const oppositeA = nodeById.get(endpoint === "source" ? edges[a].target : edges[a].source);
5387
+ const oppositeB = nodeById.get(endpoint === "source" ? edges[b].target : edges[b].source);
5388
+ const axisA = side === "top" || side === "bottom" ? oppositeA?.x ?? 0 : oppositeA?.y ?? 0;
5389
+ const axisB = side === "top" || side === "bottom" ? oppositeB?.x ?? 0 : oppositeB?.y ?? 0;
5390
+ return axisA - axisB || a - b;
5391
+ });
5392
+ indexes.forEach((edgeIndex, rank) => {
5393
+ const anchor = endpoint === "source" ? pairs[edgeIndex].sourceAnchor : pairs[edgeIndex].targetAnchor;
5394
+ anchor.offset = (rank + 1) / (indexes.length + 1);
5395
+ });
5396
+ }
5397
+ }
5398
+ function asciiDiagramFromTemplateData(nodes, edges, options = {}) {
5399
+ const ids = new Set(nodes.map((n) => n.id));
5400
+ const outNodes = nodes.map((n) => {
5401
+ const { col, row } = canvasToAsciiCell(n.x, n.y);
5402
+ const labelLines = n.label.length > 0 ? n.label.split("\n") : [];
5403
+ const innerW = labelLines.reduce((w, l) => Math.max(w, l.length), 0);
5404
+ const wCols = typeof n.w === "number" ? Math.max(3, Math.round(n.w / ASCII_CHAR_W)) : innerW + 4;
5405
+ const hRows = typeof n.h === "number" ? Math.max(3, Math.round(n.h / ASCII_CHAR_H)) : labelLines.length + 2;
5497
5406
  return {
5498
- code: "invalid-template-result",
5499
- template,
5500
- receivedType: execution.receivedType,
5501
- message: `Template "${template}" returned ${execution.receivedType}, not a layer array`
5407
+ id: n.id,
5408
+ label: n.label,
5409
+ col,
5410
+ row,
5411
+ wCols: Math.max(wCols, 3),
5412
+ hRows: Math.max(hRows, 3),
5413
+ ...n.container && ids.has(n.container) && n.container !== n.id ? { containerId: n.container } : {}
5502
5414
  };
5503
- }
5415
+ });
5504
5416
  return {
5505
- code: "template-error",
5506
- template,
5507
- cause: execution.error,
5508
- message: `Template "${template}" failed`
5417
+ nodes: outNodes,
5418
+ edges: edges.filter((e) => ids.has(e.source) && ids.has(e.target)).map((e) => ({
5419
+ source: e.source,
5420
+ target: e.target,
5421
+ ...e.label ? { label: e.label } : {},
5422
+ directed: e.directed !== false
5423
+ })),
5424
+ style: options.style ?? "unicode",
5425
+ warnings: []
5509
5426
  };
5510
5427
  }
5511
- function injectPersistentLayers(layers, block, theme, persistentLayers, expanded) {
5512
- if (!persistentLayers) return layers;
5513
- const bottomLayers = expanded?.bottomLayers ?? expandPersistentLayers(persistentLayers.bottomLayers, theme);
5514
- const topLayers = expanded?.topLayers ?? expandPersistentLayers(persistentLayers.topLayers, theme);
5515
- return wrapWithPersistentLayers(layers, block, bottomLayers, topLayers);
5428
+ function asciiDiagramFromBlocks(children, options = {}) {
5429
+ const layout = computeDiagramLayout(children);
5430
+ const nodes = layout.nodes.map((n) => ({
5431
+ id: n.id,
5432
+ label: n.label,
5433
+ x: n.x,
5434
+ y: n.y
5435
+ }));
5436
+ const edges = layout.edges.map((e) => ({
5437
+ source: e.source,
5438
+ target: e.target,
5439
+ ...e.type ? { label: e.type } : {}
5440
+ }));
5441
+ const diagram = asciiDiagramFromTemplateData(nodes, edges, options);
5442
+ diagram.warnings.push(...layout.warnings);
5443
+ return diagram;
5516
5444
  }
5517
5445
 
5518
- // src/doc/templates/metadata.ts
5519
- var TEMPLATE_METADATA = {
5520
- title: {
5521
- label: "Title",
5522
- description: "A bold opening slide with large title text, perfect for covers and chapters."
5523
- },
5524
- sectionHeader: {
5525
- label: "Section Header",
5526
- description: "A clean section break with a prominent title and optional subtitle."
5527
- },
5528
- statHighlight: {
5529
- label: "Stat Highlight",
5530
- description: "Showcases a single key number or metric with supporting context."
5531
- },
5532
- quote: {
5533
- label: "Quote",
5534
- description: "Displays a stylized pull quote with decorative marks and attribution."
5535
- },
5536
- factCard: {
5537
- label: "Fact Card",
5538
- description: "Presents a focused fact or insight with a labeled header and body text."
5539
- },
5540
- twoColumn: {
5541
- label: "Two Column",
5542
- description: "Divides the slide into two equal side-by-side content columns."
5543
- },
5544
- dateEvent: {
5545
- label: "Date Event",
5546
- description: "Highlights a date-based event or milestone with a prominent date display."
5547
- },
5548
- imageWithCaption: {
5549
- label: "Image with Caption",
5550
- description: "Displays a full-bleed image with an optional caption below."
5551
- },
5552
- leftFeature: {
5553
- label: "Left Feature",
5554
- description: "Image on the left, title and body text stacked on the right."
5555
- },
5556
- rightFeature: {
5557
- label: "Right Feature",
5558
- description: "Image on the right, title and body text stacked on the left."
5559
- },
5560
- map: {
5561
- label: "Map",
5562
- description: "Embeds an interactive map centered on a geographic location."
5563
- },
5564
- fullBleedQuote: {
5565
- label: "Full Bleed Quote",
5566
- description: "A full-width quote that spans the entire slide for maximum impact."
5567
- },
5568
- list: {
5569
- label: "List",
5570
- description: "Renders a bulleted or numbered list in a clean, card-style layout."
5571
- },
5572
- photoGrid: {
5573
- label: "Photo Grid",
5574
- description: "Arranges multiple photos in a 2\xD72 or 3\xD73 mosaic grid."
5575
- },
5576
- definitionCard: {
5577
- label: "Definition Card",
5578
- description: "Shows a term and its definition in a structured, dictionary-style card."
5579
- },
5580
- comparisonBar: {
5581
- label: "Comparison Bar",
5582
- description: "Visualizes two or more values side-by-side with labeled horizontal bars."
5583
- },
5584
- pullQuote: {
5585
- label: "Pull Quote",
5586
- description: "A stylized pull quote with large decorative marks and centered text."
5587
- },
5588
- videoWithCaption: {
5589
- label: "Video with Caption",
5590
- description: "Embeds a video player with an optional caption below."
5591
- },
5592
- videoPullQuote: {
5593
- label: "Video Pull Quote",
5594
- description: "Combines a video panel with a highlighted pull quote side-by-side."
5595
- },
5596
- dataTable: {
5597
- label: "Data Table",
5598
- description: "Renders tabular data in a clean, styled table with a header row."
5599
- },
5600
- diagram: {
5601
- label: "Diagram",
5602
- description: "Renders sub-headings as connected nodes \u2014 edit visually as a node-and-edge diagram."
5603
- },
5604
- tree: {
5605
- label: "Tree",
5606
- description: "Renders an ASCII file-tree / outline fence as a filesystem-style treeview \u2014 edit as an outline."
5607
- },
5608
- timeline: {
5609
- label: "Timeline",
5610
- description: "Plots events across one or more horizontal tracks with callouts and optional branching links."
5611
- },
5612
- layout: {
5613
- label: "Layout",
5614
- description: "Free-form 2D canvas \u2014 drag layers into place. Use for one-off block layouts that don't fit a template."
5615
- },
5616
- drawing: {
5617
- label: "Drawing",
5618
- description: "Free-form sketches \u2014 draw shapes, paths, and text directly on a 2D surface."
5619
- }
5620
- };
5446
+ // src/doc/asciiTimeline/mapping.ts
5447
+ function asciiTimelineToTemplateData(timeline) {
5448
+ const globalStart = timeline.tracks.length > 0 ? Math.min(...timeline.tracks.map((track) => track.startColumn)) : 0;
5449
+ const globalEnd = timeline.tracks.length > 0 ? Math.max(...timeline.tracks.map((track) => track.endColumn)) : 1;
5450
+ const span = Math.max(1, globalEnd - globalStart);
5451
+ const tracks = timeline.tracks.map((track) => ({
5452
+ id: track.id,
5453
+ ...track.label ? { label: track.label } : {},
5454
+ ...track.endLabel ? { endLabel: track.endLabel } : {},
5455
+ events: track.events.map(
5456
+ (event) => ({
5457
+ id: event.id,
5458
+ label: event.label,
5459
+ ...event.description ? { description: event.description } : {},
5460
+ position: clamp012((event.column - globalStart) / span),
5461
+ ...event.side ? { side: event.side } : {},
5462
+ ...event.descriptionSide ? { descriptionSide: event.descriptionSide } : {},
5463
+ ...event.callout === false ? { callout: false } : {},
5464
+ ...event.marker ? { marker: event.marker } : {}
5465
+ })
5466
+ )
5467
+ }));
5468
+ const links = timeline.links.map((link) => ({ ...link }));
5469
+ return { tracks, links };
5470
+ }
5471
+ function clamp012(value) {
5472
+ return Math.max(0, Math.min(1, value));
5473
+ }
5621
5474
 
5622
- // src/doc/templates/coverBlock.ts
5623
- var COVER_TITLE_BASE_PX = 120;
5624
- var COVER_TITLE_COMFORTABLE_CHARS = 26;
5625
- var COVER_TITLE_MIN_BASE_PX = 62;
5626
- function fitCoverTitleSize(title) {
5627
- const len = title.trim().length;
5628
- if (len <= COVER_TITLE_COMFORTABLE_CHARS) return COVER_TITLE_BASE_PX;
5629
- const scaled = COVER_TITLE_BASE_PX * Math.sqrt(COVER_TITLE_COMFORTABLE_CHARS / len);
5630
- return Math.max(COVER_TITLE_MIN_BASE_PX, Math.round(scaled));
5475
+ // src/doc/treeview/mapping.ts
5476
+ function treeToTemplateData(tree) {
5477
+ return { items: tree.roots.map(toItem) };
5631
5478
  }
5632
- function isStandardLightCover(context) {
5633
- const { theme } = context;
5634
- return theme.id === "standard" && relativeLuminance(theme.colors.background) > 0.85;
5479
+ function toItem(node) {
5480
+ return {
5481
+ id: node.id,
5482
+ label: node.label,
5483
+ ...node.isDir ? { isDir: true } : {},
5484
+ ...node.comment ? { comment: node.comment } : {},
5485
+ children: node.children.map(toItem)
5486
+ };
5635
5487
  }
5636
- var COVER_SCRIM_STOPS = [
5637
- { pos: 0, alpha: 0.95 },
5638
- { pos: 38, alpha: 0.86 },
5639
- { pos: 58, alpha: 0.4 },
5640
- { pos: 78, alpha: 0.1 },
5641
- { pos: 100, alpha: 0 }
5642
- ];
5643
- function coverScrimGradient(background) {
5644
- const stops = COVER_SCRIM_STOPS.map((s) => `${withAlpha(background, s.alpha)} ${s.pos}%`);
5645
- return `linear-gradient(0deg, ${stops.join(", ")})`;
5488
+ function treeFromTemplateData(items, options = {}) {
5489
+ const roots = items.map(fromItem);
5490
+ return { roots, style: options.style ?? "unicode", warnings: [] };
5646
5491
  }
5647
- function coverBlock(input, context) {
5648
- const treatment = themedImageTreatment(context, input.imageTreatment);
5649
- const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
5650
- const { theme, layout } = context;
5651
- const standardLightCover = !heroSrc && isStandardLightCover(context);
5652
- const titleFontSize = themedFontSize(fitCoverTitleSize(title), context, true);
5653
- const subtitleFontSize = themedFontSize(40, context, false);
5654
- const layers = [];
5655
- if (heroSrc) {
5656
- const imageAnimation = mapAmbientMotion(ambientMotion);
5657
- layers.push(
5658
- {
5659
- type: "image",
5660
- id: "cover-hero",
5661
- content: {
5662
- src: heroSrc,
5663
- alt: heroAlt || title,
5664
- fit: "cover",
5665
- credit: heroCredit,
5666
- license: heroLicense,
5667
- ...treatment ? { treatment } : {}
5668
- },
5669
- position: { x: 0, y: 0, width: "100%", height: "100%" },
5670
- animation: imageAnimation
5671
- },
5672
- {
5673
- type: "shape",
5674
- id: "cover-gradient",
5675
- content: {
5676
- shape: "rect",
5677
- fill: coverScrimGradient(theme.colors.background)
5678
- },
5679
- position: { x: 0, y: 0, width: "100%", height: "100%" }
5492
+ function fromItem(item) {
5493
+ const children = Array.isArray(item.children) ? item.children.map(fromItem) : [];
5494
+ return {
5495
+ id: typeof item.id === "string" && item.id ? item.id : "node",
5496
+ label: typeof item.label === "string" ? item.label : "",
5497
+ children,
5498
+ isDir: item.isDir === true || children.length > 0,
5499
+ ...item.comment ? { comment: item.comment } : {}
5500
+ };
5501
+ }
5502
+ function treeFromMarkdownList(list) {
5503
+ const usedIds = /* @__PURE__ */ new Map();
5504
+ const slugify2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "node";
5505
+ const idFor = (label) => {
5506
+ const base = slugify2(label.replace(/\/$/, ""));
5507
+ const count = usedIds.get(base) ?? 0;
5508
+ usedIds.set(base, count + 1);
5509
+ return count === 0 ? base : `${base}-${count + 1}`;
5510
+ };
5511
+ const itemToNode = (item) => {
5512
+ const labelNode = item.children.find((c) => c.type !== "list");
5513
+ const label = labelNode ? extractPlainText(labelNode).trim() : "";
5514
+ const childLists = item.children.filter((c) => c.type === "list");
5515
+ const children = [];
5516
+ for (const cl of childLists) for (const ci of cl.children) children.push(itemToNode(ci));
5517
+ return {
5518
+ id: idFor(label),
5519
+ label,
5520
+ children,
5521
+ isDir: label.endsWith("/") || children.length > 0
5522
+ };
5523
+ };
5524
+ return { roots: list.children.map(itemToNode), style: "unicode", warnings: [] };
5525
+ }
5526
+ function findFirstList(contents) {
5527
+ return contents?.find((n) => n.type === "list");
5528
+ }
5529
+
5530
+ // src/doc/templateInputs.ts
5531
+ var VIDEO_FILE_RE = /\.(?:webm|mp4|mov|m4v|ogv)(?:[?#].*)?$/i;
5532
+ function extractBodyPlainText(contents) {
5533
+ if (!contents || contents.length === 0) return "";
5534
+ return contents.filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((n) => extractPlainText(n)).join("\n").trim();
5535
+ }
5536
+ function extractListItems(contents) {
5537
+ if (!contents) return [];
5538
+ const items = [];
5539
+ for (const node of contents) {
5540
+ if (node.type === "list") {
5541
+ for (const item of node.children) {
5542
+ const text = extractPlainText(item).trim();
5543
+ if (text) items.push(text);
5680
5544
  }
5681
- );
5682
- } else {
5683
- if (standardLightCover) {
5684
- layers.push(
5685
- {
5686
- type: "shape",
5687
- id: "cover-bg",
5688
- content: {
5689
- shape: "rect",
5690
- fill: theme.colors.background
5691
- },
5692
- position: { x: 0, y: 0, width: "100%", height: "100%" }
5693
- },
5694
- {
5695
- type: "shape",
5696
- id: "cover-bg-tint",
5697
- content: {
5698
- shape: "rect",
5699
- fill: `radial-gradient(ellipse at 50% 40%, ${withAlpha(theme.colors.primary, 0.16)} 0%, ${withAlpha(theme.colors.primary, 0.07)} 38%, ${withAlpha(theme.colors.primary, 0)} 78%)`
5700
- },
5701
- position: { x: 0, y: 0, width: "100%", height: "100%" }
5702
- }
5703
- );
5704
- } else {
5705
- layers.push({
5706
- type: "shape",
5707
- id: "cover-bg",
5708
- content: {
5709
- shape: "rect",
5710
- fill: `radial-gradient(ellipse at 50% 40%, ${theme.colors.primary} 0%, ${theme.colors.background} 100%)`
5711
- },
5712
- position: { x: 0, y: 0, width: "100%", height: "100%" }
5713
- });
5714
5545
  }
5715
- layers.push({
5716
- type: "shape",
5717
- id: "cover-accent",
5718
- content: {
5719
- shape: "rect",
5720
- fill: standardLightCover ? withAlpha(theme.colors.primary, 0.24) : "rgba(255, 255, 255, 0.2)"
5721
- },
5722
- position: {
5723
- x: "35%",
5724
- y: subtitle ? "42%" : "58%",
5725
- width: "30%",
5726
- height: "2px"
5727
- }
5728
- });
5729
5546
  }
5730
- layers.push({
5731
- type: "text",
5732
- id: "cover-title",
5733
- content: {
5734
- text: title,
5735
- style: {
5736
- fontSize: titleFontSize,
5737
- fontFamily: getThemeFont(context, "title"),
5738
- fontWeight: "bold",
5739
- color: theme.colors.text,
5740
- textAlign: "center",
5741
- shadow: !standardLightCover
5742
- }
5743
- },
5744
- position: {
5745
- x: "50%",
5746
- y: heroSrc ? subtitle ? "70%" : "75%" : subtitle ? layout.primaryY : "50%",
5747
- anchor: "center",
5748
- width: layout.maxTextWidth
5749
- },
5750
- // No animation delay - shown immediately at rest
5751
- animation: { type: "fadeIn", duration: 0.8 }
5752
- });
5753
- if (subtitle) {
5754
- layers.push({
5755
- type: "text",
5756
- id: "cover-subtitle",
5757
- content: {
5758
- text: subtitle,
5759
- style: {
5760
- fontSize: subtitleFontSize,
5761
- fontFamily: getThemeFont(context, "body"),
5762
- fontWeight: standardLightCover ? "bold" : "normal",
5763
- color: standardLightCover ? withAlpha(theme.colors.text, 0.78) : theme.colors.textMuted,
5764
- textAlign: "center",
5765
- lineHeight: 1.5
5547
+ return items;
5548
+ }
5549
+ function parseDim(raw) {
5550
+ if (raw === void 0) return void 0;
5551
+ const n = parseFloat(raw);
5552
+ return Number.isFinite(n) && n > 0 ? n : void 0;
5553
+ }
5554
+ function extractImages(contents, limit = Infinity) {
5555
+ if (!contents || contents.length === 0) return [];
5556
+ const found = [];
5557
+ function fromHtml(nodes) {
5558
+ for (const node of nodes) {
5559
+ if (found.length >= limit) return;
5560
+ if (!node || typeof node !== "object") continue;
5561
+ const n = node;
5562
+ if (n.type === "htmlElement" && n.tagName === "img") {
5563
+ const attrs = n.attributes;
5564
+ if (attrs && typeof attrs.src === "string" && attrs.src) {
5565
+ found.push({
5566
+ src: attrs.src,
5567
+ alt: typeof attrs.alt === "string" ? attrs.alt : "",
5568
+ width: parseDim(attrs.width),
5569
+ height: parseDim(attrs.height)
5570
+ });
5766
5571
  }
5767
- },
5768
- position: {
5769
- x: "50%",
5770
- y: heroSrc ? "82%" : layout.secondaryY,
5771
- anchor: "center",
5772
- width: layout.maxTextWidth
5773
- },
5774
- animation: { type: "fadeIn", duration: 0.8, delay: 0.2 }
5775
- });
5572
+ }
5573
+ if (Array.isArray(n.children)) fromHtml(n.children);
5574
+ }
5776
5575
  }
5777
- return layers;
5576
+ function walk(node) {
5577
+ if (found.length >= limit) return;
5578
+ if (!node || typeof node !== "object") return;
5579
+ const n = node;
5580
+ if (n.type === "image" && typeof n.url === "string" && n.url) {
5581
+ found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
5582
+ return;
5583
+ }
5584
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
5585
+ fromHtml(n.htmlChildren);
5586
+ }
5587
+ if (Array.isArray(n.children)) {
5588
+ for (const child of n.children) walk(child);
5589
+ }
5590
+ }
5591
+ for (const node of contents) {
5592
+ if (found.length >= limit) break;
5593
+ walk(node);
5594
+ }
5595
+ return found;
5778
5596
  }
5779
- function startBlockToCoverInput(config) {
5780
- return {
5781
- heroSrc: config.heroSrc,
5782
- heroAlt: config.heroAlt,
5783
- title: config.title,
5784
- subtitle: config.subtitle,
5785
- ambientMotion: config.ambientMotion,
5786
- heroCredit: config.heroCredit,
5787
- heroLicense: config.heroLicense
5597
+ function extractFirstImage(contents) {
5598
+ return extractImages(contents, 1)[0] ?? null;
5599
+ }
5600
+ function extractEmbeddedVideos(contents, limit = Infinity) {
5601
+ if (!contents || contents.length === 0) return [];
5602
+ const found = [];
5603
+ const seen = /* @__PURE__ */ new Set();
5604
+ const add = (video) => {
5605
+ if (!video.src || found.length >= limit || seen.has(video.src)) return;
5606
+ seen.add(video.src);
5607
+ found.push(video);
5788
5608
  };
5609
+ function nestedSource(children) {
5610
+ if (!Array.isArray(children)) return void 0;
5611
+ for (const child of children) {
5612
+ if (!child || typeof child !== "object") continue;
5613
+ const node = child;
5614
+ if (node.tagName === "source") {
5615
+ const attrs = node.attributes;
5616
+ if (typeof attrs?.src === "string" && attrs.src) return attrs.src;
5617
+ }
5618
+ const nested = nestedSource(node.children);
5619
+ if (nested) return nested;
5620
+ }
5621
+ return void 0;
5622
+ }
5623
+ function walk(node) {
5624
+ if (found.length >= limit || !node || typeof node !== "object") return;
5625
+ const n = node;
5626
+ if (n.type === "htmlElement" && n.tagName === "video") {
5627
+ const attrs = n.attributes;
5628
+ const src = attrs?.src || nestedSource(n.children);
5629
+ if (src) {
5630
+ add({
5631
+ src,
5632
+ ...attrs?.poster ? { posterSrc: attrs.poster } : {},
5633
+ alt: attrs?.["aria-label"] || attrs?.title || attrs?.alt || ""
5634
+ });
5635
+ }
5636
+ } else if ((n.type === "link" || n.type === "image") && typeof n.url === "string" && VIDEO_FILE_RE.test(n.url)) {
5637
+ add({
5638
+ src: n.url,
5639
+ alt: typeof n.alt === "string" ? n.alt : extractPlainText(n).trim()
5640
+ });
5641
+ }
5642
+ if (Array.isArray(n.children)) n.children.forEach(walk);
5643
+ if (Array.isArray(n.htmlChildren)) n.htmlChildren.forEach(walk);
5644
+ }
5645
+ contents.forEach(walk);
5646
+ return found;
5789
5647
  }
5790
- function expandCoverBlock(config, context) {
5791
- const input = startBlockToCoverInput(config);
5792
- return coverBlock(input, context);
5648
+ function extractFirstEmbeddedVideo(contents) {
5649
+ return extractEmbeddedVideos(contents, 1)[0] ?? null;
5793
5650
  }
5794
-
5795
- // src/doc/templates/index.ts
5796
- function materializeScheduledBlock(sourceBlock, blockIndex, totalBlocks, theme, viewport, persistentLayers, expandedPersistentLayers, registry, failureMode, onDiagnostic) {
5797
- const materialized = materializeBlockLayersWithRuntime(
5798
- sourceBlock,
5799
- {
5800
- theme,
5801
- viewport,
5802
- persistentLayers: persistentLayers ?? false,
5803
- blockIndex,
5804
- totalBlocks,
5805
- failureMode
5806
- },
5807
- { registry, expandedPersistentLayers }
5808
- );
5809
- if (materialized.diagnostic) {
5810
- onDiagnostic?.(materialized.diagnostic, sourceBlock, blockIndex);
5651
+ function extractTableFromContents(contents) {
5652
+ if (!contents) return null;
5653
+ for (const node of contents) {
5654
+ if (node.type === "table") {
5655
+ const table = node;
5656
+ const [headerRow, ...bodyRows] = table.children;
5657
+ if (!headerRow) return null;
5658
+ const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
5659
+ const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
5660
+ return { headers, rows, align: table.align };
5661
+ }
5811
5662
  }
5812
- const expandedBlock = isTemplateBlock(sourceBlock) ? {
5813
- id: sourceBlock.id,
5814
- startTime: 0,
5815
- duration: sourceBlock.duration,
5816
- audioSegment: sourceBlock.audioSegment,
5817
- ...materialized.layers.length > 0 ? { layers: materialized.layers } : {},
5818
- transition: sourceBlock.transition,
5819
- template: sourceBlock.template
5820
- } : {
5821
- ...cloneData(sourceBlock),
5822
- ...materialized.layers.length > 0 ? { layers: materialized.layers } : {}
5663
+ return null;
5664
+ }
5665
+ function extractBlockquoteText(contents) {
5666
+ if (!contents) return "";
5667
+ for (const node of contents) {
5668
+ if (node.type === "blockquote") return extractPlainText(node).trim();
5669
+ }
5670
+ return "";
5671
+ }
5672
+ function firstParagraph(contents) {
5673
+ if (!contents) return null;
5674
+ const index = contents.findIndex((node) => node.type === "paragraph");
5675
+ if (index < 0) return null;
5676
+ return {
5677
+ node: contents[index],
5678
+ index
5823
5679
  };
5824
- const transition = resolveBlockTransition(expandedBlock, theme, blockIndex);
5825
- if (transition !== expandedBlock.transition) {
5826
- expandedBlock.transition = transition;
5680
+ }
5681
+ function leadingStrongStat(paragraph) {
5682
+ for (const child of paragraph.children) {
5683
+ if (child.type === "text" && child.value.trim() === "") continue;
5684
+ if (child.type !== "strong") return null;
5685
+ const text = extractPlainText(child).trim();
5686
+ const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
5687
+ return match?.[0].trim() || null;
5827
5688
  }
5828
- return expandedBlock;
5689
+ return null;
5829
5690
  }
5830
- function expandDocBlocks(blocks, options = {}) {
5831
- const opts = options;
5832
- const theme = opts.theme ?? DEFAULT_THEME;
5833
- const viewport = opts.viewport ?? VIEWPORT_PRESETS.landscape;
5834
- const {
5835
- persistentLayers,
5836
- audioSegments,
5837
- customTemplates,
5838
- failureMode = "fallback",
5839
- onDiagnostic
5840
- } = opts;
5841
- const totalBlocks = blocks.length;
5842
- const registry = customTemplates && customTemplates.length > 0 ? buildRegistry(customTemplates) : templateRegistry;
5843
- const effectivePersistentLayers = persistentLayers === false ? void 0 : persistentLayers ?? theme.persistentLayers;
5844
- const expandedPersistentLayers = effectivePersistentLayers ? {
5845
- bottomLayers: expandPersistentLayers(effectivePersistentLayers.bottomLayers, theme),
5846
- topLayers: expandPersistentLayers(effectivePersistentLayers.topLayers, theme)
5847
- } : void 0;
5848
- if (!audioSegments || audioSegments.length === 0) {
5849
- let currentTime = 0;
5850
- return blocks.map((block, index) => {
5851
- const expandedBlock = materializeScheduledBlock(
5852
- block,
5853
- index,
5854
- totalBlocks,
5855
- theme,
5856
- viewport,
5857
- effectivePersistentLayers,
5858
- expandedPersistentLayers,
5859
- registry,
5860
- failureMode,
5861
- onDiagnostic
5691
+ function removeStatPhrase(text, start, end) {
5692
+ const before = text.slice(0, start).trim();
5693
+ let after = text.slice(end);
5694
+ if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, "");
5695
+ else after = after.trim();
5696
+ return [before, after].filter(Boolean).join(" ").trim();
5697
+ }
5698
+ function bodyWithoutParagraphStat(contents, paragraphIndex, paragraphText, start, end) {
5699
+ if (!contents) return "";
5700
+ return contents.map(
5701
+ (node, index) => index === paragraphIndex ? removeStatPhrase(paragraphText, start, end) : extractPlainText(node).trim()
5702
+ ).filter(Boolean).join("\n").trim();
5703
+ }
5704
+ function deriveStatHighlightInputs(headingText, contents, bodyText3) {
5705
+ const paragraph = firstParagraph(contents);
5706
+ if (paragraph) {
5707
+ const paragraphText = extractPlainText(paragraph.node).trim();
5708
+ const strongStat = leadingStrongStat(paragraph.node);
5709
+ if (strongStat) {
5710
+ const start = paragraphText.indexOf(strongStat);
5711
+ const description = bodyWithoutParagraphStat(
5712
+ contents,
5713
+ paragraph.index,
5714
+ paragraphText,
5715
+ start,
5716
+ start + strongStat.length
5862
5717
  );
5863
- expandedBlock.startTime = currentTime;
5864
- currentTime += expandedBlock.duration;
5865
- return expandedBlock;
5866
- });
5718
+ return {
5719
+ stat: strongStat,
5720
+ description: description || headingText || strongStat
5721
+ };
5722
+ }
5867
5723
  }
5868
- const blocksBySegment = /* @__PURE__ */ new Map();
5869
- blocks.forEach((block, index) => {
5870
- const segmentIndex = block.audioSegment ?? 0;
5871
- if (!blocksBySegment.has(segmentIndex)) {
5872
- blocksBySegment.set(segmentIndex, []);
5724
+ const trimmedHeading = headingText.trim();
5725
+ const headingStat = matchNumberHighlight(trimmedHeading);
5726
+ if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
5727
+ return { stat: headingText, description: bodyText3 || headingText };
5728
+ }
5729
+ if (paragraph) {
5730
+ const paragraphText = extractPlainText(paragraph.node).trim();
5731
+ const bodyStat = matchNumberHighlight(paragraphText);
5732
+ if (bodyStat) {
5733
+ const description = bodyWithoutParagraphStat(
5734
+ contents,
5735
+ paragraph.index,
5736
+ paragraphText,
5737
+ bodyStat.index,
5738
+ bodyStat.end
5739
+ );
5740
+ return {
5741
+ stat: bodyStat.value,
5742
+ description: description || headingText || bodyStat.value
5743
+ };
5873
5744
  }
5874
- blocksBySegment.get(segmentIndex).push({ block, originalIndex: index });
5875
- });
5876
- const expandedBlocks = new Array(blocks.length);
5877
- for (const [segmentIndex, segmentBlocks] of blocksBySegment) {
5878
- const audioSegment = audioSegments[segmentIndex];
5879
- if (!audioSegment) {
5880
- let offsetTime = 0;
5881
- for (const { block, originalIndex } of segmentBlocks) {
5882
- const expandedBlock = materializeScheduledBlock(
5883
- block,
5884
- originalIndex,
5885
- totalBlocks,
5886
- theme,
5887
- viewport,
5888
- effectivePersistentLayers,
5889
- expandedPersistentLayers,
5890
- registry,
5891
- failureMode,
5892
- onDiagnostic
5893
- );
5894
- expandedBlock.startTime = offsetTime;
5895
- offsetTime += expandedBlock.duration;
5896
- expandedBlocks[originalIndex] = expandedBlock;
5897
- }
5898
- continue;
5745
+ }
5746
+ return { stat: headingText, description: bodyText3 || headingText };
5747
+ }
5748
+ function normalizeCoverageText(value) {
5749
+ return value.replace(/\s+/g, " ").trim();
5750
+ }
5751
+ function autoTemplatePreservesContent(templateName, contents) {
5752
+ const nodes = contents ?? [];
5753
+ switch (templateName) {
5754
+ case "quote":
5755
+ return nodes.length === 1 && nodes[0]?.type === "blockquote";
5756
+ case "dataTable":
5757
+ return nodes.length === 1 && nodes[0]?.type === "table";
5758
+ case "list":
5759
+ return nodes.length === 1 && nodes[0]?.type === "list";
5760
+ case "diagram":
5761
+ case "timeline":
5762
+ return nodes.length === 1 && nodes[0]?.type === "code";
5763
+ case "tree":
5764
+ return nodes.length === 1 && (nodes[0]?.type === "code" || nodes[0]?.type === "list");
5765
+ case "photoGrid": {
5766
+ const images = extractImages(nodes);
5767
+ if (images.length < 2 || images.length > 4) return false;
5768
+ const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
5769
+ const representedText = normalizeCoverageText(
5770
+ images.map((image) => image.alt).filter(Boolean).join(" ")
5771
+ );
5772
+ return bodyText3 === "" || bodyText3 === representedText;
5899
5773
  }
5900
- const sectionHeaderBlocks = [];
5901
- const contentBlocks = [];
5902
- for (const item of segmentBlocks) {
5903
- const templateBlock = item.block;
5904
- if (templateBlock.template === "sectionHeader") {
5905
- sectionHeaderBlocks.push(item);
5906
- } else {
5907
- contentBlocks.push(item);
5774
+ case "videoWithCaption": {
5775
+ const videos = extractEmbeddedVideos(nodes);
5776
+ if (videos.length !== 1) return false;
5777
+ const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
5778
+ return bodyText3 === normalizeCoverageText(videos[0]?.alt ?? "");
5779
+ }
5780
+ case "leftFeature":
5781
+ case "rightFeature":
5782
+ case "statHighlight":
5783
+ return true;
5784
+ default:
5785
+ return false;
5786
+ }
5787
+ }
5788
+ function deriveTemplateInputs(templateName, headingText, contents, options = {}) {
5789
+ const placeholders = options.placeholders === true;
5790
+ const bodyText3 = extractBodyPlainText(contents);
5791
+ switch (templateName) {
5792
+ case "statHighlight": {
5793
+ const inputs = deriveStatHighlightInputs(headingText, contents, bodyText3);
5794
+ if (options.preserveSourceHeading && headingText && inputs.stat !== headingText && inputs.description !== headingText) {
5795
+ return {
5796
+ ...inputs,
5797
+ description: headingText,
5798
+ ...inputs.description ? { detail: inputs.description } : {}
5799
+ };
5908
5800
  }
5801
+ return inputs;
5909
5802
  }
5910
- const fixedDuration = sectionHeaderBlocks.reduce((sum, { block }) => {
5911
- return sum + (block.duration ?? 0);
5912
- }, 0);
5913
- const contentBlockDuration = contentBlocks.reduce((sum, { block }) => {
5914
- return sum + (block.duration ?? 0);
5915
- }, 0);
5916
- const remainingDuration = audioSegment.duration - fixedDuration;
5917
- const scaleFactor = contentBlockDuration > 0 && remainingDuration > 0 ? remainingDuration / contentBlockDuration : 1;
5918
- const expandedInfos = [];
5919
- for (const { block, originalIndex } of segmentBlocks) {
5920
- const expandedBlock = materializeScheduledBlock(
5921
- block,
5922
- originalIndex,
5923
- totalBlocks,
5924
- theme,
5925
- viewport,
5926
- effectivePersistentLayers,
5927
- expandedPersistentLayers,
5928
- registry,
5929
- failureMode,
5930
- onDiagnostic
5931
- );
5932
- const templateBlock = block;
5933
- expandedInfos.push({
5934
- block: expandedBlock,
5935
- originalIndex,
5936
- templateBlock,
5937
- hasSourceTiming: typeof templateBlock.sourceStartTime === "number"
5938
- });
5803
+ case "quote": {
5804
+ const quote = extractBlockquoteText(contents) || bodyText3;
5805
+ if (quote) return { quote, ...headingText ? { title: headingText } : {} };
5806
+ return { quote: headingText };
5939
5807
  }
5940
- const hasAnySourceTiming = expandedInfos.some((info) => info.hasSourceTiming);
5941
- expandedInfos.sort((a, b) => {
5942
- if (hasAnySourceTiming) {
5943
- const aIsHeader = a.templateBlock.template === "sectionHeader";
5944
- const bIsHeader = b.templateBlock.template === "sectionHeader";
5945
- if (aIsHeader && !bIsHeader) return -1;
5946
- if (!aIsHeader && bIsHeader) return 1;
5947
- if (a.hasSourceTiming && b.hasSourceTiming) {
5948
- return a.templateBlock.sourceStartTime - b.templateBlock.sourceStartTime;
5949
- }
5950
- if (a.hasSourceTiming && !b.hasSourceTiming) return -1;
5951
- if (!a.hasSourceTiming && b.hasSourceTiming) return 1;
5952
- }
5953
- return a.originalIndex - b.originalIndex;
5954
- });
5955
- let offsetWithinSegment = 0;
5956
- for (let i = 0; i < expandedInfos.length; i++) {
5957
- const info = expandedInfos[i];
5958
- const { block: expandedBlock, templateBlock } = info;
5959
- if (info.hasSourceTiming) {
5960
- const sourceStart = templateBlock.sourceStartTime;
5961
- expandedBlock.startTime = audioSegment.startTime + sourceStart;
5962
- if (typeof templateBlock.sourceDuration === "number") {
5963
- expandedBlock.duration = Math.max(5, templateBlock.sourceDuration);
5964
- }
5965
- offsetWithinSegment = sourceStart + expandedBlock.duration;
5966
- } else if (templateBlock.template === "sectionHeader") {
5967
- expandedBlock.startTime = audioSegment.startTime + offsetWithinSegment;
5968
- offsetWithinSegment += expandedBlock.duration;
5969
- } else {
5970
- expandedBlock.startTime = audioSegment.startTime + offsetWithinSegment;
5971
- const scaledDuration = expandedBlock.duration * scaleFactor;
5972
- expandedBlock.duration = scaledDuration;
5973
- offsetWithinSegment += scaledDuration;
5974
- }
5975
- expandedBlocks[info.originalIndex] = expandedBlock;
5808
+ case "fullBleedQuote":
5809
+ case "pullQuote": {
5810
+ const text = extractBlockquoteText(contents) || bodyText3 || headingText;
5811
+ return { text };
5976
5812
  }
5977
- const segmentExpandedBlocks = expandedInfos.map((info) => expandedBlocks[info.originalIndex]).sort((a, b) => a.startTime - b.startTime);
5978
- for (let i = 0; i < segmentExpandedBlocks.length - 1; i++) {
5979
- const current = segmentExpandedBlocks[i];
5980
- const next = segmentExpandedBlocks[i + 1];
5981
- const currentEnd = current.startTime + current.duration;
5982
- if (currentEnd > next.startTime) {
5983
- current.duration = next.startTime - current.startTime;
5984
- } else if (currentEnd < next.startTime - 0.5) {
5985
- current.duration = Math.max(0.1, next.startTime - current.startTime);
5986
- }
5813
+ case "factCard":
5814
+ return { fact: headingText, explanation: bodyText3 || headingText };
5815
+ case "comparisonBar":
5816
+ return placeholders ? { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 } : null;
5817
+ case "list": {
5818
+ const items = extractListItems(contents);
5819
+ if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
5820
+ return placeholders ? { items: ["Item 1", "Item 2", "Item 3"] } : null;
5987
5821
  }
5988
- const MIN_TRANSITION_GAP = 5;
5989
- const segmentEnd = audioSegment.startTime + audioSegment.duration;
5990
- while (segmentExpandedBlocks.length > 1) {
5991
- const lastBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 1];
5992
- const timeFromLastToEnd = segmentEnd - lastBlock.startTime;
5993
- if (timeFromLastToEnd < MIN_TRANSITION_GAP && lastBlock.template !== "sectionHeader") {
5994
- const prevBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 2];
5995
- prevBlock.duration = segmentEnd - prevBlock.startTime;
5996
- lastBlock.duration = 0;
5997
- lastBlock.startTime = segmentEnd;
5998
- segmentExpandedBlocks.pop();
5999
- } else {
6000
- if (lastBlock.startTime + lastBlock.duration < segmentEnd) {
6001
- lastBlock.duration = segmentEnd - lastBlock.startTime;
6002
- }
6003
- break;
6004
- }
5822
+ case "definitionCard":
5823
+ return { term: headingText, definition: bodyText3 || headingText };
5824
+ case "dateEvent":
5825
+ return { date: headingText, description: bodyText3 || headingText };
5826
+ case "dataTable": {
5827
+ const tableData = extractTableFromContents(contents);
5828
+ if (tableData) return { ...tableData, ...headingText ? { title: headingText } : {} };
5829
+ return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
6005
5830
  }
6006
- if (segmentExpandedBlocks.length === 1) {
6007
- const onlyBlock = segmentExpandedBlocks[0];
6008
- if (onlyBlock.startTime + onlyBlock.duration < segmentEnd) {
6009
- onlyBlock.duration = segmentEnd - onlyBlock.startTime;
6010
- }
5831
+ case "imageWithCaption": {
5832
+ const img = extractFirstImage(contents);
5833
+ if (!img) return placeholders ? { caption: headingText } : null;
5834
+ return { imageSrc: img.src, imageAlt: img.alt || headingText, caption: headingText };
6011
5835
  }
6012
- let changed = true;
6013
- while (changed) {
6014
- changed = false;
6015
- for (let i = segmentExpandedBlocks.length - 1; i >= 1; i--) {
6016
- const block = segmentExpandedBlocks[i];
6017
- if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP) {
6018
- const prev = segmentExpandedBlocks[i - 1];
6019
- const blockEnd = block.startTime + block.duration;
6020
- prev.duration = blockEnd - prev.startTime;
6021
- block.duration = 0;
6022
- block.startTime = segmentEnd;
6023
- segmentExpandedBlocks.splice(i, 1);
6024
- changed = true;
6025
- break;
6026
- }
6027
- }
5836
+ case "photoGrid": {
5837
+ const images = extractImages(contents, 4);
5838
+ if (images.length < 2) return placeholders ? {} : null;
5839
+ return {
5840
+ images: images.map((i) => ({ src: i.src, alt: i.alt })),
5841
+ caption: headingText
5842
+ };
6028
5843
  }
6029
- const MAX_BLOCK_DURATION = 20;
6030
- for (let i = 0; i < segmentExpandedBlocks.length; i++) {
6031
- const block = segmentExpandedBlocks[i];
6032
- if (block.duration > MAX_BLOCK_DURATION) {
6033
- const numParts = Math.ceil(block.duration / MAX_BLOCK_DURATION);
6034
- const partDuration = block.duration / numParts;
6035
- if (partDuration >= MIN_TRANSITION_GAP) {
6036
- block.duration = partDuration;
6037
- for (let p = 1; p < numParts; p++) {
6038
- const splitBlock = {
6039
- id: `${block.id}-split-${p}`,
6040
- startTime: block.startTime + p * partDuration,
6041
- duration: partDuration,
6042
- audioSegment: block.audioSegment,
6043
- layers: (block.layers ?? []).map((layer) => ({
6044
- ...cloneData(layer),
6045
- id: `${layer.id}-split-${p}`
6046
- })),
6047
- transition: { type: "dissolve", duration: 1 },
6048
- template: block.template
6049
- };
6050
- expandedBlocks.push(splitBlock);
6051
- }
5844
+ case "videoWithCaption": {
5845
+ const video = extractFirstEmbeddedVideo(contents);
5846
+ if (!video) return placeholders ? { caption: headingText } : null;
5847
+ return {
5848
+ videoSrc: video.src,
5849
+ posterSrc: video.posterSrc,
5850
+ videoAlt: video.alt || headingText,
5851
+ caption: headingText
5852
+ };
5853
+ }
5854
+ case "leftFeature":
5855
+ case "rightFeature": {
5856
+ const img = extractFirstImage(contents);
5857
+ if (!img && !placeholders) return null;
5858
+ return {
5859
+ imageSrc: img?.src ?? "",
5860
+ imageAlt: img?.alt || headingText,
5861
+ imageWidth: img?.width,
5862
+ imageHeight: img?.height,
5863
+ title: headingText,
5864
+ body: bodyText3
5865
+ };
5866
+ }
5867
+ case "diagram": {
5868
+ const fences = (contents ?? []).filter((n) => n.type === "code");
5869
+ const fence = fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : void 0;
5870
+ const detection = fence ? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) }) : void 0;
5871
+ if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
5872
+ const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
5873
+ return { nodes, edges, ...headingText ? { title: headingText } : {} };
5874
+ }
5875
+ case "timeline": {
5876
+ const fences = (contents ?? []).filter((n) => n.type === "code");
5877
+ const fence = fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang) ? fences[0] : void 0;
5878
+ const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : void 0;
5879
+ if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
5880
+ const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
5881
+ return { tracks, links, ...headingText ? { title: headingText } : {} };
5882
+ }
5883
+ case "tree": {
5884
+ const fences = (contents ?? []).filter((n) => n.type === "code");
5885
+ const fence = fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : void 0;
5886
+ if (fence) {
5887
+ const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
5888
+ if (detection.isTree && detection.tree) {
5889
+ const { items } = treeToTemplateData(detection.tree);
5890
+ return { items, ...headingText ? { title: headingText } : {} };
6052
5891
  }
6053
5892
  }
5893
+ const list = findFirstList(contents);
5894
+ if (list) {
5895
+ const { items } = treeToTemplateData(treeFromMarkdownList(list));
5896
+ if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
5897
+ }
5898
+ return placeholders ? {} : null;
6054
5899
  }
5900
+ default:
5901
+ return placeholders ? {} : null;
6055
5902
  }
6056
- return expandedBlocks.filter((block) => block && block.duration > 0);
6057
- }
6058
- function getAvailableTemplates() {
6059
- return Object.keys(templateRegistry);
6060
5903
  }
6061
- function hasTemplate(name) {
6062
- return resolveTemplateName(name) in templateRegistry;
6063
- }
6064
-
6065
- // src/doc/asciiDiagram/types.ts
6066
- var ASCII_CHAR_W = 14;
6067
- var ASCII_CHAR_H = 28;
6068
5904
 
6069
- // src/doc/asciiDiagram/mapping.ts
6070
- function asciiCellToCanvas(col, row) {
6071
- return { x: col * ASCII_CHAR_W, y: row * ASCII_CHAR_H };
5905
+ // src/doc/templates/mediaLayoutPolicy.ts
5906
+ var TITLE_VARIANTS = {
5907
+ landscape: {
5908
+ wide: "title-stack",
5909
+ tall: "split",
5910
+ square: "title-stack",
5911
+ multiple: "title-stack"
5912
+ },
5913
+ square: {
5914
+ wide: "title-stack",
5915
+ tall: "split",
5916
+ square: "title-stack",
5917
+ multiple: "title-stack"
5918
+ },
5919
+ portrait: {
5920
+ wide: "title-stack",
5921
+ tall: "title-stack",
5922
+ square: "title-stack",
5923
+ multiple: "title-stack"
5924
+ }
5925
+ };
5926
+ var TEXT_VARIANTS = {
5927
+ landscape: {
5928
+ wide: "split",
5929
+ tall: "split",
5930
+ square: "split",
5931
+ multiple: "split"
5932
+ },
5933
+ square: {
5934
+ wide: "text-stack",
5935
+ tall: "split",
5936
+ square: "text-stack",
5937
+ multiple: "text-stack"
5938
+ },
5939
+ portrait: {
5940
+ wide: "text-stack",
5941
+ tall: "text-stack",
5942
+ square: "text-stack",
5943
+ multiple: "text-stack"
5944
+ }
5945
+ };
5946
+ var DENSE_VARIANTS = {
5947
+ landscape: {
5948
+ wide: "dense-stack",
5949
+ tall: "dense-stack",
5950
+ square: "dense-stack",
5951
+ multiple: "dense-stack"
5952
+ },
5953
+ square: {
5954
+ wide: "dense-stack",
5955
+ tall: "dense-stack",
5956
+ square: "dense-stack",
5957
+ multiple: "dense-stack"
5958
+ },
5959
+ portrait: {
5960
+ wide: "dense-stack",
5961
+ tall: "dense-stack",
5962
+ square: "dense-stack",
5963
+ multiple: "dense-stack"
5964
+ }
5965
+ };
5966
+ var BLOCK_MEDIA_LAYOUT_POLICIES = {
5967
+ title: {
5968
+ summary: "Text-only by default; supplemental media gets a title band plus a reserved field.",
5969
+ noMedia: "template-default",
5970
+ ownership: "supplemental",
5971
+ unconsumedMedia: "reserved-slot",
5972
+ variants: TITLE_VARIANTS
5973
+ },
5974
+ sectionHeader: {
5975
+ summary: "Uses its authored background image when present; otherwise supplemental media receives a title composition.",
5976
+ noMedia: "template-without-optional-media",
5977
+ ownership: "optional-native",
5978
+ nativeLayout: "background-image",
5979
+ additionalMediaLayout: "overlay-inset",
5980
+ unconsumedMedia: "reserve-when-no-native-media",
5981
+ variants: TITLE_VARIANTS
5982
+ },
5983
+ statHighlight: {
5984
+ summary: "Uses its optional accent image natively; otherwise supplemental media occupies a companion field.",
5985
+ noMedia: "template-without-optional-media",
5986
+ ownership: "optional-native",
5987
+ nativeLayout: "accent-strip-or-inset",
5988
+ additionalMediaLayout: "overlay-inset",
5989
+ unconsumedMedia: "reserve-when-no-native-media",
5990
+ variants: TEXT_VARIANTS
5991
+ },
5992
+ quote: {
5993
+ summary: "Uses its optional accent image natively; otherwise supplemental media is paired with the quote.",
5994
+ noMedia: "template-without-optional-media",
5995
+ ownership: "optional-native",
5996
+ nativeLayout: "accent-strip-or-inset",
5997
+ additionalMediaLayout: "overlay-inset",
5998
+ unconsumedMedia: "reserve-when-no-native-media",
5999
+ variants: TEXT_VARIANTS
6000
+ },
6001
+ factCard: {
6002
+ summary: "Uses its optional accent image natively; otherwise supplemental media is paired with the fact card.",
6003
+ noMedia: "template-without-optional-media",
6004
+ ownership: "optional-native",
6005
+ nativeLayout: "accent-strip-or-inset",
6006
+ additionalMediaLayout: "overlay-inset",
6007
+ unconsumedMedia: "reserve-when-no-native-media",
6008
+ variants: TEXT_VARIANTS
6009
+ },
6010
+ twoColumn: {
6011
+ summary: "Text comparison by default; supplemental media stacks below the dense two-column panel.",
6012
+ noMedia: "template-default",
6013
+ ownership: "supplemental",
6014
+ unconsumedMedia: "reserved-slot",
6015
+ variants: DENSE_VARIANTS
6016
+ },
6017
+ dateEvent: {
6018
+ summary: "Uses its optional accent image natively; otherwise supplemental media is paired with the event.",
6019
+ noMedia: "template-without-optional-media",
6020
+ ownership: "optional-native",
6021
+ nativeLayout: "accent-strip-or-inset",
6022
+ additionalMediaLayout: "overlay-inset",
6023
+ unconsumedMedia: "reserve-when-no-native-media",
6024
+ variants: TEXT_VARIANTS
6025
+ },
6026
+ imageWithCaption: {
6027
+ summary: "Requires one primary image and owns its full-bleed image/caption geometry.",
6028
+ noMedia: "unsupported",
6029
+ ownership: "required-native",
6030
+ nativeLayout: "full-bleed-image",
6031
+ additionalMediaLayout: "overlay-inset",
6032
+ unconsumedMedia: "retain-native-layout"
6033
+ },
6034
+ leftFeature: {
6035
+ summary: "Requires a primary image in the left feature cell; text occupies the right cell.",
6036
+ noMedia: "unsupported",
6037
+ ownership: "required-native",
6038
+ nativeLayout: "feature-left",
6039
+ additionalMediaLayout: "overlay-inset",
6040
+ unconsumedMedia: "retain-native-layout"
6041
+ },
6042
+ rightFeature: {
6043
+ summary: "Requires a primary image in the right feature cell; text occupies the left cell.",
6044
+ noMedia: "unsupported",
6045
+ ownership: "required-native",
6046
+ nativeLayout: "feature-right",
6047
+ additionalMediaLayout: "overlay-inset",
6048
+ unconsumedMedia: "retain-native-layout"
6049
+ },
6050
+ map: {
6051
+ summary: "The map canvas is the primary visual; other media must not reflow its geography.",
6052
+ noMedia: "intrinsic-visual",
6053
+ ownership: "intrinsic-visual",
6054
+ nativeLayout: "map-canvas",
6055
+ additionalMediaLayout: "overlay-inset",
6056
+ unconsumedMedia: "retain-native-layout"
6057
+ },
6058
+ fullBleedQuote: {
6059
+ summary: "Despite its name this is text-only; supplemental media receives the same dramatic title treatment.",
6060
+ noMedia: "template-default",
6061
+ ownership: "supplemental",
6062
+ unconsumedMedia: "reserved-slot",
6063
+ variants: TITLE_VARIANTS
6064
+ },
6065
+ list: {
6066
+ summary: "Uses its optional accent image natively; otherwise supplemental media is paired with the list.",
6067
+ noMedia: "template-without-optional-media",
6068
+ ownership: "optional-native",
6069
+ nativeLayout: "accent-strip-or-inset",
6070
+ additionalMediaLayout: "overlay-inset",
6071
+ unconsumedMedia: "reserve-when-no-native-media",
6072
+ variants: TEXT_VARIANTS
6073
+ },
6074
+ photoGrid: {
6075
+ summary: "Requires a primary image collection and owns the complete tiled media field.",
6076
+ noMedia: "unsupported",
6077
+ ownership: "required-native",
6078
+ nativeLayout: "photo-grid",
6079
+ additionalMediaLayout: "overlay-inset",
6080
+ unconsumedMedia: "retain-native-layout"
6081
+ },
6082
+ definitionCard: {
6083
+ summary: "Uses its optional accent image natively; otherwise supplemental media is paired with the definition.",
6084
+ noMedia: "template-without-optional-media",
6085
+ ownership: "optional-native",
6086
+ nativeLayout: "accent-strip-or-inset",
6087
+ additionalMediaLayout: "overlay-inset",
6088
+ unconsumedMedia: "reserve-when-no-native-media",
6089
+ variants: TEXT_VARIANTS
6090
+ },
6091
+ comparisonBar: {
6092
+ summary: "Data bars are primary; supplemental media stacks below to preserve horizontal scale.",
6093
+ noMedia: "template-default",
6094
+ ownership: "supplemental",
6095
+ unconsumedMedia: "reserved-slot",
6096
+ variants: DENSE_VARIANTS
6097
+ },
6098
+ pullQuote: {
6099
+ summary: "Requires and owns a full-bleed background image behind the quote lockup.",
6100
+ noMedia: "unsupported",
6101
+ ownership: "required-native",
6102
+ nativeLayout: "full-bleed-image",
6103
+ additionalMediaLayout: "overlay-inset",
6104
+ unconsumedMedia: "retain-native-layout"
6105
+ },
6106
+ videoWithCaption: {
6107
+ summary: "Requires one primary video and owns its full-bleed video/caption geometry.",
6108
+ noMedia: "unsupported",
6109
+ ownership: "required-native",
6110
+ nativeLayout: "full-bleed-video",
6111
+ additionalMediaLayout: "overlay-inset",
6112
+ unconsumedMedia: "retain-native-layout"
6113
+ },
6114
+ videoPullQuote: {
6115
+ summary: "Requires and owns a full-bleed video behind the quote lockup.",
6116
+ noMedia: "unsupported",
6117
+ ownership: "required-native",
6118
+ nativeLayout: "full-bleed-video",
6119
+ additionalMediaLayout: "overlay-inset",
6120
+ unconsumedMedia: "retain-native-layout"
6121
+ },
6122
+ dataTable: {
6123
+ summary: "The table remains wide; supplemental media stacks below the dense data region.",
6124
+ noMedia: "template-default",
6125
+ ownership: "supplemental",
6126
+ unconsumedMedia: "reserved-slot",
6127
+ variants: DENSE_VARIANTS
6128
+ },
6129
+ diagram: {
6130
+ summary: "The node-and-edge canvas is intrinsic media and retains its spatial coordinate system.",
6131
+ noMedia: "intrinsic-visual",
6132
+ ownership: "intrinsic-visual",
6133
+ nativeLayout: "spatial-canvas",
6134
+ additionalMediaLayout: "overlay-inset",
6135
+ unconsumedMedia: "retain-native-layout"
6136
+ },
6137
+ tree: {
6138
+ summary: "The interactive tree canvas is intrinsic media and retains its full layout region.",
6139
+ noMedia: "intrinsic-visual",
6140
+ ownership: "intrinsic-visual",
6141
+ nativeLayout: "spatial-canvas",
6142
+ additionalMediaLayout: "overlay-inset",
6143
+ unconsumedMedia: "retain-native-layout"
6144
+ },
6145
+ timeline: {
6146
+ summary: "The track canvas is intrinsic media and retains its shared horizontal time scale.",
6147
+ noMedia: "intrinsic-visual",
6148
+ ownership: "intrinsic-visual",
6149
+ nativeLayout: "spatial-canvas",
6150
+ additionalMediaLayout: "overlay-inset",
6151
+ unconsumedMedia: "retain-native-layout"
6152
+ },
6153
+ layout: {
6154
+ summary: "The authored layer canvas is intrinsic media and must preserve absolute placement.",
6155
+ noMedia: "intrinsic-visual",
6156
+ ownership: "intrinsic-visual",
6157
+ nativeLayout: "freeform-canvas",
6158
+ additionalMediaLayout: "overlay-inset",
6159
+ unconsumedMedia: "retain-native-layout"
6160
+ },
6161
+ drawing: {
6162
+ summary: "The drawing canvas is intrinsic media and must preserve authored geometry.",
6163
+ noMedia: "intrinsic-visual",
6164
+ ownership: "intrinsic-visual",
6165
+ nativeLayout: "freeform-canvas",
6166
+ additionalMediaLayout: "overlay-inset",
6167
+ unconsumedMedia: "retain-native-layout"
6168
+ }
6169
+ };
6170
+ function getBlockMediaLayoutPolicy(template) {
6171
+ if (!template) return void 0;
6172
+ return BLOCK_MEDIA_LAYOUT_POLICIES[template];
6072
6173
  }
6073
- function canvasToAsciiCell(x, y) {
6174
+
6175
+ // src/doc/richMediaLayout.ts
6176
+ function rect(viewport, x, y, width, height) {
6074
6177
  return {
6075
- col: Math.max(0, Math.round(x / ASCII_CHAR_W)),
6076
- row: Math.max(0, Math.round(y / ASCII_CHAR_H))
6178
+ x: viewport.width * x,
6179
+ y: viewport.height * y,
6180
+ width: viewport.width * width,
6181
+ height: viewport.height * height
6077
6182
  };
6078
6183
  }
6079
- function asciiDiagramToTemplateData(diagram) {
6080
- const nodes = diagram.nodes.map((n) => ({
6081
- id: n.id,
6082
- label: n.label,
6083
- ...asciiCellToCanvas(n.col, n.row),
6084
- w: n.wCols * ASCII_CHAR_W,
6085
- h: n.hRows * ASCII_CHAR_H,
6086
- ...n.containerId ? { container: n.containerId } : {}
6087
- }));
6088
- const anchors = inferEdgeAnchors(nodes, diagram.edges);
6089
- const edges = diagram.edges.map((e, index) => ({
6090
- source: e.source,
6091
- target: e.target,
6092
- ...e.label ? { label: e.label } : {},
6093
- ...e.directed ? {} : { directed: false },
6094
- ...anchors[index],
6095
- routing: "orthogonal"
6096
- }));
6097
- return { nodes, edges };
6184
+ function mediaShape(mediaCount, aspectRatios) {
6185
+ if (mediaCount > 1) return "multiple";
6186
+ const aspect = aspectRatios[0] ?? 16 / 9;
6187
+ if (aspect < 0.8) return "tall";
6188
+ if (aspect > 1.25) return "wide";
6189
+ return "square";
6098
6190
  }
6099
- function inferEdgeAnchors(nodes, edges) {
6100
- const nodeById = new Map(nodes.map((node) => [node.id, node]));
6101
- const pairs = edges.map((edge) => {
6102
- const source = nodeById.get(edge.source);
6103
- const target = nodeById.get(edge.target);
6104
- if (!source || !target) {
6105
- return {
6106
- sourceAnchor: { side: "right", offset: 0.5 },
6107
- targetAnchor: { side: "left", offset: 0.5 }
6108
- };
6109
- }
6110
- const sourceW = source.w ?? ASCII_CHAR_W * 12;
6111
- const sourceH = source.h ?? ASCII_CHAR_H * 3;
6112
- const targetW = target.w ?? ASCII_CHAR_W * 12;
6113
- const targetH = target.h ?? ASCII_CHAR_H * 3;
6114
- const dx = target.x + targetW / 2 - (source.x + sourceW / 2);
6115
- const dy = target.y + targetH / 2 - (source.y + sourceH / 2);
6116
- const horizontalDistance = Math.abs(dx) / Math.max(1, (sourceW + targetW) / 2);
6117
- const verticalDistance = Math.abs(dy) / Math.max(1, (sourceH + targetH) / 2);
6118
- if (verticalDistance >= horizontalDistance) {
6119
- return dy >= 0 ? {
6120
- sourceAnchor: { side: "bottom", offset: 0.5 },
6121
- targetAnchor: { side: "top", offset: 0.5 }
6122
- } : {
6123
- sourceAnchor: { side: "top", offset: 0.5 },
6124
- targetAnchor: { side: "bottom", offset: 0.5 }
6125
- };
6126
- }
6127
- return dx >= 0 ? {
6128
- sourceAnchor: { side: "right", offset: 0.5 },
6129
- targetAnchor: { side: "left", offset: 0.5 }
6130
- } : {
6131
- sourceAnchor: { side: "left", offset: 0.5 },
6132
- targetAnchor: { side: "right", offset: 0.5 }
6191
+ function stackedTitleLayout(viewport, orientation) {
6192
+ if (orientation === "portrait") {
6193
+ return {
6194
+ contentRect: rect(viewport, 0.06, 0.04, 0.88, 0.3),
6195
+ mediaRect: rect(viewport, 0.06, 0.39, 0.88, 0.55),
6196
+ fontScale: 0.76
6133
6197
  };
6134
- });
6135
- distributeOffsets(pairs, edges, nodes, "source");
6136
- distributeOffsets(pairs, edges, nodes, "target");
6137
- return pairs;
6138
- }
6139
- function distributeOffsets(pairs, edges, nodes, endpoint) {
6140
- const nodeById = new Map(nodes.map((node) => [node.id, node]));
6141
- const groups = /* @__PURE__ */ new Map();
6142
- for (let index = 0; index < edges.length; index++) {
6143
- const edge = edges[index];
6144
- const anchor = endpoint === "source" ? pairs[index].sourceAnchor : pairs[index].targetAnchor;
6145
- const nodeId = endpoint === "source" ? edge.source : edge.target;
6146
- const key = `${nodeId}:${anchor.side}`;
6147
- const group = groups.get(key) ?? [];
6148
- group.push(index);
6149
- groups.set(key, group);
6150
- }
6151
- for (const indexes of groups.values()) {
6152
- if (indexes.length < 2) continue;
6153
- const side = endpoint === "source" ? pairs[indexes[0]].sourceAnchor.side : pairs[indexes[0]].targetAnchor.side;
6154
- indexes.sort((a, b) => {
6155
- const oppositeA = nodeById.get(endpoint === "source" ? edges[a].target : edges[a].source);
6156
- const oppositeB = nodeById.get(endpoint === "source" ? edges[b].target : edges[b].source);
6157
- const axisA = side === "top" || side === "bottom" ? oppositeA?.x ?? 0 : oppositeA?.y ?? 0;
6158
- const axisB = side === "top" || side === "bottom" ? oppositeB?.x ?? 0 : oppositeB?.y ?? 0;
6159
- return axisA - axisB || a - b;
6160
- });
6161
- indexes.forEach((edgeIndex, rank) => {
6162
- const anchor = endpoint === "source" ? pairs[edgeIndex].sourceAnchor : pairs[edgeIndex].targetAnchor;
6163
- anchor.offset = (rank + 1) / (indexes.length + 1);
6164
- });
6165
6198
  }
6166
- }
6167
- function asciiDiagramFromTemplateData(nodes, edges, options = {}) {
6168
- const ids = new Set(nodes.map((n) => n.id));
6169
- const outNodes = nodes.map((n) => {
6170
- const { col, row } = canvasToAsciiCell(n.x, n.y);
6171
- const labelLines = n.label.length > 0 ? n.label.split("\n") : [];
6172
- const innerW = labelLines.reduce((w, l) => Math.max(w, l.length), 0);
6173
- const wCols = typeof n.w === "number" ? Math.max(3, Math.round(n.w / ASCII_CHAR_W)) : innerW + 4;
6174
- const hRows = typeof n.h === "number" ? Math.max(3, Math.round(n.h / ASCII_CHAR_H)) : labelLines.length + 2;
6199
+ if (orientation === "square") {
6175
6200
  return {
6176
- id: n.id,
6177
- label: n.label,
6178
- col,
6179
- row,
6180
- wCols: Math.max(wCols, 3),
6181
- hRows: Math.max(hRows, 3),
6182
- ...n.container && ids.has(n.container) && n.container !== n.id ? { containerId: n.container } : {}
6201
+ contentRect: rect(viewport, 0.05, 0.04, 0.9, 0.33),
6202
+ mediaRect: rect(viewport, 0.07, 0.42, 0.86, 0.51),
6203
+ fontScale: 0.72
6183
6204
  };
6184
- });
6185
- return {
6186
- nodes: outNodes,
6187
- edges: edges.filter((e) => ids.has(e.source) && ids.has(e.target)).map((e) => ({
6188
- source: e.source,
6189
- target: e.target,
6190
- ...e.label ? { label: e.label } : {},
6191
- directed: e.directed !== false
6192
- })),
6193
- style: options.style ?? "unicode",
6194
- warnings: []
6195
- };
6196
- }
6197
- function asciiDiagramFromBlocks(children, options = {}) {
6198
- const layout = computeDiagramLayout(children);
6199
- const nodes = layout.nodes.map((n) => ({
6200
- id: n.id,
6201
- label: n.label,
6202
- x: n.x,
6203
- y: n.y
6204
- }));
6205
- const edges = layout.edges.map((e) => ({
6206
- source: e.source,
6207
- target: e.target,
6208
- ...e.type ? { label: e.type } : {}
6209
- }));
6210
- const diagram = asciiDiagramFromTemplateData(nodes, edges, options);
6211
- diagram.warnings.push(...layout.warnings);
6212
- return diagram;
6213
- }
6214
-
6215
- // src/doc/asciiTimeline/mapping.ts
6216
- function asciiTimelineToTemplateData(timeline) {
6217
- const globalStart = timeline.tracks.length > 0 ? Math.min(...timeline.tracks.map((track) => track.startColumn)) : 0;
6218
- const globalEnd = timeline.tracks.length > 0 ? Math.max(...timeline.tracks.map((track) => track.endColumn)) : 1;
6219
- const span = Math.max(1, globalEnd - globalStart);
6220
- const tracks = timeline.tracks.map((track) => ({
6221
- id: track.id,
6222
- ...track.label ? { label: track.label } : {},
6223
- ...track.endLabel ? { endLabel: track.endLabel } : {},
6224
- events: track.events.map(
6225
- (event) => ({
6226
- id: event.id,
6227
- label: event.label,
6228
- ...event.description ? { description: event.description } : {},
6229
- position: clamp012((event.column - globalStart) / span),
6230
- ...event.side ? { side: event.side } : {},
6231
- ...event.descriptionSide ? { descriptionSide: event.descriptionSide } : {},
6232
- ...event.callout === false ? { callout: false } : {},
6233
- ...event.marker ? { marker: event.marker } : {}
6234
- })
6235
- )
6236
- }));
6237
- const links = timeline.links.map((link) => ({ ...link }));
6238
- return { tracks, links };
6239
- }
6240
- function clamp012(value) {
6241
- return Math.max(0, Math.min(1, value));
6242
- }
6243
-
6244
- // src/doc/treeview/mapping.ts
6245
- function treeToTemplateData(tree) {
6246
- return { items: tree.roots.map(toItem) };
6247
- }
6248
- function toItem(node) {
6205
+ }
6249
6206
  return {
6250
- id: node.id,
6251
- label: node.label,
6252
- ...node.isDir ? { isDir: true } : {},
6253
- ...node.comment ? { comment: node.comment } : {},
6254
- children: node.children.map(toItem)
6207
+ contentRect: rect(viewport, 0.04, 0.03, 0.92, 0.32),
6208
+ mediaRect: rect(viewport, 0.08, 0.4, 0.84, 0.52),
6209
+ fontScale: 0.72
6255
6210
  };
6256
6211
  }
6257
- function treeFromTemplateData(items, options = {}) {
6258
- const roots = items.map(fromItem);
6259
- return { roots, style: options.style ?? "unicode", warnings: [] };
6260
- }
6261
- function fromItem(item) {
6262
- const children = Array.isArray(item.children) ? item.children.map(fromItem) : [];
6212
+ function splitLayout(viewport, orientation) {
6213
+ const square = orientation === "square";
6263
6214
  return {
6264
- id: typeof item.id === "string" && item.id ? item.id : "node",
6265
- label: typeof item.label === "string" ? item.label : "",
6266
- children,
6267
- isDir: item.isDir === true || children.length > 0,
6268
- ...item.comment ? { comment: item.comment } : {}
6215
+ contentRect: rect(viewport, square ? 0.05 : 0.04, 0.05, square ? 0.42 : 0.43, 0.9),
6216
+ mediaRect: rect(viewport, square ? 0.52 : 0.53, 0.07, square ? 0.43 : 0.43, 0.86),
6217
+ fontScale: 0.72
6269
6218
  };
6270
6219
  }
6271
- function treeFromMarkdownList(list) {
6272
- const usedIds = /* @__PURE__ */ new Map();
6273
- const slugify2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "node";
6274
- const idFor = (label) => {
6275
- const base = slugify2(label.replace(/\/$/, ""));
6276
- const count = usedIds.get(base) ?? 0;
6277
- usedIds.set(base, count + 1);
6278
- return count === 0 ? base : `${base}-${count + 1}`;
6279
- };
6280
- const itemToNode = (item) => {
6281
- const labelNode = item.children.find((c) => c.type !== "list");
6282
- const label = labelNode ? extractPlainText(labelNode).trim() : "";
6283
- const childLists = item.children.filter((c) => c.type === "list");
6284
- const children = [];
6285
- for (const cl of childLists) for (const ci of cl.children) children.push(itemToNode(ci));
6220
+ function stackedTextLayout(viewport, orientation, dense) {
6221
+ if (orientation === "portrait") {
6286
6222
  return {
6287
- id: idFor(label),
6288
- label,
6289
- children,
6290
- isDir: label.endsWith("/") || children.length > 0
6223
+ contentRect: rect(viewport, 0.06, 0.04, 0.88, dense ? 0.41 : 0.36),
6224
+ mediaRect: rect(viewport, 0.06, dense ? 0.5 : 0.45, 0.88, dense ? 0.44 : 0.49),
6225
+ fontScale: dense ? 0.42 : 0.44
6226
+ };
6227
+ }
6228
+ if (orientation === "square") {
6229
+ return {
6230
+ contentRect: rect(viewport, 0.05, 0.04, 0.9, dense ? 0.43 : 0.36),
6231
+ mediaRect: rect(viewport, 0.07, dense ? 0.52 : 0.45, 0.86, dense ? 0.41 : 0.48),
6232
+ fontScale: dense ? 0.45 : 0.46
6291
6233
  };
6234
+ }
6235
+ return {
6236
+ contentRect: rect(viewport, 0.05, 0.03, 0.9, dense ? 0.46 : 0.35),
6237
+ mediaRect: rect(viewport, 0.07, dense ? 0.55 : 0.43, 0.86, dense ? 0.38 : 0.49),
6238
+ fontScale: dense ? 0.5 : 0.48
6292
6239
  };
6293
- return { roots: list.children.map(itemToNode), style: "unicode", warnings: [] };
6294
- }
6295
- function findFirstList(contents) {
6296
- return contents?.find((n) => n.type === "list");
6297
- }
6298
-
6299
- // src/doc/templateInputs.ts
6300
- function extractBodyPlainText(contents) {
6301
- if (!contents || contents.length === 0) return "";
6302
- return contents.filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((n) => extractPlainText(n)).join("\n").trim();
6303
6240
  }
6304
- function extractListItems(contents) {
6305
- if (!contents) return [];
6306
- const items = [];
6307
- for (const node of contents) {
6308
- if (node.type === "list") {
6309
- for (const item of node.children) {
6310
- const text = extractPlainText(item).trim();
6311
- if (text) items.push(text);
6312
- }
6313
- }
6241
+ function overlayMediaRect(viewport, shape) {
6242
+ const orientation = getViewportOrientation(viewport);
6243
+ if (orientation === "portrait") {
6244
+ switch (shape) {
6245
+ case "tall":
6246
+ return rect(viewport, 0.55, 0.5, 0.38, 0.44);
6247
+ case "square":
6248
+ return rect(viewport, 0.49, 0.58, 0.44, 0.35);
6249
+ case "multiple":
6250
+ return rect(viewport, 0.07, 0.67, 0.86, 0.27);
6251
+ case "wide":
6252
+ return rect(viewport, 0.07, 0.71, 0.86, 0.23);
6253
+ }
6254
+ }
6255
+ if (orientation === "square") {
6256
+ switch (shape) {
6257
+ case "tall":
6258
+ return rect(viewport, 0.68, 0.1, 0.27, 0.8);
6259
+ case "square":
6260
+ return rect(viewport, 0.55, 0.52, 0.4, 0.4);
6261
+ case "multiple":
6262
+ return rect(viewport, 0.08, 0.62, 0.84, 0.31);
6263
+ case "wide":
6264
+ return rect(viewport, 0.08, 0.68, 0.84, 0.27);
6265
+ }
6266
+ }
6267
+ switch (shape) {
6268
+ case "tall":
6269
+ return rect(viewport, 0.7, 0.1, 0.26, 0.8);
6270
+ case "square":
6271
+ return rect(viewport, 0.62, 0.22, 0.34, 0.56);
6272
+ case "multiple":
6273
+ return rect(viewport, 0.52, 0.5, 0.44, 0.42);
6274
+ case "wide":
6275
+ return rect(viewport, 0.54, 0.58, 0.42, 0.34);
6276
+ }
6277
+ }
6278
+ function designedLayout(variant, viewport) {
6279
+ const orientation = getViewportOrientation(viewport);
6280
+ switch (variant) {
6281
+ case "title-stack":
6282
+ return stackedTitleLayout(viewport, orientation);
6283
+ case "split":
6284
+ return splitLayout(viewport, orientation);
6285
+ case "text-stack":
6286
+ return stackedTextLayout(viewport, orientation, false);
6287
+ case "dense-stack":
6288
+ return stackedTextLayout(viewport, orientation, true);
6314
6289
  }
6315
- return items;
6316
6290
  }
6317
- function parseDim(raw) {
6318
- if (raw === void 0) return void 0;
6319
- const n = parseFloat(raw);
6320
- return Number.isFinite(n) && n > 0 ? n : void 0;
6291
+ function resolvePositionValue(value, dimension) {
6292
+ if (value === void 0) return 0;
6293
+ if (typeof value === "number") return value;
6294
+ const percent = value.match(/^\s*(-?\d+(?:\.\d+)?)%\s*$/);
6295
+ if (percent) return Number(percent[1]) / 100 * dimension;
6296
+ const numeric = Number.parseFloat(value);
6297
+ return Number.isFinite(numeric) ? numeric : 0;
6321
6298
  }
6322
- function extractImages(contents, limit = Infinity) {
6323
- if (!contents || contents.length === 0) return [];
6324
- const found = [];
6325
- function fromHtml(nodes) {
6326
- for (const node of nodes) {
6327
- if (found.length >= limit) return;
6328
- if (!node || typeof node !== "object") continue;
6329
- const n = node;
6330
- if (n.type === "htmlElement" && n.tagName === "img") {
6331
- const attrs = n.attributes;
6332
- if (attrs && typeof attrs.src === "string" && attrs.src) {
6333
- found.push({
6334
- src: attrs.src,
6335
- alt: typeof attrs.alt === "string" ? attrs.alt : "",
6336
- width: parseDim(attrs.width),
6337
- height: parseDim(attrs.height)
6338
- });
6339
- }
6340
- }
6341
- if (Array.isArray(n.children)) fromHtml(n.children);
6342
- }
6299
+ function isZero(value) {
6300
+ return value === 0 || value === "0" || value === "0%";
6301
+ }
6302
+ function isFullViewportBackground(layer, viewport) {
6303
+ if (layer.type !== "shape" && layer.type !== "image") return false;
6304
+ const { position } = layer;
6305
+ const fullWidth = position.width === "100%" || position.width === viewport.width;
6306
+ const fullHeight = position.height === "100%" || position.height === viewport.height;
6307
+ return isZero(position.x) && isZero(position.y) && fullWidth && fullHeight;
6308
+ }
6309
+ function mapPosition(position, contentRect, viewport) {
6310
+ const mapped = {
6311
+ ...position,
6312
+ x: contentRect.x + resolvePositionValue(position.x, viewport.width) / viewport.width * contentRect.width,
6313
+ y: contentRect.y + resolvePositionValue(position.y, viewport.height) / viewport.height * contentRect.height
6314
+ };
6315
+ if (position.width !== void 0) {
6316
+ mapped.width = resolvePositionValue(position.width, viewport.width) / viewport.width * contentRect.width;
6343
6317
  }
6344
- function walk(node) {
6345
- if (found.length >= limit) return;
6346
- if (!node || typeof node !== "object") return;
6347
- const n = node;
6348
- if (n.type === "image" && typeof n.url === "string" && n.url) {
6349
- found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
6350
- return;
6318
+ if (position.height !== void 0) {
6319
+ mapped.height = resolvePositionValue(position.height, viewport.height) / viewport.height * contentRect.height;
6320
+ }
6321
+ return mapped;
6322
+ }
6323
+ function mapForegroundLayers(layers, contentRect, viewport, fontScale, template) {
6324
+ const mapped = layers.map((layer) => {
6325
+ if (isFullViewportBackground(layer, viewport)) return layer;
6326
+ const position = mapPosition(layer.position, contentRect, viewport);
6327
+ if (layer.type === "text") {
6328
+ return {
6329
+ ...layer,
6330
+ position,
6331
+ content: {
6332
+ ...layer.content,
6333
+ style: {
6334
+ ...layer.content.style,
6335
+ fontSize: Math.max(12, Math.round(layer.content.style.fontSize * fontScale)),
6336
+ ...layer.content.style.padding !== void 0 ? { padding: Math.round(layer.content.style.padding * fontScale) } : {}
6337
+ }
6338
+ }
6339
+ };
6351
6340
  }
6352
- if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
6353
- fromHtml(n.htmlChildren);
6341
+ return { ...layer, position };
6342
+ });
6343
+ return arrangeTitleFamily(mapped, template, contentRect, viewport);
6344
+ }
6345
+ function arrangeTitleFamily(layers, template, contentRect, viewport) {
6346
+ const centerX = contentRect.x + contentRect.width / 2;
6347
+ const centerY = contentRect.y + contentRect.height / 2;
6348
+ if (template === "sectionHeader") {
6349
+ return layers.map((layer) => {
6350
+ if (layer.id === "title") {
6351
+ return {
6352
+ ...layer,
6353
+ position: {
6354
+ ...layer.position,
6355
+ x: centerX,
6356
+ y: centerY,
6357
+ width: contentRect.width * 0.9,
6358
+ anchor: "center"
6359
+ }
6360
+ };
6361
+ }
6362
+ if (layer.id === "line-top" || layer.id === "line-bottom") {
6363
+ const direction = layer.id === "line-top" ? -1 : 1;
6364
+ return {
6365
+ ...layer,
6366
+ position: {
6367
+ ...layer.position,
6368
+ x: centerX,
6369
+ y: centerY + direction * contentRect.height * 0.28,
6370
+ width: Math.min(contentRect.width * 0.22, viewport.width * 0.2),
6371
+ height: 2,
6372
+ anchor: "center"
6373
+ }
6374
+ };
6375
+ }
6376
+ return layer;
6377
+ });
6378
+ }
6379
+ if (template !== "title") return layers;
6380
+ const title = layers.find((layer) => layer.type === "text" && layer.id === "title");
6381
+ const subtitle = layers.find((layer) => layer.type === "text" && layer.id === "subtitle");
6382
+ if (!title || title.type !== "text") return layers;
6383
+ const subtitleText = subtitle?.type === "text" ? subtitle : void 0;
6384
+ const maxWidth = contentRect.width * 0.9;
6385
+ const decorationGap = Math.max(12, Math.min(viewport.width, viewport.height) * 0.018);
6386
+ const textGap = subtitleText ? decorationGap : 0;
6387
+ const availableHeight = contentRect.height * 0.78;
6388
+ let titleFontSize = title.content.style.fontSize;
6389
+ let subtitleFontSize = subtitleText?.content.style.fontSize ?? 0;
6390
+ const measure = () => {
6391
+ const titleHeight = estimateTextHeight(
6392
+ title.content.text,
6393
+ titleFontSize,
6394
+ maxWidth,
6395
+ title.content.style.lineHeight ?? 1.15
6396
+ );
6397
+ const subtitleHeight = subtitleText ? estimateTextHeight(
6398
+ subtitleText.content.text,
6399
+ subtitleFontSize,
6400
+ maxWidth,
6401
+ subtitleText.content.style.lineHeight ?? 1.5
6402
+ ) : 0;
6403
+ return { titleHeight, subtitleHeight, total: titleHeight + textGap + subtitleHeight };
6404
+ };
6405
+ let measured = measure();
6406
+ if (measured.total > availableHeight) {
6407
+ const fit = Math.max(0.5, availableHeight / measured.total);
6408
+ titleFontSize = Math.max(18, Math.round(titleFontSize * fit));
6409
+ subtitleFontSize = Math.max(14, Math.round(subtitleFontSize * fit));
6410
+ measured = measure();
6411
+ }
6412
+ const top = contentRect.y + (contentRect.height - measured.total) / 2;
6413
+ const titleY = top + measured.titleHeight / 2;
6414
+ const subtitleY = top + measured.titleHeight + textGap + measured.subtitleHeight / 2;
6415
+ return layers.map((layer) => {
6416
+ if (layer.id === "title" && layer.type === "text") {
6417
+ return {
6418
+ ...layer,
6419
+ position: { ...layer.position, x: centerX, y: titleY, width: maxWidth, anchor: "center" },
6420
+ content: {
6421
+ ...layer.content,
6422
+ style: { ...layer.content.style, fontSize: titleFontSize }
6423
+ }
6424
+ };
6354
6425
  }
6355
- if (Array.isArray(n.children)) {
6356
- for (const child of n.children) walk(child);
6426
+ if (layer.id === "subtitle" && layer.type === "text") {
6427
+ return {
6428
+ ...layer,
6429
+ position: {
6430
+ ...layer.position,
6431
+ x: centerX,
6432
+ y: subtitleY,
6433
+ width: maxWidth,
6434
+ anchor: "center"
6435
+ },
6436
+ content: {
6437
+ ...layer.content,
6438
+ style: { ...layer.content.style, fontSize: subtitleFontSize }
6439
+ }
6440
+ };
6357
6441
  }
6442
+ if (layer.id === "accent-line") {
6443
+ return {
6444
+ ...layer,
6445
+ position: {
6446
+ ...layer.position,
6447
+ x: centerX,
6448
+ y: Math.max(contentRect.y + 2, top - decorationGap * 0.75),
6449
+ width: Math.min(contentRect.width * 0.18, viewport.width * 0.12),
6450
+ height: 3,
6451
+ anchor: "center"
6452
+ }
6453
+ };
6454
+ }
6455
+ return layer;
6456
+ });
6457
+ }
6458
+ function positionRect(position, viewport, fallbackWidth, fallbackHeight) {
6459
+ const width = position.width ? resolvePositionValue(position.width, viewport.width) : fallbackWidth;
6460
+ const height = position.height ? resolvePositionValue(position.height, viewport.height) : fallbackHeight;
6461
+ let x = resolvePositionValue(position.x, viewport.width);
6462
+ let y = resolvePositionValue(position.y, viewport.height);
6463
+ const anchor = position.anchor ?? "top-left";
6464
+ if (anchor === "center") {
6465
+ x -= width / 2;
6466
+ y -= height / 2;
6467
+ } else {
6468
+ if (anchor.endsWith("right")) x -= width;
6469
+ if (anchor.startsWith("bottom")) y -= height;
6358
6470
  }
6359
- for (const node of contents) {
6360
- if (found.length >= limit) break;
6361
- walk(node);
6471
+ return { x, y, width, height };
6472
+ }
6473
+ function overlapArea(a, b) {
6474
+ const width = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
6475
+ const height = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
6476
+ return width * height;
6477
+ }
6478
+ function leastObstructedMediaRect(layers, viewport) {
6479
+ const occupied = layers.filter((layer) => layer.type !== "shape" && layer.type !== "path" && layer.type !== "mermaid").map(
6480
+ (layer) => positionRect(
6481
+ layer.position,
6482
+ viewport,
6483
+ layer.type === "text" ? viewport.width * 0.45 : viewport.width * 0.5,
6484
+ layer.type === "text" ? viewport.height * 0.2 : viewport.height * 0.55
6485
+ )
6486
+ );
6487
+ if (occupied.length === 0) return rect(viewport, 0.06, 0.08, 0.88, 0.84);
6488
+ const candidates = [
6489
+ rect(viewport, 0.52, 0.12, 0.44, 0.76),
6490
+ rect(viewport, 0.04, 0.12, 0.44, 0.76),
6491
+ rect(viewport, 0.15, 0.53, 0.7, 0.41)
6492
+ ];
6493
+ return candidates.reduce((best, candidate) => {
6494
+ const score = occupied.reduce((sum, item) => sum + overlapArea(candidate, item), 0);
6495
+ const bestScore = occupied.reduce((sum, item) => sum + overlapArea(best, item), 0);
6496
+ return score < bestScore ? candidate : best;
6497
+ });
6498
+ }
6499
+ function resolveSupplementalMediaLayout(layers, template, viewport, mediaCount, aspectRatios) {
6500
+ const policy = getBlockMediaLayoutPolicy(template);
6501
+ const shape = mediaShape(mediaCount, aspectRatios);
6502
+ const alreadyOwnsMedia = layers.some(
6503
+ (layer) => layer.type === "image" || layer.type === "video" || layer.type === "map" || layer.type === "mermaid"
6504
+ );
6505
+ const retainNativeLayout = policy?.unconsumedMedia === "retain-native-layout" || policy?.unconsumedMedia === "reserve-when-no-native-media" && alreadyOwnsMedia;
6506
+ if (policy?.additionalMediaLayout === "overlay-inset" && retainNativeLayout) {
6507
+ return {
6508
+ layers,
6509
+ mediaRect: overlayMediaRect(viewport, shape),
6510
+ framed: true
6511
+ };
6362
6512
  }
6363
- return found;
6513
+ const canReserve = policy?.unconsumedMedia === "reserved-slot" || policy?.unconsumedMedia === "reserve-when-no-native-media" && !alreadyOwnsMedia;
6514
+ if (!policy || !canReserve || !policy.variants) {
6515
+ return {
6516
+ layers,
6517
+ mediaRect: leastObstructedMediaRect(layers, viewport),
6518
+ framed: false
6519
+ };
6520
+ }
6521
+ const orientation = getViewportOrientation(viewport);
6522
+ const layout = designedLayout(policy.variants[orientation][shape], viewport);
6523
+ return {
6524
+ layers: mapForegroundLayers(
6525
+ layers,
6526
+ layout.contentRect,
6527
+ viewport,
6528
+ layout.fontScale,
6529
+ template ?? ""
6530
+ ),
6531
+ mediaRect: layout.mediaRect,
6532
+ framed: true
6533
+ };
6364
6534
  }
6365
- function extractFirstImage(contents) {
6366
- return extractImages(contents, 1)[0] ?? null;
6535
+
6536
+ // src/doc/materializeBlockLayers.ts
6537
+ function materializeBlockLayers(block, options = {}) {
6538
+ const registry = options.customTemplates && options.customTemplates.length > 0 ? buildRegistry(options.customTemplates) : templateRegistry;
6539
+ return materializeBlockLayersWithRuntime(block, options, { registry });
6367
6540
  }
6368
- function extractTableFromContents(contents) {
6369
- if (!contents) return null;
6370
- for (const node of contents) {
6371
- if (node.type === "table") {
6372
- const table = node;
6373
- const [headerRow, ...bodyRows] = table.children;
6374
- if (!headerRow) return null;
6375
- const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
6376
- const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
6377
- return { headers, rows, align: table.align };
6541
+ function materializeBlockLayersWithRuntime(block, options, runtime) {
6542
+ const theme = options.theme ?? DEFAULT_THEME;
6543
+ const viewport = options.viewport ?? VIEWPORT_PRESETS.landscape;
6544
+ const blockIndex = options.blockIndex ?? 0;
6545
+ const totalBlocks = options.totalBlocks ?? 1;
6546
+ const effectivePersistentLayers = options.persistentLayers === false ? void 0 : options.persistentLayers ?? theme.persistentLayers;
6547
+ const existingLayers = block.layers;
6548
+ if (existingLayers && existingLayers.length > 0 && !isTemplateBlock(block)) {
6549
+ const layers = appendRichContentLayers(
6550
+ cloneData(existingLayers),
6551
+ block,
6552
+ theme,
6553
+ viewport
6554
+ );
6555
+ return {
6556
+ layers: injectPersistentLayers(
6557
+ layers,
6558
+ block,
6559
+ theme,
6560
+ effectivePersistentLayers,
6561
+ runtime.expandedPersistentLayers
6562
+ ),
6563
+ source: "authored"
6564
+ };
6565
+ }
6566
+ if (!isTemplateBlock(block)) {
6567
+ const layers = appendRichContentLayers([], block, theme, viewport);
6568
+ return {
6569
+ layers: injectPersistentLayers(
6570
+ layers,
6571
+ block,
6572
+ theme,
6573
+ effectivePersistentLayers,
6574
+ runtime.expandedPersistentLayers
6575
+ ),
6576
+ source: layers.length > 0 ? "rich-content" : "empty"
6577
+ };
6578
+ }
6579
+ const context = runtime.templateContext ? cloneData(runtime.templateContext) : createTemplateContext(theme, blockIndex, totalBlocks, viewport);
6580
+ context.block = cloneData(block);
6581
+ const execution = executeTemplateMaterialization(block, context, runtime.registry);
6582
+ if (execution.status === "ok") {
6583
+ const styledLayers = runtime.applyRenderStyle !== false ? applyRenderStyleToLayers(execution.layers, block, theme) : execution.layers;
6584
+ const layers = appendRichContentLayers(styledLayers, block, theme, viewport);
6585
+ return {
6586
+ layers: injectPersistentLayers(
6587
+ layers,
6588
+ block,
6589
+ theme,
6590
+ effectivePersistentLayers,
6591
+ runtime.expandedPersistentLayers
6592
+ ),
6593
+ source: "template"
6594
+ };
6595
+ }
6596
+ const diagnostic = toLayerMaterializationDiagnostic(block.template, execution);
6597
+ if ((options.failureMode ?? "fallback") === "empty") {
6598
+ const layers = appendRichContentLayers([], block, theme, viewport);
6599
+ return {
6600
+ layers: injectPersistentLayers(
6601
+ layers,
6602
+ block,
6603
+ theme,
6604
+ effectivePersistentLayers,
6605
+ runtime.expandedPersistentLayers
6606
+ ),
6607
+ source: layers.length > 0 ? "rich-content" : "empty",
6608
+ diagnostic
6609
+ };
6610
+ }
6611
+ const fallbackLayers = appendRichContentLayers(
6612
+ fallbackBlockLayers(block, context, diagnostic.message),
6613
+ block,
6614
+ theme,
6615
+ viewport
6616
+ );
6617
+ return {
6618
+ layers: injectPersistentLayers(
6619
+ fallbackLayers,
6620
+ block,
6621
+ theme,
6622
+ effectivePersistentLayers,
6623
+ runtime.expandedPersistentLayers
6624
+ ),
6625
+ source: "fallback",
6626
+ diagnostic
6627
+ };
6628
+ }
6629
+ function mermaidSources(block) {
6630
+ return (block.contents ?? []).filter(
6631
+ (node) => node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid"
6632
+ ).map((node) => node.value).filter((source) => source.trim().length > 0);
6633
+ }
6634
+ function collectRichMediaItems(layers, block) {
6635
+ const existingImages = new Set(
6636
+ layers.filter((layer) => layer.type === "image").map((layer) => layer.content.src)
6637
+ );
6638
+ const existingVideos = new Set(
6639
+ layers.filter((layer) => layer.type === "video").map((layer) => layer.content.src)
6640
+ );
6641
+ const existingMermaid = new Set(
6642
+ layers.filter((layer) => layer.type === "mermaid").map((layer) => layer.content.source)
6643
+ );
6644
+ const embeddedVideos = extractEmbeddedVideos(block.contents);
6645
+ const videos = embeddedVideos.filter((video) => !existingVideos.has(video.src));
6646
+ const videoSources = new Set(embeddedVideos.map((video) => video.src));
6647
+ const seenImages = new Set(existingImages);
6648
+ const images = extractImages(block.contents).filter((image) => {
6649
+ if (videoSources.has(image.src) || seenImages.has(image.src)) return false;
6650
+ seenImages.add(image.src);
6651
+ return true;
6652
+ });
6653
+ const sources = mermaidSources(block).filter((source) => !existingMermaid.has(source));
6654
+ return [
6655
+ ...images.map(
6656
+ (image) => ({
6657
+ kind: "image",
6658
+ src: image.src,
6659
+ alt: image.alt,
6660
+ ...image.width && image.height ? { aspectRatio: image.width / image.height } : {}
6661
+ })
6662
+ ),
6663
+ ...videos.map(
6664
+ (video) => ({
6665
+ kind: "video",
6666
+ src: video.src,
6667
+ ...video.posterSrc ? { posterSrc: video.posterSrc } : {},
6668
+ alt: video.alt,
6669
+ aspectRatio: 16 / 9
6670
+ })
6671
+ ),
6672
+ ...sources.map((source) => ({ kind: "mermaid", source, aspectRatio: 16 / 9 }))
6673
+ ];
6674
+ }
6675
+ function gridCells(region, count, viewport, inset) {
6676
+ const unit = Math.min(viewport.width, viewport.height);
6677
+ const padding = inset ? unit * 0.025 : 0;
6678
+ const gap = count > 1 ? unit * 0.018 : 0;
6679
+ const inner = {
6680
+ x: region.x + padding,
6681
+ y: region.y + padding,
6682
+ width: Math.max(0, region.width - padding * 2),
6683
+ height: Math.max(0, region.height - padding * 2)
6684
+ };
6685
+ const columns = Math.min(
6686
+ count,
6687
+ Math.max(1, Math.ceil(Math.sqrt(count * (inner.width / Math.max(1, inner.height)))))
6688
+ );
6689
+ const rows = Math.ceil(count / columns);
6690
+ const width = (inner.width - gap * (columns - 1)) / columns;
6691
+ const height = (inner.height - gap * (rows - 1)) / rows;
6692
+ return Array.from({ length: count }, (_, index) => ({
6693
+ x: inner.x + index % columns * (width + gap),
6694
+ y: inner.y + Math.floor(index / columns) * (height + gap),
6695
+ width,
6696
+ height
6697
+ }));
6698
+ }
6699
+ function effectiveImageTreatment(theme, block) {
6700
+ const override = block.imageTreatment;
6701
+ if (override === "none") return void 0;
6702
+ const base = override ? { type: override, strength: theme.style.imageTreatment?.strength } : theme.style.imageTreatment;
6703
+ if (!base || base.type === "none") return void 0;
6704
+ return base.type === "duotone" && !base.color ? { ...base, color: theme.colors.primary } : base;
6705
+ }
6706
+ function richMediaLayer(item, position, block, theme, kindIndex) {
6707
+ if (item.kind === "image") {
6708
+ const treatment = effectiveImageTreatment(theme, block);
6709
+ return {
6710
+ id: `${block.id}-embedded-image-${kindIndex}`,
6711
+ type: "image",
6712
+ position,
6713
+ content: {
6714
+ src: item.src,
6715
+ alt: item.alt || block.title || "Embedded image",
6716
+ fit: "contain",
6717
+ ...treatment ? { treatment } : {}
6718
+ }
6719
+ };
6720
+ }
6721
+ if (item.kind === "video") {
6722
+ return {
6723
+ id: `${block.id}-embedded-video-${kindIndex}`,
6724
+ type: "video",
6725
+ position,
6726
+ content: {
6727
+ src: item.src,
6728
+ ...item.posterSrc ? { posterSrc: item.posterSrc } : {},
6729
+ alt: item.alt || block.title || "Embedded video",
6730
+ fit: "contain",
6731
+ clipStart: 0,
6732
+ clipEnd: Math.max(0, block.duration)
6733
+ }
6734
+ };
6735
+ }
6736
+ return {
6737
+ id: `${block.id}-mermaid-${kindIndex}`,
6738
+ type: "mermaid",
6739
+ position,
6740
+ content: {
6741
+ source: item.source,
6742
+ background: theme.colors.backgroundLight,
6743
+ foreground: theme.colors.text,
6744
+ padding: Math.max(12, Math.round(Math.min(position.width, position.height) * 0.025))
6745
+ }
6746
+ };
6747
+ }
6748
+ function appendRichContentLayers(layers, block, theme, viewport) {
6749
+ const items = collectRichMediaItems(layers, block);
6750
+ if (items.length === 0) return layers;
6751
+ const layout = resolveSupplementalMediaLayout(
6752
+ layers,
6753
+ block.template ? resolveTemplateName(block.template) : void 0,
6754
+ viewport,
6755
+ items.length,
6756
+ items.map((item) => item.aspectRatio)
6757
+ );
6758
+ const cells = gridCells(layout.mediaRect, items.length, viewport, layout.framed);
6759
+ const counters = { image: 0, video: 0, mermaid: 0 };
6760
+ const mediaLayers = items.map((item, index) => {
6761
+ counters[item.kind] += 1;
6762
+ return richMediaLayer(item, cells[index], block, theme, counters[item.kind]);
6763
+ });
6764
+ const frame = layout.framed ? {
6765
+ id: `${block.id}-rich-media-frame`,
6766
+ type: "shape",
6767
+ position: layout.mediaRect,
6768
+ content: {
6769
+ shape: "rect",
6770
+ fill: theme.colors.backgroundLight,
6771
+ stroke: withAlpha(theme.colors.text, 0.14),
6772
+ strokeWidth: 2,
6773
+ borderRadius: Math.max(12, Math.round(Math.min(viewport.width, viewport.height) * 0.018))
6774
+ }
6775
+ } : void 0;
6776
+ return [...layout.layers, ...frame ? [frame] : [], ...mediaLayers];
6777
+ }
6778
+ function executeTemplateMaterialization(templateBlock, context, registry) {
6779
+ const safeTemplateBlock = cloneData(templateBlock);
6780
+ const safeContext = cloneData(context);
6781
+ const maybeChildren = safeTemplateBlock.children;
6782
+ if ((!safeContext.children || safeContext.children.length === 0) && maybeChildren?.length) {
6783
+ safeContext.children = maybeChildren;
6784
+ }
6785
+ const templateFn = registry[resolveTemplateName(safeTemplateBlock.template)];
6786
+ if (!templateFn) return { status: "unknown-template", layers: [] };
6787
+ const { templateData, templateOverrides } = safeTemplateBlock;
6788
+ const sourceBlock = safeTemplateBlock;
6789
+ const derivedInputs = sourceBlock.sourceHeading ? deriveTemplateInputs(
6790
+ safeTemplateBlock.template,
6791
+ sourceBlock.title ?? "",
6792
+ sourceBlock.contents
6793
+ ) : null;
6794
+ const coercedOverrides = coerceTemplateParams(
6795
+ safeTemplateBlock.template,
6796
+ templateOverrides ?? {}
6797
+ ).input;
6798
+ const input = derivedInputs || templateData || templateOverrides ? {
6799
+ ...derivedInputs,
6800
+ ...safeTemplateBlock,
6801
+ ...templateData,
6802
+ ...coercedOverrides
6803
+ } : safeTemplateBlock;
6804
+ const explicitQuote = Object.prototype.hasOwnProperty.call(coercedOverrides, "quote") ? coercedOverrides.quote : Object.prototype.hasOwnProperty.call(templateData ?? {}, "quote") ? templateData?.quote : Object.prototype.hasOwnProperty.call(safeTemplateBlock, "quote") ? safeTemplateBlock.quote : void 0;
6805
+ if (resolveTemplateName(safeTemplateBlock.template) === "quote" && derivedInputs && !Object.prototype.hasOwnProperty.call(derivedInputs, "title") && !(typeof explicitQuote === "string" && explicitQuote.trim())) {
6806
+ const quoteInput = input;
6807
+ quoteInput.quote = derivedInputs.quote;
6808
+ if (!Object.prototype.hasOwnProperty.call(templateData ?? {}, "title") && !Object.prototype.hasOwnProperty.call(coercedOverrides, "title")) {
6809
+ delete quoteInput.title;
6810
+ }
6811
+ }
6812
+ try {
6813
+ const renderedLayers = templateFn(input, safeContext);
6814
+ if (!Array.isArray(renderedLayers)) {
6815
+ return {
6816
+ status: "invalid-result",
6817
+ layers: [],
6818
+ receivedType: typeof renderedLayers
6819
+ };
6820
+ }
6821
+ return { status: "ok", layers: cloneData(renderedLayers) };
6822
+ } catch (error) {
6823
+ return { status: "error", layers: [], error };
6824
+ }
6825
+ }
6826
+ function toLayerMaterializationDiagnostic(template, execution) {
6827
+ if (execution.status === "unknown-template") {
6828
+ return {
6829
+ code: "unknown-template",
6830
+ template,
6831
+ message: `Unknown template "${template}"`
6832
+ };
6833
+ }
6834
+ if (execution.status === "invalid-result") {
6835
+ return {
6836
+ code: "invalid-template-result",
6837
+ template,
6838
+ receivedType: execution.receivedType,
6839
+ message: `Template "${template}" returned ${execution.receivedType}, not a layer array`
6840
+ };
6841
+ }
6842
+ return {
6843
+ code: "template-error",
6844
+ template,
6845
+ cause: execution.error,
6846
+ message: `Template "${template}" failed`
6847
+ };
6848
+ }
6849
+ function injectPersistentLayers(layers, block, theme, persistentLayers, expanded) {
6850
+ if (!persistentLayers) return layers;
6851
+ const bottomLayers = expanded?.bottomLayers ?? expandPersistentLayers(persistentLayers.bottomLayers, theme);
6852
+ const topLayers = expanded?.topLayers ?? expandPersistentLayers(persistentLayers.topLayers, theme);
6853
+ return wrapWithPersistentLayers(layers, block, bottomLayers, topLayers);
6854
+ }
6855
+
6856
+ // src/doc/templates/metadata.ts
6857
+ var TEMPLATE_METADATA = {
6858
+ title: {
6859
+ label: "Title",
6860
+ description: "A bold opening slide with large title text, perfect for covers and chapters."
6861
+ },
6862
+ sectionHeader: {
6863
+ label: "Section Header",
6864
+ description: "A clean section break with a prominent title and optional subtitle."
6865
+ },
6866
+ statHighlight: {
6867
+ label: "Stat Highlight",
6868
+ description: "Showcases a single key number or metric with supporting context."
6869
+ },
6870
+ quote: {
6871
+ label: "Quote",
6872
+ description: "Displays a stylized pull quote with decorative marks and attribution."
6873
+ },
6874
+ factCard: {
6875
+ label: "Fact Card",
6876
+ description: "Presents a focused fact or insight with a labeled header and body text."
6877
+ },
6878
+ twoColumn: {
6879
+ label: "Two Column",
6880
+ description: "Divides the slide into two equal side-by-side content columns."
6881
+ },
6882
+ dateEvent: {
6883
+ label: "Date Event",
6884
+ description: "Highlights a date-based event or milestone with a prominent date display."
6885
+ },
6886
+ imageWithCaption: {
6887
+ label: "Image with Caption",
6888
+ description: "Displays a full-bleed image with an optional caption below."
6889
+ },
6890
+ leftFeature: {
6891
+ label: "Left Feature",
6892
+ description: "Image on the left, title and body text stacked on the right."
6893
+ },
6894
+ rightFeature: {
6895
+ label: "Right Feature",
6896
+ description: "Image on the right, title and body text stacked on the left."
6897
+ },
6898
+ map: {
6899
+ label: "Map",
6900
+ description: "Embeds an interactive map centered on a geographic location."
6901
+ },
6902
+ fullBleedQuote: {
6903
+ label: "Full Bleed Quote",
6904
+ description: "A full-width quote that spans the entire slide for maximum impact."
6905
+ },
6906
+ list: {
6907
+ label: "List",
6908
+ description: "Renders a bulleted or numbered list in a clean, card-style layout."
6909
+ },
6910
+ photoGrid: {
6911
+ label: "Photo Grid",
6912
+ description: "Arranges multiple photos in a 2\xD72 or 3\xD73 mosaic grid."
6913
+ },
6914
+ definitionCard: {
6915
+ label: "Definition Card",
6916
+ description: "Shows a term and its definition in a structured, dictionary-style card."
6917
+ },
6918
+ comparisonBar: {
6919
+ label: "Comparison Bar",
6920
+ description: "Visualizes two or more values side-by-side with labeled horizontal bars."
6921
+ },
6922
+ pullQuote: {
6923
+ label: "Pull Quote",
6924
+ description: "A stylized pull quote with large decorative marks and centered text."
6925
+ },
6926
+ videoWithCaption: {
6927
+ label: "Video with Caption",
6928
+ description: "Embeds a video player with an optional caption below."
6929
+ },
6930
+ videoPullQuote: {
6931
+ label: "Video Pull Quote",
6932
+ description: "Combines a video panel with a highlighted pull quote side-by-side."
6933
+ },
6934
+ dataTable: {
6935
+ label: "Data Table",
6936
+ description: "Renders tabular data in a clean, styled table with a header row."
6937
+ },
6938
+ diagram: {
6939
+ label: "Diagram",
6940
+ description: "Renders sub-headings as connected nodes \u2014 edit visually as a node-and-edge diagram."
6941
+ },
6942
+ tree: {
6943
+ label: "Tree",
6944
+ description: "Renders an ASCII file-tree / outline fence as a filesystem-style treeview \u2014 edit as an outline."
6945
+ },
6946
+ timeline: {
6947
+ label: "Timeline",
6948
+ description: "Plots events across one or more horizontal tracks with callouts and optional branching links."
6949
+ },
6950
+ layout: {
6951
+ label: "Layout",
6952
+ description: "Free-form 2D canvas \u2014 drag layers into place. Use for one-off block layouts that don't fit a template."
6953
+ },
6954
+ drawing: {
6955
+ label: "Drawing",
6956
+ description: "Free-form sketches \u2014 draw shapes, paths, and text directly on a 2D surface."
6957
+ }
6958
+ };
6959
+
6960
+ // src/doc/templates/coverBlock.ts
6961
+ var COVER_TITLE_BASE_PX = 120;
6962
+ var COVER_TITLE_COMFORTABLE_CHARS = 26;
6963
+ var COVER_TITLE_MIN_BASE_PX = 62;
6964
+ function fitCoverTitleSize(title) {
6965
+ const len = title.trim().length;
6966
+ if (len <= COVER_TITLE_COMFORTABLE_CHARS) return COVER_TITLE_BASE_PX;
6967
+ const scaled = COVER_TITLE_BASE_PX * Math.sqrt(COVER_TITLE_COMFORTABLE_CHARS / len);
6968
+ return Math.max(COVER_TITLE_MIN_BASE_PX, Math.round(scaled));
6969
+ }
6970
+ function isStandardLightCover(context) {
6971
+ const { theme } = context;
6972
+ return theme.id === "standard" && relativeLuminance(theme.colors.background) > 0.85;
6973
+ }
6974
+ var COVER_SCRIM_STOPS = [
6975
+ { pos: 0, alpha: 0.95 },
6976
+ { pos: 38, alpha: 0.86 },
6977
+ { pos: 58, alpha: 0.4 },
6978
+ { pos: 78, alpha: 0.1 },
6979
+ { pos: 100, alpha: 0 }
6980
+ ];
6981
+ function coverScrimGradient(background) {
6982
+ const stops = COVER_SCRIM_STOPS.map((s) => `${withAlpha(background, s.alpha)} ${s.pos}%`);
6983
+ return `linear-gradient(0deg, ${stops.join(", ")})`;
6984
+ }
6985
+ function coverBlock(input, context) {
6986
+ const treatment = themedImageTreatment(context, input.imageTreatment);
6987
+ const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
6988
+ const { theme, layout } = context;
6989
+ const standardLightCover = !heroSrc && isStandardLightCover(context);
6990
+ const titleFontSize = themedFontSize(fitCoverTitleSize(title), context, true);
6991
+ const subtitleFontSize = themedFontSize(40, context, false);
6992
+ const layers = [];
6993
+ if (heroSrc) {
6994
+ const imageAnimation = mapAmbientMotion(ambientMotion);
6995
+ layers.push(
6996
+ {
6997
+ type: "image",
6998
+ id: "cover-hero",
6999
+ content: {
7000
+ src: heroSrc,
7001
+ alt: heroAlt || title,
7002
+ fit: "cover",
7003
+ credit: heroCredit,
7004
+ license: heroLicense,
7005
+ ...treatment ? { treatment } : {}
7006
+ },
7007
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
7008
+ animation: imageAnimation
7009
+ },
7010
+ {
7011
+ type: "shape",
7012
+ id: "cover-gradient",
7013
+ content: {
7014
+ shape: "rect",
7015
+ fill: coverScrimGradient(theme.colors.background)
7016
+ },
7017
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
7018
+ }
7019
+ );
7020
+ } else {
7021
+ if (standardLightCover) {
7022
+ layers.push(
7023
+ {
7024
+ type: "shape",
7025
+ id: "cover-bg",
7026
+ content: {
7027
+ shape: "rect",
7028
+ fill: theme.colors.background
7029
+ },
7030
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
7031
+ },
7032
+ {
7033
+ type: "shape",
7034
+ id: "cover-bg-tint",
7035
+ content: {
7036
+ shape: "rect",
7037
+ fill: `radial-gradient(ellipse at 50% 40%, ${withAlpha(theme.colors.primary, 0.16)} 0%, ${withAlpha(theme.colors.primary, 0.07)} 38%, ${withAlpha(theme.colors.primary, 0)} 78%)`
7038
+ },
7039
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
7040
+ }
7041
+ );
7042
+ } else {
7043
+ layers.push({
7044
+ type: "shape",
7045
+ id: "cover-bg",
7046
+ content: {
7047
+ shape: "rect",
7048
+ fill: `radial-gradient(ellipse at 50% 40%, ${theme.colors.primary} 0%, ${theme.colors.background} 100%)`
7049
+ },
7050
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
7051
+ });
6378
7052
  }
7053
+ layers.push({
7054
+ type: "shape",
7055
+ id: "cover-accent",
7056
+ content: {
7057
+ shape: "rect",
7058
+ fill: standardLightCover ? withAlpha(theme.colors.primary, 0.24) : "rgba(255, 255, 255, 0.2)"
7059
+ },
7060
+ position: {
7061
+ x: "35%",
7062
+ y: subtitle ? "42%" : "58%",
7063
+ width: "30%",
7064
+ height: "2px"
7065
+ }
7066
+ });
6379
7067
  }
6380
- return null;
6381
- }
6382
- function extractBlockquoteText(contents) {
6383
- if (!contents) return "";
6384
- for (const node of contents) {
6385
- if (node.type === "blockquote") return extractPlainText(node).trim();
7068
+ layers.push({
7069
+ type: "text",
7070
+ id: "cover-title",
7071
+ content: {
7072
+ text: title,
7073
+ style: {
7074
+ fontSize: titleFontSize,
7075
+ fontFamily: getThemeFont(context, "title"),
7076
+ fontWeight: "bold",
7077
+ color: theme.colors.text,
7078
+ textAlign: "center",
7079
+ shadow: !standardLightCover
7080
+ }
7081
+ },
7082
+ position: {
7083
+ x: "50%",
7084
+ y: heroSrc ? subtitle ? "70%" : "75%" : subtitle ? layout.primaryY : "50%",
7085
+ anchor: "center",
7086
+ width: layout.maxTextWidth
7087
+ },
7088
+ // No animation delay - shown immediately at rest
7089
+ animation: { type: "fadeIn", duration: 0.8 }
7090
+ });
7091
+ if (subtitle) {
7092
+ layers.push({
7093
+ type: "text",
7094
+ id: "cover-subtitle",
7095
+ content: {
7096
+ text: subtitle,
7097
+ style: {
7098
+ fontSize: subtitleFontSize,
7099
+ fontFamily: getThemeFont(context, "body"),
7100
+ fontWeight: standardLightCover ? "bold" : "normal",
7101
+ color: standardLightCover ? withAlpha(theme.colors.text, 0.78) : theme.colors.textMuted,
7102
+ textAlign: "center",
7103
+ lineHeight: 1.5
7104
+ }
7105
+ },
7106
+ position: {
7107
+ x: "50%",
7108
+ y: heroSrc ? "82%" : layout.secondaryY,
7109
+ anchor: "center",
7110
+ width: layout.maxTextWidth
7111
+ },
7112
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.2 }
7113
+ });
6386
7114
  }
6387
- return "";
7115
+ return layers;
6388
7116
  }
6389
- function firstParagraph(contents) {
6390
- if (!contents) return null;
6391
- const index = contents.findIndex((node) => node.type === "paragraph");
6392
- if (index < 0) return null;
7117
+ function startBlockToCoverInput(config) {
6393
7118
  return {
6394
- node: contents[index],
6395
- index
7119
+ heroSrc: config.heroSrc,
7120
+ heroAlt: config.heroAlt,
7121
+ title: config.title,
7122
+ subtitle: config.subtitle,
7123
+ ambientMotion: config.ambientMotion,
7124
+ heroCredit: config.heroCredit,
7125
+ heroLicense: config.heroLicense
6396
7126
  };
6397
7127
  }
6398
- function leadingStrongStat(paragraph) {
6399
- for (const child of paragraph.children) {
6400
- if (child.type === "text" && child.value.trim() === "") continue;
6401
- if (child.type !== "strong") return null;
6402
- const text = extractPlainText(child).trim();
6403
- const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
6404
- return match?.[0].trim() || null;
6405
- }
6406
- return null;
6407
- }
6408
- function removeStatPhrase(text, start, end) {
6409
- const before = text.slice(0, start).trim();
6410
- let after = text.slice(end);
6411
- if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, "");
6412
- else after = after.trim();
6413
- return [before, after].filter(Boolean).join(" ").trim();
6414
- }
6415
- function bodyWithoutParagraphStat(contents, paragraphIndex, paragraphText, start, end) {
6416
- if (!contents) return "";
6417
- return contents.map(
6418
- (node, index) => index === paragraphIndex ? removeStatPhrase(paragraphText, start, end) : extractPlainText(node).trim()
6419
- ).filter(Boolean).join("\n").trim();
7128
+ function expandCoverBlock(config, context) {
7129
+ const input = startBlockToCoverInput(config);
7130
+ return coverBlock(input, context);
6420
7131
  }
6421
- function deriveStatHighlightInputs(headingText, contents, bodyText3) {
6422
- const paragraph = firstParagraph(contents);
6423
- if (paragraph) {
6424
- const paragraphText = extractPlainText(paragraph.node).trim();
6425
- const strongStat = leadingStrongStat(paragraph.node);
6426
- if (strongStat) {
6427
- const start = paragraphText.indexOf(strongStat);
6428
- const description = bodyWithoutParagraphStat(
6429
- contents,
6430
- paragraph.index,
6431
- paragraphText,
6432
- start,
6433
- start + strongStat.length
6434
- );
6435
- return {
6436
- stat: strongStat,
6437
- description: description || headingText || strongStat
6438
- };
6439
- }
7132
+
7133
+ // src/doc/templates/index.ts
7134
+ function materializeScheduledBlock(sourceBlock, blockIndex, totalBlocks, theme, viewport, persistentLayers, expandedPersistentLayers, registry, failureMode, onDiagnostic) {
7135
+ const materialized = materializeBlockLayersWithRuntime(
7136
+ sourceBlock,
7137
+ {
7138
+ theme,
7139
+ viewport,
7140
+ persistentLayers: persistentLayers ?? false,
7141
+ blockIndex,
7142
+ totalBlocks,
7143
+ failureMode
7144
+ },
7145
+ { registry, expandedPersistentLayers }
7146
+ );
7147
+ if (materialized.diagnostic) {
7148
+ onDiagnostic?.(materialized.diagnostic, sourceBlock, blockIndex);
6440
7149
  }
6441
- const trimmedHeading = headingText.trim();
6442
- const headingStat = matchNumberHighlight(trimmedHeading);
6443
- if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
6444
- return { stat: headingText, description: bodyText3 || headingText };
7150
+ const expandedBlock = isTemplateBlock(sourceBlock) ? {
7151
+ id: sourceBlock.id,
7152
+ startTime: 0,
7153
+ duration: sourceBlock.duration,
7154
+ audioSegment: sourceBlock.audioSegment,
7155
+ ...materialized.layers.length > 0 ? { layers: materialized.layers } : {},
7156
+ transition: sourceBlock.transition,
7157
+ template: sourceBlock.template
7158
+ } : {
7159
+ ...cloneData(sourceBlock),
7160
+ ...materialized.layers.length > 0 ? { layers: materialized.layers } : {}
7161
+ };
7162
+ const transition = resolveBlockTransition(expandedBlock, theme, blockIndex);
7163
+ if (transition !== expandedBlock.transition) {
7164
+ expandedBlock.transition = transition;
6445
7165
  }
6446
- if (paragraph) {
6447
- const paragraphText = extractPlainText(paragraph.node).trim();
6448
- const bodyStat = matchNumberHighlight(paragraphText);
6449
- if (bodyStat) {
6450
- const description = bodyWithoutParagraphStat(
6451
- contents,
6452
- paragraph.index,
6453
- paragraphText,
6454
- bodyStat.index,
6455
- bodyStat.end
7166
+ return expandedBlock;
7167
+ }
7168
+ function expandDocBlocks(blocks, options = {}) {
7169
+ const opts = options;
7170
+ const theme = opts.theme ?? DEFAULT_THEME;
7171
+ const viewport = opts.viewport ?? VIEWPORT_PRESETS.landscape;
7172
+ const {
7173
+ persistentLayers,
7174
+ audioSegments,
7175
+ customTemplates,
7176
+ failureMode = "fallback",
7177
+ onDiagnostic
7178
+ } = opts;
7179
+ const totalBlocks = blocks.length;
7180
+ const registry = customTemplates && customTemplates.length > 0 ? buildRegistry(customTemplates) : templateRegistry;
7181
+ const effectivePersistentLayers = persistentLayers === false ? void 0 : persistentLayers ?? theme.persistentLayers;
7182
+ const expandedPersistentLayers = effectivePersistentLayers ? {
7183
+ bottomLayers: expandPersistentLayers(effectivePersistentLayers.bottomLayers, theme),
7184
+ topLayers: expandPersistentLayers(effectivePersistentLayers.topLayers, theme)
7185
+ } : void 0;
7186
+ if (!audioSegments || audioSegments.length === 0) {
7187
+ let currentTime = 0;
7188
+ return blocks.map((block, index) => {
7189
+ const expandedBlock = materializeScheduledBlock(
7190
+ block,
7191
+ index,
7192
+ totalBlocks,
7193
+ theme,
7194
+ viewport,
7195
+ effectivePersistentLayers,
7196
+ expandedPersistentLayers,
7197
+ registry,
7198
+ failureMode,
7199
+ onDiagnostic
6456
7200
  );
6457
- return {
6458
- stat: bodyStat.value,
6459
- description: description || headingText || bodyStat.value
6460
- };
6461
- }
7201
+ expandedBlock.startTime = currentTime;
7202
+ currentTime += expandedBlock.duration;
7203
+ return expandedBlock;
7204
+ });
6462
7205
  }
6463
- return { stat: headingText, description: bodyText3 || headingText };
6464
- }
6465
- function deriveTemplateInputs(templateName, headingText, contents, options = {}) {
6466
- const placeholders = options.placeholders === true;
6467
- const bodyText3 = extractBodyPlainText(contents);
6468
- switch (templateName) {
6469
- case "statHighlight":
6470
- return deriveStatHighlightInputs(headingText, contents, bodyText3);
6471
- case "quote": {
6472
- const quote = extractBlockquoteText(contents) || bodyText3 || headingText;
6473
- return { quote };
6474
- }
6475
- case "fullBleedQuote":
6476
- case "pullQuote": {
6477
- const text = extractBlockquoteText(contents) || bodyText3 || headingText;
6478
- return { text };
6479
- }
6480
- case "factCard":
6481
- return { fact: headingText, explanation: bodyText3 || headingText };
6482
- case "comparisonBar":
6483
- return placeholders ? { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 } : null;
6484
- case "list": {
6485
- const items = extractListItems(contents);
6486
- if (items.length > 0) return { items };
6487
- return placeholders ? { items: ["Item 1", "Item 2", "Item 3"] } : null;
7206
+ const blocksBySegment = /* @__PURE__ */ new Map();
7207
+ blocks.forEach((block, index) => {
7208
+ const segmentIndex = block.audioSegment ?? 0;
7209
+ if (!blocksBySegment.has(segmentIndex)) {
7210
+ blocksBySegment.set(segmentIndex, []);
6488
7211
  }
6489
- case "definitionCard":
6490
- return { term: headingText, definition: bodyText3 || headingText };
6491
- case "dateEvent":
6492
- return { date: headingText, description: bodyText3 || headingText };
6493
- case "dataTable": {
6494
- const tableData = extractTableFromContents(contents);
6495
- if (tableData) return tableData;
6496
- return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
7212
+ blocksBySegment.get(segmentIndex).push({ block, originalIndex: index });
7213
+ });
7214
+ const expandedBlocks = new Array(blocks.length);
7215
+ for (const [segmentIndex, segmentBlocks] of blocksBySegment) {
7216
+ const audioSegment = audioSegments[segmentIndex];
7217
+ if (!audioSegment) {
7218
+ let offsetTime = 0;
7219
+ for (const { block, originalIndex } of segmentBlocks) {
7220
+ const expandedBlock = materializeScheduledBlock(
7221
+ block,
7222
+ originalIndex,
7223
+ totalBlocks,
7224
+ theme,
7225
+ viewport,
7226
+ effectivePersistentLayers,
7227
+ expandedPersistentLayers,
7228
+ registry,
7229
+ failureMode,
7230
+ onDiagnostic
7231
+ );
7232
+ expandedBlock.startTime = offsetTime;
7233
+ offsetTime += expandedBlock.duration;
7234
+ expandedBlocks[originalIndex] = expandedBlock;
7235
+ }
7236
+ continue;
6497
7237
  }
6498
- case "imageWithCaption": {
6499
- const img = extractFirstImage(contents);
6500
- if (!img) return placeholders ? { caption: headingText } : null;
6501
- return { imageSrc: img.src, imageAlt: img.alt || headingText, caption: headingText };
7238
+ const sectionHeaderBlocks = [];
7239
+ const contentBlocks = [];
7240
+ for (const item of segmentBlocks) {
7241
+ const templateBlock = item.block;
7242
+ if (templateBlock.template === "sectionHeader") {
7243
+ sectionHeaderBlocks.push(item);
7244
+ } else {
7245
+ contentBlocks.push(item);
7246
+ }
6502
7247
  }
6503
- case "photoGrid": {
6504
- const images = extractImages(contents, 4);
6505
- if (images.length < 2) return placeholders ? {} : null;
6506
- return {
6507
- images: images.map((i) => ({ src: i.src, alt: i.alt })),
6508
- caption: headingText
6509
- };
7248
+ const fixedDuration = sectionHeaderBlocks.reduce((sum, { block }) => {
7249
+ return sum + (block.duration ?? 0);
7250
+ }, 0);
7251
+ const contentBlockDuration = contentBlocks.reduce((sum, { block }) => {
7252
+ return sum + (block.duration ?? 0);
7253
+ }, 0);
7254
+ const remainingDuration = audioSegment.duration - fixedDuration;
7255
+ const scaleFactor = contentBlockDuration > 0 && remainingDuration > 0 ? remainingDuration / contentBlockDuration : 1;
7256
+ const expandedInfos = [];
7257
+ for (const { block, originalIndex } of segmentBlocks) {
7258
+ const expandedBlock = materializeScheduledBlock(
7259
+ block,
7260
+ originalIndex,
7261
+ totalBlocks,
7262
+ theme,
7263
+ viewport,
7264
+ effectivePersistentLayers,
7265
+ expandedPersistentLayers,
7266
+ registry,
7267
+ failureMode,
7268
+ onDiagnostic
7269
+ );
7270
+ const templateBlock = block;
7271
+ expandedInfos.push({
7272
+ block: expandedBlock,
7273
+ originalIndex,
7274
+ templateBlock,
7275
+ hasSourceTiming: typeof templateBlock.sourceStartTime === "number"
7276
+ });
6510
7277
  }
6511
- case "leftFeature":
6512
- case "rightFeature": {
6513
- const img = extractFirstImage(contents);
6514
- if (!img && !placeholders) return null;
6515
- return {
6516
- imageSrc: img?.src ?? "",
6517
- imageAlt: img?.alt || headingText,
6518
- imageWidth: img?.width,
6519
- imageHeight: img?.height,
6520
- title: headingText,
6521
- body: bodyText3
6522
- };
7278
+ const hasAnySourceTiming = expandedInfos.some((info) => info.hasSourceTiming);
7279
+ expandedInfos.sort((a, b) => {
7280
+ if (hasAnySourceTiming) {
7281
+ const aIsHeader = a.templateBlock.template === "sectionHeader";
7282
+ const bIsHeader = b.templateBlock.template === "sectionHeader";
7283
+ if (aIsHeader && !bIsHeader) return -1;
7284
+ if (!aIsHeader && bIsHeader) return 1;
7285
+ if (a.hasSourceTiming && b.hasSourceTiming) {
7286
+ return a.templateBlock.sourceStartTime - b.templateBlock.sourceStartTime;
7287
+ }
7288
+ if (a.hasSourceTiming && !b.hasSourceTiming) return -1;
7289
+ if (!a.hasSourceTiming && b.hasSourceTiming) return 1;
7290
+ }
7291
+ return a.originalIndex - b.originalIndex;
7292
+ });
7293
+ let offsetWithinSegment = 0;
7294
+ for (let i = 0; i < expandedInfos.length; i++) {
7295
+ const info = expandedInfos[i];
7296
+ const { block: expandedBlock, templateBlock } = info;
7297
+ if (info.hasSourceTiming) {
7298
+ const sourceStart = templateBlock.sourceStartTime;
7299
+ expandedBlock.startTime = audioSegment.startTime + sourceStart;
7300
+ if (typeof templateBlock.sourceDuration === "number") {
7301
+ expandedBlock.duration = Math.max(5, templateBlock.sourceDuration);
7302
+ }
7303
+ offsetWithinSegment = sourceStart + expandedBlock.duration;
7304
+ } else if (templateBlock.template === "sectionHeader") {
7305
+ expandedBlock.startTime = audioSegment.startTime + offsetWithinSegment;
7306
+ offsetWithinSegment += expandedBlock.duration;
7307
+ } else {
7308
+ expandedBlock.startTime = audioSegment.startTime + offsetWithinSegment;
7309
+ const scaledDuration = expandedBlock.duration * scaleFactor;
7310
+ expandedBlock.duration = scaledDuration;
7311
+ offsetWithinSegment += scaledDuration;
7312
+ }
7313
+ expandedBlocks[info.originalIndex] = expandedBlock;
6523
7314
  }
6524
- case "diagram": {
6525
- const fences = (contents ?? []).filter((n) => n.type === "code");
6526
- const fence = fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : void 0;
6527
- const detection = fence ? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) }) : void 0;
6528
- if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
6529
- const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
6530
- return { nodes, edges, ...headingText ? { title: headingText } : {} };
7315
+ const segmentExpandedBlocks = expandedInfos.map((info) => expandedBlocks[info.originalIndex]).sort((a, b) => a.startTime - b.startTime);
7316
+ for (let i = 0; i < segmentExpandedBlocks.length - 1; i++) {
7317
+ const current = segmentExpandedBlocks[i];
7318
+ const next = segmentExpandedBlocks[i + 1];
7319
+ const currentEnd = current.startTime + current.duration;
7320
+ if (currentEnd > next.startTime) {
7321
+ current.duration = next.startTime - current.startTime;
7322
+ } else if (currentEnd < next.startTime - 0.5) {
7323
+ current.duration = Math.max(0.1, next.startTime - current.startTime);
7324
+ }
6531
7325
  }
6532
- case "timeline": {
6533
- const fences = (contents ?? []).filter((n) => n.type === "code");
6534
- const fence = fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang) ? fences[0] : void 0;
6535
- const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : void 0;
6536
- if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
6537
- const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
6538
- return { tracks, links, ...headingText ? { title: headingText } : {} };
7326
+ const MIN_TRANSITION_GAP = 5;
7327
+ const segmentEnd = audioSegment.startTime + audioSegment.duration;
7328
+ while (segmentExpandedBlocks.length > 1) {
7329
+ const lastBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 1];
7330
+ const timeFromLastToEnd = segmentEnd - lastBlock.startTime;
7331
+ if (timeFromLastToEnd < MIN_TRANSITION_GAP && lastBlock.template !== "sectionHeader") {
7332
+ const prevBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 2];
7333
+ prevBlock.duration = segmentEnd - prevBlock.startTime;
7334
+ lastBlock.duration = 0;
7335
+ lastBlock.startTime = segmentEnd;
7336
+ segmentExpandedBlocks.pop();
7337
+ } else {
7338
+ if (lastBlock.startTime + lastBlock.duration < segmentEnd) {
7339
+ lastBlock.duration = segmentEnd - lastBlock.startTime;
7340
+ }
7341
+ break;
7342
+ }
6539
7343
  }
6540
- case "tree": {
6541
- const fences = (contents ?? []).filter((n) => n.type === "code");
6542
- const fence = fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : void 0;
6543
- if (fence) {
6544
- const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
6545
- if (detection.isTree && detection.tree) {
6546
- const { items } = treeToTemplateData(detection.tree);
6547
- return { items, ...headingText ? { title: headingText } : {} };
7344
+ if (segmentExpandedBlocks.length === 1) {
7345
+ const onlyBlock = segmentExpandedBlocks[0];
7346
+ if (onlyBlock.startTime + onlyBlock.duration < segmentEnd) {
7347
+ onlyBlock.duration = segmentEnd - onlyBlock.startTime;
7348
+ }
7349
+ }
7350
+ let changed = true;
7351
+ while (changed) {
7352
+ changed = false;
7353
+ for (let i = segmentExpandedBlocks.length - 1; i >= 1; i--) {
7354
+ const block = segmentExpandedBlocks[i];
7355
+ if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP) {
7356
+ const prev = segmentExpandedBlocks[i - 1];
7357
+ const blockEnd = block.startTime + block.duration;
7358
+ prev.duration = blockEnd - prev.startTime;
7359
+ block.duration = 0;
7360
+ block.startTime = segmentEnd;
7361
+ segmentExpandedBlocks.splice(i, 1);
7362
+ changed = true;
7363
+ break;
6548
7364
  }
6549
7365
  }
6550
- const list = findFirstList(contents);
6551
- if (list) {
6552
- const { items } = treeToTemplateData(treeFromMarkdownList(list));
6553
- if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
7366
+ }
7367
+ const MAX_BLOCK_DURATION = 20;
7368
+ for (let i = 0; i < segmentExpandedBlocks.length; i++) {
7369
+ const block = segmentExpandedBlocks[i];
7370
+ if (block.duration > MAX_BLOCK_DURATION) {
7371
+ const numParts = Math.ceil(block.duration / MAX_BLOCK_DURATION);
7372
+ const partDuration = block.duration / numParts;
7373
+ if (partDuration >= MIN_TRANSITION_GAP) {
7374
+ block.duration = partDuration;
7375
+ for (let p = 1; p < numParts; p++) {
7376
+ const splitBlock = {
7377
+ id: `${block.id}-split-${p}`,
7378
+ startTime: block.startTime + p * partDuration,
7379
+ duration: partDuration,
7380
+ audioSegment: block.audioSegment,
7381
+ layers: (block.layers ?? []).map((layer) => ({
7382
+ ...cloneData(layer),
7383
+ id: `${layer.id}-split-${p}`
7384
+ })),
7385
+ transition: { type: "dissolve", duration: 1 },
7386
+ template: block.template
7387
+ };
7388
+ expandedBlocks.push(splitBlock);
7389
+ }
7390
+ }
6554
7391
  }
6555
- return placeholders ? {} : null;
6556
7392
  }
6557
- default:
6558
- return placeholders ? {} : null;
6559
7393
  }
7394
+ return expandedBlocks.filter((block) => block && block.duration > 0);
7395
+ }
7396
+ function getAvailableTemplates() {
7397
+ return Object.keys(templateRegistry);
7398
+ }
7399
+ function hasTemplate(name) {
7400
+ return resolveTemplateName(name) in templateRegistry;
6560
7401
  }
6561
7402
 
6562
7403
  // src/doc/customTemplatesFrontmatter.ts
@@ -7498,8 +8339,10 @@ function applyAutoTemplates(blocks, resolvedDefault, state) {
7498
8339
  if (block.sourceHeading && !annotated && block.template === resolvedDefault) {
7499
8340
  const profile = profileBlockContents(block.contents ?? []);
7500
8341
  const picked = pickAutoTemplate(profile, state.featureIndex);
7501
- if (picked) {
7502
- const inputs = deriveTemplateInputs(picked, block.title ?? "", block.contents);
8342
+ if (picked && autoTemplatePreservesContent(picked, block.contents)) {
8343
+ const inputs = deriveTemplateInputs(picked, block.title ?? "", block.contents, {
8344
+ preserveSourceHeading: true
8345
+ });
7503
8346
  if (inputs) {
7504
8347
  if (picked === "leftFeature" || picked === "rightFeature") state.featureIndex += 1;
7505
8348
  block.template = picked;
@@ -7997,13 +8840,6 @@ export {
7997
8840
  coerceTemplateParams,
7998
8841
  lintTemplateParams,
7999
8842
  fallbackBlockLayers,
8000
- materializeBlockLayers,
8001
- TEMPLATE_METADATA,
8002
- coverBlock,
8003
- expandCoverBlock,
8004
- expandDocBlocks,
8005
- getAvailableTemplates,
8006
- hasTemplate,
8007
8843
  ASCII_CHAR_W,
8008
8844
  ASCII_CHAR_H,
8009
8845
  asciiCellToCanvas,
@@ -8020,9 +8856,21 @@ export {
8020
8856
  extractListItems,
8021
8857
  extractImages,
8022
8858
  extractFirstImage,
8859
+ extractEmbeddedVideos,
8860
+ extractFirstEmbeddedVideo,
8023
8861
  extractTableFromContents,
8024
8862
  extractBlockquoteText,
8863
+ autoTemplatePreservesContent,
8025
8864
  deriveTemplateInputs,
8865
+ BLOCK_MEDIA_LAYOUT_POLICIES,
8866
+ getBlockMediaLayoutPolicy,
8867
+ materializeBlockLayers,
8868
+ TEMPLATE_METADATA,
8869
+ coverBlock,
8870
+ expandCoverBlock,
8871
+ expandDocBlocks,
8872
+ getAvailableTemplates,
8873
+ hasTemplate,
8026
8874
  readCustomTemplatesFromFrontmatter,
8027
8875
  writeCustomTemplatesToFrontmatter,
8028
8876
  readCustomThemesFromFrontmatter,