@bendyline/squisq 2.7.1 → 2.9.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.
@@ -1066,1909 +1066,1999 @@ 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
- )
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));
1367
+ }
1368
+
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: [] };
1419
+ }
1420
+ function findFirstList(contents) {
1421
+ return contents?.find((n) => n.type === "list");
1422
+ }
1423
+
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 "inlineCode":
1448
+ return `<code>${escapeInlineHtml(node.value)}</code>`;
1449
+ case "link": {
1450
+ const label = renderInlineHtml(node.children);
1451
+ const href = sanitizeUrl(node.url, "link");
1452
+ if (!href) return label;
1453
+ const title = node.title ? ` title="${escapeInlineHtml(node.title)}"` : "";
1454
+ return `<a href="${escapeInlineHtml(href)}"${title}>${label}</a>`;
1455
+ }
1456
+ case "break":
1457
+ return "<br>";
1458
+ case "image":
1459
+ return escapeInlineHtml(node.alt ?? "");
1460
+ case "inlineMath":
1461
+ return escapeInlineHtml(`$${node.value}$`);
1462
+ case "footnoteReference":
1463
+ return escapeInlineHtml(`[^${node.label ?? node.identifier}]`);
1464
+ case "linkReference":
1465
+ case "textDirective":
1466
+ return renderInlineHtml(node.children);
1467
+ case "imageReference":
1468
+ return escapeInlineHtml(node.alt ?? "");
1469
+ case "mention":
1470
+ return escapeInlineHtml(`@${node.displayName}`);
1471
+ case "inlineIcon":
1472
+ return `<i class="fa-${node.family} fa-${node.name}" aria-hidden="true"></i>`;
1473
+ case "htmlInline":
1474
+ return escapeInlineHtml(node.rawHtml);
1475
+ }
1476
+ }).join("");
1477
+ }
1478
+ function renderMarkdownBlocksHtml(nodes) {
1479
+ const renderBlocks = (blocks) => blocks.map((node, index) => {
1480
+ const previous = blocks[index - 1];
1481
+ const extraBlankLines = previous ? Math.max(0, markdownBlockSeparatorLines(previous, node) - 2) : 0;
1482
+ const sourceGap = '<div data-squisq-source-gap aria-hidden="true"><br></div>'.repeat(
1483
+ extraBlankLines
1351
1484
  );
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)
1485
+ return `${sourceGap}${renderBlock(node)}`;
1486
+ }).join("");
1487
+ const renderList = (node) => {
1488
+ const tag = node.ordered ? "ol" : "ul";
1489
+ const start = node.ordered && node.start != null ? ` start="${node.start}"` : "";
1490
+ const items = node.children.map((item) => {
1491
+ const taskMarker = item.checked == null ? "" : `<span>${item.checked ? "[x]" : "[ ]"} </span>`;
1492
+ return `<li>${taskMarker}${renderBlocks(item.children)}</li>`;
1493
+ }).join("");
1494
+ return `<${tag}${start}>${items}</${tag}>`;
1495
+ };
1496
+ const renderBlock = (node) => {
1497
+ switch (node.type) {
1498
+ case "paragraph":
1499
+ return `<p>${renderInlineHtml(node.children)}</p>`;
1500
+ case "heading":
1501
+ return `<h${node.depth}>${renderInlineHtml(node.children)}</h${node.depth}>`;
1502
+ case "blockquote":
1503
+ return `<blockquote>${renderBlocks(node.children)}</blockquote>`;
1504
+ case "list":
1505
+ return renderList(node);
1506
+ case "code":
1507
+ return `<pre><code>${escapeInlineHtml(node.value)}</code></pre>`;
1508
+ case "thematicBreak":
1509
+ return "<hr>";
1510
+ case "table": {
1511
+ const [head, ...body] = node.children;
1512
+ const row = (cells, header) => `<tr>${cells.map((cell) => {
1513
+ const tag = header ? "th" : "td";
1514
+ return `<${tag}>${renderInlineHtml(cell.children)}</${tag}>`;
1515
+ }).join("")}</tr>`;
1516
+ const thead = head ? `<thead>${row(head.children, true)}</thead>` : "";
1517
+ const tbody = body.length ? `<tbody>${body.map((item) => row(item.children, false)).join("")}</tbody>` : "";
1518
+ return `<table>${thead}${tbody}</table>`;
1378
1519
  }
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)
1520
+ case "math":
1521
+ return `<pre><code>${escapeInlineHtml(node.value)}</code></pre>`;
1522
+ case "definition":
1523
+ return "";
1524
+ default: {
1525
+ const text = extractPlainText(node).trim();
1526
+ return text ? `<p>${escapeInlineHtml(text)}</p>` : "";
1400
1527
  }
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
- });
1528
+ }
1529
+ };
1530
+ return renderBlocks(nodes);
1531
+ }
1532
+ function markdownBlockSeparatorLines(previous, next) {
1533
+ const previousEndLine = previous.position?.end.line;
1534
+ const nextStartLine = next.position?.start.line;
1535
+ if (previousEndLine == null || nextStartLine == null || nextStartLine <= previousEndLine) {
1536
+ return 2;
1431
1537
  }
1432
- return layers;
1538
+ return Math.max(2, nextStartLine - previousEndLine);
1433
1539
  }
