@bendyline/squisq 2.2.0 → 2.3.1

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