@bendyline/squisq 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/{Doc-DBadkoP4.d.ts → Doc-DTyeaFuW.d.ts} +1 -1
  2. package/dist/{ImageEditDoc-Cq2a3c30.d.ts → ImageEditDoc-dFEclQ9l.d.ts} +1 -1
  3. package/dist/{annotationCoercion-CPHEggo3.d.ts → annotationCoercion-Ckqpu33y.d.ts} +1 -1
  4. package/dist/{chunk-V6NV4GDE.js → chunk-DRFJU4MK.js} +1948 -1832
  5. package/dist/{chunk-HXMV7WQB.js → chunk-IO5XO6XH.js} +1 -1
  6. package/dist/{chunk-6MVWWZHL.js → chunk-MIKKUKOD.js} +88 -2
  7. package/dist/{chunk-KEAU5NEM.js → chunk-NFHMHYUV.js} +107 -2
  8. package/dist/chunk-T3B4WJBR.js +167 -0
  9. package/dist/{chunk-PTK2SW2V.js → chunk-XSW4QTMB.js} +272 -4
  10. package/dist/doc/index.d.ts +88 -10
  11. package/dist/doc/index.js +11 -3
  12. package/dist/fence/index.d.ts +2 -2
  13. package/dist/generate/index.d.ts +2 -2
  14. package/dist/imageEdit/index.d.ts +4 -4
  15. package/dist/index.d.ts +9 -8
  16. package/dist/index.js +51 -5
  17. package/dist/jsonForm/index.d.ts +2 -2
  18. package/dist/markdown/index.d.ts +3 -3
  19. package/dist/markdown/index.js +2 -2
  20. package/dist/{materializePageSection-Rss5thAj.d.ts → materializePageSection-BRNIINlU.d.ts} +2 -2
  21. package/dist/narration/index.d.ts +109 -4
  22. package/dist/narration/index.js +10 -2
  23. package/dist/proof/index.d.ts +179 -0
  24. package/dist/proof/index.js +32 -0
  25. package/dist/recommend/index.d.ts +1 -1
  26. package/dist/schemas/index.d.ts +5 -5
  27. package/dist/{themeLibrary-8BQMY2HV.d.ts → themeLibrary-xaxN5UYu.d.ts} +1 -1
  28. package/dist/transform/index.d.ts +3 -3
  29. package/dist/{types-CcrDFdWH.d.ts → types-Be6sBUBx.d.ts} +28 -2
  30. package/package.json +6 -1
@@ -1066,1909 +1066,2003 @@ function bigText(input, context) {
1066
1066
  return layers;
1067
1067
  }
1068
1068
 