1434
-
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%" }
1540
+ function renderListItemHtml(markdown) {
1541
+ return markdown.map(
1542
+ (node) => node.type === "paragraph" || node.type === "heading" ? renderInlineHtml(node.children) : escapeInlineHtml(extractPlainText(node))
1543
+ ).join("<br>");
1544
+ }
1545
+ function extractRichListItems(contents) {
1546
+ if (!contents) return [];
1547
+ const items = [];
1548
+ for (const node of contents) {
1549
+ if (node.type !== "list") continue;
1550
+ for (const item of node.children) {
1551
+ const text = extractPlainText(item).trim();
1552
+ if (!text) continue;
1553
+ const html = renderListItemHtml(item.children);
1554
+ const plainHtml = escapeInlineHtml(text).replace(/\r?\n/g, "<br>");
1555
+ items.push({
1556
+ text,
1557
+ markdown: item.children,
1558
+ ...html && html !== plainHtml ? { html } : {}
1559
+ });
1460
1560
  }
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
1561
  }
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"
1562
+ return items;
1563
+ }
1564
+ function parseDim(raw) {
1565
+ if (raw === void 0) return void 0;
1566
+ const n = parseFloat(raw);
1567
+ return Number.isFinite(n) && n > 0 ? n : void 0;
1568
+ }
1569
+ function extractImages(contents, limit = Infinity) {
1570
+ if (!contents || contents.length === 0) return [];
1571
+ const found = [];
1572
+ function fromHtml(nodes) {
1573
+ for (const node of nodes) {
1574
+ if (found.length >= limit) return;
1575
+ if (!node || typeof node !== "object") continue;
1576
+ const n = node;
1577
+ if (n.type === "htmlElement" && n.tagName === "img") {
1578
+ const attrs = n.attributes;
1579
+ if (attrs && typeof attrs.src === "string" && attrs.src) {
1580
+ found.push({
1581
+ src: attrs.src,
1582
+ alt: typeof attrs.alt === "string" ? attrs.alt : "",
1583
+ width: parseDim(attrs.width),
1584
+ height: parseDim(attrs.height)
1585
+ });
1498
1586
  }
1499
- },
1500
- position: { x: "50%", y: headerY, anchor: "center" },
1501
- animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8 })
1502
- });
1587
+ }
1588
+ if (Array.isArray(n.children)) fromHtml(n.children);
1589
+ }
1503
1590
  }
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}%`;
1591
+ function walk(node) {
1592
+ if (found.length >= limit) return;
1593
+ if (!node || typeof node !== "object") return;
1594
+ const n = node;
1595
+ if (n.type === "image" && typeof n.url === "string" && n.url) {
1596
+ found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
1597
+ return;
1598
+ }
1599
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
1600
+ fromHtml(n.htmlChildren);
1601
+ }
1602
+ if (Array.isArray(n.children)) {
1603
+ for (const child of n.children) walk(child);
1604
+ }
1519
1605
  }
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
- });
1606
+ for (const node of contents) {
1607
+ if (found.length >= limit) break;
1608
+ walk(node);
1564
1609
  }
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"
1610
+ return found;
1611
+ }
1612
+ function extractFirstImage(contents) {
1613
+ return extractImages(contents, 1)[0] ?? null;
1614
+ }
1615
+ function extractEmbeddedVideos(contents, limit = Infinity) {
1616
+ if (!contents || contents.length === 0) return [];
1617
+ const found = [];
1618
+ const seen = /* @__PURE__ */ new Set();
1619
+ const add = (video) => {
1620
+ if (!video.src || found.length >= limit || seen.has(video.src)) return;
1621
+ seen.add(video.src);
1622
+ found.push(video);
1623
+ };
1624
+ function nestedSource(children) {
1625
+ if (!Array.isArray(children)) return void 0;
1626
+ for (const child of children) {
1627
+ if (!child || typeof child !== "object") continue;
1628
+ const node = child;
1629
+ if (node.tagName === "source") {
1630
+ const attrs = node.attributes;
1631
+ if (typeof attrs?.src === "string" && attrs.src) return attrs.src;
1579
1632
  }
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
1633
+ const nested = nestedSource(node.children);
1634
+ if (nested) return nested;
1635
+ }
1636
+ return void 0;
1637
+ }
1638
+ function walk(node) {
1639
+ if (found.length >= limit || !node || typeof node !== "object") return;
1640
+ const n = node;
1641
+ if (n.type === "htmlElement" && n.tagName === "video") {
1642
+ const attrs = n.attributes;
1643
+ const src = attrs?.src || nestedSource(n.children);
1644
+ if (src) {
1645
+ const startAt = attrs?.["data-squisq-video-start-at"] ? parseTimeSeconds(attrs["data-squisq-video-start-at"]) : null;
1646
+ const clipStart = attrs?.["data-squisq-video-clip-start"] ? parseTimeSeconds(attrs["data-squisq-video-clip-start"]) : null;
1647
+ const clipEnd = attrs?.["data-squisq-video-clip-end"] ? parseTimeSeconds(attrs["data-squisq-video-clip-end"]) : null;
1648
+ add({
1649
+ src,
1650
+ ...attrs?.poster ? { posterSrc: attrs.poster } : {},
1651
+ alt: attrs?.["aria-label"] || attrs?.title || attrs?.alt || "",
1652
+ ...startAt != null ? { startAt } : {},
1653
+ ...clipStart != null ? { clipStart } : {},
1654
+ ...clipEnd != null ? { clipEnd } : {}
1655
+ });
1600
1656
  }
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
- });
1657
+ } else if ((n.type === "link" || n.type === "image") && typeof n.url === "string" && VIDEO_FILE_RE.test(n.url)) {
1658
+ add({
1659
+ src: n.url,
1660
+ alt: typeof n.alt === "string" ? n.alt : extractPlainText(n).trim()
1661
+ });
1662
+ }
1663
+ if (Array.isArray(n.children)) n.children.forEach(walk);
1664
+ if (Array.isArray(n.htmlChildren)) n.htmlChildren.forEach(walk);
1632
1665
  }
1633
- return layers;
1666
+ contents.forEach(walk);
1667
+ return found;
1634
1668
  }
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%" }
1669
+ function extractFirstEmbeddedVideo(contents) {
1670
+ return extractEmbeddedVideos(contents, 1)[0] ?? null;
1671
+ }
1672
+ function extractTableFromContents(contents) {
1673
+ if (!contents) return null;
1674
+ for (const node of contents) {
1675
+ if (node.type === "table") {
1676
+ const table = node;
1677
+ const [headerRow, ...bodyRows] = table.children;
1678
+ if (!headerRow) return null;
1679
+ const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
1680
+ const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
1681
+ return { headers, rows, align: table.align };
1662
1682
  }
1663
- ];
1664
- if (accentImage) {
1665
- layers.push(
1666
- ...createAccentLayers(
1667
- accentImage,
1668
- input.id,
1669
- themedImageTreatment(context, input.imageTreatment)
1670
- )
1671
- );
1672
1683
  }
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
- });
1684
+ return null;
1685
+ }
1686
+ function extractBlockquoteText(contents) {
1687
+ if (!contents) return "";
1688
+ for (const node of contents) {
1689
+ if (node.type === "blockquote") return extractPlainText(node).trim();
1735
1690
  }
1736
- return layers;
1691
+ return "";
1737
1692
  }
1738
-
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
1693
+ function firstParagraph(contents) {
1694
+ if (!contents) return null;
1695
+ const index = contents.findIndex((node) => node.type === "paragraph");
1696
+ if (index < 0) return null;
1697
+ return {
1698
+ node: contents[index],
1699
+ index
1700
+ };
1701
+ }
1702
+ function leadingStrongStat(paragraph) {
1703
+ for (const child of paragraph.children) {
1704
+ if (child.type === "text" && child.value.trim() === "") continue;
1705
+ if (child.type !== "strong") return null;
1706
+ const text = extractPlainText(child).trim();
1707
+ const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
1708
+ return match?.[0].trim() || null;
1709
+ }
1710
+ return null;
1711
+ }
1712
+ function removeStatPhrase(text, start, end) {
1713
+ const before = text.slice(0, start).trim();
1714
+ let after = text.slice(end);
1715
+ if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, "");
1716
+ else after = after.trim();
1717
+ return [before, after].filter(Boolean).join(" ").trim();
1718
+ }
1719
+ function bodyWithoutParagraphStat(contents, paragraphIndex, paragraphText, start, end) {
1720
+ if (!contents) return "";
1721
+ return contents.map(
1722
+ (node, index) => index === paragraphIndex ? removeStatPhrase(paragraphText, start, end) : extractPlainText(node).trim()
1723
+ ).filter(Boolean).join("\n").trim();
1724
+ }
1725
+ function deriveStatHighlightInputs(headingText, contents, bodyText3) {
1726
+ const paragraph = firstParagraph(contents);
1727
+ if (paragraph) {
1728
+ const paragraphText = extractPlainText(paragraph.node).trim();
1729
+ const strongStat = leadingStrongStat(paragraph.node);
1730
+ if (strongStat) {
1731
+ const start = paragraphText.indexOf(strongStat);
1732
+ const description = bodyWithoutParagraphStat(
1733
+ contents,
1734
+ paragraph.index,
1735
+ paragraphText,
1736
+ start,
1737
+ start + strongStat.length
1738
+ );
1739
+ return {
1740
+ stat: strongStat,
1741
+ description: description || headingText || strongStat
1742
+ };
1776
1743
  }
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
- });
1833
- }
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%"
1851
- }
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
1744
  }
1877
- return layers;
1878
- }
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";
1745
+ const trimmedHeading = headingText.trim();
1746
+ const headingStat = matchNumberHighlight(trimmedHeading);
1747
+ if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
1748
+ return { stat: headingText, description: bodyText3 || headingText };
1913
1749
  }
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%" }
1750
+ if (paragraph) {
1751
+ const paragraphText = extractPlainText(paragraph.node).trim();
1752
+ const bodyStat = matchNumberHighlight(paragraphText);
1753
+ if (bodyStat) {
1754
+ const description = bodyWithoutParagraphStat(
1755
+ contents,
1756
+ paragraph.index,
1757
+ paragraphText,
1758
+ bodyStat.index,
1759
+ bodyStat.end
1760
+ );
1761
+ return {
1762
+ stat: bodyStat.value,
1763
+ description: description || headingText || bodyStat.value
1764
+ };
1927
1765
  }
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
- }
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
1955
- }
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
- });
1965
- }
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
- });
1988
1766
  }
1989
- return layers;
1767
+ return { stat: headingText, description: bodyText3 || headingText };
1990
1768
  }
1991
- function leftFeature(input, context) {
1992
- return buildFeatureLayers(input, context, "left");
1769
+ function normalizeCoverageText(value) {
1770
+ return value.replace(/\s+/g, " ").trim();
1993
1771
  }
1994
- function rightFeature(input, context) {
1995
- return buildFeatureLayers(input, context, "right");
1772
+ function autoTemplatePreservesContent(templateName, contents) {
1773
+ const nodes = contents ?? [];
1774
+ switch (templateName) {
1775
+ case "quote":
1776
+ return nodes.length === 1 && nodes[0]?.type === "blockquote";
1777
+ case "dataTable":
1778
+ return nodes.length === 1 && nodes[0]?.type === "table";
1779
+ case "list":
1780
+ return nodes.length === 1 && nodes[0]?.type === "list";
1781
+ case "diagram":
1782
+ case "timeline":
1783
+ return nodes.length === 1 && nodes[0]?.type === "code";
1784
+ case "tree":
1785
+ return nodes.length === 1 && (nodes[0]?.type === "code" || nodes[0]?.type === "list");
1786
+ case "photoGrid": {
1787
+ const images = extractImages(nodes);
1788
+ if (images.length < 2 || images.length > 4) return false;
1789
+ const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
1790
+ const representedText = normalizeCoverageText(
1791
+ images.map((image) => image.alt).filter(Boolean).join(" ")
1792
+ );
1793
+ return bodyText3 === "" || bodyText3 === representedText;
1794
+ }
1795
+ case "videoWithCaption": {
1796
+ const videos = extractEmbeddedVideos(nodes);
1797
+ if (videos.length !== 1) return false;
1798
+ const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
1799
+ return bodyText3 === normalizeCoverageText(videos[0]?.alt ?? "");
1800
+ }
1801
+ case "leftFeature":
1802
+ case "rightFeature":
1803
+ case "statHighlight":
1804
+ return true;
1805
+ default:
1806
+ return false;
1807
+ }
1996
1808
  }
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
1809
+ function deriveTemplateInputs(templateName, headingText, contents, options = {}) {
1810
+ const placeholders = options.placeholders === true;
1811
+ const bodyText3 = extractBodyPlainText(contents);
1812
+ switch (templateName) {
1813
+ case "statHighlight": {
1814
+ const inputs = deriveStatHighlightInputs(headingText, contents, bodyText3);
1815
+ if (options.preserveSourceHeading && headingText && inputs.stat !== headingText && inputs.description !== headingText) {
1816
+ return {
1817
+ ...inputs,
1818
+ description: headingText,
1819
+ ...inputs.description ? { detail: inputs.description } : {}
1820
+ };
1821
+ }
1822
+ return inputs;
2030
1823
  }
2031
- ];
2032
- const bg = theme.colors.background;
1824
+ case "quote": {
1825
+ const quote = extractBlockquoteText(contents) || bodyText3;
1826
+ if (quote) return { quote, ...headingText ? { title: headingText } : {} };
1827
+ return { quote: headingText };
1828
+ }
1829
+ case "fullBleedQuote":
1830
+ case "pullQuote": {
1831
+ const text = extractBlockquoteText(contents) || bodyText3 || headingText;
1832
+ return { text };
1833
+ }
1834
+ case "factCard":
1835
+ return { fact: headingText, explanation: bodyText3 || headingText };
1836
+ case "comparisonBar":
1837
+ return placeholders ? { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 } : null;
1838
+ case "list": {
1839
+ const items = extractListItems(contents);
1840
+ if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
1841
+ return placeholders ? { items: ["Item 1", "Item 2", "Item 3"] } : null;
1842
+ }
1843
+ case "definitionCard":
1844
+ return { term: headingText, definition: bodyText3 || headingText };
1845
+ case "dateEvent":
1846
+ return { date: headingText, description: bodyText3 || headingText };
1847
+ case "dataTable": {
1848
+ const tableData = extractTableFromContents(contents);
1849
+ if (tableData) return { ...tableData, ...headingText ? { title: headingText } : {} };
1850
+ return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
1851
+ }
1852
+ case "barChart":
1853
+ case "columnChart":
1854
+ case "pieChart":
1855
+ case "donutChart":
1856
+ case "lineChart":
1857
+ case "areaChart":
1858
+ case "scatterChart": {
1859
+ const tableData = extractTableFromContents(contents);
1860
+ if (tableData) {
1861
+ return {
1862
+ headers: tableData.headers,
1863
+ rows: tableData.rows,
1864
+ ...headingText ? { title: headingText } : {}
1865
+ };
1866
+ }
1867
+ return placeholders && !bodyText3.trim() ? {
1868
+ headers: ["Quarter", "Revenue", "Costs"],
1869
+ rows: [
1870
+ ["Q1", "40", "28"],
1871
+ ["Q2", "55", "31"],
1872
+ ["Q3", "48", "30"],
1873
+ ["Q4", "62", "35"]
1874
+ ],
1875
+ ...headingText ? { title: headingText } : {}
1876
+ } : null;
1877
+ }
1878
+ case "imageWithCaption": {
1879
+ const img = extractFirstImage(contents);
1880
+ if (!img) return placeholders ? { caption: headingText } : null;
1881
+ return { imageSrc: img.src, imageAlt: img.alt || headingText, caption: headingText };
1882
+ }
1883
+ case "bigText": {
1884
+ const img = extractFirstImage(contents);
1885
+ return {
1886
+ title: headingText,
1887
+ ...img ? { imageSrc: img.src, imageAlt: img.alt || headingText } : {}
1888
+ };
1889
+ }
1890
+ case "photoGrid": {
1891
+ const images = extractImages(contents, 4);
1892
+ if (images.length < 2) return placeholders ? {} : null;
1893
+ return {
1894
+ images: images.map((i) => ({ src: i.src, alt: i.alt })),
1895
+ caption: headingText
1896
+ };
1897
+ }
1898
+ case "videoWithCaption": {
1899
+ const video = extractFirstEmbeddedVideo(contents);
1900
+ if (!video) return placeholders ? { caption: headingText } : null;
1901
+ return {
1902
+ videoSrc: video.src,
1903
+ posterSrc: video.posterSrc,
1904
+ videoAlt: video.alt || headingText,
1905
+ caption: headingText
1906
+ };
1907
+ }
1908
+ case "leftFeature":
1909
+ case "rightFeature": {
1910
+ const img = extractFirstImage(contents);
1911
+ if (!img && !placeholders) return null;
1912
+ return {
1913
+ imageSrc: img?.src ?? "",
1914
+ imageAlt: img?.alt || headingText,
1915
+ imageWidth: img?.width,
1916
+ imageHeight: img?.height,
1917
+ title: headingText,
1918
+ body: bodyText3
1919
+ };
1920
+ }
1921
+ case "diagram": {
1922
+ const fences = (contents ?? []).filter((n) => n.type === "code");
1923
+ const fence = fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : void 0;
1924
+ const detection = fence ? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) }) : void 0;
1925
+ if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
1926
+ const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
1927
+ return { nodes, edges, ...headingText ? { title: headingText } : {} };
1928
+ }
1929
+ case "timeline": {
1930
+ const fences = (contents ?? []).filter((n) => n.type === "code");
1931
+ const fence = fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang) ? fences[0] : void 0;
1932
+ const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : void 0;
1933
+ if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
1934
+ const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
1935
+ return { tracks, links, ...headingText ? { title: headingText } : {} };
1936
+ }
1937
+ case "tree": {
1938
+ const fences = (contents ?? []).filter((n) => n.type === "code");
1939
+ const fence = fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : void 0;
1940
+ if (fence) {
1941
+ const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
1942
+ if (detection.isTree && detection.tree) {
1943
+ const { items } = treeToTemplateData(detection.tree);
1944
+ return { items, ...headingText ? { title: headingText } : {} };
1945
+ }
1946
+ }
1947
+ const list = findFirstList(contents);
1948
+ if (list) {
1949
+ const { items } = treeToTemplateData(treeFromMarkdownList(list));
1950
+ if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
1951
+ }
1952
+ return placeholders ? {} : null;
1953
+ }
1954
+ default:
1955
+ return placeholders ? {} : null;
1956
+ }
1957
+ }
1958
+
1959
+ // src/doc/templates/contentBlock.ts
1960
+ function bodyNodes(context) {
1961
+ return (context.block?.contents ?? []).filter(
1962
+ // Mermaid source is already materialized as a visual layer. Repeating the
1963
+ // fence text as prose makes the loss-averse default noisy without
1964
+ // preserving any additional authored meaning.
1965
+ (node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")
1966
+ );
1967
+ }
1968
+ function plainListText(list, depth = 0) {
1969
+ const indent = " ".repeat(depth);
1970
+ return list.children.flatMap((item, index) => {
1971
+ const ownText = item.children.filter((node) => node.type !== "list").map((node) => extractPlainText(node).trim()).filter(Boolean).join(" ");
1972
+ const marker = list.ordered ? `${(list.start ?? 1) + index}.` : "\u2022";
1973
+ const lines = ownText ? [`${indent}${marker} ${ownText}`] : [];
1974
+ for (const child of item.children) {
1975
+ if (child.type === "list") lines.push(plainListText(child, depth + 1));
1976
+ }
1977
+ return lines;
1978
+ }).join("\n");
1979
+ }
1980
+ function completeBodyText(nodes) {
1981
+ return nodes.map((node, index) => {
1982
+ const text = node.type === "list" ? plainListText(node) : extractPlainText(node);
1983
+ if (index === 0) return text;
1984
+ const separator = "\n".repeat(markdownBlockSeparatorLines(nodes[index - 1], node));
1985
+ return `${separator}${text}`;
1986
+ }).join("").trim();
1987
+ }
1988
+ function bodyFontSize(body, context) {
1989
+ const base = body.length > 1800 ? 18 : body.length > 1100 ? 21 : body.length > 650 ? 24 : 28;
1990
+ return themedFontSize(base, context, false);
1991
+ }
1992
+ function contentBlock(input, context) {
1993
+ const { theme } = context;
1994
+ const title = input.title ?? context.block?.title ?? "";
1995
+ const markdown = bodyNodes(context);
1996
+ const body = completeBodyText(markdown);
1997
+ const bodyHtml = renderMarkdownBlocksHtml(markdown);
1998
+ const layers = [createBackgroundLayer("bg", theme.colors.background)];
2033
1999
  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
2000
  layers.push({
2044
2001
  type: "text",
2045
2002
  id: "title",
2046
2003
  content: {
2047
2004
  text: title,
2048
2005
  style: {
2049
- fontSize: titleFontSize,
2006
+ fontSize: themedFontSize(46, context, true),
2050
2007
  fontFamily: getThemeFont(context, "title"),
2051
2008
  fontWeight: "bold",
2052
2009
  color: theme.colors.text,
2053
- textAlign: "center",
2010
+ textAlign: "left",
2011
+ lineHeight: 1.15,
2054
2012
  shadow: shouldUseShadow(context)
2055
2013
  }
2056
2014
  },
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 }
2015
+ position: { x: "8%", y: "9%", width: "84%", height: "15%", anchor: "top-left" }
2064
2016
  });
2065
2017
  }
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
- });
2018
+ if (body) {
2076
2019
  layers.push({
2077
2020
  type: "text",
2078
- id: "caption",
2021
+ id: "body",
2079
2022
  content: {
2080
- text: caption,
2023
+ text: body,
2024
+ ...bodyHtml ? { html: bodyHtml } : {},
2081
2025
  style: {
2082
- fontSize: captionFontSize,
2026
+ fontSize: bodyFontSize(body, context),
2083
2027
  fontFamily: getThemeFont(context, "body"),
2084
2028
  color: theme.colors.text,
2085
- textAlign: "center",
2029
+ textAlign: "left",
2030
+ lineHeight: 1.35,
2086
2031
  shadow: shouldUseShadow(context)
2087
2032
  }
2088
2033
  },
2089
2034
  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 }
2035
+ x: "8%",
2036
+ y: title ? "28%" : "10%",
2037
+ width: "84%",
2038
+ height: title ? "64%" : "82%",
2039
+ anchor: "top-left"
2040
+ }
2096
2041
  });
2097
2042
  }
2098
2043
  return layers;
2099
2044
  }
2100
2045
 
2101
- // src/doc/templates/fullBleedQuote.ts
2102
- function fullBleedQuote(input, context) {
2103
- const { text, colorScheme = "blue" } = input;
2046
+ // src/doc/templates/statHighlight.ts
2047
+ function statHighlight(input, context) {
2048
+ const { stat, description, detail, colorScheme = "blue", accentImage } = input;
2049
+ const { theme } = context;
2104
2050
  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%" }
2051
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2052
+ const statFontSize = themedFontSize(148, context, true);
2053
+ const descFontSize = themedFontSize(32, context, false);
2054
+ const detailFontSize = themedFontSize(26, context, false);
2055
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 180))];
2056
+ if (accentImage) {
2057
+ layers.push(
2058
+ ...createAccentLayers(
2059
+ accentImage,
2060
+ input.id,
2061
+ themedImageTreatment(context, input.imageTreatment)
2062
+ )
2063
+ );
2064
+ }
2065
+ layers.push({
2066
+ type: "text",
2067
+ id: "stat",
2068
+ content: {
2069
+ text: stat,
2070
+ style: {
2071
+ fontSize: statFontSize,
2072
+ fontFamily: getThemeFont(context, "title"),
2073
+ fontWeight: "bold",
2074
+ color: colors.text,
2075
+ shadow: shouldUseShadow(context)
2076
+ }
2118
2077
  },
2119
- // The text — massive, centered
2120
- {
2078
+ position: {
2079
+ x: accentLayout.textCenterX,
2080
+ y: adjustY("36%", accentLayout),
2081
+ anchor: "center"
2082
+ },
2083
+ animation: getTemplateHint(context, "statHighlight", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.4 } : { type: "zoomIn", duration: 0.6 }
2084
+ });
2085
+ layers.push({
2086
+ type: "text",
2087
+ id: "description",
2088
+ content: {
2089
+ text: description,
2090
+ style: {
2091
+ fontSize: descFontSize,
2092
+ fontFamily: getThemeFont(context, "body"),
2093
+ color: theme.colors.textMuted,
2094
+ textAlign: "center",
2095
+ lineHeight: 1.5,
2096
+ shadow: shouldUseShadow(context)
2097
+ }
2098
+ },
2099
+ position: {
2100
+ x: accentLayout.textCenterX,
2101
+ y: adjustY("54%", accentLayout),
2102
+ width: accentLayout.textWidth,
2103
+ anchor: "center"
2104
+ },
2105
+ animation: { type: "fadeIn", duration: 1, delay: 0.3 }
2106
+ });
2107
+ if (detail) {
2108
+ layers.push({
2121
2109
  type: "text",
2122
- id: "impact-text",
2110
+ id: "detail",
2123
2111
  content: {
2124
- text,
2112
+ text: detail,
2125
2113
  style: {
2126
- fontSize: textFontSize,
2127
- fontFamily: getThemeFont(context, "title"),
2128
- fontWeight: "bold",
2114
+ fontSize: detailFontSize,
2115
+ fontFamily: getThemeFont(context, "body"),
2116
+ // Scheme *text* rather than *accent*: accents in the built-in
2117
+ // schemes are tuned as chart/fill colors and drop below legible
2118
+ // contrast as body copy on light surfaces.
2129
2119
  color: colors.text,
2130
2120
  textAlign: "center",
2131
- lineHeight: 1.2,
2132
2121
  shadow: shouldUseShadow(context)
2133
2122
  }
2134
2123
  },
2135
2124
  position: {
2136
- x: "50%",
2137
- y: "50%",
2138
- anchor: "center",
2139
- width: "85%"
2125
+ x: accentLayout.textCenterX,
2126
+ y: adjustY("66%", accentLayout),
2127
+ width: accentLayout.textWidth,
2128
+ anchor: "center"
2140
2129
  },
2141
- animation: getTemplateHint(context, "fullBleedQuote", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.8 } : { type: "fadeIn", duration: 1.5 }
2142
- }
2143
- ];
2130
+ animation: { type: "fadeIn", duration: 1, delay: 1 }
2131
+ });
2132
+ }
2133
+ return layers;
2144
2134
  }
2145
2135
 
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);
2168
- }
2136
+ // src/doc/templates/quoteBlock.ts
2137
+ function quoteBlock(input, context) {
2138
+ const { title, quote, attribution, accentImage } = input;
2139
+ const { theme, viewport } = context;
2140
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2141
+ const quoteFontSize = themedFontSize(48, context, true);
2142
+ const titleFontSize = themedFontSize(34, context, true);
2143
+ const attrFontSize = themedFontSize(26, context, false);
2144
+ const quoteLineHeight = Math.max(theme.typography.titleLineHeight ?? 1.4, 1.25);
2145
+ const decorativeQuoteFontSize = themedFontSize(280, context, true);
2146
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 160))];
2147
+ if (accentImage) {
2148
+ layers.push(
2149
+ ...createAccentLayers(
2150
+ accentImage,
2151
+ input.id,
2152
+ themedImageTreatment(context, input.imageTreatment)
2153
+ )
2154
+ );
2169
2155
  }
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;
2180
- }
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;
2156
+ if (title) {
2157
+ layers.push({
2158
+ type: "text",
2159
+ id: "quote-title",
2160
+ content: {
2161
+ text: title,
2162
+ style: {
2163
+ fontSize: titleFontSize,
2164
+ fontFamily: getThemeFont(context, "body"),
2165
+ fontWeight: "bold",
2166
+ color: theme.colors.textMuted,
2167
+ textAlign: "center",
2168
+ shadow: shouldUseShadow(context)
2169
+ }
2170
+ },
2171
+ position: {
2172
+ x: accentLayout.textCenterX,
2173
+ y: adjustY("14%", accentLayout),
2174
+ width: accentLayout.textWidth,
2175
+ anchor: "center"
2176
+ },
2177
+ animation: { type: "fadeIn", duration: 0.8 }
2186
2178
  });
2187
2179
  }
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;
2180
+ layers.push({
2181
+ type: "text",
2182
+ id: "deco-quote",
2183
+ content: {
2184
+ text: "\u201C",
2185
+ style: {
2186
+ fontSize: decorativeQuoteFontSize,
2187
+ fontFamily: getThemeFont(context, "title"),
2188
+ color: withAlpha(theme.colors.text, 0.09),
2189
+ textAlign: "center"
2205
2190
  }
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
- });
2191
+ },
2192
+ position: {
2193
+ x: accentLayout.textCenterX,
2194
+ y: adjustY("26%", accentLayout),
2195
+ anchor: "center"
2215
2196
  }
2216
- }
2217
- return { nodes, edges, warnings };
2218
- }
2219
- function makeEdgeId(source, conn) {
2220
- return conn.type ? `${source}->${conn.target}:${conn.type}` : `${source}->${conn.target}`;
2221
- }
2222
-
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 };
2230
- }
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
- };
2236
- }
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 };
2256
- }
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
2197
  });
2293
- distributeOffsets(pairs, edges, nodes, "source");
2294
- distributeOffsets(pairs, edges, nodes, "target");
2295
- coalesceAlignedFanOutAnchors(pairs, edges, nodes);
2296
- return pairs;
2297
- }
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);
2198
+ const quoteYPct = attribution ? 45 : title ? 52 : 50;
2199
+ layers.push({
2200
+ type: "text",
2201
+ id: "quote",
2202
+ content: {
2203
+ text: quote,
2204
+ style: {
2205
+ fontSize: quoteFontSize,
2206
+ fontFamily: getThemeFont(context, "title"),
2207
+ color: theme.colors.text,
2208
+ textAlign: "center",
2209
+ lineHeight: quoteLineHeight,
2210
+ shadow: shouldUseShadow(context)
2211
+ }
2212
+ },
2213
+ position: {
2214
+ x: accentLayout.textCenterX,
2215
+ y: adjustY(`${quoteYPct}%`, accentLayout),
2216
+ anchor: "center",
2217
+ width: accentLayout.textWidth
2218
+ },
2219
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 2 })
2305
2220
  });
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
- }
2221
+ if (attribution) {
2222
+ const quoteWidthPx = parseFloat(accentLayout.textWidth) / 100 * viewport.width;
2223
+ const quoteHeightPx = estimateTextHeight(quote, quoteFontSize, quoteWidthPx, quoteLineHeight);
2224
+ const gapPx = quoteFontSize * 1.7;
2225
+ const attrYPct = Math.min(
2226
+ 82,
2227
+ quoteYPct + (quoteHeightPx / 2 + gapPx) / viewport.height * 100
2228
+ );
2229
+ layers.push({
2230
+ type: "text",
2231
+ id: "attribution",
2232
+ content: {
2233
+ text: `\u2014 ${attribution}`,
2234
+ style: {
2235
+ fontSize: attrFontSize,
2236
+ fontFamily: getThemeFont(context, "body"),
2237
+ color: theme.colors.textMuted,
2238
+ textAlign: "center",
2239
+ shadow: shouldUseShadow(context)
2240
+ }
2241
+ },
2242
+ position: {
2243
+ x: accentLayout.textCenterX,
2244
+ y: adjustY(`${attrYPct}%`, accentLayout),
2245
+ anchor: "center"
2246
+ },
2247
+ animation: { type: "fadeIn", duration: 1, delay: 1.5 }
2248
+ });
2339
2249
  }
2250
+ return layers;
2340
2251
  }
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);
2252
+
2253
+ // src/doc/templates/factCard.ts
2254
+ function factCard(input, context) {
2255
+ const { fact, explanation, source, accentImage } = input;
2256
+ const { theme, viewport } = context;
2257
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2258
+ const factFontSize = themedFontSize(56, context, true);
2259
+ const explainFontSize = themedFontSize(34, context, false);
2260
+ const sourceFontSize = themedFontSize(20, context, false);
2261
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
2262
+ if (accentImage) {
2263
+ layers.push(
2264
+ ...createAccentLayers(
2265
+ accentImage,
2266
+ input.id,
2267
+ themedImageTreatment(context, input.imageTreatment)
2268
+ )
2269
+ );
2352
2270
  }
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);
2271
+ const textWidthPx = parseFloat(accentLayout.textWidth) / 100 * viewport.width;
2272
+ const factH = estimateTextHeight(fact, factFontSize, textWidthPx, 1.3);
2273
+ const explainH = estimateTextHeight(explanation, explainFontSize, textWidthPx, 1.5);
2274
+ const sourceH = source ? sourceFontSize * 1.4 : 0;
2275
+ const gap = factFontSize * 0.9;
2276
+ const sourceGap = source ? explainFontSize * 1.1 : 0;
2277
+ const totalH = factH + gap + explainH + sourceGap + sourceH;
2278
+ const groupTopPct = 47 - totalH / 2 / viewport.height * 100;
2279
+ const pct = (px) => groupTopPct + px / viewport.height * 100;
2280
+ const factY = pct(factH / 2);
2281
+ const explainY = pct(factH + gap + explainH / 2);
2282
+ const sourceY = pct(factH + gap + explainH + sourceGap + sourceH / 2);
2283
+ layers.push({
2284
+ type: "text",
2285
+ id: "fact",
2286
+ content: {
2287
+ text: fact,
2288
+ style: {
2289
+ fontSize: factFontSize,
2290
+ fontFamily: getThemeFont(context, "title"),
2291
+ fontWeight: "bold",
2292
+ color: theme.colors.text,
2293
+ textAlign: "center",
2294
+ lineHeight: 1.3,
2295
+ shadow: shouldUseShadow(context)
2296
+ }
2297
+ },
2298
+ position: {
2299
+ x: accentLayout.textCenterX,
2300
+ y: adjustY(`${factY}%`, accentLayout),
2301
+ width: accentLayout.textWidth,
2302
+ anchor: "center"
2303
+ },
2304
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1.5 })
2305
+ });
2306
+ layers.push({
2307
+ type: "text",
2308
+ id: "explanation",
2309
+ content: {
2310
+ text: explanation,
2311
+ style: {
2312
+ fontSize: explainFontSize,
2313
+ fontFamily: getThemeFont(context, "body"),
2314
+ color: theme.colors.textMuted,
2315
+ textAlign: "center",
2316
+ lineHeight: 1.5,
2317
+ shadow: shouldUseShadow(context)
2318
+ }
2319
+ },
2320
+ position: {
2321
+ x: accentLayout.textCenterX,
2322
+ y: adjustY(`${explainY}%`, accentLayout),
2323
+ width: accentLayout.textWidth,
2324
+ anchor: "center"
2325
+ },
2326
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1, delay: 0.8 })
2327
+ });
2328
+ if (source) {
2329
+ layers.push({
2330
+ type: "text",
2331
+ id: "source",
2332
+ content: {
2333
+ text: source,
2334
+ style: {
2335
+ fontSize: sourceFontSize,
2336
+ fontFamily: getThemeFont(context, "body"),
2337
+ color: theme.colors.textMuted,
2338
+ textAlign: "center",
2339
+ shadow: shouldUseShadow(context)
2340
+ }
2341
+ },
2342
+ position: {
2343
+ x: accentLayout.textCenterX,
2344
+ y: adjustY(`${sourceY}%`, accentLayout),
2345
+ anchor: "center"
2346
+ },
2347
+ animation: { type: "fadeIn", duration: 0.8, delay: 1.5 }
2366
2348
  });
2367
2349
  }
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
- };
2386
- });
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;
2350
+ return layers;
2415
2351
  }
2416
2352
 
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");
2499
- }
2500
-
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>`;
2532
- }
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>");
2559
- }
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
- });
2575
- }
2576
- }
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
- });
2601
- }
2602
- }
2603
- if (Array.isArray(n.children)) fromHtml(n.children);
2604
- }
2605
- }
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);
2353
+ // src/doc/templates/twoColumn.ts
2354
+ function twoColumn(input, context) {
2355
+ const { left, right, header, leftColor = "green", rightColor = "blue" } = input;
2356
+ if (!left?.label || !right?.label) return [];
2357
+ const { theme, layout, orientation } = context;
2358
+ const leftColors = resolveColorScheme(context, leftColor);
2359
+ const rightColors = resolveColorScheme(context, rightColor);
2360
+ const positions = getTwoColumnPositions(orientation);
2361
+ const isStacked = layout.stackColumns;
2362
+ const columnWidth = isStacked ? "85%" : "42%";
2363
+ const headerFontSize = themedFontSize(36, context, true);
2364
+ const labelFontSize = themedFontSize(44, context, true);
2365
+ const sublabelFontSize = themedFontSize(22, context, false);
2366
+ const panelTop = header && !isStacked ? 22 : 15;
2367
+ const panelHeight = header && !isStacked ? 64 : 70;
2368
+ const layers = [
2369
+ // Background subtle gradient to add depth
2370
+ {
2371
+ type: "shape",
2372
+ id: "bg",
2373
+ content: {
2374
+ shape: "rect",
2375
+ fill: themedSurfaceGradient(context, 135)
2376
+ },
2377
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2619
2378
  }
2379
+ ];
2380
+ if (!isStacked) {
2381
+ const panelFill = withAlpha(theme.colors.text, 0.05);
2382
+ layers.push({
2383
+ type: "shape",
2384
+ id: "left-panel",
2385
+ content: {
2386
+ shape: "rect",
2387
+ fill: panelFill,
2388
+ borderRadius: 12
2389
+ },
2390
+ position: { x: "3%", y: `${panelTop}%`, width: "44%", height: `${panelHeight}%` }
2391
+ });
2392
+ layers.push({
2393
+ type: "shape",
2394
+ id: "right-panel",
2395
+ content: {
2396
+ shape: "rect",
2397
+ fill: panelFill,
2398
+ borderRadius: 12
2399
+ },
2400
+ position: { x: "53%", y: `${panelTop}%`, width: "44%", height: `${panelHeight}%` }
2401
+ });
2620
2402
  }
2621
- for (const node of contents) {
2622
- if (found.length >= limit) break;
2623
- walk(node);
2403
+ const headerY = isStacked ? "8%" : "12%";
2404
+ if (header) {
2405
+ layers.push({
2406
+ type: "text",
2407
+ id: "header",
2408
+ content: {
2409
+ text: header,
2410
+ style: {
2411
+ fontSize: headerFontSize,
2412
+ fontFamily: getThemeFont(context, "title"),
2413
+ fontWeight: "bold",
2414
+ color: theme.colors.text,
2415
+ textAlign: "center"
2416
+ }
2417
+ },
2418
+ position: { x: "50%", y: headerY, anchor: "center" },
2419
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8 })
2420
+ });
2624
2421
  }
2625
- return found;
2626
- }
2627
- function extractFirstImage(contents) {
2628
- return extractImages(contents, 1)[0] ?? null;
2629
- }
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;
2650
- }
2651
- return void 0;
2422
+ let leftY;
2423
+ let leftSublabelY;
2424
+ let rightY;
2425
+ let rightSublabelY;
2426
+ if (isStacked) {
2427
+ leftY = header ? "30%" : "25%";
2428
+ leftSublabelY = header ? "40%" : "35%";
2429
+ rightY = header ? "60%" : "55%";
2430
+ rightSublabelY = header ? "70%" : "65%";
2431
+ } else {
2432
+ const panelCenter = panelTop + panelHeight / 2;
2433
+ leftY = `${panelCenter - 4}%`;
2434
+ leftSublabelY = `${panelCenter + 7}%`;
2435
+ rightY = `${panelCenter - 4}%`;
2436
+ rightSublabelY = `${panelCenter + 7}%`;
2652
2437
  }
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
- });
2438
+ layers.push({
2439
+ type: "text",
2440
+ id: "left-label",
2441
+ content: {
2442
+ text: left.label,
2443
+ style: {
2444
+ fontSize: labelFontSize,
2445
+ fontFamily: getThemeFont(context, "body"),
2446
+ fontWeight: "bold",
2447
+ color: leftColors.text,
2448
+ textAlign: "center",
2449
+ lineHeight: 1.3
2671
2450
  }
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()
2676
- });
2677
- }
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();
2451
+ },
2452
+ position: {
2453
+ x: positions.left.x,
2454
+ y: leftY,
2455
+ anchor: "center",
2456
+ width: columnWidth
2457
+ },
2458
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1 })
2459
+ });
2460
+ if (left.sublabel) {
2461
+ layers.push({
2462
+ type: "text",
2463
+ id: "left-sublabel",
2464
+ content: {
2465
+ text: left.sublabel,
2466
+ style: {
2467
+ fontSize: sublabelFontSize,
2468
+ fontFamily: getThemeFont(context, "body"),
2469
+ color: theme.colors.textMuted,
2470
+ textAlign: "center",
2471
+ lineHeight: 1.4
2472
+ }
2473
+ },
2474
+ position: {
2475
+ x: positions.left.x,
2476
+ y: leftSublabelY,
2477
+ anchor: "center",
2478
+ width: columnWidth
2479
+ },
2480
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.3 }
2481
+ });
2705
2482
  }
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;
2483
+ const connectorFontSize = themedFontSize(64, context, true);
2484
+ const connectorY = isStacked ? "48%" : `${panelTop + panelHeight / 2}%`;
2485
+ const connectorSymbol = isStacked ? "\u2193" : "\u2192";
2486
+ layers.push({
2487
+ type: "text",
2488
+ id: "connector",
2489
+ content: {
2490
+ text: connectorSymbol,
2491
+ style: {
2492
+ fontSize: connectorFontSize,
2493
+ fontFamily: getThemeFont(context, "title"),
2494
+ fontWeight: "bold",
2495
+ color: withAlpha(theme.colors.text, 0.45),
2496
+ textAlign: "center"
2497
+ }
2498
+ },
2499
+ position: {
2500
+ x: "50%",
2501
+ y: connectorY,
2502
+ anchor: "center"
2503
+ },
2504
+ animation: { type: "fadeIn", duration: 0.6, delay: 0.4 }
2505
+ });
2506
+ layers.push({
2507
+ type: "text",
2508
+ id: "right-label",
2509
+ content: {
2510
+ text: right.label,
2511
+ style: {
2512
+ fontSize: labelFontSize,
2513
+ fontFamily: getThemeFont(context, "body"),
2514
+ fontWeight: "bold",
2515
+ color: rightColors.text,
2516
+ textAlign: "center",
2517
+ lineHeight: 1.3
2518
+ }
2519
+ },
2520
+ position: {
2521
+ x: positions.right.x,
2522
+ y: rightY,
2523
+ anchor: "center",
2524
+ width: columnWidth
2525
+ },
2526
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 1, delay: 0.5 })
2527
+ });
2528
+ if (right.sublabel) {
2529
+ layers.push({
2530
+ type: "text",
2531
+ id: "right-sublabel",
2532
+ content: {
2533
+ text: right.sublabel,
2534
+ style: {
2535
+ fontSize: sublabelFontSize,
2536
+ fontFamily: getThemeFont(context, "body"),
2537
+ color: theme.colors.textMuted,
2538
+ textAlign: "center",
2539
+ lineHeight: 1.4
2540
+ }
2541
+ },
2542
+ position: {
2543
+ x: positions.right.x,
2544
+ y: rightSublabelY,
2545
+ anchor: "center",
2546
+ width: columnWidth
2547
+ },
2548
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.8 }
2549
+ });
2724
2550
  }
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();
2551
+ return layers;
2739
2552
  }
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
- };
2553
+
2554
+ // src/doc/templates/dateEvent.ts
2555
+ var MOOD_ACCENTS = {
2556
+ neutral: "#63b3ed",
2557
+ somber: "#e53e3e",
2558
+ celebratory: "#68d391"
2559
+ };
2560
+ function dateEvent(input, context) {
2561
+ const { date, description, footer, mood = "neutral", accentImage } = input;
2562
+ const { theme } = context;
2563
+ const moodAccent = MOOD_ACCENTS[mood];
2564
+ const dateColor = relativeLuminance(theme.colors.background) > 0.5 ? oklchDarken(moodAccent, 0.25) : moodAccent;
2565
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2566
+ const dateFontSize = themedFontSize(96, context, true);
2567
+ const descFontSize = themedFontSize(30, context, false);
2568
+ const footerFontSize = themedFontSize(26, context, false);
2569
+ const layers = [
2570
+ // Background — theme surface gradient (diagonal to differentiate
2571
+ // from other text templates)
2572
+ {
2573
+ type: "shape",
2574
+ id: "bg",
2575
+ content: {
2576
+ shape: "rect",
2577
+ fill: themedSurfaceGradient(context, 135)
2578
+ },
2579
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2758
2580
  }
2581
+ ];
2582
+ if (accentImage) {
2583
+ layers.push(
2584
+ ...createAccentLayers(
2585
+ accentImage,
2586
+ input.id,
2587
+ themedImageTreatment(context, input.imageTreatment)
2588
+ )
2589
+ );
2759
2590
  }
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 };
2591
+ layers.push({
2592
+ type: "text",
2593
+ id: "date",
2594
+ content: {
2595
+ text: date,
2596
+ style: {
2597
+ fontSize: dateFontSize,
2598
+ fontFamily: getThemeFont(context, "title"),
2599
+ fontWeight: "bold",
2600
+ color: dateColor,
2601
+ shadow: shouldUseShadow(context)
2602
+ }
2603
+ },
2604
+ position: {
2605
+ x: accentLayout.textCenterX,
2606
+ y: adjustY("35%", accentLayout),
2607
+ anchor: "center"
2608
+ },
2609
+ animation: { type: "fadeIn", duration: 1.5 }
2610
+ });
2611
+ layers.push({
2612
+ type: "text",
2613
+ id: "description",
2614
+ content: {
2615
+ text: description,
2616
+ style: {
2617
+ fontSize: descFontSize,
2618
+ fontFamily: getThemeFont(context, "body"),
2619
+ color: theme.colors.text,
2620
+ textAlign: "center",
2621
+ lineHeight: 1.6,
2622
+ shadow: shouldUseShadow(context)
2623
+ }
2624
+ },
2625
+ position: {
2626
+ x: accentLayout.textCenterX,
2627
+ y: adjustY("58%", accentLayout),
2628
+ width: accentLayout.textWidth,
2629
+ anchor: "center"
2630
+ },
2631
+ animation: { type: "fadeIn", duration: 2, delay: 1 }
2632
+ });
2633
+ if (footer) {
2634
+ layers.push({
2635
+ type: "text",
2636
+ id: "footer",
2637
+ content: {
2638
+ text: footer,
2639
+ style: {
2640
+ fontSize: footerFontSize,
2641
+ fontFamily: getThemeFont(context, "body"),
2642
+ color: theme.colors.textMuted,
2643
+ shadow: shouldUseShadow(context)
2644
+ }
2645
+ },
2646
+ position: {
2647
+ x: accentLayout.textCenterX,
2648
+ y: adjustY("72%", accentLayout),
2649
+ anchor: "center"
2650
+ },
2651
+ animation: { type: "fadeIn", duration: 1, delay: 3 }
2652
+ });
2764
2653
  }
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
- };
2654
+ return layers;
2655
+ }
2656
+
2657
+ // src/doc/templates/imageWithCaption.ts
2658
+ function imageWithCaption(input, context) {
2659
+ const {
2660
+ imageSrc,
2661
+ imageAlt,
2662
+ caption: rawCaption,
2663
+ captionPosition = "bottom",
2664
+ ambientMotion,
2665
+ isTitle,
2666
+ subtitle,
2667
+ imageCredit,
2668
+ imageLicense
2669
+ } = input;
2670
+ const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
2671
+ const { theme, layout } = context;
2672
+ const treatment = themedImageTreatment(context, input.imageTreatment);
2673
+ const captionFontSize = themedFontSize(30, context, false);
2674
+ const titleFontSize = themedFontSize(96, context, true);
2675
+ const subtitleFontSize = themedFontSize(36, context, false);
2676
+ const legacyKenBurns = input.kenBurns;
2677
+ const motion = ambientMotion || legacyKenBurns;
2678
+ const imageAnimation = mapAmbientMotion(motion);
2679
+ const layers = [
2680
+ // Background image
2681
+ {
2682
+ type: "image",
2683
+ id: "bg-image",
2684
+ content: {
2685
+ src: imageSrc,
2686
+ alt: imageAlt,
2687
+ fit: "cover",
2688
+ credit: imageCredit,
2689
+ license: imageLicense,
2690
+ ...treatment ? { treatment } : {}
2691
+ },
2692
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
2693
+ animation: imageAnimation
2694
+ }
2695
+ ];
2696
+ const bg = theme.colors.background;
2697
+ if (isTitle && caption) {
2698
+ layers.push({
2699
+ type: "shape",
2700
+ id: "title-gradient",
2701
+ content: {
2702
+ shape: "rect",
2703
+ fill: `linear-gradient(0deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.45)} 40%, ${withAlpha(bg, 0.1)} 70%, ${withAlpha(bg, 0)} 100%)`
2704
+ },
2705
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2706
+ });
2707
+ layers.push({
2708
+ type: "text",
2709
+ id: "title",
2710
+ content: {
2711
+ text: caption,
2712
+ style: {
2713
+ fontSize: titleFontSize,
2714
+ fontFamily: getThemeFont(context, "title"),
2715
+ fontWeight: "bold",
2716
+ color: theme.colors.text,
2717
+ textAlign: "center",
2718
+ shadow: shouldUseShadow(context)
2719
+ }
2720
+ },
2721
+ position: {
2722
+ x: "50%",
2723
+ y: subtitle ? "70%" : "75%",
2724
+ anchor: "center",
2725
+ width: layout.maxTextWidth
2726
+ },
2727
+ animation: { type: "fadeIn", duration: 0.8 }
2728
+ });
2729
+ if (subtitle) {
2730
+ layers.push({
2731
+ type: "text",
2732
+ id: "subtitle",
2733
+ content: {
2734
+ text: subtitle,
2735
+ style: {
2736
+ fontSize: subtitleFontSize,
2737
+ fontFamily: getThemeFont(context, "body"),
2738
+ color: theme.colors.textMuted,
2739
+ textAlign: "center",
2740
+ lineHeight: 1.5
2741
+ }
2742
+ },
2743
+ position: {
2744
+ x: "50%",
2745
+ y: "82%",
2746
+ anchor: "center",
2747
+ width: layout.maxTextWidth
2748
+ },
2749
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.2 }
2750
+ });
2780
2751
  }
2752
+ return layers;
2781
2753
  }