1069
- // src/doc/templates/contentBlock.ts
1070
- function completeBodyText(context) {
1071
- return (context.block?.contents ?? []).filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((node) => extractPlainText(node)).join("\n").trim();
1072
- }
1073
- function bodyFontSize(body, context) {
1074
- const base = body.length > 1800 ? 18 : body.length > 1100 ? 21 : body.length > 650 ? 24 : 28;
1075
- return themedFontSize(base, context, false);
1076
- }
1077
- function contentBlock(input, context) {
1078
- const { theme } = context;
1079
- const title = input.title ?? context.block?.title ?? "";
1080
- const body = completeBodyText(context);
1081
- const layers = [createBackgroundLayer("bg", theme.colors.background)];
1082
- if (title) {
1083
- layers.push({
1084
- type: "text",
1085
- id: "title",
1086
- content: {
1087
- text: title,
1088
- style: {
1089
- fontSize: themedFontSize(46, context, true),
1090
- fontFamily: getThemeFont(context, "title"),
1091
- fontWeight: "bold",
1092
- color: theme.colors.text,
1093
- textAlign: "left",
1094
- lineHeight: 1.15,
1095
- shadow: shouldUseShadow(context)
1096
- }
1097
- },
1098
- position: { x: "8%", y: "9%", width: "84%", height: "15%", anchor: "top-left" }
1099
- });
1069
+ // src/doc/templates/diagramLayout.ts
1070
+ var DEFAULT_OPTS = {
1071
+ gapX: 260,
1072
+ gapY: 140,
1073
+ gridPad: 80,
1074
+ gridOriginX: 80,
1075
+ gridOriginY: 80
1076
+ };
1077
+ function computeDiagramLayout(children, options = {}) {
1078
+ const opts = { ...DEFAULT_OPTS, ...options };
1079
+ const warnings = [];
1080
+ const entries = [];
1081
+ const unpositionedIdx = [];
1082
+ for (let i = 0; i < children.length; i++) {
1083
+ const c = children[i];
1084
+ const hasX = typeof c.x === "number";
1085
+ const hasY = typeof c.y === "number";
1086
+ if (hasX && hasY) {
1087
+ entries.push({ child: c, pinned: true, x: c.x, y: c.y });
1088
+ } else {
1089
+ entries.push({ child: c, pinned: false, x: 0, y: 0 });
1090
+ unpositionedIdx.push(i);
1091
+ }
1100
1092
  }
1101
- if (body) {
1102
- layers.push({
1103
- type: "text",
1104
- id: "body",
1105
- content: {
1106
- text: body,
1107
- style: {
1108
- fontSize: bodyFontSize(body, context),
1109
- fontFamily: getThemeFont(context, "body"),
1110
- color: theme.colors.text,
1111
- textAlign: "left",
1112
- lineHeight: 1.35,
1113
- shadow: shouldUseShadow(context)
1114
- }
1115
- },
1116
- position: {
1117
- x: "8%",
1118
- y: title ? "28%" : "10%",
1119
- width: "84%",
1120
- height: title ? "64%" : "82%",
1121
- anchor: "top-left"
1122
- }
1093
+ if (unpositionedIdx.length > 0) {
1094
+ const cols = Math.max(1, Math.ceil(Math.sqrt(unpositionedIdx.length)));
1095
+ let baseX = opts.gridOriginX;
1096
+ let baseY = opts.gridOriginY;
1097
+ const pinned = entries.filter((e) => e.pinned);
1098
+ if (pinned.length > 0) {
1099
+ const minX = Math.min(...pinned.map((e) => e.x));
1100
+ const maxY = Math.max(...pinned.map((e) => e.y));
1101
+ baseX = minX;
1102
+ baseY = maxY + opts.gridPad + opts.gapY;
1103
+ }
1104
+ unpositionedIdx.forEach((entryIdx, k) => {
1105
+ const col = k % cols;
1106
+ const row = Math.floor(k / cols);
1107
+ entries[entryIdx].x = baseX + col * opts.gapX;
1108
+ entries[entryIdx].y = baseY + row * opts.gapY;
1123
1109
  });
1124
1110
  }
1125
- return layers;
1126
- }
1127
-
1128
- // src/doc/templates/statHighlight.ts
1129
- function statHighlight(input, context) {
1130
- const { stat, description, detail, colorScheme = "blue", accentImage } = input;
1131
- const { theme } = context;
1132
- const colors = resolveColorScheme(context, colorScheme);
1133
- const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1134
- const statFontSize = themedFontSize(148, context, true);
1135
- const descFontSize = themedFontSize(32, context, false);
1136
- const detailFontSize = themedFontSize(26, context, false);
1137
- const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 180))];
1138
- if (accentImage) {
1139
- layers.push(
1140
- ...createAccentLayers(
1141
- accentImage,
1142
- input.id,
1143
- themedImageTreatment(context, input.imageTreatment)
1144
- )
1145
- );
1146
- }
1147
- layers.push({
1148
- type: "text",
1149
- id: "stat",
1150
- content: {
1151
- text: stat,
1152
- style: {
1153
- fontSize: statFontSize,
1154
- fontFamily: getThemeFont(context, "title"),
1155
- fontWeight: "bold",
1156
- color: colors.text,
1157
- shadow: shouldUseShadow(context)
1158
- }
1159
- },
1160
- position: {
1161
- x: accentLayout.textCenterX,
1162
- y: adjustY("36%", accentLayout),
1163
- anchor: "center"
1164
- },
1165
- animation: getTemplateHint(context, "statHighlight", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.4 } : { type: "zoomIn", duration: 0.6 }
1166
- });
1167
- layers.push({
1168
- type: "text",
1169
- id: "description",
1170
- content: {
1171
- text: description,
1172
- style: {
1173
- fontSize: descFontSize,
1174
- fontFamily: getThemeFont(context, "body"),
1175
- color: theme.colors.textMuted,
1176
- textAlign: "center",
1177
- lineHeight: 1.5,
1178
- shadow: shouldUseShadow(context)
1111
+ const nodes = entries.map((e) => ({
1112
+ id: e.child.id,
1113
+ label: e.child.title ?? e.child.id,
1114
+ x: e.x,
1115
+ y: e.y,
1116
+ pinned: e.pinned,
1117
+ ...e.child.classes && e.child.classes.length > 0 ? { classes: e.child.classes } : {}
1118
+ }));
1119
+ const nodeIds = new Set(nodes.map((n) => n.id));
1120
+ const edges = [];
1121
+ const seenEdgeIds = /* @__PURE__ */ new Set();
1122
+ for (const child of children) {
1123
+ if (!child.connectsTo) continue;
1124
+ for (const conn of child.connectsTo) {
1125
+ if (!nodeIds.has(conn.target)) {
1126
+ warnings.push(`Edge from "${child.id}" to "${conn.target}" dropped: no such sibling node`);
1127
+ continue;
1179
1128
  }
1180
- },
1181
- position: {
1182
- x: accentLayout.textCenterX,
1183
- y: adjustY("54%", accentLayout),
1184
- width: accentLayout.textWidth,
1185
- anchor: "center"
1186
- },
1187
- animation: { type: "fadeIn", duration: 1, delay: 0.3 }
1188
- });
1189
- if (detail) {
1190
- layers.push({
1191
- type: "text",
1192
- id: "detail",
1193
- content: {
1194
- text: detail,
1195
- style: {
1196
- fontSize: detailFontSize,
1197
- fontFamily: getThemeFont(context, "body"),
1198
- // Scheme *text* rather than *accent*: accents in the built-in
1199
- // schemes are tuned as chart/fill colors and drop below legible
1200
- // contrast as body copy on light surfaces.
1201
- color: colors.text,
1202
- textAlign: "center",
1203
- shadow: shouldUseShadow(context)
1204
- }
1205
- },
1206
- position: {
1207
- x: accentLayout.textCenterX,
1208
- y: adjustY("66%", accentLayout),
1209
- width: accentLayout.textWidth,
1210
- anchor: "center"
1211
- },
1212
- animation: { type: "fadeIn", duration: 1, delay: 1 }
1213
- });
1129
+ const edgeId = makeEdgeId(child.id, conn);
1130
+ if (seenEdgeIds.has(edgeId)) continue;
1131
+ seenEdgeIds.add(edgeId);
1132
+ edges.push({
1133
+ id: edgeId,
1134
+ source: child.id,
1135
+ target: conn.target,
1136
+ ...conn.type ? { type: conn.type } : {}
1137
+ });
1138
+ }
1214
1139
  }
1215
- return layers;
1140
+ return { nodes, edges, warnings };
1141
+ }
1142
+ function makeEdgeId(source, conn) {
1143
+ return conn.type ? `${source}->${conn.target}:${conn.type}` : `${source}->${conn.target}`;
1216
1144
  }
1217
1145
 
1218
- // src/doc/templates/quoteBlock.ts
1219
- function quoteBlock(input, context) {
1220
- const { title, quote, attribution, accentImage } = input;
1221
- const { theme, viewport } = context;
1222
- const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1223
- const quoteFontSize = themedFontSize(48, context, true);
1224
- const titleFontSize = themedFontSize(34, context, true);
1225
- const attrFontSize = themedFontSize(26, context, false);
1226
- const quoteLineHeight = Math.max(theme.typography.titleLineHeight ?? 1.4, 1.25);
1227
- const decorativeQuoteFontSize = themedFontSize(280, context, true);
1228
- const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 160))];
1229
- if (accentImage) {
1230
- layers.push(
1231
- ...createAccentLayers(
1232
- accentImage,
1233
- input.id,
1234
- themedImageTreatment(context, input.imageTreatment)
1235
- )
1236
- );
1237
- }
1238
- if (title) {
1239
- layers.push({
1240
- type: "text",
1241
- id: "quote-title",
1242
- content: {
1243
- text: title,
1244
- style: {
1245
- fontSize: titleFontSize,
1246
- fontFamily: getThemeFont(context, "body"),
1247
- fontWeight: "bold",
1248
- color: theme.colors.textMuted,
1249
- textAlign: "center",
1250
- shadow: shouldUseShadow(context)
1251
- }
1252
- },
1253
- position: {
1254
- x: accentLayout.textCenterX,
1255
- y: adjustY("14%", accentLayout),
1256
- width: accentLayout.textWidth,
1257
- anchor: "center"
1258
- },
1259
- animation: { type: "fadeIn", duration: 0.8 }
1260
- });
1261
- }
1262
- layers.push({
1263
- type: "text",
1264
- id: "deco-quote",
1265
- content: {
1266
- text: "\u201C",
1267
- style: {
1268
- fontSize: decorativeQuoteFontSize,
1269
- fontFamily: getThemeFont(context, "title"),
1270
- color: withAlpha(theme.colors.text, 0.09),
1271
- textAlign: "center"
1272
- }
1273
- },
1274
- position: {
1275
- x: accentLayout.textCenterX,
1276
- y: adjustY("26%", accentLayout),
1277
- anchor: "center"
1146
+ // src/doc/asciiDiagram/types.ts
1147
+ var ASCII_CHAR_W = 14;
1148
+ var ASCII_CHAR_H = 28;
1149
+
1150
+ // src/doc/asciiDiagram/mapping.ts
1151
+ function asciiCellToCanvas(col, row) {
1152
+ return { x: col * ASCII_CHAR_W, y: row * ASCII_CHAR_H };
1153
+ }
1154
+ function canvasToAsciiCell(x, y) {
1155
+ return {
1156
+ col: Math.max(0, Math.round(x / ASCII_CHAR_W)),
1157
+ row: Math.max(0, Math.round(y / ASCII_CHAR_H))
1158
+ };
1159
+ }
1160
+ function asciiDiagramToTemplateData(diagram) {
1161
+ const nodes = diagram.nodes.map((n) => ({
1162
+ id: n.id,
1163
+ label: n.label,
1164
+ ...asciiCellToCanvas(n.col, n.row),
1165
+ w: n.wCols * ASCII_CHAR_W,
1166
+ h: n.hRows * ASCII_CHAR_H,
1167
+ ...n.containerId ? { container: n.containerId } : {}
1168
+ }));
1169
+ const anchors = inferEdgeAnchors(nodes, diagram.edges);
1170
+ const edges = diagram.edges.map((e, index) => ({
1171
+ source: e.source,
1172
+ target: e.target,
1173
+ ...e.label ? { label: e.label } : {},
1174
+ ...e.directed ? {} : { directed: false },
1175
+ ...anchors[index],
1176
+ routing: "orthogonal"
1177
+ }));
1178
+ return { nodes, edges };
1179
+ }
1180
+ function inferEdgeAnchors(nodes, edges) {
1181
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
1182
+ const pairs = edges.map((edge) => {
1183
+ const source = nodeById.get(edge.source);
1184
+ const target = nodeById.get(edge.target);
1185
+ if (!source || !target) {
1186
+ return {
1187
+ sourceAnchor: { side: "right", offset: 0.5 },
1188
+ targetAnchor: { side: "left", offset: 0.5 }
1189
+ };
1190
+ }
1191
+ const sourceW = source.w ?? ASCII_CHAR_W * 12;
1192
+ const sourceH = source.h ?? ASCII_CHAR_H * 3;
1193
+ const targetW = target.w ?? ASCII_CHAR_W * 12;
1194
+ const targetH = target.h ?? ASCII_CHAR_H * 3;
1195
+ const dx = target.x + targetW / 2 - (source.x + sourceW / 2);
1196
+ const dy = target.y + targetH / 2 - (source.y + sourceH / 2);
1197
+ const horizontalDistance = Math.abs(dx) / Math.max(1, (sourceW + targetW) / 2);
1198
+ const verticalDistance = Math.abs(dy) / Math.max(1, (sourceH + targetH) / 2);
1199
+ if (verticalDistance >= horizontalDistance) {
1200
+ return dy >= 0 ? {
1201
+ sourceAnchor: { side: "bottom", offset: 0.5 },
1202
+ targetAnchor: { side: "top", offset: 0.5 }
1203
+ } : {
1204
+ sourceAnchor: { side: "top", offset: 0.5 },
1205
+ targetAnchor: { side: "bottom", offset: 0.5 }
1206
+ };
1278
1207
  }
1208
+ return dx >= 0 ? {
1209
+ sourceAnchor: { side: "right", offset: 0.5 },
1210
+ targetAnchor: { side: "left", offset: 0.5 }
1211
+ } : {
1212
+ sourceAnchor: { side: "left", offset: 0.5 },
1213
+ targetAnchor: { side: "right", offset: 0.5 }
1214
+ };
1279
1215
  });
1280
- const quoteYPct = attribution ? 45 : title ? 52 : 50;
1281
- layers.push({
1282
- type: "text",
1283
- id: "quote",
1284
- content: {
1285
- text: quote,
1286
- style: {
1287
- fontSize: quoteFontSize,
1288
- fontFamily: getThemeFont(context, "title"),
1289
- color: theme.colors.text,
1290
- textAlign: "center",
1291
- lineHeight: quoteLineHeight,
1292
- shadow: shouldUseShadow(context)
1293
- }
1294
- },
1295
- position: {
1296
- x: accentLayout.textCenterX,
1297
- y: adjustY(`${quoteYPct}%`, accentLayout),
1298
- anchor: "center",
1299
- width: accentLayout.textWidth
1300
- },
1301
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 2 })
1216
+ distributeOffsets(pairs, edges, nodes, "source");
1217
+ distributeOffsets(pairs, edges, nodes, "target");
1218
+ coalesceAlignedFanOutAnchors(pairs, edges, nodes);
1219
+ return pairs;
1220
+ }
1221
+ function coalesceAlignedFanOutAnchors(pairs, edges, nodes) {
1222
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
1223
+ const groups = /* @__PURE__ */ new Map();
1224
+ edges.forEach((edge, index) => {
1225
+ const group = groups.get(edge.source) ?? [];
1226
+ group.push(index);
1227
+ groups.set(edge.source, group);
1302
1228
  });
1303
- if (attribution) {
1304
- const quoteWidthPx = parseFloat(accentLayout.textWidth) / 100 * viewport.width;
1305
- const quoteHeightPx = estimateTextHeight(quote, quoteFontSize, quoteWidthPx, quoteLineHeight);
1306
- const gapPx = quoteFontSize * 1.7;
1307
- const attrYPct = Math.min(
1308
- 82,
1309
- quoteYPct + (quoteHeightPx / 2 + gapPx) / viewport.height * 100
1310
- );
1311
- layers.push({
1312
- type: "text",
1313
- id: "attribution",
1314
- content: {
1315
- text: `\u2014 ${attribution}`,
1316
- style: {
1317
- fontSize: attrFontSize,
1318
- fontFamily: getThemeFont(context, "body"),
1319
- color: theme.colors.textMuted,
1320
- textAlign: "center",
1321
- shadow: shouldUseShadow(context)
1322
- }
1323
- },
1324
- position: {
1325
- x: accentLayout.textCenterX,
1326
- y: adjustY(`${attrYPct}%`, accentLayout),
1327
- anchor: "center"
1328
- },
1329
- animation: { type: "fadeIn", duration: 1, delay: 1.5 }
1229
+ for (const [sourceId, indexes] of groups) {
1230
+ if (indexes.length < 2) continue;
1231
+ const source = nodeById.get(sourceId);
1232
+ const targets = indexes.map((index) => nodeById.get(edges[index].target)).filter((node) => node !== void 0);
1233
+ if (!source || targets.length !== indexes.length) continue;
1234
+ const sourceW = source.w ?? ASCII_CHAR_W * 12;
1235
+ const sourceH = source.h ?? ASCII_CHAR_H * 3;
1236
+ const sourceRight = source.x + sourceW;
1237
+ const sourceBottom = source.y + sourceH;
1238
+ const targetRight = (target) => target.x + (target.w ?? ASCII_CHAR_W * 12);
1239
+ const targetBottom = (target) => target.y + (target.h ?? ASCII_CHAR_H * 3);
1240
+ let sourceSide;
1241
+ let targetSide;
1242
+ if (targets.every((target) => target.y >= sourceBottom)) {
1243
+ sourceSide = "bottom";
1244
+ targetSide = "top";
1245
+ } else if (targets.every((target) => targetBottom(target) <= source.y)) {
1246
+ sourceSide = "top";
1247
+ targetSide = "bottom";
1248
+ } else if (targets.every((target) => target.x >= sourceRight)) {
1249
+ sourceSide = "right";
1250
+ targetSide = "left";
1251
+ } else if (targets.every((target) => targetRight(target) <= source.x)) {
1252
+ sourceSide = "left";
1253
+ targetSide = "right";
1254
+ }
1255
+ if (!sourceSide || !targetSide) continue;
1256
+ for (const index of indexes) {
1257
+ pairs[index] = {
1258
+ sourceAnchor: { side: sourceSide, offset: 0.5 },
1259
+ targetAnchor: { side: targetSide, offset: 0.5 }
1260
+ };
1261
+ }
1262
+ }
1263
+ }
1264
+ function distributeOffsets(pairs, edges, nodes, endpoint) {
1265
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
1266
+ const groups = /* @__PURE__ */ new Map();
1267
+ for (let index = 0; index < edges.length; index++) {
1268
+ const edge = edges[index];
1269
+ const anchor = endpoint === "source" ? pairs[index].sourceAnchor : pairs[index].targetAnchor;
1270
+ const nodeId = endpoint === "source" ? edge.source : edge.target;
1271
+ const key = `${nodeId}:${anchor.side}`;
1272
+ const group = groups.get(key) ?? [];
1273
+ group.push(index);
1274
+ groups.set(key, group);
1275
+ }
1276
+ for (const indexes of groups.values()) {
1277
+ if (indexes.length < 2) continue;
1278
+ const side = endpoint === "source" ? pairs[indexes[0]].sourceAnchor.side : pairs[indexes[0]].targetAnchor.side;
1279
+ indexes.sort((a, b) => {
1280
+ const oppositeA = nodeById.get(endpoint === "source" ? edges[a].target : edges[a].source);
1281
+ const oppositeB = nodeById.get(endpoint === "source" ? edges[b].target : edges[b].source);
1282
+ const axisA = side === "top" || side === "bottom" ? oppositeA?.x ?? 0 : oppositeA?.y ?? 0;
1283
+ const axisB = side === "top" || side === "bottom" ? oppositeB?.x ?? 0 : oppositeB?.y ?? 0;
1284
+ return axisA - axisB || a - b;
1285
+ });
1286
+ indexes.forEach((edgeIndex, rank) => {
1287
+ const anchor = endpoint === "source" ? pairs[edgeIndex].sourceAnchor : pairs[edgeIndex].targetAnchor;
1288
+ anchor.offset = (rank + 1) / (indexes.length + 1);
1330
1289
  });
1331
1290
  }
1332
- return layers;
1291
+ }
1292
+ function asciiDiagramFromTemplateData(nodes, edges, options = {}) {
1293
+ const ids = new Set(nodes.map((n) => n.id));
1294
+ const outNodes = nodes.map((n) => {
1295
+ const { col, row } = canvasToAsciiCell(n.x, n.y);
1296
+ const labelLines = n.label.length > 0 ? n.label.split("\n") : [];
1297
+ const innerW = labelLines.reduce((w, l) => Math.max(w, l.length), 0);
1298
+ const wCols = typeof n.w === "number" ? Math.max(3, Math.round(n.w / ASCII_CHAR_W)) : innerW + 4;
1299
+ const hRows = typeof n.h === "number" ? Math.max(3, Math.round(n.h / ASCII_CHAR_H)) : labelLines.length + 2;
1300
+ return {
1301
+ id: n.id,
1302
+ label: n.label,
1303
+ col,
1304
+ row,
1305
+ wCols: Math.max(wCols, 3),
1306
+ hRows: Math.max(hRows, 3),
1307
+ ...n.container && ids.has(n.container) && n.container !== n.id ? { containerId: n.container } : {}
1308
+ };
1309
+ });
1310
+ return {
1311
+ nodes: outNodes,
1312
+ edges: edges.filter((e) => ids.has(e.source) && ids.has(e.target)).map((e) => ({
1313
+ source: e.source,
1314
+ target: e.target,
1315
+ ...e.label ? { label: e.label } : {},
1316
+ directed: e.directed !== false
1317
+ })),
1318
+ style: options.style ?? "unicode",
1319
+ warnings: []
1320
+ };
1321
+ }
1322
+ function asciiDiagramFromBlocks(children, options = {}) {
1323
+ const layout = computeDiagramLayout(children);
1324
+ const nodes = layout.nodes.map((n) => ({
1325
+ id: n.id,
1326
+ label: n.label,
1327
+ x: n.x,
1328
+ y: n.y
1329
+ }));
1330
+ const edges = layout.edges.map((e) => ({
1331
+ source: e.source,
1332
+ target: e.target,
1333
+ ...e.type ? { label: e.type } : {}
1334
+ }));
1335
+ const diagram = asciiDiagramFromTemplateData(nodes, edges, options);
1336
+ diagram.warnings.push(...layout.warnings);
1337
+ return diagram;
1333
1338
  }
1334
1339
 
1335
- // src/doc/templates/factCard.ts
1336
- function factCard(input, context) {
1337
- const { fact, explanation, source, accentImage } = input;
1338
- const { theme, viewport } = context;
1339
- const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1340
- const factFontSize = themedFontSize(56, context, true);
1341
- const explainFontSize = themedFontSize(34, context, false);
1342
- const sourceFontSize = themedFontSize(20, context, false);
1343
- const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
1344
- if (accentImage) {
1345
- layers.push(
1346
- ...createAccentLayers(
1347
- accentImage,
1348
- input.id,
1349
- themedImageTreatment(context, input.imageTreatment)
1350
- )
1351
- );
1352
- }
1353
- const textWidthPx = parseFloat(accentLayout.textWidth) / 100 * viewport.width;
1354
- const factH = estimateTextHeight(fact, factFontSize, textWidthPx, 1.3);
1355
- const explainH = estimateTextHeight(explanation, explainFontSize, textWidthPx, 1.5);
1356
- const sourceH = source ? sourceFontSize * 1.4 : 0;
1357
- const gap = factFontSize * 0.9;
1358
- const sourceGap = source ? explainFontSize * 1.1 : 0;
1359
- const totalH = factH + gap + explainH + sourceGap + sourceH;
1360
- const groupTopPct = 47 - totalH / 2 / viewport.height * 100;
1361
- const pct = (px) => groupTopPct + px / viewport.height * 100;
1362
- const factY = pct(factH / 2);
1363
- const explainY = pct(factH + gap + explainH / 2);
1364
- const sourceY = pct(factH + gap + explainH + sourceGap + sourceH / 2);
1365
- layers.push({
1366
- type: "text",
1367
- id: "fact",
1368
- content: {
1369
- text: fact,
1370
- style: {
1371
- fontSize: factFontSize,
1372
- fontFamily: getThemeFont(context, "title"),
1373
- fontWeight: "bold",
1374
- color: theme.colors.text,
1375
- textAlign: "center",
1376
- lineHeight: 1.3,
1377
- shadow: shouldUseShadow(context)
1378
- }
1379
- },
1380
- position: {
1381
- x: accentLayout.textCenterX,
1382
- y: adjustY(`${factY}%`, accentLayout),
1383
- width: accentLayout.textWidth,
1384
- anchor: "center"
1385
- },
1386
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1.5 })
1387
- });
1388
- layers.push({
1389
- type: "text",
1390
- id: "explanation",
1391
- content: {
1392
- text: explanation,
1393
- style: {
1394
- fontSize: explainFontSize,
1395
- fontFamily: getThemeFont(context, "body"),
1396
- color: theme.colors.textMuted,
1397
- textAlign: "center",
1398
- lineHeight: 1.5,
1399
- shadow: shouldUseShadow(context)
1400
- }
1401
- },
1402
- position: {
1403
- x: accentLayout.textCenterX,
1404
- y: adjustY(`${explainY}%`, accentLayout),
1405
- width: accentLayout.textWidth,
1406
- anchor: "center"
1407
- },
1408
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1, delay: 0.8 })
1409
- });
1410
- if (source) {
1411
- layers.push({
1412
- type: "text",
1413
- id: "source",
1414
- content: {
1415
- text: source,
1416
- style: {
1417
- fontSize: sourceFontSize,
1418
- fontFamily: getThemeFont(context, "body"),
1419
- color: theme.colors.textMuted,
1420
- textAlign: "center",
1421
- shadow: shouldUseShadow(context)
1422
- }
1423
- },
1424
- position: {
1425
- x: accentLayout.textCenterX,
1426
- y: adjustY(`${sourceY}%`, accentLayout),
1427
- anchor: "center"
1428
- },
1429
- animation: { type: "fadeIn", duration: 0.8, delay: 1.5 }
1430
- });
1431
- }
1432
- return layers;
1340
+ // src/doc/asciiTimeline/mapping.ts
1341
+ function asciiTimelineToTemplateData(timeline) {
1342
+ const globalStart = timeline.tracks.length > 0 ? Math.min(...timeline.tracks.map((track) => track.startColumn)) : 0;
1343
+ const globalEnd = timeline.tracks.length > 0 ? Math.max(...timeline.tracks.map((track) => track.endColumn)) : 1;
1344
+ const span = Math.max(1, globalEnd - globalStart);
1345
+ const tracks = timeline.tracks.map((track) => ({
1346
+ id: track.id,
1347
+ ...track.label ? { label: track.label } : {},
1348
+ ...track.endLabel ? { endLabel: track.endLabel } : {},
1349
+ events: track.events.map(
1350
+ (event) => ({
1351
+ id: event.id,
1352
+ label: event.label,
1353
+ ...event.description ? { description: event.description } : {},
1354
+ position: clamp01((event.column - globalStart) / span),
1355
+ ...event.side ? { side: event.side } : {},
1356
+ ...event.descriptionSide ? { descriptionSide: event.descriptionSide } : {},
1357
+ ...event.callout === false ? { callout: false } : {},
1358
+ ...event.marker ? { marker: event.marker } : {}
1359
+ })
1360
+ )
1361
+ }));
1362
+ const links = timeline.links.map((link) => ({ ...link }));
1363
+ return { tracks, links };
1364
+ }
1365
+ function clamp01(value) {
1366
+ return Math.max(0, Math.min(1, value));
1433
1367
  }
1434
1368
 
1435
- // src/doc/templates/twoColumn.ts
1436
- function twoColumn(input, context) {
1437
- const { left, right, header, leftColor = "green", rightColor = "blue" } = input;
1438
- if (!left?.label || !right?.label) return [];
1439
- const { theme, layout, orientation } = context;
1440
- const leftColors = resolveColorScheme(context, leftColor);
1441
- const rightColors = resolveColorScheme(context, rightColor);
1442
- const positions = getTwoColumnPositions(orientation);
1443
- const isStacked = layout.stackColumns;
1444
- const columnWidth = isStacked ? "85%" : "42%";
1445
- const headerFontSize = themedFontSize(36, context, true);
1446
- const labelFontSize = themedFontSize(44, context, true);
1447
- const sublabelFontSize = themedFontSize(22, context, false);
1448
- const panelTop = header && !isStacked ? 22 : 15;
1449
- const panelHeight = header && !isStacked ? 64 : 70;
1450
- const layers = [
1451
- // Background — subtle gradient to add depth
1452
- {
1453
- type: "shape",
1454
- id: "bg",
1455
- content: {
1456
- shape: "rect",
1457
- fill: themedSurfaceGradient(context, 135)
1458
- },
1459
- position: { x: 0, y: 0, width: "100%", height: "100%" }
1460
- }
1461
- ];
1462
- if (!isStacked) {
1463
- const panelFill = withAlpha(theme.colors.text, 0.05);
1464
- layers.push({
1465
- type: "shape",
1466
- id: "left-panel",
1467
- content: {
1468
- shape: "rect",
1469
- fill: panelFill,
1470
- borderRadius: 12
1471
- },
1472
- position: { x: "3%", y: `${panelTop}%`, width: "44%", height: `${panelHeight}%` }
1473
- });
1474
- layers.push({
1475
- type: "shape",
1476
- id: "right-panel",
1477
- content: {
1478
- shape: "rect",
1479
- fill: panelFill,
1480
- borderRadius: 12
1481
- },
1482
- position: { x: "53%", y: `${panelTop}%`, width: "44%", height: `${panelHeight}%` }
1483
- });
1484
- }
1485
- const headerY = isStacked ? "8%" : "12%";
1486
- if (header) {
1487
- layers.push({
1488
- type: "text",
1489
- id: "header",
1490
- content: {
1491
- text: header,
1492
- style: {
1493
- fontSize: headerFontSize,
1494
- fontFamily: getThemeFont(context, "title"),
1495
- fontWeight: "bold",
1496
- color: theme.colors.text,
1497
- textAlign: "center"
1498
- }
1499
- },
1500
- position: { x: "50%", y: headerY, anchor: "center" },
1501
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8 })
1502
- });
1503
- }
1504
- let leftY;
1505
- let leftSublabelY;
1506
- let rightY;
1507
- let rightSublabelY;
1508
- if (isStacked) {
1509
- leftY = header ? "30%" : "25%";
1510
- leftSublabelY = header ? "40%" : "35%";
1511
- rightY = header ? "60%" : "55%";
1512
- rightSublabelY = header ? "70%" : "65%";
1513
- } else {
1514
- const panelCenter = panelTop + panelHeight / 2;
1515
- leftY = `${panelCenter - 4}%`;
1516
- leftSublabelY = `${panelCenter + 7}%`;
1517
- rightY = `${panelCenter - 4}%`;
1518
- rightSublabelY = `${panelCenter + 7}%`;
1519
- }
1520
- layers.push({
1521
- type: "text",
1522
- id: "left-label",
1523
- content: {
1524
- text: left.label,
1525
- style: {
1526
- fontSize: labelFontSize,
1527
- fontFamily: getThemeFont(context, "body"),
1528
- fontWeight: "bold",
1529
- color: leftColors.text,
1530
- textAlign: "center",
1531
- lineHeight: 1.3
1532
- }
1533
- },
1534
- position: {
1535
- x: positions.left.x,
1536
- y: leftY,
1537
- anchor: "center",
1538
- width: columnWidth
1539
- },
1540
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1 })
1541
- });
1542
- if (left.sublabel) {
1543
- layers.push({
1544
- type: "text",
1545
- id: "left-sublabel",
1546
- content: {
1547
- text: left.sublabel,
1548
- style: {
1549
- fontSize: sublabelFontSize,
1550
- fontFamily: getThemeFont(context, "body"),
1551
- color: theme.colors.textMuted,
1552
- textAlign: "center",
1553
- lineHeight: 1.4
1554
- }
1555
- },
1556
- position: {
1557
- x: positions.left.x,
1558
- y: leftSublabelY,
1559
- anchor: "center",
1560
- width: columnWidth
1561
- },
1562
- animation: { type: "fadeIn", duration: 0.8, delay: 0.3 }
1563
- });
1564
- }
1565
- const connectorFontSize = themedFontSize(64, context, true);
1566
- const connectorY = isStacked ? "48%" : `${panelTop + panelHeight / 2}%`;
1567
- const connectorSymbol = isStacked ? "\u2193" : "\u2192";
1568
- layers.push({
1569
- type: "text",
1570
- id: "connector",
1571
- content: {
1572
- text: connectorSymbol,
1573
- style: {
1574
- fontSize: connectorFontSize,
1575
- fontFamily: getThemeFont(context, "title"),
1576
- fontWeight: "bold",
1577
- color: withAlpha(theme.colors.text, 0.45),
1578
- textAlign: "center"
1579
- }
1580
- },
1581
- position: {
1582
- x: "50%",
1583
- y: connectorY,
1584
- anchor: "center"
1585
- },
1586
- animation: { type: "fadeIn", duration: 0.6, delay: 0.4 }
1587
- });
1588
- layers.push({
1589
- type: "text",
1590
- id: "right-label",
1591
- content: {
1592
- text: right.label,
1593
- style: {
1594
- fontSize: labelFontSize,
1595
- fontFamily: getThemeFont(context, "body"),
1596
- fontWeight: "bold",
1597
- color: rightColors.text,
1598
- textAlign: "center",
1599
- lineHeight: 1.3
1600
- }
1601
- },
1602
- position: {
1603
- x: positions.right.x,
1604
- y: rightY,
1605
- anchor: "center",
1606
- width: columnWidth
1607
- },
1608
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1, delay: 0.5 })
1609
- });
1610
- if (right.sublabel) {
1611
- layers.push({
1612
- type: "text",
1613
- id: "right-sublabel",
1614
- content: {
1615
- text: right.sublabel,
1616
- style: {
1617
- fontSize: sublabelFontSize,
1618
- fontFamily: getThemeFont(context, "body"),
1619
- color: theme.colors.textMuted,
1620
- textAlign: "center",
1621
- lineHeight: 1.4
1622
- }
1623
- },
1624
- position: {
1625
- x: positions.right.x,
1626
- y: rightSublabelY,
1627
- anchor: "center",
1628
- width: columnWidth
1629
- },
1630
- animation: { type: "fadeIn", duration: 0.8, delay: 0.8 }
1631
- });
1632
- }
1633
- return layers;
1369
+ // src/doc/treeview/mapping.ts
1370
+ function treeToTemplateData(tree) {
1371
+ return { items: tree.roots.map(toItem) };
1372
+ }
1373
+ function toItem(node) {
1374
+ return {
1375
+ id: node.id,
1376
+ label: node.label,
1377
+ ...node.isDir ? { isDir: true } : {},
1378
+ ...node.comment ? { comment: node.comment } : {},
1379
+ children: node.children.map(toItem)
1380
+ };
1381
+ }
1382
+ function treeFromTemplateData(items, options = {}) {
1383
+ const roots = items.map(fromItem);
1384
+ return { roots, style: options.style ?? "unicode", warnings: [] };
1385
+ }
1386
+ function fromItem(item) {
1387
+ const children = Array.isArray(item.children) ? item.children.map(fromItem) : [];
1388
+ return {
1389
+ id: typeof item.id === "string" && item.id ? item.id : "node",
1390
+ label: typeof item.label === "string" ? item.label : "",
1391
+ children,
1392
+ isDir: item.isDir === true || children.length > 0,
1393
+ ...item.comment ? { comment: item.comment } : {}
1394
+ };
1395
+ }
1396
+ function treeFromMarkdownList(list) {
1397
+ const usedIds = /* @__PURE__ */ new Map();
1398
+ const slugify2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "node";
1399
+ const idFor = (label) => {
1400
+ const base = slugify2(label.replace(/\/$/, ""));
1401
+ const count = usedIds.get(base) ?? 0;
1402
+ usedIds.set(base, count + 1);
1403
+ return count === 0 ? base : `${base}-${count + 1}`;
1404
+ };
1405
+ const itemToNode = (item) => {
1406
+ const labelNode = item.children.find((c) => c.type !== "list");
1407
+ const label = labelNode ? extractPlainText(labelNode).trim() : "";
1408
+ const childLists = item.children.filter((c) => c.type === "list");
1409
+ const children = [];
1410
+ for (const cl of childLists) for (const ci of cl.children) children.push(itemToNode(ci));
1411
+ return {
1412
+ id: idFor(label),
1413
+ label,
1414
+ children,
1415
+ isDir: label.endsWith("/") || children.length > 0
1416
+ };
1417
+ };
1418
+ return { roots: list.children.map(itemToNode), style: "unicode", warnings: [] };
1634
1419
  }