2782
- return { stat: headingText, description: bodyText3 || headingText };
2783
- }
2784
- function normalizeCoverageText(value) {
2785
- return value.replace(/\s+/g, " ").trim();
2786
- }
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 ?? "");
2815
- }
2816
- case "leftFeature":
2817
- case "rightFeature":
2818
- case "statHighlight":
2819
- return true;
2820
- default:
2821
- return false;
2754
+ if (caption) {
2755
+ const captionBgY = captionPosition === "top" ? "7%" : captionPosition === "center" ? "42%" : "68%";
2756
+ const captionY = captionPosition === "top" ? "13%" : captionPosition === "center" ? "48%" : "74%";
2757
+ layers.push({
2758
+ type: "shape",
2759
+ id: "caption-gradient",
2760
+ content: {
2761
+ shape: "rect",
2762
+ fill: `linear-gradient(90deg, ${withAlpha(bg, 0)}, ${withAlpha(bg, 0.78)} 16%, ${withAlpha(bg, 0.78)} 84%, ${withAlpha(bg, 0)})`
2763
+ },
2764
+ position: {
2765
+ x: 0,
2766
+ y: captionBgY,
2767
+ width: "100%",
2768
+ height: "16%"
2769
+ }
2770
+ });
2771
+ layers.push({
2772
+ type: "text",
2773
+ id: "caption",
2774
+ content: {
2775
+ text: caption,
2776
+ style: {
2777
+ fontSize: captionFontSize,
2778
+ fontFamily: getThemeFont(context, "body"),
2779
+ color: theme.colors.text,
2780
+ textAlign: "center",
2781
+ shadow: shouldUseShadow(context),
2782
+ lineHeight: 1.18,
2783
+ maxLines: 2
2784
+ }
2785
+ },
2786
+ position: {
2787
+ x: "50%",
2788
+ y: captionY,
2789
+ anchor: "center",
2790
+ width: "78%"
2791
+ },
2792
+ animation: { type: "fadeIn", duration: 1.5, delay: 0.5 }
2793
+ });
2822
2794
  }
2795
+ return layers;
2823
2796
  }
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 } : {} };
2797
+
2798
+ // src/doc/templates/featureBlock.ts
2799
+ function buildFeatureLayers(input, context, side) {
2800
+ const { imageSrc, imageAlt, imageWidth, imageHeight, title, body } = input;
2801
+ const { theme, layout } = context;
2802
+ const treatment = themedImageTreatment(context, input.imageTreatment);
2803
+ const sized = (imageWidth ?? 0) > 0 || (imageHeight ?? 0) > 0;
2804
+ const stack = layout.stackColumns;
2805
+ const titleFontSize = themedFontSize(48, context, true);
2806
+ const bodyFontSize2 = themedFontSize(24, context, false);
2807
+ let imgX;
2808
+ let imgY;
2809
+ let imgW;
2810
+ let imgH;
2811
+ let imageFit;
2812
+ if (stack) {
2813
+ imgX = "0";
2814
+ imgY = "0";
2815
+ imgW = "100%";
2816
+ imgH = "50%";
2817
+ imageFit = sized ? "contain" : "cover";
2818
+ } else if (sized) {
2819
+ const halfStart = side === "left" ? 5 : 55;
2820
+ imgX = `${halfStart}%`;
2821
+ imgY = "5%";
2822
+ imgW = "40%";
2823
+ imgH = "90%";
2824
+ imageFit = "contain";
2825
+ } else {
2826
+ imgX = side === "left" ? "0" : "50%";
2827
+ imgY = "0";
2828
+ imgW = "50%";
2829
+ imgH = "100%";
2830
+ imageFit = "cover";
2831
+ }
2832
+ const COLUMN_INSET = 4;
2833
+ const textColumnWidth = stack ? 90 : 42;
2834
+ const textX = stack ? `${(100 - textColumnWidth) / 2}%` : side === "left" ? `${50 + COLUMN_INSET}%` : `${COLUMN_INSET + 2}%`;
2835
+ const titleY = stack ? "60%" : body ? "42%" : "50%";
2836
+ const bodyY = stack ? "78%" : title ? "55%" : "50%";
2837
+ const textAnchor = "top-left";
2838
+ const textAlign = "left";
2839
+ const layers = [
2840
+ {
2841
+ type: "shape",
2842
+ id: "feature-bg",
2843
+ content: { shape: "rect", fill: theme.colors.background },
2844
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2951
2845
  }
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 } : {} };
2846
+ ];
2847
+ if (imageSrc) {
2848
+ layers.push({
2849
+ type: "image",
2850
+ id: "feature-image",
2851
+ content: {
2852
+ src: imageSrc,
2853
+ alt: imageAlt ?? title ?? "",
2854
+ fit: imageFit,
2855
+ ...treatment ? { treatment } : {}
2856
+ },
2857
+ position: { x: imgX, y: imgY, width: imgW, height: imgH }
2858
+ });
2859
+ }
2860
+ if (title) {
2861
+ layers.push({
2862
+ type: "text",
2863
+ id: "feature-title",
2864
+ content: {
2865
+ text: title,
2866
+ style: {
2867
+ fontSize: titleFontSize,
2868
+ fontFamily: getThemeFont(context, "title"),
2869
+ fontWeight: "bold",
2870
+ color: theme.colors.text,
2871
+ textAlign,
2872
+ lineHeight: 1.2
2960
2873
  }
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;
2874
+ },
2875
+ position: {
2876
+ x: textX,
2877
+ y: titleY,
2878
+ anchor: textAnchor,
2879
+ width: `${textColumnWidth}%`
2880
+ },
2881
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8 })
2882
+ });
2883
+ }
2884
+ if (body) {
2885
+ layers.push({
2886
+ type: "text",
2887
+ id: "feature-body",
2888
+ content: {
2889
+ text: body,
2890
+ style: {
2891
+ fontSize: bodyFontSize2,
2892
+ fontFamily: getThemeFont(context, "body"),
2893
+ color: theme.colors.textMuted,
2894
+ textAlign,
2895
+ lineHeight: 1.5
2896
+ }
2897
+ },
2898
+ position: {
2899
+ x: textX,
2900
+ y: bodyY,
2901
+ anchor: textAnchor,
2902
+ width: `${textColumnWidth}%`
2903
+ },
2904
+ animation: themedEntrance(context, "text", { type: "fadeIn", duration: 0.8, delay: 0.2 })
2905
+ });
2906
+ }
2907
+ return layers;
2908
+ }
2909
+ function leftFeature(input, context) {
2910
+ return buildFeatureLayers(input, context, "left");
2911
+ }
2912
+ function rightFeature(input, context) {
2913
+ return buildFeatureLayers(input, context, "right");
2914
+ }
2915
+
2916
+ // src/doc/templates/mapBlock.ts
2917
+ function mapBlock(input, context) {
2918
+ const {
2919
+ center,
2920
+ zoom,
2921
+ mapStyle = "terrain",
2922
+ title,
2923
+ caption,
2924
+ markers,
2925
+ ambientMotion,
2926
+ staticSrc
2927
+ } = input;
2928
+ const { theme, layout } = context;
2929
+ const titleFontSize = themedFontSize(64, context, true);
2930
+ const captionFontSize = themedFontSize(32, context, false);
2931
+ const mapAnimation = mapAmbientMotion(ambientMotion);
2932
+ const layers = [
2933
+ // Map background
2934
+ {
2935
+ type: "map",
2936
+ id: "map-bg",
2937
+ content: {
2938
+ center,
2939
+ zoom,
2940
+ style: mapStyle,
2941
+ markers,
2942
+ showAttribution: true,
2943
+ staticSrc
2944
+ // Use pre-rendered image if available
2945
+ },
2946
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
2947
+ animation: mapAnimation
2968
2948
  }
2969
- default:
2970
- return placeholders ? {} : null;
2949
+ ];
2950
+ const bg = theme.colors.background;
2951
+ if (title) {
2952
+ layers.push({
2953
+ type: "shape",
2954
+ id: "title-overlay",
2955
+ content: {
2956
+ shape: "rect",
2957
+ fill: `linear-gradient(180deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.6)} 70%, ${withAlpha(bg, 0)} 100%)`
2958
+ },
2959
+ position: { x: 0, y: 0, width: "100%", height: "20%" }
2960
+ });
2961
+ layers.push({
2962
+ type: "text",
2963
+ id: "title",
2964
+ content: {
2965
+ text: title,
2966
+ style: {
2967
+ fontSize: titleFontSize,
2968
+ fontFamily: getThemeFont(context, "title"),
2969
+ fontWeight: "bold",
2970
+ color: theme.colors.text,
2971
+ textAlign: "center",
2972
+ shadow: shouldUseShadow(context)
2973
+ }
2974
+ },
2975
+ position: {
2976
+ x: "50%",
2977
+ y: "9%",
2978
+ anchor: "center",
2979
+ width: layout.maxTextWidth
2980
+ },
2981
+ animation: { type: "fadeIn", duration: 1, delay: 0.3 }
2982
+ });
2983
+ }
2984
+ if (caption) {
2985
+ layers.push({
2986
+ type: "shape",
2987
+ id: "caption-overlay",
2988
+ content: {
2989
+ shape: "rect",
2990
+ fill: `linear-gradient(0deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.6)} 70%, ${withAlpha(bg, 0)} 100%)`
2991
+ },
2992
+ position: { x: 0, y: "80%", width: "100%", height: "20%" }
2993
+ });
2994
+ layers.push({
2995
+ type: "text",
2996
+ id: "caption",
2997
+ content: {
2998
+ text: caption,
2999
+ style: {
3000
+ fontSize: captionFontSize,
3001
+ fontFamily: getThemeFont(context, "body"),
3002
+ color: theme.colors.text,
3003
+ textAlign: "center",
3004
+ shadow: shouldUseShadow(context)
3005
+ }
3006
+ },
3007
+ position: {
3008
+ x: "50%",
3009
+ y: layout.captionY,
3010
+ anchor: "center",
3011
+ width: layout.maxTextWidth
3012
+ },
3013
+ animation: { type: "fadeIn", duration: 1, delay: 0.5 }
3014
+ });
2971
3015
  }
3016
+ return layers;
3017
+ }
3018
+
3019
+ // src/doc/templates/fullBleedQuote.ts
3020
+ function fullBleedQuote(input, context) {
3021
+ const { text, colorScheme = "blue" } = input;
3022
+ const colors = resolveColorScheme(context, colorScheme);
3023
+ const textFontSize = themedFontSize(120, context, true);
3024
+ return [
3025
+ // Background — radial vignette built from the color scheme's own
3026
+ // surface, so the card takes on the scheme (and the theme behind it)
3027
+ // instead of vignetting every theme into hard black.
3028
+ {
3029
+ type: "shape",
3030
+ id: "bg",
3031
+ content: {
3032
+ shape: "rect",
3033
+ fill: `radial-gradient(ellipse at 50% 50%, ${colors.bg} 0%, ${oklchDarken(colors.bg, 0.12)} 100%)`
3034
+ },
3035
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
3036
+ },
3037
+ // The text — massive, centered
3038
+ {
3039
+ type: "text",
3040
+ id: "impact-text",
3041
+ content: {
3042
+ text,
3043
+ style: {
3044
+ fontSize: textFontSize,
3045
+ fontFamily: getThemeFont(context, "title"),
3046
+ fontWeight: "bold",
3047
+ color: colors.text,
3048
+ textAlign: "center",
3049
+ lineHeight: 1.2,
3050
+ shadow: shouldUseShadow(context)
3051
+ }
3052
+ },
3053
+ position: {
3054
+ x: "50%",
3055
+ y: "50%",
3056
+ anchor: "center",
3057
+ width: "85%"
3058
+ },
3059
+ animation: getTemplateHint(context, "fullBleedQuote", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.8 } : { type: "fadeIn", duration: 1.5 }
3060
+ }
3061
+ ];
2972
3062
  }