1635
-
1636
- // src/doc/templates/dateEvent.ts
1637
- var MOOD_ACCENTS = {
1638
- neutral: "#63b3ed",
1639
- somber: "#e53e3e",
1640
- celebratory: "#68d391"
1641
- };
1642
- function dateEvent(input, context) {
1643
- const { date, description, footer, mood = "neutral", accentImage } = input;
1644
- const { theme } = context;
1645
- const moodAccent = MOOD_ACCENTS[mood];
1646
- const dateColor = relativeLuminance(theme.colors.background) > 0.5 ? oklchDarken(moodAccent, 0.25) : moodAccent;
1647
- const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1648
- const dateFontSize = themedFontSize(96, context, true);
1649
- const descFontSize = themedFontSize(30, context, false);
1650
- const footerFontSize = themedFontSize(26, context, false);
1651
- const layers = [
1652
- // Background — theme surface gradient (diagonal to differentiate
1653
- // from other text templates)
1654
- {
1655
- type: "shape",
1656
- id: "bg",
1657
- content: {
1658
- shape: "rect",
1659
- fill: themedSurfaceGradient(context, 135)
1660
- },
1661
- position: { x: 0, y: 0, width: "100%", height: "100%" }
1662
- }
1663
- ];
1664
- if (accentImage) {
1665
- layers.push(
1666
- ...createAccentLayers(
1667
- accentImage,
1668
- input.id,
1669
- themedImageTreatment(context, input.imageTreatment)
1670
- )
1671
- );
1672
- }
1673
- layers.push({
1674
- type: "text",
1675
- id: "date",
1676
- content: {
1677
- text: date,
1678
- style: {
1679
- fontSize: dateFontSize,
1680
- fontFamily: getThemeFont(context, "title"),
1681
- fontWeight: "bold",
1682
- color: dateColor,
1683
- shadow: shouldUseShadow(context)
1684
- }
1685
- },
1686
- position: {
1687
- x: accentLayout.textCenterX,
1688
- y: adjustY("35%", accentLayout),
1689
- anchor: "center"
1690
- },
1691
- animation: { type: "fadeIn", duration: 1.5 }
1692
- });
1693
- layers.push({
1694
- type: "text",
1695
- id: "description",
1696
- content: {
1697
- text: description,
1698
- style: {
1699
- fontSize: descFontSize,
1700
- fontFamily: getThemeFont(context, "body"),
1701
- color: theme.colors.text,
1702
- textAlign: "center",
1703
- lineHeight: 1.6,
1704
- shadow: shouldUseShadow(context)
1705
- }
1706
- },
1707
- position: {
1708
- x: accentLayout.textCenterX,
1709
- y: adjustY("58%", accentLayout),
1710
- width: accentLayout.textWidth,
1711
- anchor: "center"
1712
- },
1713
- animation: { type: "fadeIn", duration: 2, delay: 1 }
1714
- });
1715
- if (footer) {
1716
- layers.push({
1717
- type: "text",
1718
- id: "footer",
1719
- content: {
1720
- text: footer,
1721
- style: {
1722
- fontSize: footerFontSize,
1723
- fontFamily: getThemeFont(context, "body"),
1724
- color: theme.colors.textMuted,
1725
- shadow: shouldUseShadow(context)
1726
- }
1727
- },
1728
- position: {
1729
- x: accentLayout.textCenterX,
1730
- y: adjustY("72%", accentLayout),
1731
- anchor: "center"
1732
- },
1733
- animation: { type: "fadeIn", duration: 1, delay: 3 }
1734
- });
1735
- }
1736
- return layers;
1420
+ function findFirstList(contents) {
1421
+ return contents?.find((n) => n.type === "list");
1737
1422
  }
1738
1423
 
1739
- // src/doc/templates/imageWithCaption.ts
1740
- function imageWithCaption(input, context) {
1741
- const {
1742
- imageSrc,
1743
- imageAlt,
1744
- caption: rawCaption,
1745
- captionPosition = "bottom",
1746
- ambientMotion,
1747
- isTitle,
1748
- subtitle,
1749
- imageCredit,
1750
- imageLicense
1751
- } = input;
1752
- const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
1753
- const { theme, layout } = context;
1754
- const treatment = themedImageTreatment(context, input.imageTreatment);
1755
- const captionFontSize = themedFontSize(30, context, false);
1756
- const titleFontSize = themedFontSize(96, context, true);
1757
- const subtitleFontSize = themedFontSize(36, context, false);
1758
- const legacyKenBurns = input.kenBurns;
1759
- const motion = ambientMotion || legacyKenBurns;
1760
- const imageAnimation = mapAmbientMotion(motion);
1761
- const layers = [
1762
- // Background image
1763
- {
1764
- type: "image",
1765
- id: "bg-image",
1766
- content: {
1767
- src: imageSrc,
1768
- alt: imageAlt,
1769
- fit: "cover",
1770
- credit: imageCredit,
1771
- license: imageLicense,
1772
- ...treatment ? { treatment } : {}
1773
- },
1774
- position: { x: 0, y: 0, width: "100%", height: "100%" },
1775
- animation: imageAnimation
1776
- }
1777
- ];
1778
- const bg = theme.colors.background;
1779
- if (isTitle && caption) {
1780
- layers.push({
1781
- type: "shape",
1782
- id: "title-gradient",
1783
- content: {
1784
- shape: "rect",
1785
- fill: `linear-gradient(0deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.45)} 40%, ${withAlpha(bg, 0.1)} 70%, ${withAlpha(bg, 0)} 100%)`
1786
- },
1787
- position: { x: 0, y: 0, width: "100%", height: "100%" }
1788
- });
1789
- layers.push({
1790
- type: "text",
1791
- id: "title",
1792
- content: {
1793
- text: caption,
1794
- style: {
1795
- fontSize: titleFontSize,
1796
- fontFamily: getThemeFont(context, "title"),
1797
- fontWeight: "bold",
1798
- color: theme.colors.text,
1799
- textAlign: "center",
1800
- shadow: shouldUseShadow(context)
1801
- }
1802
- },
1803
- position: {
1804
- x: "50%",
1805
- y: subtitle ? "70%" : "75%",
1806
- anchor: "center",
1807
- width: layout.maxTextWidth
1808
- },
1809
- animation: { type: "fadeIn", duration: 0.8 }
1810
- });
1811
- if (subtitle) {
1812
- layers.push({
1813
- type: "text",
1814
- id: "subtitle",
1815
- content: {
1816
- text: subtitle,
1817
- style: {
1818
- fontSize: subtitleFontSize,
1819
- fontFamily: getThemeFont(context, "body"),
1820
- color: theme.colors.textMuted,
1821
- textAlign: "center",
1822
- lineHeight: 1.5
1823
- }
1824
- },
1825
- position: {
1826
- x: "50%",
1827
- y: "82%",
1828
- anchor: "center",
1829
- width: layout.maxTextWidth
1830
- },
1831
- animation: { type: "fadeIn", duration: 0.8, delay: 0.2 }
1832
- });
1424
+ // src/doc/templateInputs.ts
1425
+ var VIDEO_FILE_RE = /\.(?:webm|mp4|mov|m4v|ogv)(?:[?#].*)?$/i;
1426
+ function extractBodyPlainText(contents) {
1427
+ if (!contents || contents.length === 0) return "";
1428
+ return contents.filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((n) => extractPlainText(n)).join("\n").trim();
1429
+ }
1430
+ function extractListItems(contents) {
1431
+ return extractRichListItems(contents).map((item) => item.text);
1432
+ }
1433
+ function escapeInlineHtml(value) {
1434
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1435
+ }
1436
+ function renderInlineHtml(nodes) {
1437
+ return nodes.map((node) => {
1438
+ switch (node.type) {
1439
+ case "text":
1440
+ return escapeInlineHtml(node.value);
1441
+ case "emphasis":
1442
+ return `<em>${renderInlineHtml(node.children)}</em>`;
1443
+ case "strong":
1444
+ return `<strong>${renderInlineHtml(node.children)}</strong>`;
1445
+ case "delete":
1446
+ return `<del>${renderInlineHtml(node.children)}</del>`;
1447
+ case "superscript":
1448
+ return `<sup>${renderInlineHtml(node.children)}</sup>`;
1449
+ case "subscript":
1450
+ return `<sub>${renderInlineHtml(node.children)}</sub>`;
1451
+ case "inlineCode":
1452
+ return `<code>${escapeInlineHtml(node.value)}</code>`;
1453
+ case "link": {
1454
+ const label = renderInlineHtml(node.children);
1455
+ const href = sanitizeUrl(node.url, "link");
1456
+ if (!href) return label;
1457
+ const title = node.title ? ` title="${escapeInlineHtml(node.title)}"` : "";
1458
+ return `<a href="${escapeInlineHtml(href)}"${title}>${label}</a>`;
1459
+ }
1460
+ case "break":
1461
+ return "<br>";
1462
+ case "image":
1463
+ return escapeInlineHtml(node.alt ?? "");
1464
+ case "inlineMath":
1465
+ return escapeInlineHtml(`$${node.value}$`);
1466
+ case "footnoteReference":
1467
+ return escapeInlineHtml(`[^${node.label ?? node.identifier}]`);
1468
+ case "linkReference":
1469
+ case "textDirective":
1470
+ return renderInlineHtml(node.children);
1471
+ case "imageReference":
1472
+ return escapeInlineHtml(node.alt ?? "");
1473
+ case "mention":
1474
+ return escapeInlineHtml(`@${node.displayName}`);
1475
+ case "inlineIcon":
1476
+ return `<i class="fa-${node.family} fa-${node.name}" aria-hidden="true"></i>`;
1477
+ case "htmlInline":
1478
+ return escapeInlineHtml(node.rawHtml);
1833
1479
  }
1834
- return layers;
1835
- }
1836
- if (caption) {
1837
- const captionBgY = captionPosition === "top" ? "7%" : captionPosition === "center" ? "42%" : "68%";
1838
- const captionY = captionPosition === "top" ? "13%" : captionPosition === "center" ? "48%" : "74%";
1839
- layers.push({
1840
- type: "shape",
1841
- id: "caption-gradient",
1842
- content: {
1843
- shape: "rect",
1844
- fill: `linear-gradient(90deg, ${withAlpha(bg, 0)}, ${withAlpha(bg, 0.78)} 16%, ${withAlpha(bg, 0.78)} 84%, ${withAlpha(bg, 0)})`
1845
- },
1846
- position: {
1847
- x: 0,
1848
- y: captionBgY,
1849
- width: "100%",
1850
- height: "16%"
1480
+ }).join("");
1481
+ }
1482
+ function renderMarkdownBlocksHtml(nodes) {
1483
+ const renderBlocks = (blocks) => blocks.map((node, index) => {
1484
+ const previous = blocks[index - 1];
1485
+ const extraBlankLines = previous ? Math.max(0, markdownBlockSeparatorLines(previous, node) - 2) : 0;
1486
+ const sourceGap = '<div data-squisq-source-gap aria-hidden="true"><br></div>'.repeat(
1487
+ extraBlankLines
1488
+ );
1489
+ return `${sourceGap}${renderBlock(node)}`;
1490
+ }).join("");
1491
+ const renderList = (node) => {
1492
+ const tag = node.ordered ? "ol" : "ul";
1493
+ const start = node.ordered && node.start != null ? ` start="${node.start}"` : "";
1494
+ const items = node.children.map((item) => {
1495
+ const taskMarker = item.checked == null ? "" : `<span>${item.checked ? "[x]" : "[ ]"} </span>`;
1496
+ return `<li>${taskMarker}${renderBlocks(item.children)}</li>`;
1497
+ }).join("");
1498
+ return `<${tag}${start}>${items}</${tag}>`;
1499
+ };
1500
+ const renderBlock = (node) => {
1501
+ switch (node.type) {
1502
+ case "paragraph":
1503
+ return `<p>${renderInlineHtml(node.children)}</p>`;
1504
+ case "heading":
1505
+ return `<h${node.depth}>${renderInlineHtml(node.children)}</h${node.depth}>`;
1506
+ case "blockquote":
1507
+ return `<blockquote>${renderBlocks(node.children)}</blockquote>`;
1508
+ case "list":
1509
+ return renderList(node);
1510
+ case "code":
1511
+ return `<pre><code>${escapeInlineHtml(node.value)}</code></pre>`;
1512
+ case "thematicBreak":
1513
+ return "<hr>";
1514
+ case "table": {
1515
+ const [head, ...body] = node.children;
1516
+ const row = (cells, header) => `<tr>${cells.map((cell) => {
1517
+ const tag = header ? "th" : "td";
1518
+ return `<${tag}>${renderInlineHtml(cell.children)}</${tag}>`;
1519
+ }).join("")}</tr>`;
1520
+ const thead = head ? `<thead>${row(head.children, true)}</thead>` : "";
1521
+ const tbody = body.length ? `<tbody>${body.map((item) => row(item.children, false)).join("")}</tbody>` : "";
1522
+ return `<table>${thead}${tbody}</table>`;
1851
1523
  }
1852
- });
1853
- layers.push({
1854
- type: "text",
1855
- id: "caption",
1856
- content: {
1857
- text: caption,
1858
- style: {
1859
- fontSize: captionFontSize,
1860
- fontFamily: getThemeFont(context, "body"),
1861
- color: theme.colors.text,
1862
- textAlign: "center",
1863
- shadow: shouldUseShadow(context),
1864
- lineHeight: 1.18,
1865
- maxLines: 2
1866
- }
1867
- },
1868
- position: {
1869
- x: "50%",
1870
- y: captionY,
1871
- anchor: "center",
1872
- width: "78%"
1873
- },
1874
- animation: { type: "fadeIn", duration: 1.5, delay: 0.5 }
1875
- });
1876
- }
1877
- return layers;
1524
+ case "math":
1525
+ return `<pre><code>${escapeInlineHtml(node.value)}</code></pre>`;
1526
+ case "definition":
1527
+ return "";
1528
+ default: {
1529
+ const text = extractPlainText(node).trim();
1530
+ return text ? `<p>${escapeInlineHtml(text)}</p>` : "";
1531
+ }
1532
+ }
1533
+ };
1534
+ return renderBlocks(nodes);
1878
1535
  }
1879
-
1880
- // src/doc/templates/featureBlock.ts
1881
- function buildFeatureLayers(input, context, side) {
1882
- const { imageSrc, imageAlt, imageWidth, imageHeight, title, body } = input;
1883
- const { theme, layout } = context;
1884
- const treatment = themedImageTreatment(context, input.imageTreatment);
1885
- const sized = (imageWidth ?? 0) > 0 || (imageHeight ?? 0) > 0;
1886
- const stack = layout.stackColumns;
1887
- const titleFontSize = themedFontSize(48, context, true);
1888
- const bodyFontSize2 = themedFontSize(24, context, false);
1889
- let imgX;
1890
- let imgY;
1891
- let imgW;
1892
- let imgH;
1893
- let imageFit;
1894
- if (stack) {
1895
- imgX = "0";
1896
- imgY = "0";
1897
- imgW = "100%";
1898
- imgH = "50%";
1899
- imageFit = sized ? "contain" : "cover";
1900
- } else if (sized) {
1901
- const halfStart = side === "left" ? 5 : 55;
1902
- imgX = `${halfStart}%`;
1903
- imgY = "5%";
1904
- imgW = "40%";
1905
- imgH = "90%";
1906
- imageFit = "contain";
1907
- } else {
1908
- imgX = side === "left" ? "0" : "50%";
1909
- imgY = "0";
1910
- imgW = "50%";
1911
- imgH = "100%";
1912
- imageFit = "cover";
1536
+ function markdownBlockSeparatorLines(previous, next) {
1537
+ const previousEndLine = previous.position?.end.line;
1538
+ const nextStartLine = next.position?.start.line;
1539
+ if (previousEndLine == null || nextStartLine == null || nextStartLine <= previousEndLine) {
1540
+ return 2;
1913
1541
  }
1914
- const COLUMN_INSET = 4;
1915
- const textColumnWidth = stack ? 90 : 42;
1916
- const textX = stack ? `${(100 - textColumnWidth) / 2}%` : side === "left" ? `${50 + COLUMN_INSET}%` : `${COLUMN_INSET + 2}%`;
1917
- const titleY = stack ? "60%" : body ? "42%" : "50%";
1918
- const bodyY = stack ? "78%" : title ? "55%" : "50%";
1919
- const textAnchor = "top-left";
1920
- const textAlign = "left";
1921
- const layers = [
1922
- {
1923
- type: "shape",
1924
- id: "feature-bg",
1925
- content: { shape: "rect", fill: theme.colors.background },
1926
- position: { x: 0, y: 0, width: "100%", height: "100%" }
1542
+ return Math.max(2, nextStartLine - previousEndLine);
1543
+ }
1544
+ function renderListItemHtml(markdown) {
1545
+ return markdown.map(
1546
+ (node) => node.type === "paragraph" || node.type === "heading" ? renderInlineHtml(node.children) : escapeInlineHtml(extractPlainText(node))
1547
+ ).join("<br>");
1548
+ }
1549
+ function extractRichListItems(contents) {
1550
+ if (!contents) return [];
1551
+ const items = [];
1552
+ for (const node of contents) {
1553
+ if (node.type !== "list") continue;
1554
+ for (const item of node.children) {
1555
+ const text = extractPlainText(item).trim();
1556
+ if (!text) continue;
1557
+ const html = renderListItemHtml(item.children);
1558
+ const plainHtml = escapeInlineHtml(text).replace(/\r?\n/g, "<br>");
1559
+ items.push({
1560
+ text,
1561
+ markdown: item.children,
1562
+ ...html && html !== plainHtml ? { html } : {}
1563
+ });
1927
1564
  }
1928
- ];
1929
- if (imageSrc) {
1930
- layers.push({
1931
- type: "image",
1932
- id: "feature-image",
1933
- content: {
1934
- src: imageSrc,
1935
- alt: imageAlt ?? title ?? "",
1936
- fit: imageFit,
1937
- ...treatment ? { treatment } : {}
1938
- },
1939
- position: { x: imgX, y: imgY, width: imgW, height: imgH }
1940
- });
1941
1565
  }
1942
- if (title) {
1943
- layers.push({
1944
- type: "text",
1945
- id: "feature-title",
1946
- content: {
1947
- text: title,
1948
- style: {
1949
- fontSize: titleFontSize,
1950
- fontFamily: getThemeFont(context, "title"),
1951
- fontWeight: "bold",
1952
- color: theme.colors.text,
1953
- textAlign,
1954
- lineHeight: 1.2
1566
+ return items;
1567
+ }
1568
+ function parseDim(raw) {
1569
+ if (raw === void 0) return void 0;
1570
+ const n = parseFloat(raw);
1571
+ return Number.isFinite(n) && n > 0 ? n : void 0;
1572
+ }
1573
+ function extractImages(contents, limit = Infinity) {
1574
+ if (!contents || contents.length === 0) return [];
1575
+ const found = [];
1576
+ function fromHtml(nodes) {
1577
+ for (const node of nodes) {
1578
+ if (found.length >= limit) return;
1579
+ if (!node || typeof node !== "object") continue;
1580
+ const n = node;
1581
+ if (n.type === "htmlElement" && n.tagName === "img") {
1582
+ const attrs = n.attributes;
1583
+ if (attrs && typeof attrs.src === "string" && attrs.src) {
1584
+ found.push({
1585
+ src: attrs.src,
1586
+ alt: typeof attrs.alt === "string" ? attrs.alt : "",
1587
+ width: parseDim(attrs.width),
1588
+ height: parseDim(attrs.height)
1589
+ });
1955
1590
  }
1956
- },
1957
- position: {
1958
- x: textX,
1959
- y: titleY,
1960
- anchor: textAnchor,
1961
- width: `${textColumnWidth}%`
1962
- },
1963
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8 })
1964
- });
1591
+ }
1592
+ if (Array.isArray(n.children)) fromHtml(n.children);
1593
+ }
1965
1594
  }
1966
- if (body) {
1967
- layers.push({
1968
- type: "text",
1969
- id: "feature-body",
1970
- content: {
1971
- text: body,
1972
- style: {
1973
- fontSize: bodyFontSize2,
1974
- fontFamily: getThemeFont(context, "body"),
1975
- color: theme.colors.textMuted,
1976
- textAlign,
1977
- lineHeight: 1.5
1978
- }
1979
- },
1980
- position: {
1981
- x: textX,
1982
- y: bodyY,
1983
- anchor: textAnchor,
1984
- width: `${textColumnWidth}%`
1985
- },
1986
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8, delay: 0.2 })
1987
- });
1595
+ function walk(node) {
1596
+ if (found.length >= limit) return;
1597
+ if (!node || typeof node !== "object") return;
1598
+ const n = node;
1599
+ if (n.type === "image" && typeof n.url === "string" && n.url) {
1600
+ found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
1601
+ return;
1602
+ }
1603
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
1604
+ fromHtml(n.htmlChildren);
1605
+ }
1606
+ if (Array.isArray(n.children)) {
1607
+ for (const child of n.children) walk(child);
1608
+ }
1609
+ }
1610
+ for (const node of contents) {
1611
+ if (found.length >= limit) break;
1612
+ walk(node);
1613
+ }
1614
+ return found;
1615
+ }
1616
+ function extractFirstImage(contents) {
1617
+ return extractImages(contents, 1)[0] ?? null;
1618
+ }
1619
+ function extractEmbeddedVideos(contents, limit = Infinity) {
1620
+ if (!contents || contents.length === 0) return [];
1621
+ const found = [];
1622
+ const seen = /* @__PURE__ */ new Set();
1623
+ const add = (video) => {
1624
+ if (!video.src || found.length >= limit || seen.has(video.src)) return;
1625
+ seen.add(video.src);
1626
+ found.push(video);
1627
+ };
1628
+ function nestedSource(children) {
1629
+ if (!Array.isArray(children)) return void 0;
1630
+ for (const child of children) {
1631
+ if (!child || typeof child !== "object") continue;
1632
+ const node = child;
1633
+ if (node.tagName === "source") {
1634
+ const attrs = node.attributes;
1635
+ if (typeof attrs?.src === "string" && attrs.src) return attrs.src;
1636
+ }
1637
+ const nested = nestedSource(node.children);
1638
+ if (nested) return nested;
1639
+ }
1640
+ return void 0;
1988
1641
  }
1989
- return layers;
1990
- }
1991
- function leftFeature(input, context) {
1992
- return buildFeatureLayers(input, context, "left");
1642
+ function walk(node) {
1643
+ if (found.length >= limit || !node || typeof node !== "object") return;
1644
+ const n = node;
1645
+ if (n.type === "htmlElement" && n.tagName === "video") {
1646
+ const attrs = n.attributes;
1647
+ const src = attrs?.src || nestedSource(n.children);
1648
+ if (src) {
1649
+ const startAt = attrs?.["data-squisq-video-start-at"] ? parseTimeSeconds(attrs["data-squisq-video-start-at"]) : null;
1650
+ const clipStart = attrs?.["data-squisq-video-clip-start"] ? parseTimeSeconds(attrs["data-squisq-video-clip-start"]) : null;
1651
+ const clipEnd = attrs?.["data-squisq-video-clip-end"] ? parseTimeSeconds(attrs["data-squisq-video-clip-end"]) : null;
1652
+ add({
1653
+ src,
1654
+ ...attrs?.poster ? { posterSrc: attrs.poster } : {},
1655
+ alt: attrs?.["aria-label"] || attrs?.title || attrs?.alt || "",
1656
+ ...startAt != null ? { startAt } : {},
1657
+ ...clipStart != null ? { clipStart } : {},
1658
+ ...clipEnd != null ? { clipEnd } : {}
1659
+ });
1660
+ }
1661
+ } else if ((n.type === "link" || n.type === "image") && typeof n.url === "string" && VIDEO_FILE_RE.test(n.url)) {
1662
+ add({
1663
+ src: n.url,
1664
+ alt: typeof n.alt === "string" ? n.alt : extractPlainText(n).trim()
1665
+ });
1666
+ }
1667
+ if (Array.isArray(n.children)) n.children.forEach(walk);
1668
+ if (Array.isArray(n.htmlChildren)) n.htmlChildren.forEach(walk);
1669
+ }
1670
+ contents.forEach(walk);
1671
+ return found;
1993
1672
  }
1994
- function rightFeature(input, context) {
1995
- return buildFeatureLayers(input, context, "right");
1673
+ function extractFirstEmbeddedVideo(contents) {
1674
+ return extractEmbeddedVideos(contents, 1)[0] ?? null;
1996
1675
  }
1997
-
1998
- // src/doc/templates/mapBlock.ts
1999
- function mapBlock(input, context) {
2000
- const {
2001
- center,
2002
- zoom,
2003
- mapStyle = "terrain",
2004
- title,
2005
- caption,
2006
- markers,
2007
- ambientMotion,
2008
- staticSrc
2009
- } = input;
2010
- const { theme, layout } = context;
2011
- const titleFontSize = themedFontSize(64, context, true);
2012
- const captionFontSize = themedFontSize(32, context, false);
2013
- const mapAnimation = mapAmbientMotion(ambientMotion);
2014
- const layers = [
2015
- // Map background
2016
- {
2017
- type: "map",
2018
- id: "map-bg",
2019
- content: {
2020
- center,
2021
- zoom,
2022
- style: mapStyle,
2023
- markers,
2024
- showAttribution: true,
2025
- staticSrc
2026
- // Use pre-rendered image if available
2027
- },
2028
- position: { x: 0, y: 0, width: "100%", height: "100%" },
2029
- animation: mapAnimation
1676
+ function extractTableFromContents(contents) {
1677
+ if (!contents) return null;
1678
+ for (const node of contents) {
1679
+ if (node.type === "table") {
1680
+ const table = node;
1681
+ const [headerRow, ...bodyRows] = table.children;
1682
+ if (!headerRow) return null;
1683
+ const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
1684
+ const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
1685
+ return { headers, rows, align: table.align };
2030
1686
  }
2031
- ];
2032
- const bg = theme.colors.background;
2033
- if (title) {
2034
- layers.push({
2035
- type: "shape",
2036
- id: "title-overlay",
2037
- content: {
2038
- shape: "rect",
2039
- fill: `linear-gradient(180deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.6)} 70%, ${withAlpha(bg, 0)} 100%)`
2040
- },
2041
- position: { x: 0, y: 0, width: "100%", height: "20%" }
2042
- });
2043
- layers.push({
2044
- type: "text",
2045
- id: "title",
2046
- content: {
2047
- text: title,
2048
- style: {
2049
- fontSize: titleFontSize,
2050
- fontFamily: getThemeFont(context, "title"),
2051
- fontWeight: "bold",
2052
- color: theme.colors.text,
2053
- textAlign: "center",
2054
- shadow: shouldUseShadow(context)
2055
- }
2056
- },
2057
- position: {
2058
- x: "50%",
2059
- y: "9%",
2060
- anchor: "center",
2061
- width: layout.maxTextWidth
2062
- },
2063
- animation: { type: "fadeIn", duration: 1, delay: 0.3 }
2064
- });
2065
1687
  }
2066
- if (caption) {
2067
- layers.push({
2068
- type: "shape",
2069
- id: "caption-overlay",
2070
- content: {
2071
- shape: "rect",
2072
- fill: `linear-gradient(0deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.6)} 70%, ${withAlpha(bg, 0)} 100%)`
2073
- },
2074
- position: { x: 0, y: "80%", width: "100%", height: "20%" }
2075
- });
2076
- layers.push({
2077
- type: "text",
2078
- id: "caption",
2079
- content: {
2080
- text: caption,
2081
- style: {
2082
- fontSize: captionFontSize,
2083
- fontFamily: getThemeFont(context, "body"),
2084
- color: theme.colors.text,
2085
- textAlign: "center",
2086
- shadow: shouldUseShadow(context)
2087
- }
2088
- },
2089
- position: {
2090
- x: "50%",
2091
- y: layout.captionY,
2092
- anchor: "center",
2093
- width: layout.maxTextWidth
2094
- },
2095
- animation: { type: "fadeIn", duration: 1, delay: 0.5 }
2096
- });
1688
+ return null;
1689
+ }
1690
+ function extractBlockquoteText(contents) {
1691
+ if (!contents) return "";
1692
+ for (const node of contents) {
1693
+ if (node.type === "blockquote") return extractPlainText(node).trim();
2097
1694
  }
2098
- return layers;
1695
+ return "";
2099
1696
  }
2100
-
2101
- // src/doc/templates/fullBleedQuote.ts
2102
- function fullBleedQuote(input, context) {
2103
- const { text, colorScheme = "blue" } = input;
2104
- const colors = resolveColorScheme(context, colorScheme);
2105
- const textFontSize = themedFontSize(120, context, true);
2106
- return [
2107
- // Background — radial vignette built from the color scheme's own
2108
- // surface, so the card takes on the scheme (and the theme behind it)
2109
- // instead of vignetting every theme into hard black.
2110
- {
2111
- type: "shape",
2112
- id: "bg",
2113
- content: {
2114
- shape: "rect",
2115
- fill: `radial-gradient(ellipse at 50% 50%, ${colors.bg} 0%, ${oklchDarken(colors.bg, 0.12)} 100%)`
2116
- },
2117
- position: { x: 0, y: 0, width: "100%", height: "100%" }
2118
- },
2119
- // The text — massive, centered
2120
- {
2121
- type: "text",
2122
- id: "impact-text",
2123
- content: {
2124
- text,
2125
- style: {
2126
- fontSize: textFontSize,
2127
- fontFamily: getThemeFont(context, "title"),
2128
- fontWeight: "bold",
2129
- color: colors.text,
2130
- textAlign: "center",
2131
- lineHeight: 1.2,
2132
- shadow: shouldUseShadow(context)
2133
- }
2134
- },
2135
- position: {
2136
- x: "50%",
2137
- y: "50%",
2138
- anchor: "center",
2139
- width: "85%"
2140
- },
2141
- animation: getTemplateHint(context, "fullBleedQuote", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.8 } : { type: "fadeIn", duration: 1.5 }
1697
+ function firstParagraph(contents) {
1698
+ if (!contents) return null;
1699
+ const index = contents.findIndex((node) => node.type === "paragraph");
1700
+ if (index < 0) return null;
1701
+ return {
1702
+ node: contents[index],
1703
+ index
1704
+ };
1705
+ }
1706
+ function leadingStrongStat(paragraph) {
1707
+ for (const child of paragraph.children) {
1708
+ if (child.type === "text" && child.value.trim() === "") continue;
1709
+ if (child.type !== "strong") return null;
1710
+ const text = extractPlainText(child).trim();
1711
+ const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
1712
+ return match?.[0].trim() || null;
1713
+ }
1714
+ return null;
1715
+ }
1716
+ function removeStatPhrase(text, start, end) {
1717
+ const before = text.slice(0, start).trim();
1718
+ let after = text.slice(end);
1719
+ if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, "");
1720
+ else after = after.trim();
1721
+ return [before, after].filter(Boolean).join(" ").trim();
1722
+ }
1723
+ function bodyWithoutParagraphStat(contents, paragraphIndex, paragraphText, start, end) {
1724
+ if (!contents) return "";
1725
+ return contents.map(
1726
+ (node, index) => index === paragraphIndex ? removeStatPhrase(paragraphText, start, end) : extractPlainText(node).trim()
1727
+ ).filter(Boolean).join("\n").trim();
1728
+ }
1729
+ function deriveStatHighlightInputs(headingText, contents, bodyText3) {
1730
+ const paragraph = firstParagraph(contents);
1731
+ if (paragraph) {
1732
+ const paragraphText = extractPlainText(paragraph.node).trim();
1733
+ const strongStat = leadingStrongStat(paragraph.node);
1734
+ if (strongStat) {
1735
+ const start = paragraphText.indexOf(strongStat);
1736
+ const description = bodyWithoutParagraphStat(
1737
+ contents,
1738
+ paragraph.index,
1739
+ paragraphText,
1740
+ start,
1741
+ start + strongStat.length
1742
+ );
1743
+ return {
1744
+ stat: strongStat,
1745
+ description: description || headingText || strongStat
1746
+ };
2142
1747
  }
2143
- ];
2144
- }
2145
-
2146
- // src/doc/templates/diagramLayout.ts
2147
- var DEFAULT_OPTS = {
2148
- gapX: 260,
2149
- gapY: 140,
2150
- gridPad: 80,
2151
- gridOriginX: 80,
2152
- gridOriginY: 80
2153
- };
2154
- function computeDiagramLayout(children, options = {}) {
2155
- const opts = { ...DEFAULT_OPTS, ...options };
2156
- const warnings = [];
2157
- const entries = [];
2158
- const unpositionedIdx = [];
2159
- for (let i = 0; i < children.length; i++) {
2160
- const c = children[i];
2161
- const hasX = typeof c.x === "number";
2162
- const hasY = typeof c.y === "number";
2163
- if (hasX && hasY) {
2164
- entries.push({ child: c, pinned: true, x: c.x, y: c.y });
2165
- } else {
2166
- entries.push({ child: c, pinned: false, x: 0, y: 0 });
2167
- unpositionedIdx.push(i);
1748
+ }
1749
+ const trimmedHeading = headingText.trim();
1750
+ const headingStat = matchNumberHighlight(trimmedHeading);
1751
+ if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
1752
+ return { stat: headingText, description: bodyText3 || headingText };
1753
+ }
1754
+ if (paragraph) {
1755
+ const paragraphText = extractPlainText(paragraph.node).trim();
1756
+ const bodyStat = matchNumberHighlight(paragraphText);
1757
+ if (bodyStat) {
1758
+ const description = bodyWithoutParagraphStat(
1759
+ contents,
1760
+ paragraph.index,
1761
+ paragraphText,
1762
+ bodyStat.index,
1763
+ bodyStat.end
1764
+ );
1765
+ return {
1766
+ stat: bodyStat.value,
1767
+ description: description || headingText || bodyStat.value
1768
+ };
2168
1769
  }
2169
1770
  }
2170
- if (unpositionedIdx.length > 0) {
2171
- const cols = Math.max(1, Math.ceil(Math.sqrt(unpositionedIdx.length)));
2172
- let baseX = opts.gridOriginX;
2173
- let baseY = opts.gridOriginY;
2174
- const pinned = entries.filter((e) => e.pinned);
2175
- if (pinned.length > 0) {
2176
- const minX = Math.min(...pinned.map((e) => e.x));
2177
- const maxY = Math.max(...pinned.map((e) => e.y));
2178
- baseX = minX;
2179
- baseY = maxY + opts.gridPad + opts.gapY;
1771
+ return { stat: headingText, description: bodyText3 || headingText };
1772
+ }
1773
+ function normalizeCoverageText(value) {
1774
+ return value.replace(/\s+/g, " ").trim();
1775
+ }
1776
+ function autoTemplatePreservesContent(templateName, contents) {
1777
+ const nodes = contents ?? [];
1778
+ switch (templateName) {
1779
+ case "quote":
1780
+ return nodes.length === 1 && nodes[0]?.type === "blockquote";
1781
+ case "dataTable":
1782
+ return nodes.length === 1 && nodes[0]?.type === "table";
1783
+ case "list":
1784
+ return nodes.length === 1 && nodes[0]?.type === "list";
1785
+ case "diagram":
1786
+ case "timeline":
1787
+ return nodes.length === 1 && nodes[0]?.type === "code";
1788
+ case "tree":
1789
+ return nodes.length === 1 && (nodes[0]?.type === "code" || nodes[0]?.type === "list");
1790
+ case "photoGrid": {
1791
+ const images = extractImages(nodes);
1792
+ if (images.length < 2 || images.length > 4) return false;
1793
+ const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
1794
+ const representedText = normalizeCoverageText(
1795
+ images.map((image) => image.alt).filter(Boolean).join(" ")
1796
+ );
1797
+ return bodyText3 === "" || bodyText3 === representedText;
2180
1798
  }
2181
- unpositionedIdx.forEach((entryIdx, k) => {
2182
- const col = k % cols;
2183
- const row = Math.floor(k / cols);
2184
- entries[entryIdx].x = baseX + col * opts.gapX;
2185
- entries[entryIdx].y = baseY + row * opts.gapY;
2186
- });
1799
+ case "videoWithCaption": {
1800
+ const videos = extractEmbeddedVideos(nodes);
1801
+ if (videos.length !== 1) return false;
1802
+ const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
1803
+ return bodyText3 === normalizeCoverageText(videos[0]?.alt ?? "");
1804
+ }
1805
+ case "leftFeature":
1806
+ case "rightFeature":
1807
+ case "statHighlight":
1808
+ return true;
1809
+ default:
1810
+ return false;
2187
1811
  }
2188
- const nodes = entries.map((e) => ({
2189
- id: e.child.id,
2190
- label: e.child.title ?? e.child.id,
2191
- x: e.x,
2192
- y: e.y,
2193
- pinned: e.pinned,
2194
- ...e.child.classes && e.child.classes.length > 0 ? { classes: e.child.classes } : {}
2195
- }));
2196
- const nodeIds = new Set(nodes.map((n) => n.id));
2197
- const edges = [];
2198
- const seenEdgeIds = /* @__PURE__ */ new Set();
2199
- for (const child of children) {
2200
- if (!child.connectsTo) continue;
2201
- for (const conn of child.connectsTo) {
2202
- if (!nodeIds.has(conn.target)) {
2203
- warnings.push(`Edge from "${child.id}" to "${conn.target}" dropped: no such sibling node`);
2204
- continue;
1812
+ }
1813
+ function deriveTemplateInputs(templateName, headingText, contents, options = {}) {
1814
+ const placeholders = options.placeholders === true;
1815
+ const bodyText3 = extractBodyPlainText(contents);
1816
+ switch (templateName) {
1817
+ case "statHighlight": {
1818
+ const inputs = deriveStatHighlightInputs(headingText, contents, bodyText3);
1819
+ if (options.preserveSourceHeading && headingText && inputs.stat !== headingText && inputs.description !== headingText) {
1820
+ return {
1821
+ ...inputs,
1822
+ description: headingText,
1823
+ ...inputs.description ? { detail: inputs.description } : {}
1824
+ };
1825
+ }
1826
+ return inputs;
1827
+ }
1828
+ case "quote": {
1829
+ const quote = extractBlockquoteText(contents) || bodyText3;
1830
+ if (quote) return { quote, ...headingText ? { title: headingText } : {} };
1831
+ return { quote: headingText };
1832
+ }
1833
+ case "fullBleedQuote":
1834
+ case "pullQuote": {
1835
+ const text = extractBlockquoteText(contents) || bodyText3 || headingText;
1836
+ return { text };
1837
+ }
1838
+ case "factCard":
1839
+ return { fact: headingText, explanation: bodyText3 || headingText };
1840
+ case "comparisonBar":
1841
+ return placeholders ? { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 } : null;
1842
+ case "list": {
1843
+ const items = extractListItems(contents);
1844
+ if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
1845
+ return placeholders ? { items: ["Item 1", "Item 2", "Item 3"] } : null;
1846
+ }
1847
+ case "definitionCard":
1848
+ return { term: headingText, definition: bodyText3 || headingText };
1849
+ case "dateEvent":
1850
+ return { date: headingText, description: bodyText3 || headingText };
1851
+ case "dataTable": {
1852
+ const tableData = extractTableFromContents(contents);
1853
+ if (tableData) return { ...tableData, ...headingText ? { title: headingText } : {} };
1854
+ return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
1855
+ }
1856
+ case "barChart":
1857
+ case "columnChart":
1858
+ case "pieChart":
1859
+ case "donutChart":
1860
+ case "lineChart":
1861
+ case "areaChart":
1862
+ case "scatterChart": {
1863
+ const tableData = extractTableFromContents(contents);
1864
+ if (tableData) {
1865
+ return {
1866
+ headers: tableData.headers,
1867
+ rows: tableData.rows,
1868
+ ...headingText ? { title: headingText } : {}
1869
+ };
1870
+ }
1871
+ return placeholders && !bodyText3.trim() ? {
1872
+ headers: ["Quarter", "Revenue", "Costs"],
1873
+ rows: [
1874
+ ["Q1", "40", "28"],
1875
+ ["Q2", "55", "31"],
1876
+ ["Q3", "48", "30"],
1877
+ ["Q4", "62", "35"]
1878
+ ],
1879
+ ...headingText ? { title: headingText } : {}
1880
+ } : null;
1881
+ }
1882
+ case "imageWithCaption": {
1883
+ const img = extractFirstImage(contents);
1884
+ if (!img) return placeholders ? { caption: headingText } : null;
1885
+ return { imageSrc: img.src, imageAlt: img.alt || headingText, caption: headingText };
1886
+ }
1887
+ case "bigText": {
1888
+ const img = extractFirstImage(contents);
1889
+ return {
1890
+ title: headingText,
1891
+ ...img ? { imageSrc: img.src, imageAlt: img.alt || headingText } : {}
1892
+ };
1893
+ }
1894
+ case "photoGrid": {
1895
+ const images = extractImages(contents, 4);
1896
+ if (images.length < 2) return placeholders ? {} : null;
1897
+ return {
1898
+ images: images.map((i) => ({ src: i.src, alt: i.alt })),
1899
+ caption: headingText
1900
+ };
1901
+ }
1902
+ case "videoWithCaption": {
1903
+ const video = extractFirstEmbeddedVideo(contents);
1904
+ if (!video) return placeholders ? { caption: headingText } : null;
1905
+ return {
1906
+ videoSrc: video.src,
1907
+ posterSrc: video.posterSrc,
1908
+ videoAlt: video.alt || headingText,
1909
+ caption: headingText
1910
+ };
1911
+ }
1912
+ case "leftFeature":
1913
+ case "rightFeature": {
1914
+ const img = extractFirstImage(contents);
1915
+ if (!img && !placeholders) return null;
1916
+ return {
1917
+ imageSrc: img?.src ?? "",
1918
+ imageAlt: img?.alt || headingText,
1919
+ imageWidth: img?.width,
1920
+ imageHeight: img?.height,
1921
+ title: headingText,
1922
+ body: bodyText3
1923
+ };
1924
+ }
1925
+ case "diagram": {
1926
+ const fences = (contents ?? []).filter((n) => n.type === "code");
1927
+ const fence = fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : void 0;
1928
+ const detection = fence ? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) }) : void 0;
1929
+ if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
1930
+ const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
1931
+ return { nodes, edges, ...headingText ? { title: headingText } : {} };
1932
+ }
1933
+ case "timeline": {
1934
+ const fences = (contents ?? []).filter((n) => n.type === "code");
1935
+ const fence = fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang) ? fences[0] : void 0;
1936
+ const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : void 0;
1937
+ if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
1938
+ const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
1939
+ return { tracks, links, ...headingText ? { title: headingText } : {} };
1940
+ }
1941
+ case "tree": {
1942
+ const fences = (contents ?? []).filter((n) => n.type === "code");
1943
+ const fence = fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : void 0;
1944
+ if (fence) {
1945
+ const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
1946
+ if (detection.isTree && detection.tree) {
1947
+ const { items } = treeToTemplateData(detection.tree);
1948
+ return { items, ...headingText ? { title: headingText } : {} };
1949
+ }
2205
1950
  }
2206
- const edgeId = makeEdgeId(child.id, conn);
2207
- if (seenEdgeIds.has(edgeId)) continue;
2208
- seenEdgeIds.add(edgeId);
2209
- edges.push({
2210
- id: edgeId,
2211
- source: child.id,
2212
- target: conn.target,
2213
- ...conn.type ? { type: conn.type } : {}
2214
- });
1951
+ const list = findFirstList(contents);
1952
+ if (list) {
1953
+ const { items } = treeToTemplateData(treeFromMarkdownList(list));
1954
+ if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
1955
+ }
1956
+ return placeholders ? {} : null;
2215
1957
  }
1958
+ default:
1959
+ return placeholders ? {} : null;
2216
1960
  }
2217
- return { nodes, edges, warnings };
2218
- }
2219
- function makeEdgeId(source, conn) {
2220
- return conn.type ? `${source}->${conn.target}:${conn.type}` : `${source}->${conn.target}`;
2221
1961
  }
2222
1962
 
2223
- // src/doc/asciiDiagram/types.ts
2224
- var ASCII_CHAR_W = 14;
2225
- var ASCII_CHAR_H = 28;
2226
-
2227
- // src/doc/asciiDiagram/mapping.ts
2228
- function asciiCellToCanvas(col, row) {
2229
- return { x: col * ASCII_CHAR_W, y: row * ASCII_CHAR_H };
1963
+ // src/doc/templates/contentBlock.ts
1964
+ function bodyNodes(context) {
1965
+ return (context.block?.contents ?? []).filter(
1966
+ // Mermaid source is already materialized as a visual layer. Repeating the
1967
+ // fence text as prose makes the loss-averse default noisy without
1968
+ // preserving any additional authored meaning.
1969
+ (node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")
1970
+ );
2230
1971
  }
2231
- function canvasToAsciiCell(x, y) {
2232
- return {
2233
- col: Math.max(0, Math.round(x / ASCII_CHAR_W)),
2234
- row: Math.max(0, Math.round(y / ASCII_CHAR_H))
2235
- };
1972
+ function plainListText(list, depth = 0) {
1973
+ const indent = " ".repeat(depth);
1974
+ return list.children.flatMap((item, index) => {
1975
+ const ownText = item.children.filter((node) => node.type !== "list").map((node) => extractPlainText(node).trim()).filter(Boolean).join(" ");
1976
+ const marker = list.ordered ? `${(list.start ?? 1) + index}.` : "\u2022";
1977
+ const lines = ownText ? [`${indent}${marker} ${ownText}`] : [];
1978
+ for (const child of item.children) {
1979
+ if (child.type === "list") lines.push(plainListText(child, depth + 1));
1980
+ }
1981
+ return lines;
1982
+ }).join("\n");
1983
+ }
1984
+ function completeBodyText(nodes) {
1985
+ return nodes.map((node, index) => {
1986
+ const text = node.type === "list" ? plainListText(node) : extractPlainText(node);
1987
+ if (index === 0) return text;
1988
+ const separator = "\n".repeat(markdownBlockSeparatorLines(nodes[index - 1], node));
1989
+ return `${separator}${text}`;
1990
+ }).join("").trim();
2236
1991
  }
2237
- function asciiDiagramToTemplateData(diagram) {
2238
- const nodes = diagram.nodes.map((n) => ({
2239
- id: n.id,
2240
- label: n.label,
2241
- ...asciiCellToCanvas(n.col, n.row),
2242
- w: n.wCols * ASCII_CHAR_W,
2243
- h: n.hRows * ASCII_CHAR_H,
2244
- ...n.containerId ? { container: n.containerId } : {}
2245
- }));
2246
- const anchors = inferEdgeAnchors(nodes, diagram.edges);
2247
- const edges = diagram.edges.map((e, index) => ({
2248
- source: e.source,
2249
- target: e.target,
2250
- ...e.label ? { label: e.label } : {},
2251
- ...e.directed ? {} : { directed: false },
2252
- ...anchors[index],
2253
- routing: "orthogonal"
2254
- }));
2255
- return { nodes, edges };
1992
+ function bodyFontSize(body, context) {
1993
+ const base = body.length > 1800 ? 18 : body.length > 1100 ? 21 : body.length > 650 ? 24 : 28;
1994
+ return themedFontSize(base, context, false);
2256
1995
  }
2257
- function inferEdgeAnchors(nodes, edges) {
2258
- const nodeById = new Map(nodes.map((node) => [node.id, node]));
2259
- const pairs = edges.map((edge) => {
2260
- const source = nodeById.get(edge.source);
2261
- const target = nodeById.get(edge.target);
2262
- if (!source || !target) {
2263
- return {
2264
- sourceAnchor: { side: "right", offset: 0.5 },
2265
- targetAnchor: { side: "left", offset: 0.5 }
2266
- };
2267
- }
2268
- const sourceW = source.w ?? ASCII_CHAR_W * 12;
2269
- const sourceH = source.h ?? ASCII_CHAR_H * 3;
2270
- const targetW = target.w ?? ASCII_CHAR_W * 12;
2271
- const targetH = target.h ?? ASCII_CHAR_H * 3;
2272
- const dx = target.x + targetW / 2 - (source.x + sourceW / 2);
2273
- const dy = target.y + targetH / 2 - (source.y + sourceH / 2);
2274
- const horizontalDistance = Math.abs(dx) / Math.max(1, (sourceW + targetW) / 2);
2275
- const verticalDistance = Math.abs(dy) / Math.max(1, (sourceH + targetH) / 2);
2276
- if (verticalDistance >= horizontalDistance) {
2277
- return dy >= 0 ? {
2278
- sourceAnchor: { side: "bottom", offset: 0.5 },
2279
- targetAnchor: { side: "top", offset: 0.5 }
2280
- } : {
2281
- sourceAnchor: { side: "top", offset: 0.5 },
2282
- targetAnchor: { side: "bottom", offset: 0.5 }
2283
- };
2284
- }
2285
- return dx >= 0 ? {
2286
- sourceAnchor: { side: "right", offset: 0.5 },
2287
- targetAnchor: { side: "left", offset: 0.5 }
2288
- } : {
2289
- sourceAnchor: { side: "left", offset: 0.5 },
2290
- targetAnchor: { side: "right", offset: 0.5 }
2291
- };
2292
- });
2293
- distributeOffsets(pairs, edges, nodes, "source");
2294
- distributeOffsets(pairs, edges, nodes, "target");
2295
- coalesceAlignedFanOutAnchors(pairs, edges, nodes);
2296
- return pairs;
1996
+ function contentBlock(input, context) {
1997
+ const { theme } = context;
1998
+ const title = input.title ?? context.block?.title ?? "";
1999
+ const markdown = bodyNodes(context);
2000
+ const body = completeBodyText(markdown);
2001
+ const bodyHtml = renderMarkdownBlocksHtml(markdown);
2002
+ const layers = [createBackgroundLayer("bg", theme.colors.background)];
2003
+ if (title) {
2004
+ layers.push({
2005
+ type: "text",
2006
+ id: "title",
2007
+ content: {
2008
+ text: title,
2009
+ style: {
2010
+ fontSize: themedFontSize(46, context, true),
2011
+ fontFamily: getThemeFont(context, "title"),
2012
+ fontWeight: "bold",
2013
+ color: theme.colors.text,
2014
+ textAlign: "left",
2015
+ lineHeight: 1.15,
2016
+ shadow: shouldUseShadow(context)
2017
+ }
2018
+ },
2019
+ position: { x: "8%", y: "9%", width: "84%", height: "15%", anchor: "top-left" }
2020
+ });
2021
+ }
2022
+ if (body) {
2023
+ layers.push({
2024
+ type: "text",
2025
+ id: "body",
2026
+ content: {
2027
+ text: body,
2028
+ ...bodyHtml ? { html: bodyHtml } : {},
2029
+ style: {
2030
+ fontSize: bodyFontSize(body, context),
2031
+ fontFamily: getThemeFont(context, "body"),
2032
+ color: theme.colors.text,
2033
+ textAlign: "left",
2034
+ lineHeight: 1.35,
2035
+ shadow: shouldUseShadow(context)
2036
+ }
2037
+ },
2038
+ position: {
2039
+ x: "8%",
2040
+ y: title ? "28%" : "10%",
2041
+ width: "84%",
2042
+ height: title ? "64%" : "82%",
2043
+ anchor: "top-left"
2044
+ }
2045
+ });
2046
+ }
2047
+ return layers;
2297
2048
  }
2298
- function coalesceAlignedFanOutAnchors(pairs, edges, nodes) {
2299
- const nodeById = new Map(nodes.map((node) => [node.id, node]));
2300
- const groups = /* @__PURE__ */ new Map();
2301
- edges.forEach((edge, index) => {
2302
- const group = groups.get(edge.source) ?? [];
2303
- group.push(index);
2304
- groups.set(edge.source, group);
2049
+
2050
+ // src/doc/templates/statHighlight.ts
2051
+ function statHighlight(input, context) {
2052
+ const { stat, description, detail, colorScheme = "blue", accentImage } = input;
2053
+ const { theme } = context;
2054
+ const colors = resolveColorScheme(context, colorScheme);
2055
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2056
+ const statFontSize = themedFontSize(148, context, true);
2057
+ const descFontSize = themedFontSize(32, context, false);
2058
+ const detailFontSize = themedFontSize(26, context, false);
2059
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 180))];
2060
+ if (accentImage) {
2061
+ layers.push(
2062
+ ...createAccentLayers(
2063
+ accentImage,
2064
+ input.id,
2065
+ themedImageTreatment(context, input.imageTreatment)
2066
+ )
2067
+ );
2068
+ }
2069
+ layers.push({
2070
+ type: "text",
2071
+ id: "stat",
2072
+ content: {
2073
+ text: stat,
2074
+ style: {
2075
+ fontSize: statFontSize,
2076
+ fontFamily: getThemeFont(context, "title"),
2077
+ fontWeight: "bold",
2078
+ color: colors.text,
2079
+ shadow: shouldUseShadow(context)
2080
+ }
2081
+ },
2082
+ position: {
2083
+ x: accentLayout.textCenterX,
2084
+ y: adjustY("36%", accentLayout),
2085
+ anchor: "center"
2086
+ },
2087
+ animation: getTemplateHint(context, "statHighlight", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.4 } : { type: "zoomIn", duration: 0.6 }
2305
2088
  });
2306
- for (const [sourceId, indexes] of groups) {
2307
- if (indexes.length < 2) continue;
2308
- const source = nodeById.get(sourceId);
2309
- const targets = indexes.map((index) => nodeById.get(edges[index].target)).filter((node) => node !== void 0);
2310
- if (!source || targets.length !== indexes.length) continue;
2311
- const sourceW = source.w ?? ASCII_CHAR_W * 12;
2312
- const sourceH = source.h ?? ASCII_CHAR_H * 3;
2313
- const sourceRight = source.x + sourceW;
2314
- const sourceBottom = source.y + sourceH;
2315
- const targetRight = (target) => target.x + (target.w ?? ASCII_CHAR_W * 12);
2316
- const targetBottom = (target) => target.y + (target.h ?? ASCII_CHAR_H * 3);
2317
- let sourceSide;
2318
- let targetSide;
2319
- if (targets.every((target) => target.y >= sourceBottom)) {
2320
- sourceSide = "bottom";
2321
- targetSide = "top";
2322
- } else if (targets.every((target) => targetBottom(target) <= source.y)) {
2323
- sourceSide = "top";
2324
- targetSide = "bottom";
2325
- } else if (targets.every((target) => target.x >= sourceRight)) {
2326
- sourceSide = "right";
2327
- targetSide = "left";
2328
- } else if (targets.every((target) => targetRight(target) <= source.x)) {
2329
- sourceSide = "left";
2330
- targetSide = "right";
2331
- }
2332
- if (!sourceSide || !targetSide) continue;
2333
- for (const index of indexes) {
2334
- pairs[index] = {
2335
- sourceAnchor: { side: sourceSide, offset: 0.5 },
2336
- targetAnchor: { side: targetSide, offset: 0.5 }
2337
- };
2338
- }
2089
+ layers.push({
2090
+ type: "text",
2091
+ id: "description",
2092
+ content: {
2093
+ text: description,
2094
+ style: {
2095
+ fontSize: descFontSize,
2096
+ fontFamily: getThemeFont(context, "body"),
2097
+ color: theme.colors.textMuted,
2098
+ textAlign: "center",
2099
+ lineHeight: 1.5,
2100
+ shadow: shouldUseShadow(context)
2101
+ }
2102
+ },
2103
+ position: {
2104
+ x: accentLayout.textCenterX,
2105
+ y: adjustY("54%", accentLayout),
2106
+ width: accentLayout.textWidth,
2107
+ anchor: "center"
2108
+ },
2109
+ animation: { type: "fadeIn", duration: 1, delay: 0.3 }
2110
+ });
2111
+ if (detail) {
2112
+ layers.push({
2113
+ type: "text",
2114
+ id: "detail",
2115
+ content: {
2116
+ text: detail,
2117
+ style: {
2118
+ fontSize: detailFontSize,
2119
+ fontFamily: getThemeFont(context, "body"),
2120
+ // Scheme *text* rather than *accent*: accents in the built-in
2121
+ // schemes are tuned as chart/fill colors and drop below legible
2122
+ // contrast as body copy on light surfaces.
2123
+ color: colors.text,
2124
+ textAlign: "center",
2125
+ shadow: shouldUseShadow(context)
2126
+ }
2127
+ },
2128
+ position: {
2129
+ x: accentLayout.textCenterX,
2130
+ y: adjustY("66%", accentLayout),
2131
+ width: accentLayout.textWidth,
2132
+ anchor: "center"
2133
+ },
2134
+ animation: { type: "fadeIn", duration: 1, delay: 1 }
2135
+ });
2339
2136
  }
2137
+ return layers;
2340
2138
  }
2341
- function distributeOffsets(pairs, edges, nodes, endpoint) {
2342
- const nodeById = new Map(nodes.map((node) => [node.id, node]));
2343
- const groups = /* @__PURE__ */ new Map();
2344
- for (let index = 0; index < edges.length; index++) {
2345
- const edge = edges[index];
2346
- const anchor = endpoint === "source" ? pairs[index].sourceAnchor : pairs[index].targetAnchor;
2347
- const nodeId = endpoint === "source" ? edge.source : edge.target;
2348
- const key = `${nodeId}:${anchor.side}`;
2349
- const group = groups.get(key) ?? [];
2350
- group.push(index);
2351
- groups.set(key, group);
2139
+
2140
+ // src/doc/templates/quoteBlock.ts
2141
+ function quoteBlock(input, context) {
2142
+ const { title, quote, attribution, accentImage } = input;
2143
+ const { theme, viewport } = context;
2144
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2145
+ const quoteFontSize = themedFontSize(48, context, true);
2146
+ const titleFontSize = themedFontSize(34, context, true);
2147
+ const attrFontSize = themedFontSize(26, context, false);
2148
+ const quoteLineHeight = Math.max(theme.typography.titleLineHeight ?? 1.4, 1.25);
2149
+ const decorativeQuoteFontSize = themedFontSize(280, context, true);
2150
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 160))];
2151
+ if (accentImage) {
2152
+ layers.push(
2153
+ ...createAccentLayers(
2154
+ accentImage,
2155
+ input.id,
2156
+ themedImageTreatment(context, input.imageTreatment)
2157
+ )
2158
+ );
2352
2159
  }
2353
- for (const indexes of groups.values()) {
2354
- if (indexes.length < 2) continue;
2355
- const side = endpoint === "source" ? pairs[indexes[0]].sourceAnchor.side : pairs[indexes[0]].targetAnchor.side;
2356
- indexes.sort((a, b) => {
2357
- const oppositeA = nodeById.get(endpoint === "source" ? edges[a].target : edges[a].source);
2358
- const oppositeB = nodeById.get(endpoint === "source" ? edges[b].target : edges[b].source);
2359
- const axisA = side === "top" || side === "bottom" ? oppositeA?.x ?? 0 : oppositeA?.y ?? 0;
2360
- const axisB = side === "top" || side === "bottom" ? oppositeB?.x ?? 0 : oppositeB?.y ?? 0;
2361
- return axisA - axisB || a - b;
2362
- });
2363
- indexes.forEach((edgeIndex, rank) => {
2364
- const anchor = endpoint === "source" ? pairs[edgeIndex].sourceAnchor : pairs[edgeIndex].targetAnchor;
2365
- anchor.offset = (rank + 1) / (indexes.length + 1);
2160
+ if (title) {
2161
+ layers.push({
2162
+ type: "text",
2163
+ id: "quote-title",
2164
+ content: {
2165
+ text: title,
2166
+ style: {
2167
+ fontSize: titleFontSize,
2168
+ fontFamily: getThemeFont(context, "body"),
2169
+ fontWeight: "bold",
2170
+ color: theme.colors.textMuted,
2171
+ textAlign: "center",
2172
+ shadow: shouldUseShadow(context)
2173
+ }
2174
+ },
2175
+ position: {
2176
+ x: accentLayout.textCenterX,
2177
+ y: adjustY("14%", accentLayout),
2178
+ width: accentLayout.textWidth,
2179
+ anchor: "center"
2180
+ },
2181
+ animation: { type: "fadeIn", duration: 0.8 }
2366
2182
  });
2367
2183
  }
2368
- }
2369
- function asciiDiagramFromTemplateData(nodes, edges, options = {}) {
2370
- const ids = new Set(nodes.map((n) => n.id));
2371
- const outNodes = nodes.map((n) => {
2372
- const { col, row } = canvasToAsciiCell(n.x, n.y);
2373
- const labelLines = n.label.length > 0 ? n.label.split("\n") : [];
2374
- const innerW = labelLines.reduce((w, l) => Math.max(w, l.length), 0);
2375
- const wCols = typeof n.w === "number" ? Math.max(3, Math.round(n.w / ASCII_CHAR_W)) : innerW + 4;
2376
- const hRows = typeof n.h === "number" ? Math.max(3, Math.round(n.h / ASCII_CHAR_H)) : labelLines.length + 2;
2377
- return {
2378
- id: n.id,
2379
- label: n.label,
2380
- col,
2381
- row,
2382
- wCols: Math.max(wCols, 3),
2383
- hRows: Math.max(hRows, 3),
2384
- ...n.container && ids.has(n.container) && n.container !== n.id ? { containerId: n.container } : {}
2385
- };
2184
+ layers.push({
2185
+ type: "text",
2186
+ id: "deco-quote",
2187
+ content: {
2188
+ text: "\u201C",
2189
+ style: {
2190
+ fontSize: decorativeQuoteFontSize,
2191
+ fontFamily: getThemeFont(context, "title"),
2192
+ color: withAlpha(theme.colors.text, 0.09),
2193
+ textAlign: "center"
2194
+ }
2195
+ },
2196
+ position: {
2197
+ x: accentLayout.textCenterX,
2198
+ y: adjustY("26%", accentLayout),
2199
+ anchor: "center"
2200
+ }
2386
2201
  });
2387
- return {
2388
- nodes: outNodes,
2389
- edges: edges.filter((e) => ids.has(e.source) && ids.has(e.target)).map((e) => ({
2390
- source: e.source,
2391
- target: e.target,
2392
- ...e.label ? { label: e.label } : {},
2393
- directed: e.directed !== false
2394
- })),
2395
- style: options.style ?? "unicode",
2396
- warnings: []
2397
- };
2398
- }
2399
- function asciiDiagramFromBlocks(children, options = {}) {
2400
- const layout = computeDiagramLayout(children);
2401
- const nodes = layout.nodes.map((n) => ({
2402
- id: n.id,
2403
- label: n.label,
2404
- x: n.x,
2405
- y: n.y
2406
- }));
2407
- const edges = layout.edges.map((e) => ({
2408
- source: e.source,
2409
- target: e.target,
2410
- ...e.type ? { label: e.type } : {}
2411
- }));
2412
- const diagram = asciiDiagramFromTemplateData(nodes, edges, options);
2413
- diagram.warnings.push(...layout.warnings);
2414
- return diagram;
2415
- }
2416
-
2417
- // src/doc/asciiTimeline/mapping.ts
2418
- function asciiTimelineToTemplateData(timeline) {
2419
- const globalStart = timeline.tracks.length > 0 ? Math.min(...timeline.tracks.map((track) => track.startColumn)) : 0;
2420
- const globalEnd = timeline.tracks.length > 0 ? Math.max(...timeline.tracks.map((track) => track.endColumn)) : 1;
2421
- const span = Math.max(1, globalEnd - globalStart);
2422
- const tracks = timeline.tracks.map((track) => ({
2423
- id: track.id,
2424
- ...track.label ? { label: track.label } : {},
2425
- ...track.endLabel ? { endLabel: track.endLabel } : {},
2426
- events: track.events.map(
2427
- (event) => ({
2428
- id: event.id,
2429
- label: event.label,
2430
- ...event.description ? { description: event.description } : {},
2431
- position: clamp01((event.column - globalStart) / span),
2432
- ...event.side ? { side: event.side } : {},
2433
- ...event.descriptionSide ? { descriptionSide: event.descriptionSide } : {},
2434
- ...event.callout === false ? { callout: false } : {},
2435
- ...event.marker ? { marker: event.marker } : {}
2436
- })
2437
- )
2438
- }));
2439
- const links = timeline.links.map((link) => ({ ...link }));
2440
- return { tracks, links };
2441
- }
2442
- function clamp01(value) {
2443
- return Math.max(0, Math.min(1, value));
2444
- }
2445
-
2446
- // src/doc/treeview/mapping.ts
2447
- function treeToTemplateData(tree) {
2448
- return { items: tree.roots.map(toItem) };
2449
- }
2450
- function toItem(node) {
2451
- return {
2452
- id: node.id,
2453
- label: node.label,
2454
- ...node.isDir ? { isDir: true } : {},
2455
- ...node.comment ? { comment: node.comment } : {},
2456
- children: node.children.map(toItem)
2457
- };
2458
- }
2459
- function treeFromTemplateData(items, options = {}) {
2460
- const roots = items.map(fromItem);
2461
- return { roots, style: options.style ?? "unicode", warnings: [] };
2462
- }
2463
- function fromItem(item) {
2464
- const children = Array.isArray(item.children) ? item.children.map(fromItem) : [];
2465
- return {
2466
- id: typeof item.id === "string" && item.id ? item.id : "node",
2467
- label: typeof item.label === "string" ? item.label : "",
2468
- children,
2469
- isDir: item.isDir === true || children.length > 0,
2470
- ...item.comment ? { comment: item.comment } : {}
2471
- };
2472
- }
2473
- function treeFromMarkdownList(list) {
2474
- const usedIds = /* @__PURE__ */ new Map();
2475
- const slugify2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "node";
2476
- const idFor = (label) => {
2477
- const base = slugify2(label.replace(/\/$/, ""));
2478
- const count = usedIds.get(base) ?? 0;
2479
- usedIds.set(base, count + 1);
2480
- return count === 0 ? base : `${base}-${count + 1}`;
2481
- };
2482
- const itemToNode = (item) => {
2483
- const labelNode = item.children.find((c) => c.type !== "list");
2484
- const label = labelNode ? extractPlainText(labelNode).trim() : "";
2485
- const childLists = item.children.filter((c) => c.type === "list");
2486
- const children = [];
2487
- for (const cl of childLists) for (const ci of cl.children) children.push(itemToNode(ci));
2488
- return {
2489
- id: idFor(label),
2490
- label,
2491
- children,
2492
- isDir: label.endsWith("/") || children.length > 0
2493
- };
2494
- };
2495
- return { roots: list.children.map(itemToNode), style: "unicode", warnings: [] };
2496
- }
2497
- function findFirstList(contents) {
2498
- return contents?.find((n) => n.type === "list");
2202
+ const quoteYPct = attribution ? 45 : title ? 52 : 50;
2203
+ layers.push({
2204
+ type: "text",
2205
+ id: "quote",
2206
+ content: {
2207
+ text: quote,
2208
+ style: {
2209
+ fontSize: quoteFontSize,
2210
+ fontFamily: getThemeFont(context, "title"),
2211
+ color: theme.colors.text,
2212
+ textAlign: "center",
2213
+ lineHeight: quoteLineHeight,
2214
+ shadow: shouldUseShadow(context)
2215
+ }
2216
+ },
2217
+ position: {
2218
+ x: accentLayout.textCenterX,
2219
+ y: adjustY(`${quoteYPct}%`, accentLayout),
2220
+ anchor: "center",
2221
+ width: accentLayout.textWidth
2222
+ },
2223
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 2 })
2224
+ });
2225
+ if (attribution) {
2226
+ const quoteWidthPx = parseFloat(accentLayout.textWidth) / 100 * viewport.width;
2227
+ const quoteHeightPx = estimateTextHeight(quote, quoteFontSize, quoteWidthPx, quoteLineHeight);
2228
+ const gapPx = quoteFontSize * 1.7;
2229
+ const attrYPct = Math.min(
2230
+ 82,
2231
+ quoteYPct + (quoteHeightPx / 2 + gapPx) / viewport.height * 100
2232
+ );
2233
+ layers.push({
2234
+ type: "text",
2235
+ id: "attribution",
2236
+ content: {
2237
+ text: `\u2014 ${attribution}`,
2238
+ style: {
2239
+ fontSize: attrFontSize,
2240
+ fontFamily: getThemeFont(context, "body"),
2241
+ color: theme.colors.textMuted,
2242
+ textAlign: "center",
2243
+ shadow: shouldUseShadow(context)
2244
+ }
2245
+ },
2246
+ position: {
2247
+ x: accentLayout.textCenterX,
2248
+ y: adjustY(`${attrYPct}%`, accentLayout),
2249
+ anchor: "center"
2250
+ },
2251
+ animation: { type: "fadeIn", duration: 1, delay: 1.5 }
2252
+ });
2253
+ }
2254
+ return layers;
2499
2255
  }
2500
2256
 
2501
- // src/doc/templateInputs.ts
2502
- var VIDEO_FILE_RE = /\.(?:webm|mp4|mov|m4v|ogv)(?:[?#].*)?$/i;
2503
- function extractBodyPlainText(contents) {
2504
- if (!contents || contents.length === 0) return "";
2505
- return contents.filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((n) => extractPlainText(n)).join("\n").trim();
2506
- }
2507
- function extractListItems(contents) {
2508
- return extractRichListItems(contents).map((item) => item.text);
2509
- }
2510
- function escapeInlineHtml(value) {
2511
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2512
- }
2513
- function renderInlineHtml(nodes) {
2514
- return nodes.map((node) => {
2515
- switch (node.type) {
2516
- case "text":
2517
- return escapeInlineHtml(node.value);
2518
- case "emphasis":
2519
- return `<em>${renderInlineHtml(node.children)}</em>`;
2520
- case "strong":
2521
- return `<strong>${renderInlineHtml(node.children)}</strong>`;
2522
- case "delete":
2523
- return `<del>${renderInlineHtml(node.children)}</del>`;
2524
- case "inlineCode":
2525
- return `<code>${escapeInlineHtml(node.value)}</code>`;
2526
- case "link": {
2527
- const label = renderInlineHtml(node.children);
2528
- const href = sanitizeUrl(node.url, "link");
2529
- if (!href) return label;
2530
- const title = node.title ? ` title="${escapeInlineHtml(node.title)}"` : "";
2531
- return `<a href="${escapeInlineHtml(href)}"${title}>${label}</a>`;
2257
+ // src/doc/templates/factCard.ts
2258
+ function factCard(input, context) {
2259
+ const { fact, explanation, source, accentImage } = input;
2260
+ const { theme, viewport } = context;
2261
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2262
+ const factFontSize = themedFontSize(56, context, true);
2263
+ const explainFontSize = themedFontSize(34, context, false);
2264
+ const sourceFontSize = themedFontSize(20, context, false);
2265
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
2266
+ if (accentImage) {
2267
+ layers.push(
2268
+ ...createAccentLayers(
2269
+ accentImage,
2270
+ input.id,
2271
+ themedImageTreatment(context, input.imageTreatment)
2272
+ )
2273
+ );
2274
+ }
2275
+ const textWidthPx = parseFloat(accentLayout.textWidth) / 100 * viewport.width;
2276
+ const factH = estimateTextHeight(fact, factFontSize, textWidthPx, 1.3);
2277
+ const explainH = estimateTextHeight(explanation, explainFontSize, textWidthPx, 1.5);
2278
+ const sourceH = source ? sourceFontSize * 1.4 : 0;
2279
+ const gap = factFontSize * 0.9;
2280
+ const sourceGap = source ? explainFontSize * 1.1 : 0;
2281
+ const totalH = factH + gap + explainH + sourceGap + sourceH;
2282
+ const groupTopPct = 47 - totalH / 2 / viewport.height * 100;
2283
+ const pct = (px) => groupTopPct + px / viewport.height * 100;
2284
+ const factY = pct(factH / 2);
2285
+ const explainY = pct(factH + gap + explainH / 2);
2286
+ const sourceY = pct(factH + gap + explainH + sourceGap + sourceH / 2);
2287
+ layers.push({
2288
+ type: "text",
2289
+ id: "fact",
2290
+ content: {
2291
+ text: fact,
2292
+ style: {
2293
+ fontSize: factFontSize,
2294
+ fontFamily: getThemeFont(context, "title"),
2295
+ fontWeight: "bold",
2296
+ color: theme.colors.text,
2297
+ textAlign: "center",
2298
+ lineHeight: 1.3,
2299
+ shadow: shouldUseShadow(context)
2532
2300
  }
2533
- case "break":
2534
- return "<br>";
2535
- case "image":
2536
- return escapeInlineHtml(node.alt ?? "");
2537
- case "inlineMath":
2538
- return escapeInlineHtml(`$${node.value}$`);
2539
- case "footnoteReference":
2540
- return escapeInlineHtml(`[^${node.label ?? node.identifier}]`);
2541
- case "linkReference":
2542
- case "textDirective":
2543
- return renderInlineHtml(node.children);
2544
- case "imageReference":
2545
- return escapeInlineHtml(node.alt ?? "");
2546
- case "mention":
2547
- return escapeInlineHtml(`@${node.displayName}`);
2548
- case "inlineIcon":
2549
- return `<i class="fa-${node.family} fa-${node.name}" aria-hidden="true"></i>`;
2550
- case "htmlInline":
2551
- return escapeInlineHtml(node.rawHtml);
2552
- }
2553
- }).join("");
2554
- }
2555
- function renderListItemHtml(markdown) {
2556
- return markdown.map(
2557
- (node) => node.type === "paragraph" || node.type === "heading" ? renderInlineHtml(node.children) : escapeInlineHtml(extractPlainText(node))
2558
- ).join("<br>");
2301
+ },
2302
+ position: {
2303
+ x: accentLayout.textCenterX,
2304
+ y: adjustY(`${factY}%`, accentLayout),
2305
+ width: accentLayout.textWidth,
2306
+ anchor: "center"
2307
+ },
2308
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1.5 })
2309
+ });
2310
+ layers.push({
2311
+ type: "text",
2312
+ id: "explanation",
2313
+ content: {
2314
+ text: explanation,
2315
+ style: {
2316
+ fontSize: explainFontSize,
2317
+ fontFamily: getThemeFont(context, "body"),
2318
+ color: theme.colors.textMuted,
2319
+ textAlign: "center",
2320
+ lineHeight: 1.5,
2321
+ shadow: shouldUseShadow(context)
2322
+ }
2323
+ },
2324
+ position: {
2325
+ x: accentLayout.textCenterX,
2326
+ y: adjustY(`${explainY}%`, accentLayout),
2327
+ width: accentLayout.textWidth,
2328
+ anchor: "center"
2329
+ },
2330
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1, delay: 0.8 })
2331
+ });
2332
+ if (source) {
2333
+ layers.push({
2334
+ type: "text",
2335
+ id: "source",
2336
+ content: {
2337
+ text: source,
2338
+ style: {
2339
+ fontSize: sourceFontSize,
2340
+ fontFamily: getThemeFont(context, "body"),
2341
+ color: theme.colors.textMuted,
2342
+ textAlign: "center",
2343
+ shadow: shouldUseShadow(context)
2344
+ }
2345
+ },
2346
+ position: {
2347
+ x: accentLayout.textCenterX,
2348
+ y: adjustY(`${sourceY}%`, accentLayout),
2349
+ anchor: "center"
2350
+ },
2351
+ animation: { type: "fadeIn", duration: 0.8, delay: 1.5 }
2352
+ });
2353
+ }
2354
+ return layers;
2559
2355
  }
2560
- function extractRichListItems(contents) {
2561
- if (!contents) return [];
2562
- const items = [];
2563
- for (const node of contents) {
2564
- if (node.type !== "list") continue;
2565
- for (const item of node.children) {
2566
- const text = extractPlainText(item).trim();
2567
- if (!text) continue;
2568
- const html = renderListItemHtml(item.children);
2569
- const plainHtml = escapeInlineHtml(text).replace(/\r?\n/g, "<br>");
2570
- items.push({
2571
- text,
2572
- markdown: item.children,
2573
- ...html && html !== plainHtml ? { html } : {}
2574
- });
2356
+
2357
+ // src/doc/templates/twoColumn.ts
2358
+ function twoColumn(input, context) {
2359
+ const { left, right, header, leftColor = "green", rightColor = "blue" } = input;
2360
+ if (!left?.label || !right?.label) return [];
2361
+ const { theme, layout, orientation } = context;
2362
+ const leftColors = resolveColorScheme(context, leftColor);
2363
+ const rightColors = resolveColorScheme(context, rightColor);
2364
+ const positions = getTwoColumnPositions(orientation);
2365
+ const isStacked = layout.stackColumns;
2366
+ const columnWidth = isStacked ? "85%" : "42%";
2367
+ const headerFontSize = themedFontSize(36, context, true);
2368
+ const labelFontSize = themedFontSize(44, context, true);
2369
+ const sublabelFontSize = themedFontSize(22, context, false);
2370
+ const panelTop = header && !isStacked ? 22 : 15;
2371
+ const panelHeight = header && !isStacked ? 64 : 70;
2372
+ const layers = [
2373
+ // Background — subtle gradient to add depth
2374
+ {
2375
+ type: "shape",
2376
+ id: "bg",
2377
+ content: {
2378
+ shape: "rect",
2379
+ fill: themedSurfaceGradient(context, 135)
2380
+ },
2381
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2575
2382
  }
2383
+ ];
2384
+ if (!isStacked) {
2385
+ const panelFill = withAlpha(theme.colors.text, 0.05);
2386
+ layers.push({
2387
+ type: "shape",
2388
+ id: "left-panel",
2389
+ content: {
2390
+ shape: "rect",
2391
+ fill: panelFill,
2392
+ borderRadius: 12
2393
+ },
2394
+ position: { x: "3%", y: `${panelTop}%`, width: "44%", height: `${panelHeight}%` }
2395
+ });
2396
+ layers.push({
2397
+ type: "shape",
2398
+ id: "right-panel",
2399
+ content: {
2400
+ shape: "rect",
2401
+ fill: panelFill,
2402
+ borderRadius: 12
2403
+ },
2404
+ position: { x: "53%", y: `${panelTop}%`, width: "44%", height: `${panelHeight}%` }
2405
+ });
2406
+ }
2407
+ const headerY = isStacked ? "8%" : "12%";
2408
+ if (header) {
2409
+ layers.push({
2410
+ type: "text",
2411
+ id: "header",
2412
+ content: {
2413
+ text: header,
2414
+ style: {
2415
+ fontSize: headerFontSize,
2416
+ fontFamily: getThemeFont(context, "title"),
2417
+ fontWeight: "bold",
2418
+ color: theme.colors.text,
2419
+ textAlign: "center"
2420
+ }
2421
+ },
2422
+ position: { x: "50%", y: headerY, anchor: "center" },
2423
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8 })
2424
+ });
2576
2425
  }
2577
- return items;
2578
- }
2579
- function parseDim(raw) {
2580
- if (raw === void 0) return void 0;
2581
- const n = parseFloat(raw);
2582
- return Number.isFinite(n) && n > 0 ? n : void 0;
2583
- }
2584
- function extractImages(contents, limit = Infinity) {
2585
- if (!contents || contents.length === 0) return [];
2586
- const found = [];
2587
- function fromHtml(nodes) {
2588
- for (const node of nodes) {
2589
- if (found.length >= limit) return;
2590
- if (!node || typeof node !== "object") continue;
2591
- const n = node;
2592
- if (n.type === "htmlElement" && n.tagName === "img") {
2593
- const attrs = n.attributes;
2594
- if (attrs && typeof attrs.src === "string" && attrs.src) {
2595
- found.push({
2596
- src: attrs.src,
2597
- alt: typeof attrs.alt === "string" ? attrs.alt : "",
2598
- width: parseDim(attrs.width),
2599
- height: parseDim(attrs.height)
2600
- });
2426
+ let leftY;
2427
+ let leftSublabelY;
2428
+ let rightY;
2429
+ let rightSublabelY;
2430
+ if (isStacked) {
2431
+ leftY = header ? "30%" : "25%";
2432
+ leftSublabelY = header ? "40%" : "35%";
2433
+ rightY = header ? "60%" : "55%";
2434
+ rightSublabelY = header ? "70%" : "65%";
2435
+ } else {
2436
+ const panelCenter = panelTop + panelHeight / 2;
2437
+ leftY = `${panelCenter - 4}%`;
2438
+ leftSublabelY = `${panelCenter + 7}%`;
2439
+ rightY = `${panelCenter - 4}%`;
2440
+ rightSublabelY = `${panelCenter + 7}%`;
2441
+ }
2442
+ layers.push({
2443
+ type: "text",
2444
+ id: "left-label",
2445
+ content: {
2446
+ text: left.label,
2447
+ style: {
2448
+ fontSize: labelFontSize,
2449
+ fontFamily: getThemeFont(context, "body"),
2450
+ fontWeight: "bold",
2451
+ color: leftColors.text,
2452
+ textAlign: "center",
2453
+ lineHeight: 1.3
2454
+ }
2455
+ },
2456
+ position: {
2457
+ x: positions.left.x,
2458
+ y: leftY,
2459
+ anchor: "center",
2460
+ width: columnWidth
2461
+ },
2462
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1 })
2463
+ });
2464
+ if (left.sublabel) {
2465
+ layers.push({
2466
+ type: "text",
2467
+ id: "left-sublabel",
2468
+ content: {
2469
+ text: left.sublabel,
2470
+ style: {
2471
+ fontSize: sublabelFontSize,
2472
+ fontFamily: getThemeFont(context, "body"),
2473
+ color: theme.colors.textMuted,
2474
+ textAlign: "center",
2475
+ lineHeight: 1.4
2601
2476
  }
2477
+ },
2478
+ position: {
2479
+ x: positions.left.x,
2480
+ y: leftSublabelY,
2481
+ anchor: "center",
2482
+ width: columnWidth
2483
+ },
2484
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.3 }
2485
+ });
2486
+ }
2487
+ const connectorFontSize = themedFontSize(64, context, true);
2488
+ const connectorY = isStacked ? "48%" : `${panelTop + panelHeight / 2}%`;
2489
+ const connectorSymbol = isStacked ? "\u2193" : "\u2192";
2490
+ layers.push({
2491
+ type: "text",
2492
+ id: "connector",
2493
+ content: {
2494
+ text: connectorSymbol,
2495
+ style: {
2496
+ fontSize: connectorFontSize,
2497
+ fontFamily: getThemeFont(context, "title"),
2498
+ fontWeight: "bold",
2499
+ color: withAlpha(theme.colors.text, 0.45),
2500
+ textAlign: "center"
2602
2501
  }
2603
- if (Array.isArray(n.children)) fromHtml(n.children);
2604
- }
2502
+ },
2503
+ position: {
2504
+ x: "50%",
2505
+ y: connectorY,
2506
+ anchor: "center"
2507
+ },
2508
+ animation: { type: "fadeIn", duration: 0.6, delay: 0.4 }
2509
+ });
2510
+ layers.push({
2511
+ type: "text",
2512
+ id: "right-label",
2513
+ content: {
2514
+ text: right.label,
2515
+ style: {
2516
+ fontSize: labelFontSize,
2517
+ fontFamily: getThemeFont(context, "body"),
2518
+ fontWeight: "bold",
2519
+ color: rightColors.text,
2520
+ textAlign: "center",
2521
+ lineHeight: 1.3
2522
+ }
2523
+ },
2524
+ position: {
2525
+ x: positions.right.x,
2526
+ y: rightY,
2527
+ anchor: "center",
2528
+ width: columnWidth
2529
+ },
2530
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1, delay: 0.5 })
2531
+ });
2532
+ if (right.sublabel) {
2533
+ layers.push({
2534
+ type: "text",
2535
+ id: "right-sublabel",
2536
+ content: {
2537
+ text: right.sublabel,
2538
+ style: {
2539
+ fontSize: sublabelFontSize,
2540
+ fontFamily: getThemeFont(context, "body"),
2541
+ color: theme.colors.textMuted,
2542
+ textAlign: "center",
2543
+ lineHeight: 1.4
2544
+ }
2545
+ },
2546
+ position: {
2547
+ x: positions.right.x,
2548
+ y: rightSublabelY,
2549
+ anchor: "center",
2550
+ width: columnWidth
2551
+ },
2552
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.8 }
2553
+ });
2605
2554
  }
2606
- function walk(node) {
2607
- if (found.length >= limit) return;
2608
- if (!node || typeof node !== "object") return;
2609
- const n = node;
2610
- if (n.type === "image" && typeof n.url === "string" && n.url) {
2611
- found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
2612
- return;
2613
- }
2614
- if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
2615
- fromHtml(n.htmlChildren);
2616
- }
2617
- if (Array.isArray(n.children)) {
2618
- for (const child of n.children) walk(child);
2555
+ return layers;
2556
+ }
2557
+
2558
+ // src/doc/templates/dateEvent.ts
2559
+ var MOOD_ACCENTS = {
2560
+ neutral: "#63b3ed",
2561
+ somber: "#e53e3e",
2562
+ celebratory: "#68d391"
2563
+ };
2564
+ function dateEvent(input, context) {
2565
+ const { date, description, footer, mood = "neutral", accentImage } = input;
2566
+ const { theme } = context;
2567
+ const moodAccent = MOOD_ACCENTS[mood];
2568
+ const dateColor = relativeLuminance(theme.colors.background) > 0.5 ? oklchDarken(moodAccent, 0.25) : moodAccent;
2569
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2570
+ const dateFontSize = themedFontSize(96, context, true);
2571
+ const descFontSize = themedFontSize(30, context, false);
2572
+ const footerFontSize = themedFontSize(26, context, false);
2573
+ const layers = [
2574
+ // Background — theme surface gradient (diagonal to differentiate
2575
+ // from other text templates)
2576
+ {
2577
+ type: "shape",
2578
+ id: "bg",
2579
+ content: {
2580
+ shape: "rect",
2581
+ fill: themedSurfaceGradient(context, 135)
2582
+ },
2583
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2619
2584
  }
2585
+ ];
2586
+ if (accentImage) {
2587
+ layers.push(
2588
+ ...createAccentLayers(
2589
+ accentImage,
2590
+ input.id,
2591
+ themedImageTreatment(context, input.imageTreatment)
2592
+ )
2593
+ );
2620
2594
  }
2621
- for (const node of contents) {
2622
- if (found.length >= limit) break;
2623
- walk(node);
2595
+ layers.push({
2596
+ type: "text",
2597
+ id: "date",
2598
+ content: {
2599
+ text: date,
2600
+ style: {
2601
+ fontSize: dateFontSize,
2602
+ fontFamily: getThemeFont(context, "title"),
2603
+ fontWeight: "bold",
2604
+ color: dateColor,
2605
+ shadow: shouldUseShadow(context)
2606
+ }
2607
+ },
2608
+ position: {
2609
+ x: accentLayout.textCenterX,
2610
+ y: adjustY("35%", accentLayout),
2611
+ anchor: "center"
2612
+ },
2613
+ animation: { type: "fadeIn", duration: 1.5 }
2614
+ });
2615
+ layers.push({
2616
+ type: "text",
2617
+ id: "description",
2618
+ content: {
2619
+ text: description,
2620
+ style: {
2621
+ fontSize: descFontSize,
2622
+ fontFamily: getThemeFont(context, "body"),
2623
+ color: theme.colors.text,
2624
+ textAlign: "center",
2625
+ lineHeight: 1.6,
2626
+ shadow: shouldUseShadow(context)
2627
+ }
2628
+ },
2629
+ position: {
2630
+ x: accentLayout.textCenterX,
2631
+ y: adjustY("58%", accentLayout),
2632
+ width: accentLayout.textWidth,
2633
+ anchor: "center"
2634
+ },
2635
+ animation: { type: "fadeIn", duration: 2, delay: 1 }
2636
+ });
2637
+ if (footer) {
2638
+ layers.push({
2639
+ type: "text",
2640
+ id: "footer",
2641
+ content: {
2642
+ text: footer,
2643
+ style: {
2644
+ fontSize: footerFontSize,
2645
+ fontFamily: getThemeFont(context, "body"),
2646
+ color: theme.colors.textMuted,
2647
+ shadow: shouldUseShadow(context)
2648
+ }
2649
+ },
2650
+ position: {
2651
+ x: accentLayout.textCenterX,
2652
+ y: adjustY("72%", accentLayout),
2653
+ anchor: "center"
2654
+ },
2655
+ animation: { type: "fadeIn", duration: 1, delay: 3 }
2656
+ });
2624
2657
  }
2625
- return found;
2626
- }
2627
- function extractFirstImage(contents) {
2628
- return extractImages(contents, 1)[0] ?? null;
2658
+ return layers;
2629
2659
  }
2630
- function extractEmbeddedVideos(contents, limit = Infinity) {
2631
- if (!contents || contents.length === 0) return [];
2632
- const found = [];
2633
- const seen = /* @__PURE__ */ new Set();
2634
- const add = (video) => {
2635
- if (!video.src || found.length >= limit || seen.has(video.src)) return;
2636
- seen.add(video.src);
2637
- found.push(video);
2638
- };
2639
- function nestedSource(children) {
2640
- if (!Array.isArray(children)) return void 0;
2641
- for (const child of children) {
2642
- if (!child || typeof child !== "object") continue;
2643
- const node = child;
2644
- if (node.tagName === "source") {
2645
- const attrs = node.attributes;
2646
- if (typeof attrs?.src === "string" && attrs.src) return attrs.src;
2647
- }
2648
- const nested = nestedSource(node.children);
2649
- if (nested) return nested;
2660
+
2661
+ // src/doc/templates/imageWithCaption.ts
2662
+ function imageWithCaption(input, context) {
2663
+ const {
2664
+ imageSrc,
2665
+ imageAlt,
2666
+ caption: rawCaption,
2667
+ captionPosition = "bottom",
2668
+ ambientMotion,
2669
+ isTitle,
2670
+ subtitle,
2671
+ imageCredit,
2672
+ imageLicense
2673
+ } = input;
2674
+ const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
2675
+ const { theme, layout } = context;
2676
+ const treatment = themedImageTreatment(context, input.imageTreatment);
2677
+ const captionFontSize = themedFontSize(30, context, false);
2678
+ const titleFontSize = themedFontSize(96, context, true);
2679
+ const subtitleFontSize = themedFontSize(36, context, false);
2680
+ const legacyKenBurns = input.kenBurns;
2681
+ const motion = ambientMotion || legacyKenBurns;
2682
+ const imageAnimation = mapAmbientMotion(motion);
2683
+ const layers = [
2684
+ // Background image
2685
+ {
2686
+ type: "image",
2687
+ id: "bg-image",
2688
+ content: {
2689
+ src: imageSrc,
2690
+ alt: imageAlt,
2691
+ fit: "cover",
2692
+ credit: imageCredit,
2693
+ license: imageLicense,
2694
+ ...treatment ? { treatment } : {}
2695
+ },
2696
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
2697
+ animation: imageAnimation
2650
2698
  }
2651
- return void 0;
2652
- }
2653
- function walk(node) {
2654
- if (found.length >= limit || !node || typeof node !== "object") return;
2655
- const n = node;
2656
- if (n.type === "htmlElement" && n.tagName === "video") {
2657
- const attrs = n.attributes;
2658
- const src = attrs?.src || nestedSource(n.children);
2659
- if (src) {
2660
- const startAt = attrs?.["data-squisq-video-start-at"] ? parseTimeSeconds(attrs["data-squisq-video-start-at"]) : null;
2661
- const clipStart = attrs?.["data-squisq-video-clip-start"] ? parseTimeSeconds(attrs["data-squisq-video-clip-start"]) : null;
2662
- const clipEnd = attrs?.["data-squisq-video-clip-end"] ? parseTimeSeconds(attrs["data-squisq-video-clip-end"]) : null;
2663
- add({
2664
- src,
2665
- ...attrs?.poster ? { posterSrc: attrs.poster } : {},
2666
- alt: attrs?.["aria-label"] || attrs?.title || attrs?.alt || "",
2667
- ...startAt != null ? { startAt } : {},
2668
- ...clipStart != null ? { clipStart } : {},
2669
- ...clipEnd != null ? { clipEnd } : {}
2670
- });
2671
- }
2672
- } else if ((n.type === "link" || n.type === "image") && typeof n.url === "string" && VIDEO_FILE_RE.test(n.url)) {
2673
- add({
2674
- src: n.url,
2675
- alt: typeof n.alt === "string" ? n.alt : extractPlainText(n).trim()
2699
+ ];
2700
+ const bg = theme.colors.background;
2701
+ if (isTitle && caption) {
2702
+ layers.push({
2703
+ type: "shape",
2704
+ id: "title-gradient",
2705
+ content: {
2706
+ shape: "rect",
2707
+ fill: `linear-gradient(0deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.45)} 40%, ${withAlpha(bg, 0.1)} 70%, ${withAlpha(bg, 0)} 100%)`
2708
+ },
2709
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2710
+ });
2711
+ layers.push({
2712
+ type: "text",
2713
+ id: "title",
2714
+ content: {
2715
+ text: caption,
2716
+ style: {
2717
+ fontSize: titleFontSize,
2718
+ fontFamily: getThemeFont(context, "title"),
2719
+ fontWeight: "bold",
2720
+ color: theme.colors.text,
2721
+ textAlign: "center",
2722
+ shadow: shouldUseShadow(context)
2723
+ }
2724
+ },
2725
+ position: {
2726
+ x: "50%",
2727
+ y: subtitle ? "70%" : "75%",
2728
+ anchor: "center",
2729
+ width: layout.maxTextWidth
2730
+ },
2731
+ animation: { type: "fadeIn", duration: 0.8 }
2732
+ });
2733
+ if (subtitle) {
2734
+ layers.push({
2735
+ type: "text",
2736
+ id: "subtitle",
2737
+ content: {
2738
+ text: subtitle,
2739
+ style: {
2740
+ fontSize: subtitleFontSize,
2741
+ fontFamily: getThemeFont(context, "body"),
2742
+ color: theme.colors.textMuted,
2743
+ textAlign: "center",
2744
+ lineHeight: 1.5
2745
+ }
2746
+ },
2747
+ position: {
2748
+ x: "50%",
2749
+ y: "82%",
2750
+ anchor: "center",
2751
+ width: layout.maxTextWidth
2752
+ },
2753
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.2 }
2676
2754
  });
2677
2755
  }
2678
- if (Array.isArray(n.children)) n.children.forEach(walk);
2679
- if (Array.isArray(n.htmlChildren)) n.htmlChildren.forEach(walk);
2680
- }
2681
- contents.forEach(walk);
2682
- return found;
2683
- }
2684
- function extractFirstEmbeddedVideo(contents) {
2685
- return extractEmbeddedVideos(contents, 1)[0] ?? null;
2686
- }
2687
- function extractTableFromContents(contents) {
2688
- if (!contents) return null;
2689
- for (const node of contents) {
2690
- if (node.type === "table") {
2691
- const table = node;
2692
- const [headerRow, ...bodyRows] = table.children;
2693
- if (!headerRow) return null;
2694
- const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
2695
- const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
2696
- return { headers, rows, align: table.align };
2697
- }
2698
- }
2699
- return null;
2700
- }
2701
- function extractBlockquoteText(contents) {
2702
- if (!contents) return "";
2703
- for (const node of contents) {
2704
- if (node.type === "blockquote") return extractPlainText(node).trim();
2705
- }
2706
- return "";
2707
- }
2708
- function firstParagraph(contents) {
2709
- if (!contents) return null;
2710
- const index = contents.findIndex((node) => node.type === "paragraph");
2711
- if (index < 0) return null;
2712
- return {
2713
- node: contents[index],
2714
- index
2715
- };
2716
- }
2717
- function leadingStrongStat(paragraph) {
2718
- for (const child of paragraph.children) {
2719
- if (child.type === "text" && child.value.trim() === "") continue;
2720
- if (child.type !== "strong") return null;
2721
- const text = extractPlainText(child).trim();
2722
- const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
2723
- return match?.[0].trim() || null;
2756
+ return layers;
2724
2757
  }
2725
- return null;
2726
- }
2727
- function removeStatPhrase(text, start, end) {
2728
- const before = text.slice(0, start).trim();
2729
- let after = text.slice(end);
2730
- if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, "");
2731
- else after = after.trim();
2732
- return [before, after].filter(Boolean).join(" ").trim();
2733
- }
2734
- function bodyWithoutParagraphStat(contents, paragraphIndex, paragraphText, start, end) {
2735
- if (!contents) return "";
2736
- return contents.map(
2737
- (node, index) => index === paragraphIndex ? removeStatPhrase(paragraphText, start, end) : extractPlainText(node).trim()
2738
- ).filter(Boolean).join("\n").trim();
2758
+ if (caption) {
2759
+ const captionBgY = captionPosition === "top" ? "7%" : captionPosition === "center" ? "42%" : "68%";
2760
+ const captionY = captionPosition === "top" ? "13%" : captionPosition === "center" ? "48%" : "74%";
2761
+ layers.push({
2762
+ type: "shape",
2763
+ id: "caption-gradient",
2764
+ content: {
2765
+ shape: "rect",
2766
+ fill: `linear-gradient(90deg, ${withAlpha(bg, 0)}, ${withAlpha(bg, 0.78)} 16%, ${withAlpha(bg, 0.78)} 84%, ${withAlpha(bg, 0)})`
2767
+ },
2768
+ position: {
2769
+ x: 0,
2770
+ y: captionBgY,
2771
+ width: "100%",
2772
+ height: "16%"
2773
+ }
2774
+ });
2775
+ layers.push({
2776
+ type: "text",
2777
+ id: "caption",
2778
+ content: {
2779
+ text: caption,
2780
+ style: {
2781
+ fontSize: captionFontSize,
2782
+ fontFamily: getThemeFont(context, "body"),
2783
+ color: theme.colors.text,
2784
+ textAlign: "center",
2785
+ shadow: shouldUseShadow(context),
2786
+ lineHeight: 1.18,
2787
+ maxLines: 2
2788
+ }
2789
+ },
2790
+ position: {
2791
+ x: "50%",
2792
+ y: captionY,
2793
+ anchor: "center",
2794
+ width: "78%"
2795
+ },
2796
+ animation: { type: "fadeIn", duration: 1.5, delay: 0.5 }
2797
+ });
2798
+ }
2799
+ return layers;
2739
2800
  }
2740
- function deriveStatHighlightInputs(headingText, contents, bodyText3) {
2741
- const paragraph = firstParagraph(contents);
2742
- if (paragraph) {
2743
- const paragraphText = extractPlainText(paragraph.node).trim();
2744
- const strongStat = leadingStrongStat(paragraph.node);
2745
- if (strongStat) {
2746
- const start = paragraphText.indexOf(strongStat);
2747
- const description = bodyWithoutParagraphStat(
2748
- contents,
2749
- paragraph.index,
2750
- paragraphText,
2751
- start,
2752
- start + strongStat.length
2753
- );
2754
- return {
2755
- stat: strongStat,
2756
- description: description || headingText || strongStat
2757
- };
2801
+
2802
+ // src/doc/templates/featureBlock.ts
2803
+ function buildFeatureLayers(input, context, side) {
2804
+ const { imageSrc, imageAlt, imageWidth, imageHeight, title, body } = input;
2805
+ const { theme, layout } = context;
2806
+ const treatment = themedImageTreatment(context, input.imageTreatment);
2807
+ const sized = (imageWidth ?? 0) > 0 || (imageHeight ?? 0) > 0;
2808
+ const stack = layout.stackColumns;
2809
+ const titleFontSize = themedFontSize(48, context, true);
2810
+ const bodyFontSize2 = themedFontSize(24, context, false);
2811
+ let imgX;
2812
+ let imgY;
2813
+ let imgW;
2814
+ let imgH;
2815
+ let imageFit;
2816
+ if (stack) {
2817
+ imgX = "0";
2818
+ imgY = "0";
2819
+ imgW = "100%";
2820
+ imgH = "50%";
2821
+ imageFit = sized ? "contain" : "cover";
2822
+ } else if (sized) {
2823
+ const halfStart = side === "left" ? 5 : 55;
2824
+ imgX = `${halfStart}%`;
2825
+ imgY = "5%";
2826
+ imgW = "40%";
2827
+ imgH = "90%";
2828
+ imageFit = "contain";
2829
+ } else {
2830
+ imgX = side === "left" ? "0" : "50%";
2831
+ imgY = "0";
2832
+ imgW = "50%";
2833
+ imgH = "100%";
2834
+ imageFit = "cover";
2835
+ }
2836
+ const COLUMN_INSET = 4;
2837
+ const textColumnWidth = stack ? 90 : 42;
2838
+ const textX = stack ? `${(100 - textColumnWidth) / 2}%` : side === "left" ? `${50 + COLUMN_INSET}%` : `${COLUMN_INSET + 2}%`;
2839
+ const titleY = stack ? "60%" : body ? "42%" : "50%";
2840
+ const bodyY = stack ? "78%" : title ? "55%" : "50%";
2841
+ const textAnchor = "top-left";
2842
+ const textAlign = "left";
2843
+ const layers = [
2844
+ {
2845
+ type: "shape",
2846
+ id: "feature-bg",
2847
+ content: { shape: "rect", fill: theme.colors.background },
2848
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2758
2849
  }
2850
+ ];
2851
+ if (imageSrc) {
2852
+ layers.push({
2853
+ type: "image",
2854
+ id: "feature-image",
2855
+ content: {
2856
+ src: imageSrc,
2857
+ alt: imageAlt ?? title ?? "",
2858
+ fit: imageFit,
2859
+ ...treatment ? { treatment } : {}
2860
+ },
2861
+ position: { x: imgX, y: imgY, width: imgW, height: imgH }
2862
+ });
2759
2863
  }
2760
- const trimmedHeading = headingText.trim();
2761
- const headingStat = matchNumberHighlight(trimmedHeading);
2762
- if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
2763
- return { stat: headingText, description: bodyText3 || headingText };
2864
+ if (title) {
2865
+ layers.push({
2866
+ type: "text",
2867
+ id: "feature-title",
2868
+ content: {
2869
+ text: title,
2870
+ style: {
2871
+ fontSize: titleFontSize,
2872
+ fontFamily: getThemeFont(context, "title"),
2873
+ fontWeight: "bold",
2874
+ color: theme.colors.text,
2875
+ textAlign,
2876
+ lineHeight: 1.2
2877
+ }
2878
+ },
2879
+ position: {
2880
+ x: textX,
2881
+ y: titleY,
2882
+ anchor: textAnchor,
2883
+ width: `${textColumnWidth}%`
2884
+ },
2885
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8 })
2886
+ });
2764
2887
  }
2765
- if (paragraph) {
2766
- const paragraphText = extractPlainText(paragraph.node).trim();
2767
- const bodyStat = matchNumberHighlight(paragraphText);
2768
- if (bodyStat) {
2769
- const description = bodyWithoutParagraphStat(
2770
- contents,
2771
- paragraph.index,
2772
- paragraphText,
2773
- bodyStat.index,
2774
- bodyStat.end
2775
- );
2776
- return {
2777
- stat: bodyStat.value,
2778
- description: description || headingText || bodyStat.value
2779
- };
2780
- }
2888
+ if (body) {
2889
+ layers.push({
2890
+ type: "text",
2891
+ id: "feature-body",
2892
+ content: {
2893
+ text: body,
2894
+ style: {
2895
+ fontSize: bodyFontSize2,
2896
+ fontFamily: getThemeFont(context, "body"),
2897
+ color: theme.colors.textMuted,
2898
+ textAlign,
2899
+ lineHeight: 1.5
2900
+ }
2901
+ },
2902
+ position: {
2903
+ x: textX,
2904
+ y: bodyY,
2905
+ anchor: textAnchor,
2906
+ width: `${textColumnWidth}%`
2907
+ },
2908
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8, delay: 0.2 })
2909
+ });
2781
2910
  }
2782
- return { stat: headingText, description: bodyText3 || headingText };
2911
+ return layers;
2783
2912
  }
2784
- function normalizeCoverageText(value) {
2785
- return value.replace(/\s+/g, " ").trim();
2913
+ function leftFeature(input, context) {
2914
+ return buildFeatureLayers(input, context, "left");
2786
2915
  }
2787
- function autoTemplatePreservesContent(templateName, contents) {
2788
- const nodes = contents ?? [];
2789
- switch (templateName) {
2790
- case "quote":
2791
- return nodes.length === 1 && nodes[0]?.type === "blockquote";
2792
- case "dataTable":
2793
- return nodes.length === 1 && nodes[0]?.type === "table";
2794
- case "list":
2795
- return nodes.length === 1 && nodes[0]?.type === "list";
2796
- case "diagram":
2797
- case "timeline":
2798
- return nodes.length === 1 && nodes[0]?.type === "code";
2799
- case "tree":
2800
- return nodes.length === 1 && (nodes[0]?.type === "code" || nodes[0]?.type === "list");
2801
- case "photoGrid": {
2802
- const images = extractImages(nodes);
2803
- if (images.length < 2 || images.length > 4) return false;
2804
- const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
2805
- const representedText = normalizeCoverageText(
2806
- images.map((image) => image.alt).filter(Boolean).join(" ")
2807
- );
2808
- return bodyText3 === "" || bodyText3 === representedText;
2809
- }
2810
- case "videoWithCaption": {
2811
- const videos = extractEmbeddedVideos(nodes);
2812
- if (videos.length !== 1) return false;
2813
- const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
2814
- return bodyText3 === normalizeCoverageText(videos[0]?.alt ?? "");
2916
+ function rightFeature(input, context) {
2917
+ return buildFeatureLayers(input, context, "right");
2918
+ }
2919
+
2920
+ // src/doc/templates/mapBlock.ts
2921
+ function mapBlock(input, context) {
2922
+ const {
2923
+ center,
2924
+ zoom,
2925
+ mapStyle = "terrain",
2926
+ title,
2927
+ caption,
2928
+ markers,
2929
+ ambientMotion,
2930
+ staticSrc
2931
+ } = input;
2932
+ const { theme, layout } = context;
2933
+ const titleFontSize = themedFontSize(64, context, true);
2934
+ const captionFontSize = themedFontSize(32, context, false);
2935
+ const mapAnimation = mapAmbientMotion(ambientMotion);
2936
+ const layers = [
2937
+ // Map background
2938
+ {
2939
+ type: "map",
2940
+ id: "map-bg",
2941
+ content: {
2942
+ center,
2943
+ zoom,
2944
+ style: mapStyle,
2945
+ markers,
2946
+ showAttribution: true,
2947
+ staticSrc
2948
+ // Use pre-rendered image if available
2949
+ },
2950
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
2951
+ animation: mapAnimation
2815
2952
  }
2816
- case "leftFeature":
2817
- case "rightFeature":
2818
- case "statHighlight":
2819
- return true;
2820
- default:
2821
- return false;
2953
+ ];
2954
+ const bg = theme.colors.background;
2955
+ if (title) {
2956
+ layers.push({
2957
+ type: "shape",
2958
+ id: "title-overlay",
2959
+ content: {
2960
+ shape: "rect",
2961
+ fill: `linear-gradient(180deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.6)} 70%, ${withAlpha(bg, 0)} 100%)`
2962
+ },
2963
+ position: { x: 0, y: 0, width: "100%", height: "20%" }
2964
+ });
2965
+ layers.push({
2966
+ type: "text",
2967
+ id: "title",
2968
+ content: {
2969
+ text: title,
2970
+ style: {
2971
+ fontSize: titleFontSize,
2972
+ fontFamily: getThemeFont(context, "title"),
2973
+ fontWeight: "bold",
2974
+ color: theme.colors.text,
2975
+ textAlign: "center",
2976
+ shadow: shouldUseShadow(context)
2977
+ }
2978
+ },
2979
+ position: {
2980
+ x: "50%",
2981
+ y: "9%",
2982
+ anchor: "center",
2983
+ width: layout.maxTextWidth
2984
+ },
2985
+ animation: { type: "fadeIn", duration: 1, delay: 0.3 }
2986
+ });
2987
+ }
2988
+ if (caption) {
2989
+ layers.push({
2990
+ type: "shape",
2991
+ id: "caption-overlay",
2992
+ content: {
2993
+ shape: "rect",
2994
+ fill: `linear-gradient(0deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.6)} 70%, ${withAlpha(bg, 0)} 100%)`
2995
+ },
2996
+ position: { x: 0, y: "80%", width: "100%", height: "20%" }
2997
+ });
2998
+ layers.push({
2999
+ type: "text",
3000
+ id: "caption",
3001
+ content: {
3002
+ text: caption,
3003
+ style: {
3004
+ fontSize: captionFontSize,
3005
+ fontFamily: getThemeFont(context, "body"),
3006
+ color: theme.colors.text,
3007
+ textAlign: "center",
3008
+ shadow: shouldUseShadow(context)
3009
+ }
3010
+ },
3011
+ position: {
3012
+ x: "50%",
3013
+ y: layout.captionY,
3014
+ anchor: "center",
3015
+ width: layout.maxTextWidth
3016
+ },
3017
+ animation: { type: "fadeIn", duration: 1, delay: 0.5 }
3018
+ });
2822
3019
  }
3020
+ return layers;
2823
3021
  }
2824
- function deriveTemplateInputs(templateName, headingText, contents, options = {}) {
2825
- const placeholders = options.placeholders === true;
2826
- const bodyText3 = extractBodyPlainText(contents);
2827
- switch (templateName) {
2828
- case "statHighlight": {
2829
- const inputs = deriveStatHighlightInputs(headingText, contents, bodyText3);
2830
- if (options.preserveSourceHeading && headingText && inputs.stat !== headingText && inputs.description !== headingText) {
2831
- return {
2832
- ...inputs,
2833
- description: headingText,
2834
- ...inputs.description ? { detail: inputs.description } : {}
2835
- };
2836
- }
2837
- return inputs;
2838
- }
2839
- case "quote": {
2840
- const quote = extractBlockquoteText(contents) || bodyText3;
2841
- if (quote) return { quote, ...headingText ? { title: headingText } : {} };
2842
- return { quote: headingText };
2843
- }
2844
- case "fullBleedQuote":
2845
- case "pullQuote": {
2846
- const text = extractBlockquoteText(contents) || bodyText3 || headingText;
2847
- return { text };
2848
- }
2849
- case "factCard":
2850
- return { fact: headingText, explanation: bodyText3 || headingText };
2851
- case "comparisonBar":
2852
- return placeholders ? { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 } : null;
2853
- case "list": {
2854
- const items = extractListItems(contents);
2855
- if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
2856
- return placeholders ? { items: ["Item 1", "Item 2", "Item 3"] } : null;
2857
- }
2858
- case "definitionCard":
2859
- return { term: headingText, definition: bodyText3 || headingText };
2860
- case "dateEvent":
2861
- return { date: headingText, description: bodyText3 || headingText };
2862
- case "dataTable": {
2863
- const tableData = extractTableFromContents(contents);
2864
- if (tableData) return { ...tableData, ...headingText ? { title: headingText } : {} };
2865
- return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
2866
- }
2867
- case "barChart":
2868
- case "columnChart":
2869
- case "pieChart":
2870
- case "donutChart":
2871
- case "lineChart":
2872
- case "areaChart":
2873
- case "scatterChart": {
2874
- const tableData = extractTableFromContents(contents);
2875
- if (tableData) {
2876
- return {
2877
- headers: tableData.headers,
2878
- rows: tableData.rows,
2879
- ...headingText ? { title: headingText } : {}
2880
- };
2881
- }
2882
- return placeholders && !bodyText3.trim() ? {
2883
- headers: ["Quarter", "Revenue", "Costs"],
2884
- rows: [
2885
- ["Q1", "40", "28"],
2886
- ["Q2", "55", "31"],
2887
- ["Q3", "48", "30"],
2888
- ["Q4", "62", "35"]
2889
- ],
2890
- ...headingText ? { title: headingText } : {}
2891
- } : null;
2892
- }
2893
- case "imageWithCaption": {
2894
- const img = extractFirstImage(contents);
2895
- if (!img) return placeholders ? { caption: headingText } : null;
2896
- return { imageSrc: img.src, imageAlt: img.alt || headingText, caption: headingText };
2897
- }
2898
- case "bigText": {
2899
- const img = extractFirstImage(contents);
2900
- return {
2901
- title: headingText,
2902
- ...img ? { imageSrc: img.src, imageAlt: img.alt || headingText } : {}
2903
- };
2904
- }
2905
- case "photoGrid": {
2906
- const images = extractImages(contents, 4);
2907
- if (images.length < 2) return placeholders ? {} : null;
2908
- return {
2909
- images: images.map((i) => ({ src: i.src, alt: i.alt })),
2910
- caption: headingText
2911
- };
2912
- }
2913
- case "videoWithCaption": {
2914
- const video = extractFirstEmbeddedVideo(contents);
2915
- if (!video) return placeholders ? { caption: headingText } : null;
2916
- return {
2917
- videoSrc: video.src,
2918
- posterSrc: video.posterSrc,
2919
- videoAlt: video.alt || headingText,
2920
- caption: headingText
2921
- };
2922
- }
2923
- case "leftFeature":
2924
- case "rightFeature": {
2925
- const img = extractFirstImage(contents);
2926
- if (!img && !placeholders) return null;
2927
- return {
2928
- imageSrc: img?.src ?? "",
2929
- imageAlt: img?.alt || headingText,
2930
- imageWidth: img?.width,
2931
- imageHeight: img?.height,
2932
- title: headingText,
2933
- body: bodyText3
2934
- };
2935
- }
2936
- case "diagram": {
2937
- const fences = (contents ?? []).filter((n) => n.type === "code");
2938
- const fence = fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : void 0;
2939
- const detection = fence ? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) }) : void 0;
2940
- if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
2941
- const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
2942
- return { nodes, edges, ...headingText ? { title: headingText } : {} };
2943
- }
2944
- case "timeline": {
2945
- const fences = (contents ?? []).filter((n) => n.type === "code");
2946
- const fence = fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang) ? fences[0] : void 0;
2947
- const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : void 0;
2948
- if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
2949
- const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
2950
- return { tracks, links, ...headingText ? { title: headingText } : {} };
2951
- }
2952
- case "tree": {
2953
- const fences = (contents ?? []).filter((n) => n.type === "code");
2954
- const fence = fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : void 0;
2955
- if (fence) {
2956
- const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
2957
- if (detection.isTree && detection.tree) {
2958
- const { items } = treeToTemplateData(detection.tree);
2959
- return { items, ...headingText ? { title: headingText } : {} };
3022
+
3023
+ // src/doc/templates/fullBleedQuote.ts
3024
+ function fullBleedQuote(input, context) {
3025
+ const { text, colorScheme = "blue" } = input;
3026
+ const colors = resolveColorScheme(context, colorScheme);
3027
+ const textFontSize = themedFontSize(120, context, true);
3028
+ return [
3029
+ // Background — radial vignette built from the color scheme's own
3030
+ // surface, so the card takes on the scheme (and the theme behind it)
3031
+ // instead of vignetting every theme into hard black.
3032
+ {
3033
+ type: "shape",
3034
+ id: "bg",
3035
+ content: {
3036
+ shape: "rect",
3037
+ fill: `radial-gradient(ellipse at 50% 50%, ${colors.bg} 0%, ${oklchDarken(colors.bg, 0.12)} 100%)`
3038
+ },
3039
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
3040
+ },
3041
+ // The text — massive, centered
3042
+ {
3043
+ type: "text",
3044
+ id: "impact-text",
3045
+ content: {
3046
+ text,
3047
+ style: {
3048
+ fontSize: textFontSize,
3049
+ fontFamily: getThemeFont(context, "title"),
3050
+ fontWeight: "bold",
3051
+ color: colors.text,
3052
+ textAlign: "center",
3053
+ lineHeight: 1.2,
3054
+ shadow: shouldUseShadow(context)
2960
3055
  }
2961
- }
2962
- const list = findFirstList(contents);
2963
- if (list) {
2964
- const { items } = treeToTemplateData(treeFromMarkdownList(list));
2965
- if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
2966
- }
2967
- return placeholders ? {} : null;
3056
+ },
3057
+ position: {
3058
+ x: "50%",
3059
+ y: "50%",
3060
+ anchor: "center",
3061
+ width: "85%"
3062
+ },
3063
+ animation: getTemplateHint(context, "fullBleedQuote", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.8 } : { type: "fadeIn", duration: 1.5 }
2968
3064
  }
2969
- default:
2970
- return placeholders ? {} : null;
2971
- }
3065
+ ];
2972
3066
  }
2973
3067
 
2974
3068
  // src/doc/templates/listBlock.ts
@@ -7070,7 +7164,25 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
7070
7164
  // as unknown when supplied via a data fence's overriding params.
7071
7165
  { key: "rows", description: "Table rows (supply via a data fence)" },
7072
7166
  { key: "align", description: "Per-column alignment (supply via a data fence)" },
7073
- { key: "colorScheme", description: "Header color scheme", valueHint: COLOR_SCHEME_HINT }
7167
+ { key: "colorScheme", description: "Header color scheme", valueHint: COLOR_SCHEME_HINT },
7168
+ // Spreadsheet provenance. These do not affect rendering — they record where
7169
+ // a table came from so `markdownDocToXlsx` can put it back at the right
7170
+ // address instead of piling every table at A1. Declared here for the same
7171
+ // reason `rows`/`align` are: so a round-tripped XLSX import doesn't light up
7172
+ // with `unknown-input` warnings.
7173
+ { key: "sheet", description: "Source worksheet name (XLSX round-trip)" },
7174
+ { key: "anchor", description: "Top-left cell of the region, e.g. 'B7'" },
7175
+ {
7176
+ key: "role",
7177
+ description: "What the table holds within its region",
7178
+ values: ["values", "formulas", "loose"]
7179
+ },
7180
+ {
7181
+ key: "headerRow",
7182
+ description: "Whether row 1 of the source region is a header row",
7183
+ coerce: "boolean"
7184
+ },
7185
+ { key: "titleAnchor", description: "Cell this heading text was absorbed from" }
7074
7186
  ],
7075
7187
  barChart: CHART_DESCRIPTORS([STACKED_DESCRIPTOR]),
7076
7188
  columnChart: CHART_DESCRIPTORS([STACKED_DESCRIPTOR]),
@@ -10657,8 +10769,10 @@ function buildStartBlock(markdownDoc, rootBlocks, fileName) {
10657
10769
  const headingTitle = headingBlock ? headingBlock.title ?? extractPlainText(headingBlock.sourceHeading) : void 0;
10658
10770
  const title = frontmatterTitle || headingTitle || fileNameCoverTitle(fileName);
10659
10771
  if (!title) return void 0;
10772
+ const frontmatterSubtitle = typeof markdownDoc.frontmatter?.subtitle === "string" ? markdownDoc.frontmatter.subtitle.trim() : void 0;
10660
10773
  const subtitleBlock = headingBlock ?? rootBlocks[0];
10661
- const subtitle = subtitleBlock?.contents?.[0]?.type === "paragraph" ? extractPlainText(subtitleBlock.contents[0]) : void 0;
10774
+ const inferredSubtitle = subtitleBlock?.contents?.[0]?.type === "paragraph" ? extractPlainText(subtitleBlock.contents[0]) : void 0;
10775
+ const subtitle = frontmatterSubtitle || inferredSubtitle;
10662
10776
  const firstImage = findFirstImage(markdownDoc);
10663
10777
  const config = {
10664
10778
  title,
@@ -11008,17 +11122,6 @@ export {
11008
11122
  sectionHeader,
11009
11123
  fitBigTextSize,
11010
11124
  bigText,
11011
- contentBlock,
11012
- statHighlight,
11013
- quoteBlock,
11014
- factCard,
11015
- twoColumn,
11016
- dateEvent,
11017
- imageWithCaption,
11018
- leftFeature,
11019
- rightFeature,
11020
- mapBlock,
11021
- fullBleedQuote,
11022
11125
  computeDiagramLayout,
11023
11126
  ASCII_CHAR_W,
11024
11127
  ASCII_CHAR_H,
@@ -11034,6 +11137,8 @@ export {
11034
11137
  findFirstList,
11035
11138
  extractBodyPlainText,
11036
11139
  extractListItems,
11140
+ renderMarkdownBlocksHtml,
11141
+ markdownBlockSeparatorLines,
11037
11142
  extractRichListItems,
11038
11143
  extractImages,
11039
11144
  extractFirstImage,
@@ -11043,6 +11148,17 @@ export {
11043
11148
  extractBlockquoteText,
11044
11149
  autoTemplatePreservesContent,
11045
11150
  deriveTemplateInputs,
11151
+ contentBlock,
11152
+ statHighlight,
11153
+ quoteBlock,
11154
+ factCard,
11155
+ twoColumn,
11156
+ dateEvent,
11157
+ imageWithCaption,
11158
+ leftFeature,
11159
+ rightFeature,
11160
+ mapBlock,
11161
+ fullBleedQuote,
11046
11162
  listBlock,
11047
11163
  photoGrid,
11048
11164
  definitionCard,