2973
3063
 
2974
3064
  // src/doc/templates/listBlock.ts
@@ -10657,8 +10747,10 @@ function buildStartBlock(markdownDoc, rootBlocks, fileName) {
10657
10747
  const headingTitle = headingBlock ? headingBlock.title ?? extractPlainText(headingBlock.sourceHeading) : void 0;
10658
10748
  const title = frontmatterTitle || headingTitle || fileNameCoverTitle(fileName);
10659
10749
  if (!title) return void 0;
10750
+ const frontmatterSubtitle = typeof markdownDoc.frontmatter?.subtitle === "string" ? markdownDoc.frontmatter.subtitle.trim() : void 0;
10660
10751
  const subtitleBlock = headingBlock ?? rootBlocks[0];
10661
- const subtitle = subtitleBlock?.contents?.[0]?.type === "paragraph" ? extractPlainText(subtitleBlock.contents[0]) : void 0;
10752
+ const inferredSubtitle = subtitleBlock?.contents?.[0]?.type === "paragraph" ? extractPlainText(subtitleBlock.contents[0]) : void 0;
10753
+ const subtitle = frontmatterSubtitle || inferredSubtitle;
10662
10754
  const firstImage = findFirstImage(markdownDoc);
10663
10755
  const config = {
10664
10756
  title,
@@ -11008,17 +11100,6 @@ export {
11008
11100
  sectionHeader,
11009
11101
  fitBigTextSize,
11010
11102
  bigText,
11011
- contentBlock,
11012
- statHighlight,
11013
- quoteBlock,
11014
- factCard,
11015
- twoColumn,
11016
- dateEvent,
11017
- imageWithCaption,
11018
- leftFeature,
11019
- rightFeature,
11020
- mapBlock,
11021
- fullBleedQuote,
11022
11103
  computeDiagramLayout,
11023
11104
  ASCII_CHAR_W,
11024
11105
  ASCII_CHAR_H,
@@ -11034,6 +11115,8 @@ export {
11034
11115
  findFirstList,
11035
11116
  extractBodyPlainText,
11036
11117
  extractListItems,
11118
+ renderMarkdownBlocksHtml,
11119
+ markdownBlockSeparatorLines,
11037
11120
  extractRichListItems,
11038
11121
  extractImages,
11039
11122
  extractFirstImage,
@@ -11043,6 +11126,17 @@ export {
11043
11126
  extractBlockquoteText,
11044
11127
  autoTemplatePreservesContent,
11045
11128
  deriveTemplateInputs,
11129
+ contentBlock,
11130
+ statHighlight,
11131
+ quoteBlock,
11132
+ factCard,
11133
+ twoColumn,
11134
+ dateEvent,
11135
+ imageWithCaption,
11136
+ leftFeature,
11137
+ rightFeature,
11138
+ mapBlock,
11139
+ fullBleedQuote,
11046
11140
  listBlock,
11047
11141
  photoGrid,
11048
11142
  definitionCard,
@@ -11079,7 +11173,9 @@ export {
11079
11173
  fallbackBlockLayers,
11080
11174
  BLOCK_MEDIA_LAYOUT_POLICIES,
11081
11175
  getBlockMediaLayoutPolicy,
11176
+ placeLayersInRect,
11082
11177
  materializeBlockLayers,
11178
+ materializeBlockLayersWithRuntime,
11083
11179
  TEMPLATE_METADATA,
11084
11180
  TEMPLATE_AUTHORING_METADATA,
11085
11181
  coverBlock,