@bendyline/squisq 2.3.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/{Doc-BrgZC7SE.d.ts → Doc-DLpyOAXJ.d.ts} +87 -2
  2. package/dist/{ImageEditDoc-rum0Xb9P.d.ts → ImageEditDoc-Cu30xb9b.d.ts} +1 -1
  3. package/dist/{chunk-2ZWIXGAC.js → chunk-2S74DPJH.js} +4 -0
  4. package/dist/{chunk-ZQKZSJAX.js → chunk-2VCNTDNZ.js} +245 -0
  5. package/dist/{chunk-RS5AP3J4.js → chunk-AE3IUKMM.js} +1 -1
  6. package/dist/{chunk-QWCFK5FN.js → chunk-CUYHFOFL.js} +5 -1
  7. package/dist/{chunk-ACZOVWX3.js → chunk-D4LPP3P5.js} +44 -15
  8. package/dist/{chunk-CKZY6K5R.js → chunk-F2IEPSMA.js} +2565 -1143
  9. package/dist/{chunk-ZAFLMJPD.js → chunk-KPBZZVGY.js} +37 -7
  10. package/dist/{chunk-BCCXTMN5.js → chunk-O7JILDEF.js} +7 -0
  11. package/dist/{chunk-SPTY4C6F.js → chunk-REDUXXDJ.js} +1 -1
  12. package/dist/{chunk-BCHAXKKI.js → chunk-ZHLNKB2I.js} +1 -1
  13. package/dist/chunk-ZYO3DBTA.js +983 -0
  14. package/dist/doc/index.d.ts +111 -6
  15. package/dist/doc/index.js +11 -7
  16. package/dist/generate/index.d.ts +1 -1
  17. package/dist/imageEdit/index.d.ts +3 -3
  18. package/dist/index.d.ts +6 -6
  19. package/dist/index.js +34 -14
  20. package/dist/jsonForm/index.d.ts +1 -1
  21. package/dist/jsonForm/index.js +2 -2
  22. package/dist/markdown/index.d.ts +118 -1
  23. package/dist/markdown/index.js +24 -8
  24. package/dist/{materializePageSection-DAbyLi-k.d.ts → materializePageSection-CgNs5Qmw.d.ts} +4 -2
  25. package/dist/narration/index.d.ts +1 -1
  26. package/dist/narration/index.js +5 -5
  27. package/dist/recommend/index.js +2 -2
  28. package/dist/schemas/index.d.ts +4 -4
  29. package/dist/schemas/index.js +2 -2
  30. package/dist/{themeLibrary-DJt89gyP.d.ts → themeLibrary-RWsNtlOu.d.ts} +1 -1
  31. package/dist/transform/index.d.ts +2 -2
  32. package/dist/transform/index.js +2 -2
  33. package/package.json +1 -1
  34. package/dist/chunk-3IGPAPQ7.js +0 -645
@@ -21,7 +21,7 @@ import {
21
21
  parseAsciiTimeline,
22
22
  pickAutoTemplate,
23
23
  profileBlockContents
24
- } from "./chunk-QWCFK5FN.js";
24
+ } from "./chunk-CUYHFOFL.js";
25
25
  import {
26
26
  DEFAULT_THEME,
27
27
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
@@ -49,14 +49,15 @@ import {
49
49
  matchTrailingTemplateAnnotation,
50
50
  parseNumber,
51
51
  parseTimeSeconds,
52
+ sanitizeUrl,
52
53
  splitKeyValueToken,
53
54
  tokenizeAttrTokens
54
- } from "./chunk-ZQKZSJAX.js";
55
+ } from "./chunk-2VCNTDNZ.js";
55
56
  import {
56
57
  extractPlainText,
57
58
  readFrontmatterThemeId,
58
59
  walkMarkdownTree
59
- } from "./chunk-BCCXTMN5.js";
60
+ } from "./chunk-O7JILDEF.js";
60
61
  import {
61
62
  resolveBlockTransition
62
63
  } from "./chunk-4VOD55SX.js";
@@ -422,6 +423,16 @@ var TEMPLATE_ALIASES = Object.freeze({
422
423
  function resolveTemplateName(name) {
423
424
  return TEMPLATE_ALIASES[name] ?? name;
424
425
  }
426
+ var TABLE_FED_TEMPLATES = /* @__PURE__ */ new Set([
427
+ "dataTable",
428
+ "barChart",
429
+ "columnChart",
430
+ "pieChart",
431
+ "donutChart",
432
+ "lineChart",
433
+ "areaChart",
434
+ "scatterChart"
435
+ ]);
425
436
  var CONTAINER_TEMPLATES = /* @__PURE__ */ new Set(["diagram", "drawing", "layout"]);
426
437
  function isContainerTemplate(name) {
427
438
  return !!name && CONTAINER_TEMPLATES.has(resolveTemplateName(name));
@@ -534,6 +545,8 @@ function titleBlock(input, context) {
534
545
  const baseBg = backgroundColor || theme.colors.primary;
535
546
  const maxWidthPx = parseFloat(layout.maxTextWidth) / 100 * viewport.width;
536
547
  const titleH = estimateTextHeight(title, titleFontSize, maxWidthPx, 1.15);
548
+ const titleLineCount = Math.max(1, Math.round(titleH / (titleFontSize * 1.15)));
549
+ const titleSubtitleGapPx = 56 + (titleLineCount > 1 ? titleFontSize * 0.25 : 0);
537
550
  const titleYPct = subtitle ? 42 : 48;
538
551
  const px = (v) => v / viewport.height * 100;
539
552
  const layers = [
@@ -618,7 +631,7 @@ function titleBlock(input, context) {
618
631
  },
619
632
  position: {
620
633
  x: "50%",
621
- y: `${titleYPct + px(titleH / 2 + 56 + subH / 2)}%`,
634
+ y: `${titleYPct + px(titleH / 2 + titleSubtitleGapPx + subH / 2)}%`,
622
635
  anchor: "center",
623
636
  width: layout.maxTextWidth
624
637
  },
@@ -946,7 +959,7 @@ function sectionHeader(input, context) {
946
959
 
947
960
  // src/doc/templates/contentBlock.ts
948
961
  function completeBodyText(context) {
949
- return (context.block?.contents ?? []).map((node) => extractPlainText(node)).join("\n").trim();
962
+ return (context.block?.contents ?? []).filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((node) => extractPlainText(node)).join("\n").trim();
950
963
  }
951
964
  function bodyFontSize(body, context) {
952
965
  const base = body.length > 1800 ? 18 : body.length > 1100 ? 21 : body.length > 650 ? 24 : 28;
@@ -2021,6 +2034,821 @@ function fullBleedQuote(input, context) {
2021
2034
  ];
2022
2035
  }
2023
2036
 
2037
+ // src/doc/templates/diagramLayout.ts
2038
+ var DEFAULT_OPTS = {
2039
+ gapX: 260,
2040
+ gapY: 140,
2041
+ gridPad: 80,
2042
+ gridOriginX: 80,
2043
+ gridOriginY: 80
2044
+ };
2045
+ function computeDiagramLayout(children, options = {}) {
2046
+ const opts = { ...DEFAULT_OPTS, ...options };
2047
+ const warnings = [];
2048
+ const entries = [];
2049
+ const unpositionedIdx = [];
2050
+ for (let i = 0; i < children.length; i++) {
2051
+ const c = children[i];
2052
+ const hasX = typeof c.x === "number";
2053
+ const hasY = typeof c.y === "number";
2054
+ if (hasX && hasY) {
2055
+ entries.push({ child: c, pinned: true, x: c.x, y: c.y });
2056
+ } else {
2057
+ entries.push({ child: c, pinned: false, x: 0, y: 0 });
2058
+ unpositionedIdx.push(i);
2059
+ }
2060
+ }
2061
+ if (unpositionedIdx.length > 0) {
2062
+ const cols = Math.max(1, Math.ceil(Math.sqrt(unpositionedIdx.length)));
2063
+ let baseX = opts.gridOriginX;
2064
+ let baseY = opts.gridOriginY;
2065
+ const pinned = entries.filter((e) => e.pinned);
2066
+ if (pinned.length > 0) {
2067
+ const minX = Math.min(...pinned.map((e) => e.x));
2068
+ const maxY = Math.max(...pinned.map((e) => e.y));
2069
+ baseX = minX;
2070
+ baseY = maxY + opts.gridPad + opts.gapY;
2071
+ }
2072
+ unpositionedIdx.forEach((entryIdx, k) => {
2073
+ const col = k % cols;
2074
+ const row = Math.floor(k / cols);
2075
+ entries[entryIdx].x = baseX + col * opts.gapX;
2076
+ entries[entryIdx].y = baseY + row * opts.gapY;
2077
+ });
2078
+ }
2079
+ const nodes = entries.map((e) => ({
2080
+ id: e.child.id,
2081
+ label: e.child.title ?? e.child.id,
2082
+ x: e.x,
2083
+ y: e.y,
2084
+ pinned: e.pinned,
2085
+ ...e.child.classes && e.child.classes.length > 0 ? { classes: e.child.classes } : {}
2086
+ }));
2087
+ const nodeIds = new Set(nodes.map((n) => n.id));
2088
+ const edges = [];
2089
+ const seenEdgeIds = /* @__PURE__ */ new Set();
2090
+ for (const child of children) {
2091
+ if (!child.connectsTo) continue;
2092
+ for (const conn of child.connectsTo) {
2093
+ if (!nodeIds.has(conn.target)) {
2094
+ warnings.push(`Edge from "${child.id}" to "${conn.target}" dropped: no such sibling node`);
2095
+ continue;
2096
+ }
2097
+ const edgeId = makeEdgeId(child.id, conn);
2098
+ if (seenEdgeIds.has(edgeId)) continue;
2099
+ seenEdgeIds.add(edgeId);
2100
+ edges.push({
2101
+ id: edgeId,
2102
+ source: child.id,
2103
+ target: conn.target,
2104
+ ...conn.type ? { type: conn.type } : {}
2105
+ });
2106
+ }
2107
+ }
2108
+ return { nodes, edges, warnings };
2109
+ }
2110
+ function makeEdgeId(source, conn) {
2111
+ return conn.type ? `${source}->${conn.target}:${conn.type}` : `${source}->${conn.target}`;
2112
+ }
2113
+
2114
+ // src/doc/asciiDiagram/types.ts
2115
+ var ASCII_CHAR_W = 14;
2116
+ var ASCII_CHAR_H = 28;
2117
+
2118
+ // src/doc/asciiDiagram/mapping.ts
2119
+ function asciiCellToCanvas(col, row) {
2120
+ return { x: col * ASCII_CHAR_W, y: row * ASCII_CHAR_H };
2121
+ }
2122
+ function canvasToAsciiCell(x, y) {
2123
+ return {
2124
+ col: Math.max(0, Math.round(x / ASCII_CHAR_W)),
2125
+ row: Math.max(0, Math.round(y / ASCII_CHAR_H))
2126
+ };
2127
+ }
2128
+ function asciiDiagramToTemplateData(diagram) {
2129
+ const nodes = diagram.nodes.map((n) => ({
2130
+ id: n.id,
2131
+ label: n.label,
2132
+ ...asciiCellToCanvas(n.col, n.row),
2133
+ w: n.wCols * ASCII_CHAR_W,
2134
+ h: n.hRows * ASCII_CHAR_H,
2135
+ ...n.containerId ? { container: n.containerId } : {}
2136
+ }));
2137
+ const anchors = inferEdgeAnchors(nodes, diagram.edges);
2138
+ const edges = diagram.edges.map((e, index) => ({
2139
+ source: e.source,
2140
+ target: e.target,
2141
+ ...e.label ? { label: e.label } : {},
2142
+ ...e.directed ? {} : { directed: false },
2143
+ ...anchors[index],
2144
+ routing: "orthogonal"
2145
+ }));
2146
+ return { nodes, edges };
2147
+ }
2148
+ function inferEdgeAnchors(nodes, edges) {
2149
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
2150
+ const pairs = edges.map((edge) => {
2151
+ const source = nodeById.get(edge.source);
2152
+ const target = nodeById.get(edge.target);
2153
+ if (!source || !target) {
2154
+ return {
2155
+ sourceAnchor: { side: "right", offset: 0.5 },
2156
+ targetAnchor: { side: "left", offset: 0.5 }
2157
+ };
2158
+ }
2159
+ const sourceW = source.w ?? ASCII_CHAR_W * 12;
2160
+ const sourceH = source.h ?? ASCII_CHAR_H * 3;
2161
+ const targetW = target.w ?? ASCII_CHAR_W * 12;
2162
+ const targetH = target.h ?? ASCII_CHAR_H * 3;
2163
+ const dx = target.x + targetW / 2 - (source.x + sourceW / 2);
2164
+ const dy = target.y + targetH / 2 - (source.y + sourceH / 2);
2165
+ const horizontalDistance = Math.abs(dx) / Math.max(1, (sourceW + targetW) / 2);
2166
+ const verticalDistance = Math.abs(dy) / Math.max(1, (sourceH + targetH) / 2);
2167
+ if (verticalDistance >= horizontalDistance) {
2168
+ return dy >= 0 ? {
2169
+ sourceAnchor: { side: "bottom", offset: 0.5 },
2170
+ targetAnchor: { side: "top", offset: 0.5 }
2171
+ } : {
2172
+ sourceAnchor: { side: "top", offset: 0.5 },
2173
+ targetAnchor: { side: "bottom", offset: 0.5 }
2174
+ };
2175
+ }
2176
+ return dx >= 0 ? {
2177
+ sourceAnchor: { side: "right", offset: 0.5 },
2178
+ targetAnchor: { side: "left", offset: 0.5 }
2179
+ } : {
2180
+ sourceAnchor: { side: "left", offset: 0.5 },
2181
+ targetAnchor: { side: "right", offset: 0.5 }
2182
+ };
2183
+ });
2184
+ distributeOffsets(pairs, edges, nodes, "source");
2185
+ distributeOffsets(pairs, edges, nodes, "target");
2186
+ coalesceAlignedFanOutAnchors(pairs, edges, nodes);
2187
+ return pairs;
2188
+ }
2189
+ function coalesceAlignedFanOutAnchors(pairs, edges, nodes) {
2190
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
2191
+ const groups = /* @__PURE__ */ new Map();
2192
+ edges.forEach((edge, index) => {
2193
+ const group = groups.get(edge.source) ?? [];
2194
+ group.push(index);
2195
+ groups.set(edge.source, group);
2196
+ });
2197
+ for (const [sourceId, indexes] of groups) {
2198
+ if (indexes.length < 2) continue;
2199
+ const source = nodeById.get(sourceId);
2200
+ const targets = indexes.map((index) => nodeById.get(edges[index].target)).filter((node) => node !== void 0);
2201
+ if (!source || targets.length !== indexes.length) continue;
2202
+ const sourceW = source.w ?? ASCII_CHAR_W * 12;
2203
+ const sourceH = source.h ?? ASCII_CHAR_H * 3;
2204
+ const sourceRight = source.x + sourceW;
2205
+ const sourceBottom = source.y + sourceH;
2206
+ const targetRight = (target) => target.x + (target.w ?? ASCII_CHAR_W * 12);
2207
+ const targetBottom = (target) => target.y + (target.h ?? ASCII_CHAR_H * 3);
2208
+ let sourceSide;
2209
+ let targetSide;
2210
+ if (targets.every((target) => target.y >= sourceBottom)) {
2211
+ sourceSide = "bottom";
2212
+ targetSide = "top";
2213
+ } else if (targets.every((target) => targetBottom(target) <= source.y)) {
2214
+ sourceSide = "top";
2215
+ targetSide = "bottom";
2216
+ } else if (targets.every((target) => target.x >= sourceRight)) {
2217
+ sourceSide = "right";
2218
+ targetSide = "left";
2219
+ } else if (targets.every((target) => targetRight(target) <= source.x)) {
2220
+ sourceSide = "left";
2221
+ targetSide = "right";
2222
+ }
2223
+ if (!sourceSide || !targetSide) continue;
2224
+ for (const index of indexes) {
2225
+ pairs[index] = {
2226
+ sourceAnchor: { side: sourceSide, offset: 0.5 },
2227
+ targetAnchor: { side: targetSide, offset: 0.5 }
2228
+ };
2229
+ }
2230
+ }
2231
+ }
2232
+ function distributeOffsets(pairs, edges, nodes, endpoint) {
2233
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
2234
+ const groups = /* @__PURE__ */ new Map();
2235
+ for (let index = 0; index < edges.length; index++) {
2236
+ const edge = edges[index];
2237
+ const anchor = endpoint === "source" ? pairs[index].sourceAnchor : pairs[index].targetAnchor;
2238
+ const nodeId = endpoint === "source" ? edge.source : edge.target;
2239
+ const key = `${nodeId}:${anchor.side}`;
2240
+ const group = groups.get(key) ?? [];
2241
+ group.push(index);
2242
+ groups.set(key, group);
2243
+ }
2244
+ for (const indexes of groups.values()) {
2245
+ if (indexes.length < 2) continue;
2246
+ const side = endpoint === "source" ? pairs[indexes[0]].sourceAnchor.side : pairs[indexes[0]].targetAnchor.side;
2247
+ indexes.sort((a, b) => {
2248
+ const oppositeA = nodeById.get(endpoint === "source" ? edges[a].target : edges[a].source);
2249
+ const oppositeB = nodeById.get(endpoint === "source" ? edges[b].target : edges[b].source);
2250
+ const axisA = side === "top" || side === "bottom" ? oppositeA?.x ?? 0 : oppositeA?.y ?? 0;
2251
+ const axisB = side === "top" || side === "bottom" ? oppositeB?.x ?? 0 : oppositeB?.y ?? 0;
2252
+ return axisA - axisB || a - b;
2253
+ });
2254
+ indexes.forEach((edgeIndex, rank) => {
2255
+ const anchor = endpoint === "source" ? pairs[edgeIndex].sourceAnchor : pairs[edgeIndex].targetAnchor;
2256
+ anchor.offset = (rank + 1) / (indexes.length + 1);
2257
+ });
2258
+ }
2259
+ }
2260
+ function asciiDiagramFromTemplateData(nodes, edges, options = {}) {
2261
+ const ids = new Set(nodes.map((n) => n.id));
2262
+ const outNodes = nodes.map((n) => {
2263
+ const { col, row } = canvasToAsciiCell(n.x, n.y);
2264
+ const labelLines = n.label.length > 0 ? n.label.split("\n") : [];
2265
+ const innerW = labelLines.reduce((w, l) => Math.max(w, l.length), 0);
2266
+ const wCols = typeof n.w === "number" ? Math.max(3, Math.round(n.w / ASCII_CHAR_W)) : innerW + 4;
2267
+ const hRows = typeof n.h === "number" ? Math.max(3, Math.round(n.h / ASCII_CHAR_H)) : labelLines.length + 2;
2268
+ return {
2269
+ id: n.id,
2270
+ label: n.label,
2271
+ col,
2272
+ row,
2273
+ wCols: Math.max(wCols, 3),
2274
+ hRows: Math.max(hRows, 3),
2275
+ ...n.container && ids.has(n.container) && n.container !== n.id ? { containerId: n.container } : {}
2276
+ };
2277
+ });
2278
+ return {
2279
+ nodes: outNodes,
2280
+ edges: edges.filter((e) => ids.has(e.source) && ids.has(e.target)).map((e) => ({
2281
+ source: e.source,
2282
+ target: e.target,
2283
+ ...e.label ? { label: e.label } : {},
2284
+ directed: e.directed !== false
2285
+ })),
2286
+ style: options.style ?? "unicode",
2287
+ warnings: []
2288
+ };
2289
+ }
2290
+ function asciiDiagramFromBlocks(children, options = {}) {
2291
+ const layout = computeDiagramLayout(children);
2292
+ const nodes = layout.nodes.map((n) => ({
2293
+ id: n.id,
2294
+ label: n.label,
2295
+ x: n.x,
2296
+ y: n.y
2297
+ }));
2298
+ const edges = layout.edges.map((e) => ({
2299
+ source: e.source,
2300
+ target: e.target,
2301
+ ...e.type ? { label: e.type } : {}
2302
+ }));
2303
+ const diagram = asciiDiagramFromTemplateData(nodes, edges, options);
2304
+ diagram.warnings.push(...layout.warnings);
2305
+ return diagram;
2306
+ }
2307
+
2308
+ // src/doc/asciiTimeline/mapping.ts
2309
+ function asciiTimelineToTemplateData(timeline) {
2310
+ const globalStart = timeline.tracks.length > 0 ? Math.min(...timeline.tracks.map((track) => track.startColumn)) : 0;
2311
+ const globalEnd = timeline.tracks.length > 0 ? Math.max(...timeline.tracks.map((track) => track.endColumn)) : 1;
2312
+ const span = Math.max(1, globalEnd - globalStart);
2313
+ const tracks = timeline.tracks.map((track) => ({
2314
+ id: track.id,
2315
+ ...track.label ? { label: track.label } : {},
2316
+ ...track.endLabel ? { endLabel: track.endLabel } : {},
2317
+ events: track.events.map(
2318
+ (event) => ({
2319
+ id: event.id,
2320
+ label: event.label,
2321
+ ...event.description ? { description: event.description } : {},
2322
+ position: clamp01((event.column - globalStart) / span),
2323
+ ...event.side ? { side: event.side } : {},
2324
+ ...event.descriptionSide ? { descriptionSide: event.descriptionSide } : {},
2325
+ ...event.callout === false ? { callout: false } : {},
2326
+ ...event.marker ? { marker: event.marker } : {}
2327
+ })
2328
+ )
2329
+ }));
2330
+ const links = timeline.links.map((link) => ({ ...link }));
2331
+ return { tracks, links };
2332
+ }
2333
+ function clamp01(value) {
2334
+ return Math.max(0, Math.min(1, value));
2335
+ }
2336
+
2337
+ // src/doc/treeview/mapping.ts
2338
+ function treeToTemplateData(tree) {
2339
+ return { items: tree.roots.map(toItem) };
2340
+ }
2341
+ function toItem(node) {
2342
+ return {
2343
+ id: node.id,
2344
+ label: node.label,
2345
+ ...node.isDir ? { isDir: true } : {},
2346
+ ...node.comment ? { comment: node.comment } : {},
2347
+ children: node.children.map(toItem)
2348
+ };
2349
+ }
2350
+ function treeFromTemplateData(items, options = {}) {
2351
+ const roots = items.map(fromItem);
2352
+ return { roots, style: options.style ?? "unicode", warnings: [] };
2353
+ }
2354
+ function fromItem(item) {
2355
+ const children = Array.isArray(item.children) ? item.children.map(fromItem) : [];
2356
+ return {
2357
+ id: typeof item.id === "string" && item.id ? item.id : "node",
2358
+ label: typeof item.label === "string" ? item.label : "",
2359
+ children,
2360
+ isDir: item.isDir === true || children.length > 0,
2361
+ ...item.comment ? { comment: item.comment } : {}
2362
+ };
2363
+ }
2364
+ function treeFromMarkdownList(list) {
2365
+ const usedIds = /* @__PURE__ */ new Map();
2366
+ const slugify2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "node";
2367
+ const idFor = (label) => {
2368
+ const base = slugify2(label.replace(/\/$/, ""));
2369
+ const count = usedIds.get(base) ?? 0;
2370
+ usedIds.set(base, count + 1);
2371
+ return count === 0 ? base : `${base}-${count + 1}`;
2372
+ };
2373
+ const itemToNode = (item) => {
2374
+ const labelNode = item.children.find((c) => c.type !== "list");
2375
+ const label = labelNode ? extractPlainText(labelNode).trim() : "";
2376
+ const childLists = item.children.filter((c) => c.type === "list");
2377
+ const children = [];
2378
+ for (const cl of childLists) for (const ci of cl.children) children.push(itemToNode(ci));
2379
+ return {
2380
+ id: idFor(label),
2381
+ label,
2382
+ children,
2383
+ isDir: label.endsWith("/") || children.length > 0
2384
+ };
2385
+ };
2386
+ return { roots: list.children.map(itemToNode), style: "unicode", warnings: [] };
2387
+ }
2388
+ function findFirstList(contents) {
2389
+ return contents?.find((n) => n.type === "list");
2390
+ }
2391
+
2392
+ // src/doc/templateInputs.ts
2393
+ var VIDEO_FILE_RE = /\.(?:webm|mp4|mov|m4v|ogv)(?:[?#].*)?$/i;
2394
+ function extractBodyPlainText(contents) {
2395
+ if (!contents || contents.length === 0) return "";
2396
+ return contents.filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((n) => extractPlainText(n)).join("\n").trim();
2397
+ }
2398
+ function extractListItems(contents) {
2399
+ return extractRichListItems(contents).map((item) => item.text);
2400
+ }
2401
+ function escapeInlineHtml(value) {
2402
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2403
+ }
2404
+ function renderInlineHtml(nodes) {
2405
+ return nodes.map((node) => {
2406
+ switch (node.type) {
2407
+ case "text":
2408
+ return escapeInlineHtml(node.value);
2409
+ case "emphasis":
2410
+ return `<em>${renderInlineHtml(node.children)}</em>`;
2411
+ case "strong":
2412
+ return `<strong>${renderInlineHtml(node.children)}</strong>`;
2413
+ case "delete":
2414
+ return `<del>${renderInlineHtml(node.children)}</del>`;
2415
+ case "inlineCode":
2416
+ return `<code>${escapeInlineHtml(node.value)}</code>`;
2417
+ case "link": {
2418
+ const label = renderInlineHtml(node.children);
2419
+ const href = sanitizeUrl(node.url, "link");
2420
+ if (!href) return label;
2421
+ const title = node.title ? ` title="${escapeInlineHtml(node.title)}"` : "";
2422
+ return `<a href="${escapeInlineHtml(href)}"${title}>${label}</a>`;
2423
+ }
2424
+ case "break":
2425
+ return "<br>";
2426
+ case "image":
2427
+ return escapeInlineHtml(node.alt ?? "");
2428
+ case "inlineMath":
2429
+ return escapeInlineHtml(`$${node.value}$`);
2430
+ case "footnoteReference":
2431
+ return escapeInlineHtml(`[^${node.label ?? node.identifier}]`);
2432
+ case "linkReference":
2433
+ case "textDirective":
2434
+ return renderInlineHtml(node.children);
2435
+ case "imageReference":
2436
+ return escapeInlineHtml(node.alt ?? "");
2437
+ case "mention":
2438
+ return escapeInlineHtml(`@${node.displayName}`);
2439
+ case "inlineIcon":
2440
+ return escapeInlineHtml(`{[${node.token}]}`);
2441
+ case "htmlInline":
2442
+ return escapeInlineHtml(node.rawHtml);
2443
+ }
2444
+ }).join("");
2445
+ }
2446
+ function renderListItemHtml(markdown) {
2447
+ return markdown.map(
2448
+ (node) => node.type === "paragraph" || node.type === "heading" ? renderInlineHtml(node.children) : escapeInlineHtml(extractPlainText(node))
2449
+ ).join("<br>");
2450
+ }
2451
+ function extractRichListItems(contents) {
2452
+ if (!contents) return [];
2453
+ const items = [];
2454
+ for (const node of contents) {
2455
+ if (node.type !== "list") continue;
2456
+ for (const item of node.children) {
2457
+ const text = extractPlainText(item).trim();
2458
+ if (!text) continue;
2459
+ const html = renderListItemHtml(item.children);
2460
+ const plainHtml = escapeInlineHtml(text).replace(/\r?\n/g, "<br>");
2461
+ items.push({
2462
+ text,
2463
+ markdown: item.children,
2464
+ ...html && html !== plainHtml ? { html } : {}
2465
+ });
2466
+ }
2467
+ }
2468
+ return items;
2469
+ }
2470
+ function parseDim(raw) {
2471
+ if (raw === void 0) return void 0;
2472
+ const n = parseFloat(raw);
2473
+ return Number.isFinite(n) && n > 0 ? n : void 0;
2474
+ }
2475
+ function extractImages(contents, limit = Infinity) {
2476
+ if (!contents || contents.length === 0) return [];
2477
+ const found = [];
2478
+ function fromHtml(nodes) {
2479
+ for (const node of nodes) {
2480
+ if (found.length >= limit) return;
2481
+ if (!node || typeof node !== "object") continue;
2482
+ const n = node;
2483
+ if (n.type === "htmlElement" && n.tagName === "img") {
2484
+ const attrs = n.attributes;
2485
+ if (attrs && typeof attrs.src === "string" && attrs.src) {
2486
+ found.push({
2487
+ src: attrs.src,
2488
+ alt: typeof attrs.alt === "string" ? attrs.alt : "",
2489
+ width: parseDim(attrs.width),
2490
+ height: parseDim(attrs.height)
2491
+ });
2492
+ }
2493
+ }
2494
+ if (Array.isArray(n.children)) fromHtml(n.children);
2495
+ }
2496
+ }
2497
+ function walk(node) {
2498
+ if (found.length >= limit) return;
2499
+ if (!node || typeof node !== "object") return;
2500
+ const n = node;
2501
+ if (n.type === "image" && typeof n.url === "string" && n.url) {
2502
+ found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
2503
+ return;
2504
+ }
2505
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
2506
+ fromHtml(n.htmlChildren);
2507
+ }
2508
+ if (Array.isArray(n.children)) {
2509
+ for (const child of n.children) walk(child);
2510
+ }
2511
+ }
2512
+ for (const node of contents) {
2513
+ if (found.length >= limit) break;
2514
+ walk(node);
2515
+ }
2516
+ return found;
2517
+ }
2518
+ function extractFirstImage(contents) {
2519
+ return extractImages(contents, 1)[0] ?? null;
2520
+ }
2521
+ function extractEmbeddedVideos(contents, limit = Infinity) {
2522
+ if (!contents || contents.length === 0) return [];
2523
+ const found = [];
2524
+ const seen = /* @__PURE__ */ new Set();
2525
+ const add = (video) => {
2526
+ if (!video.src || found.length >= limit || seen.has(video.src)) return;
2527
+ seen.add(video.src);
2528
+ found.push(video);
2529
+ };
2530
+ function nestedSource(children) {
2531
+ if (!Array.isArray(children)) return void 0;
2532
+ for (const child of children) {
2533
+ if (!child || typeof child !== "object") continue;
2534
+ const node = child;
2535
+ if (node.tagName === "source") {
2536
+ const attrs = node.attributes;
2537
+ if (typeof attrs?.src === "string" && attrs.src) return attrs.src;
2538
+ }
2539
+ const nested = nestedSource(node.children);
2540
+ if (nested) return nested;
2541
+ }
2542
+ return void 0;
2543
+ }
2544
+ function walk(node) {
2545
+ if (found.length >= limit || !node || typeof node !== "object") return;
2546
+ const n = node;
2547
+ if (n.type === "htmlElement" && n.tagName === "video") {
2548
+ const attrs = n.attributes;
2549
+ const src = attrs?.src || nestedSource(n.children);
2550
+ if (src) {
2551
+ add({
2552
+ src,
2553
+ ...attrs?.poster ? { posterSrc: attrs.poster } : {},
2554
+ alt: attrs?.["aria-label"] || attrs?.title || attrs?.alt || ""
2555
+ });
2556
+ }
2557
+ } else if ((n.type === "link" || n.type === "image") && typeof n.url === "string" && VIDEO_FILE_RE.test(n.url)) {
2558
+ add({
2559
+ src: n.url,
2560
+ alt: typeof n.alt === "string" ? n.alt : extractPlainText(n).trim()
2561
+ });
2562
+ }
2563
+ if (Array.isArray(n.children)) n.children.forEach(walk);
2564
+ if (Array.isArray(n.htmlChildren)) n.htmlChildren.forEach(walk);
2565
+ }
2566
+ contents.forEach(walk);
2567
+ return found;
2568
+ }
2569
+ function extractFirstEmbeddedVideo(contents) {
2570
+ return extractEmbeddedVideos(contents, 1)[0] ?? null;
2571
+ }
2572
+ function extractTableFromContents(contents) {
2573
+ if (!contents) return null;
2574
+ for (const node of contents) {
2575
+ if (node.type === "table") {
2576
+ const table = node;
2577
+ const [headerRow, ...bodyRows] = table.children;
2578
+ if (!headerRow) return null;
2579
+ const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
2580
+ const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
2581
+ return { headers, rows, align: table.align };
2582
+ }
2583
+ }
2584
+ return null;
2585
+ }
2586
+ function extractBlockquoteText(contents) {
2587
+ if (!contents) return "";
2588
+ for (const node of contents) {
2589
+ if (node.type === "blockquote") return extractPlainText(node).trim();
2590
+ }
2591
+ return "";
2592
+ }
2593
+ function firstParagraph(contents) {
2594
+ if (!contents) return null;
2595
+ const index = contents.findIndex((node) => node.type === "paragraph");
2596
+ if (index < 0) return null;
2597
+ return {
2598
+ node: contents[index],
2599
+ index
2600
+ };
2601
+ }
2602
+ function leadingStrongStat(paragraph) {
2603
+ for (const child of paragraph.children) {
2604
+ if (child.type === "text" && child.value.trim() === "") continue;
2605
+ if (child.type !== "strong") return null;
2606
+ const text = extractPlainText(child).trim();
2607
+ const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
2608
+ return match?.[0].trim() || null;
2609
+ }
2610
+ return null;
2611
+ }
2612
+ function removeStatPhrase(text, start, end) {
2613
+ const before = text.slice(0, start).trim();
2614
+ let after = text.slice(end);
2615
+ if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, "");
2616
+ else after = after.trim();
2617
+ return [before, after].filter(Boolean).join(" ").trim();
2618
+ }
2619
+ function bodyWithoutParagraphStat(contents, paragraphIndex, paragraphText, start, end) {
2620
+ if (!contents) return "";
2621
+ return contents.map(
2622
+ (node, index) => index === paragraphIndex ? removeStatPhrase(paragraphText, start, end) : extractPlainText(node).trim()
2623
+ ).filter(Boolean).join("\n").trim();
2624
+ }
2625
+ function deriveStatHighlightInputs(headingText, contents, bodyText3) {
2626
+ const paragraph = firstParagraph(contents);
2627
+ if (paragraph) {
2628
+ const paragraphText = extractPlainText(paragraph.node).trim();
2629
+ const strongStat = leadingStrongStat(paragraph.node);
2630
+ if (strongStat) {
2631
+ const start = paragraphText.indexOf(strongStat);
2632
+ const description = bodyWithoutParagraphStat(
2633
+ contents,
2634
+ paragraph.index,
2635
+ paragraphText,
2636
+ start,
2637
+ start + strongStat.length
2638
+ );
2639
+ return {
2640
+ stat: strongStat,
2641
+ description: description || headingText || strongStat
2642
+ };
2643
+ }
2644
+ }
2645
+ const trimmedHeading = headingText.trim();
2646
+ const headingStat = matchNumberHighlight(trimmedHeading);
2647
+ if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
2648
+ return { stat: headingText, description: bodyText3 || headingText };
2649
+ }
2650
+ if (paragraph) {
2651
+ const paragraphText = extractPlainText(paragraph.node).trim();
2652
+ const bodyStat = matchNumberHighlight(paragraphText);
2653
+ if (bodyStat) {
2654
+ const description = bodyWithoutParagraphStat(
2655
+ contents,
2656
+ paragraph.index,
2657
+ paragraphText,
2658
+ bodyStat.index,
2659
+ bodyStat.end
2660
+ );
2661
+ return {
2662
+ stat: bodyStat.value,
2663
+ description: description || headingText || bodyStat.value
2664
+ };
2665
+ }
2666
+ }
2667
+ return { stat: headingText, description: bodyText3 || headingText };
2668
+ }
2669
+ function normalizeCoverageText(value) {
2670
+ return value.replace(/\s+/g, " ").trim();
2671
+ }
2672
+ function autoTemplatePreservesContent(templateName, contents) {
2673
+ const nodes = contents ?? [];
2674
+ switch (templateName) {
2675
+ case "quote":
2676
+ return nodes.length === 1 && nodes[0]?.type === "blockquote";
2677
+ case "dataTable":
2678
+ return nodes.length === 1 && nodes[0]?.type === "table";
2679
+ case "list":
2680
+ return nodes.length === 1 && nodes[0]?.type === "list";
2681
+ case "diagram":
2682
+ case "timeline":
2683
+ return nodes.length === 1 && nodes[0]?.type === "code";
2684
+ case "tree":
2685
+ return nodes.length === 1 && (nodes[0]?.type === "code" || nodes[0]?.type === "list");
2686
+ case "photoGrid": {
2687
+ const images = extractImages(nodes);
2688
+ if (images.length < 2 || images.length > 4) return false;
2689
+ const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
2690
+ const representedText = normalizeCoverageText(
2691
+ images.map((image) => image.alt).filter(Boolean).join(" ")
2692
+ );
2693
+ return bodyText3 === "" || bodyText3 === representedText;
2694
+ }
2695
+ case "videoWithCaption": {
2696
+ const videos = extractEmbeddedVideos(nodes);
2697
+ if (videos.length !== 1) return false;
2698
+ const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
2699
+ return bodyText3 === normalizeCoverageText(videos[0]?.alt ?? "");
2700
+ }
2701
+ case "leftFeature":
2702
+ case "rightFeature":
2703
+ case "statHighlight":
2704
+ return true;
2705
+ default:
2706
+ return false;
2707
+ }
2708
+ }
2709
+ function deriveTemplateInputs(templateName, headingText, contents, options = {}) {
2710
+ const placeholders = options.placeholders === true;
2711
+ const bodyText3 = extractBodyPlainText(contents);
2712
+ switch (templateName) {
2713
+ case "statHighlight": {
2714
+ const inputs = deriveStatHighlightInputs(headingText, contents, bodyText3);
2715
+ if (options.preserveSourceHeading && headingText && inputs.stat !== headingText && inputs.description !== headingText) {
2716
+ return {
2717
+ ...inputs,
2718
+ description: headingText,
2719
+ ...inputs.description ? { detail: inputs.description } : {}
2720
+ };
2721
+ }
2722
+ return inputs;
2723
+ }
2724
+ case "quote": {
2725
+ const quote = extractBlockquoteText(contents) || bodyText3;
2726
+ if (quote) return { quote, ...headingText ? { title: headingText } : {} };
2727
+ return { quote: headingText };
2728
+ }
2729
+ case "fullBleedQuote":
2730
+ case "pullQuote": {
2731
+ const text = extractBlockquoteText(contents) || bodyText3 || headingText;
2732
+ return { text };
2733
+ }
2734
+ case "factCard":
2735
+ return { fact: headingText, explanation: bodyText3 || headingText };
2736
+ case "comparisonBar":
2737
+ return placeholders ? { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 } : null;
2738
+ case "list": {
2739
+ const items = extractListItems(contents);
2740
+ if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
2741
+ return placeholders ? { items: ["Item 1", "Item 2", "Item 3"] } : null;
2742
+ }
2743
+ case "definitionCard":
2744
+ return { term: headingText, definition: bodyText3 || headingText };
2745
+ case "dateEvent":
2746
+ return { date: headingText, description: bodyText3 || headingText };
2747
+ case "dataTable": {
2748
+ const tableData = extractTableFromContents(contents);
2749
+ if (tableData) return { ...tableData, ...headingText ? { title: headingText } : {} };
2750
+ return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
2751
+ }
2752
+ case "barChart":
2753
+ case "columnChart":
2754
+ case "pieChart":
2755
+ case "donutChart":
2756
+ case "lineChart":
2757
+ case "areaChart":
2758
+ case "scatterChart": {
2759
+ const tableData = extractTableFromContents(contents);
2760
+ if (tableData) {
2761
+ return {
2762
+ headers: tableData.headers,
2763
+ rows: tableData.rows,
2764
+ ...headingText ? { title: headingText } : {}
2765
+ };
2766
+ }
2767
+ return placeholders && !bodyText3.trim() ? {
2768
+ headers: ["Quarter", "Revenue", "Costs"],
2769
+ rows: [
2770
+ ["Q1", "40", "28"],
2771
+ ["Q2", "55", "31"],
2772
+ ["Q3", "48", "30"],
2773
+ ["Q4", "62", "35"]
2774
+ ],
2775
+ ...headingText ? { title: headingText } : {}
2776
+ } : null;
2777
+ }
2778
+ case "imageWithCaption": {
2779
+ const img = extractFirstImage(contents);
2780
+ if (!img) return placeholders ? { caption: headingText } : null;
2781
+ return { imageSrc: img.src, imageAlt: img.alt || headingText, caption: headingText };
2782
+ }
2783
+ case "photoGrid": {
2784
+ const images = extractImages(contents, 4);
2785
+ if (images.length < 2) return placeholders ? {} : null;
2786
+ return {
2787
+ images: images.map((i) => ({ src: i.src, alt: i.alt })),
2788
+ caption: headingText
2789
+ };
2790
+ }
2791
+ case "videoWithCaption": {
2792
+ const video = extractFirstEmbeddedVideo(contents);
2793
+ if (!video) return placeholders ? { caption: headingText } : null;
2794
+ return {
2795
+ videoSrc: video.src,
2796
+ posterSrc: video.posterSrc,
2797
+ videoAlt: video.alt || headingText,
2798
+ caption: headingText
2799
+ };
2800
+ }
2801
+ case "leftFeature":
2802
+ case "rightFeature": {
2803
+ const img = extractFirstImage(contents);
2804
+ if (!img && !placeholders) return null;
2805
+ return {
2806
+ imageSrc: img?.src ?? "",
2807
+ imageAlt: img?.alt || headingText,
2808
+ imageWidth: img?.width,
2809
+ imageHeight: img?.height,
2810
+ title: headingText,
2811
+ body: bodyText3
2812
+ };
2813
+ }
2814
+ case "diagram": {
2815
+ const fences = (contents ?? []).filter((n) => n.type === "code");
2816
+ const fence = fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : void 0;
2817
+ const detection = fence ? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) }) : void 0;
2818
+ if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
2819
+ const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
2820
+ return { nodes, edges, ...headingText ? { title: headingText } : {} };
2821
+ }
2822
+ case "timeline": {
2823
+ const fences = (contents ?? []).filter((n) => n.type === "code");
2824
+ const fence = fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang) ? fences[0] : void 0;
2825
+ const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : void 0;
2826
+ if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
2827
+ const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
2828
+ return { tracks, links, ...headingText ? { title: headingText } : {} };
2829
+ }
2830
+ case "tree": {
2831
+ const fences = (contents ?? []).filter((n) => n.type === "code");
2832
+ const fence = fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : void 0;
2833
+ if (fence) {
2834
+ const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
2835
+ if (detection.isTree && detection.tree) {
2836
+ const { items } = treeToTemplateData(detection.tree);
2837
+ return { items, ...headingText ? { title: headingText } : {} };
2838
+ }
2839
+ }
2840
+ const list = findFirstList(contents);
2841
+ if (list) {
2842
+ const { items } = treeToTemplateData(treeFromMarkdownList(list));
2843
+ if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
2844
+ }
2845
+ return placeholders ? {} : null;
2846
+ }
2847
+ default:
2848
+ return placeholders ? {} : null;
2849
+ }
2850
+ }
2851
+
2024
2852
  // src/doc/templates/listBlock.ts
2025
2853
  var LIST_ITEM_LINE_HEIGHT = 1.2;
2026
2854
  var LIST_ITEM_GAP_PX = 18;
@@ -2049,6 +2877,7 @@ function estimateWrappedLineCount(text, fontSize, maxWidth) {
2049
2877
  function listBlock(input, context) {
2050
2878
  const { title, accentImage } = input;
2051
2879
  const items = Array.isArray(input.items) ? input.items : [];
2880
+ const richItems = extractRichListItems(context.block?.contents);
2052
2881
  const { theme } = context;
2053
2882
  const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2054
2883
  const titleFontSize = themedFontSize(44, context, true);
@@ -2112,6 +2941,7 @@ function listBlock(input, context) {
2112
2941
  let itemY = startY;
2113
2942
  for (let i = 0; i < items.length; i++) {
2114
2943
  const itemText = items[i];
2944
+ const itemHtml = richItems[i]?.text === itemText ? richItems[i]?.html : void 0;
2115
2945
  const lineCount = estimateWrappedLineCount(itemText, itemFontSize, bodyWidthPx);
2116
2946
  const animation = themedEntrance(context, "text", {
2117
2947
  type: "fadeIn",
@@ -2144,6 +2974,7 @@ function listBlock(input, context) {
2144
2974
  id: `item-${i}`,
2145
2975
  content: {
2146
2976
  text: itemText,
2977
+ ...itemHtml ? { html: itemHtml } : {},
2147
2978
  style: {
2148
2979
  fontSize: itemFontSize,
2149
2980
  fontFamily: getThemeFont(context, "body"),
@@ -2985,83 +3816,6 @@ function wrappedLineCount(text, fontSize, maxWidth) {
2985
3816
  return Math.max(1, lines);
2986
3817
  }
2987
3818
 
2988
- // src/doc/templates/diagramLayout.ts
2989
- var DEFAULT_OPTS = {
2990
- gapX: 260,
2991
- gapY: 140,
2992
- gridPad: 80,
2993
- gridOriginX: 80,
2994
- gridOriginY: 80
2995
- };
2996
- function computeDiagramLayout(children, options = {}) {
2997
- const opts = { ...DEFAULT_OPTS, ...options };
2998
- const warnings = [];
2999
- const entries = [];
3000
- const unpositionedIdx = [];
3001
- for (let i = 0; i < children.length; i++) {
3002
- const c = children[i];
3003
- const hasX = typeof c.x === "number";
3004
- const hasY = typeof c.y === "number";
3005
- if (hasX && hasY) {
3006
- entries.push({ child: c, pinned: true, x: c.x, y: c.y });
3007
- } else {
3008
- entries.push({ child: c, pinned: false, x: 0, y: 0 });
3009
- unpositionedIdx.push(i);
3010
- }
3011
- }
3012
- if (unpositionedIdx.length > 0) {
3013
- const cols = Math.max(1, Math.ceil(Math.sqrt(unpositionedIdx.length)));
3014
- let baseX = opts.gridOriginX;
3015
- let baseY = opts.gridOriginY;
3016
- const pinned = entries.filter((e) => e.pinned);
3017
- if (pinned.length > 0) {
3018
- const minX = Math.min(...pinned.map((e) => e.x));
3019
- const maxY = Math.max(...pinned.map((e) => e.y));
3020
- baseX = minX;
3021
- baseY = maxY + opts.gridPad + opts.gapY;
3022
- }
3023
- unpositionedIdx.forEach((entryIdx, k) => {
3024
- const col = k % cols;
3025
- const row = Math.floor(k / cols);
3026
- entries[entryIdx].x = baseX + col * opts.gapX;
3027
- entries[entryIdx].y = baseY + row * opts.gapY;
3028
- });
3029
- }
3030
- const nodes = entries.map((e) => ({
3031
- id: e.child.id,
3032
- label: e.child.title ?? e.child.id,
3033
- x: e.x,
3034
- y: e.y,
3035
- pinned: e.pinned,
3036
- ...e.child.classes && e.child.classes.length > 0 ? { classes: e.child.classes } : {}
3037
- }));
3038
- const nodeIds = new Set(nodes.map((n) => n.id));
3039
- const edges = [];
3040
- const seenEdgeIds = /* @__PURE__ */ new Set();
3041
- for (const child of children) {
3042
- if (!child.connectsTo) continue;
3043
- for (const conn of child.connectsTo) {
3044
- if (!nodeIds.has(conn.target)) {
3045
- warnings.push(`Edge from "${child.id}" to "${conn.target}" dropped: no such sibling node`);
3046
- continue;
3047
- }
3048
- const edgeId = makeEdgeId(child.id, conn);
3049
- if (seenEdgeIds.has(edgeId)) continue;
3050
- seenEdgeIds.add(edgeId);
3051
- edges.push({
3052
- id: edgeId,
3053
- source: child.id,
3054
- target: conn.target,
3055
- ...conn.type ? { type: conn.type } : {}
3056
- });
3057
- }
3058
- }
3059
- return { nodes, edges, warnings };
3060
- }
3061
- function makeEdgeId(source, conn) {
3062
- return conn.type ? `${source}->${conn.target}:${conn.type}` : `${source}->${conn.target}`;
3063
- }
3064
-
3065
3819
  // src/doc/templates/diagramBlock.ts
3066
3820
  var NODE_WIDTH = 180;
3067
3821
  var NODE_HEIGHT = 64;
@@ -3135,7 +3889,7 @@ function nodesFromTemplateData(input) {
3135
3889
  }
3136
3890
  function diagramBlock(input, context) {
3137
3891
  const { theme, viewport, children = [] } = context;
3138
- const colors = resolveColorScheme(context, input.colorScheme ?? "blue");
3892
+ const colors = input.colorScheme ? resolveColorScheme(context, input.colorScheme) : void 0;
3139
3893
  const resolved = children.length > 0 ? nodesFromChildren(context) : input.nodes && input.nodes.length > 0 ? nodesFromTemplateData(input) : { nodes: [], edges: [] };
3140
3894
  if (resolved.nodes.length === 0) {
3141
3895
  return [
@@ -3205,9 +3959,9 @@ function diagramBlock(input, context) {
3205
3959
  id: `node-card-${node.id}`,
3206
3960
  content: {
3207
3961
  shape: "rect",
3208
- fill: colors.bg ?? theme.colors.backgroundLight,
3962
+ fill: colors?.bg ?? theme.colors.backgroundLight,
3209
3963
  fillOpacity: 0.25,
3210
- stroke: colors.text ?? theme.colors.primary,
3964
+ stroke: colors?.text ?? theme.colors.primary,
3211
3965
  strokeWidth: Math.max(1, strokeW - 1),
3212
3966
  borderRadius: 10
3213
3967
  },
@@ -3236,7 +3990,7 @@ function diagramBlock(input, context) {
3236
3990
  id: `edge-${edge.id}`,
3237
3991
  content: {
3238
3992
  d: connectorPath(edge.routing ?? input.edgeStyle ?? "curved", start, end),
3239
- stroke: colors.text ?? theme.colors.primary,
3993
+ stroke: colors?.text ?? theme.colors.primary,
3240
3994
  strokeWidth: strokeW,
3241
3995
  fill: "none",
3242
3996
  ...edgeDash ? { dasharray: edgeDash } : {},
@@ -3274,8 +4028,8 @@ function diagramBlock(input, context) {
3274
4028
  id: `node-card-${node.id}`,
3275
4029
  content: {
3276
4030
  shape: "rect",
3277
- fill: colors.bg ?? theme.colors.backgroundLight,
3278
- stroke: colors.text ?? theme.colors.primary,
4031
+ fill: colors?.bg ?? theme.colors.backgroundLight,
4032
+ stroke: colors?.text ?? theme.colors.primary,
3279
4033
  strokeWidth: strokeW,
3280
4034
  borderRadius: input.nodeShape === "pill" ? t.h / 2 : 10
3281
4035
  },
@@ -3293,7 +4047,7 @@ function diagramBlock(input, context) {
3293
4047
  fontSize: fit.fontSize,
3294
4048
  fontFamily: getThemeFont(context, "body"),
3295
4049
  fontWeight: "bold",
3296
- color: colors.text ?? theme.colors.text,
4050
+ color: colors?.text ?? theme.colors.text,
3297
4051
  textAlign: "center",
3298
4052
  lineHeight: DIAGRAM_LABEL_LINE_HEIGHT
3299
4053
  }
@@ -3425,7 +4179,7 @@ function treeBlock(input, context) {
3425
4179
  }
3426
4180
 
3427
4181
  // src/doc/templates/timelineBlock.ts
3428
- var clamp01 = (value) => Math.max(0, Math.min(1, value));
4182
+ var clamp012 = (value) => Math.max(0, Math.min(1, value));
3429
4183
  var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
3430
4184
  function safeId(value) {
3431
4185
  return value.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "item";
@@ -3452,7 +4206,7 @@ function coerceTracks(raw) {
3452
4206
  events.push({
3453
4207
  id: eventId,
3454
4208
  label: event.label,
3455
- position: clamp01(position),
4209
+ position: clamp012(position),
3456
4210
  ...typeof event.description === "string" && event.description.trim() ? { description: event.description.trim() } : {},
3457
4211
  ...side ? { side } : {},
3458
4212
  ...descriptionSide ? { descriptionSide } : {},
@@ -3583,7 +4337,7 @@ function timelineBlock(input, context) {
3583
4337
  const eventFontSize = Math.min(baseEventFontSize, 24 * minViewportScale);
3584
4338
  const descriptionFontSize = Math.min(baseDescriptionFontSize, 17 * minViewportScale);
3585
4339
  const trackMarkerRadii = tracks.map((track) => {
3586
- const positions = track.events.map((event) => clamp01(event.position)).sort((a, b) => a - b);
4340
+ const positions = track.events.map((event) => clamp012(event.position)).sort((a, b) => a - b);
3587
4341
  let minGapPx = Number.POSITIVE_INFINITY;
3588
4342
  for (let index = 1; index < positions.length; index++) {
3589
4343
  minGapPx = Math.min(minGapPx, (positions[index] - positions[index - 1]) * axisWidth);
@@ -3662,7 +4416,7 @@ function timelineBlock(input, context) {
3662
4416
  event,
3663
4417
  trackIndex,
3664
4418
  eventIndex,
3665
- x: axisStartX + axisWidth * clamp01(event.position),
4419
+ x: axisStartX + axisWidth * clamp012(event.position),
3666
4420
  y
3667
4421
  };
3668
4422
  eventPoints.push(point);
@@ -4860,1119 +5614,1580 @@ function resolveLayer(layer, block, images, item) {
4860
5614
 
4861
5615
  // src/doc/templates/customTemplate.ts
4862
5616
  function makeCustomTemplateFn(def) {
4863
- return (input, context) => {
4864
- const block = context.block ?? input;
4865
- if (!block) return def.layers.slice();
4866
- return resolveTokens(def.layers, block);
4867
- };
4868
- }
4869
-
4870
- // src/doc/templates/registry.ts
4871
- var templateRegistry = {
4872
- title: titleBlock,
4873
- sectionHeader,
4874
- content: contentBlock,
4875
- statHighlight,
4876
- quote: quoteBlock,
4877
- factCard,
4878
- twoColumn,
4879
- dateEvent,
4880
- imageWithCaption,
4881
- leftFeature,
4882
- rightFeature,
4883
- map: mapBlock,
4884
- fullBleedQuote,
4885
- list: listBlock,
4886
- photoGrid,
4887
- definitionCard,
4888
- comparisonBar,
4889
- pullQuote,
4890
- videoWithCaption,
4891
- videoPullQuote,
4892
- dataTable,
4893
- diagram: diagramBlock,
4894
- tree: treeBlock,
4895
- timeline: timelineBlock,
4896
- layout: layoutBlock,
4897
- drawing: drawingBlock
4898
- };
4899
- function buildRegistry(custom) {
4900
- const merged = {
4901
- ...templateRegistry
4902
- };
4903
- if (custom) {
4904
- for (const definition of custom) {
4905
- if (merged[definition.name]) continue;
4906
- merged[definition.name] = makeCustomTemplateFn(
4907
- definition
4908
- );
4909
- }
4910
- }
4911
- return merged;
4912
- }
4913
-
4914
- // src/doc/utils/applyRenderStyle.ts
4915
- var AMBIENT_VARIANTS = [
4916
- { type: "slowZoom", direction: "in" },
4917
- { type: "slowZoom", direction: "out" },
4918
- { type: "slowZoom", panDirection: "left" },
4919
- { type: "slowZoom", panDirection: "right" }
4920
- ];
4921
- var MIN_AMBIENT_DURATION_SECONDS = 8;
4922
- function isZeroish(v) {
4923
- return v === 0 || v === "0" || v === "0%";
4924
- }
4925
- function isFullBleedCoverMedia(layer) {
4926
- if (layer.type !== "image" && layer.type !== "video") return false;
4927
- const { position } = layer;
4928
- if (!isZeroish(position.x) || !isZeroish(position.y)) return false;
4929
- if (position.width !== "100%" || position.height !== "100%") return false;
4930
- const fit = layer.content.fit;
4931
- return fit === void 0 || fit === "cover";
4932
- }
4933
- function themeWantsAmbientMotion(theme) {
4934
- return theme.renderStyle.ambientMotion === true;
4935
- }
4936
- function applyRenderStyleToLayers(layers, block, theme) {
4937
- const speed = theme.style.animationSpeed ?? 1;
4938
- const ambient = themeWantsAmbientMotion(theme);
4939
- if (speed === 1 && !ambient) return layers;
4940
- return layers.map((layer) => {
4941
- let animation = layer.animation;
4942
- if (!animation && ambient && isFullBleedCoverMedia(layer)) {
4943
- const rng = new SeededRandom(hashString(`${block.id}:${layer.id}`));
4944
- const variant = rng.pickRequired(AMBIENT_VARIANTS);
4945
- animation = {
4946
- ...variant,
4947
- duration: Math.max(MIN_AMBIENT_DURATION_SECONDS, block.duration ?? 0)
4948
- };
4949
- }
4950
- if (!animation || animation.type === "none") {
4951
- return animation === layer.animation ? layer : { ...layer, animation };
4952
- }
4953
- if (speed !== 1) {
4954
- animation = {
4955
- ...animation,
4956
- ...animation.duration != null ? { duration: animation.duration * speed } : {},
4957
- ...animation.delay != null ? { delay: animation.delay * speed } : {}
4958
- };
4959
- }
4960
- return animation === layer.animation ? layer : { ...layer, animation };
4961
- });
4962
- }
4963
-
4964
- // src/doc/utils/nearest.ts
4965
- function levenshtein(a, b) {
4966
- if (a === b) return 0;
4967
- const m = a.length;
4968
- const n = b.length;
4969
- if (m === 0) return n;
4970
- if (n === 0) return m;
4971
- let prev = new Array(n + 1);
4972
- let curr = new Array(n + 1);
4973
- for (let j = 0; j <= n; j++) prev[j] = j;
4974
- for (let i = 1; i <= m; i++) {
4975
- curr[0] = i;
4976
- for (let j = 1; j <= n; j++) {
4977
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4978
- curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
4979
- }
4980
- [prev, curr] = [curr, prev];
5617
+ return (input, context) => {
5618
+ const block = context.block ?? input;
5619
+ if (!block) return def.layers.slice();
5620
+ return resolveTokens(def.layers, block);
5621
+ };
5622
+ }
5623
+
5624
+ // src/doc/templates/chart/parse.ts
5625
+ var NUMERIC_COLUMN_THRESHOLD = 0.6;
5626
+ function parseCellNumber(raw) {
5627
+ if (typeof raw !== "string") return null;
5628
+ let text = raw.trim();
5629
+ if (!text) return null;
5630
+ let negative = false;
5631
+ const accounting = /^\((.*)\)$/.exec(text);
5632
+ if (accounting) {
5633
+ negative = true;
5634
+ text = accounting[1].trim();
5635
+ }
5636
+ text = text.replace(/^([+-]?)\s*[$€£¥]\s*/, "$1");
5637
+ text = text.replace(/%$/, "").trim();
5638
+ text = text.replace(/,/g, "");
5639
+ if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(text)) {
5640
+ text = text.replace(/ /g, "");
5641
+ }
5642
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text)) return null;
5643
+ const value = Number(text);
5644
+ if (!Number.isFinite(value)) return null;
5645
+ return negative ? -value : value;
5646
+ }
5647
+ function isNumericColumn(rows, colIndex) {
5648
+ let nonEmpty = 0;
5649
+ let numeric = 0;
5650
+ for (const row of rows) {
5651
+ const cell = (row[colIndex] ?? "").trim();
5652
+ if (!cell) continue;
5653
+ nonEmpty += 1;
5654
+ if (parseCellNumber(cell) !== null) numeric += 1;
5655
+ }
5656
+ return numeric > 0 && numeric / Math.max(1, nonEmpty) >= NUMERIC_COLUMN_THRESHOLD;
5657
+ }
5658
+ function resolveColumnRef(ref, headers) {
5659
+ const text = String(ref).trim();
5660
+ if (!text) return null;
5661
+ const lower = text.toLowerCase();
5662
+ const byName = headers.findIndex((h) => h.trim().toLowerCase() === lower);
5663
+ if (byName >= 0) return byName;
5664
+ if (/^\d+$/.test(text)) {
5665
+ const index = Number(text);
5666
+ if (index < headers.length) return index;
4981
5667
  }
4982
- return prev[n];
5668
+ return null;
4983
5669
  }
4984
- function nearestName(input, candidates, options) {
4985
- const maxDistance = options?.maxDistance ?? 2;
4986
- let best;
4987
- let bestDist = Infinity;
4988
- const lower = input.toLowerCase();
4989
- for (const candidate of candidates) {
4990
- const dist = levenshtein(lower, candidate.toLowerCase());
4991
- if (dist < bestDist) {
4992
- bestDist = dist;
4993
- best = candidate;
5670
+ function buildChartData(table, options = {}) {
5671
+ const headers = Array.isArray(table.headers) ? table.headers : [];
5672
+ const rows = Array.isArray(table.rows) ? table.rows : [];
5673
+ if (headers.length === 0 || rows.length === 0) return null;
5674
+ const warnings = [];
5675
+ let labelIndex = 0;
5676
+ if (options.labelColumn !== void 0 && String(options.labelColumn).trim() !== "") {
5677
+ const resolved = resolveColumnRef(String(options.labelColumn), headers);
5678
+ if (resolved === null) {
5679
+ warnings.push(
5680
+ `labelColumn "${options.labelColumn}" matches no column; using the first column`
5681
+ );
5682
+ } else {
5683
+ labelIndex = resolved;
5684
+ }
5685
+ }
5686
+ let valueIndexes = [];
5687
+ const requested = (options.valueColumns ?? []).map((ref) => String(ref)).filter((ref) => ref.trim() !== "");
5688
+ if (requested.length > 0) {
5689
+ for (const ref of requested) {
5690
+ const resolved = resolveColumnRef(ref, headers);
5691
+ if (resolved === null) {
5692
+ warnings.push(`valueColumns entry "${ref}" matches no column; skipped`);
5693
+ } else if (!valueIndexes.includes(resolved)) {
5694
+ valueIndexes.push(resolved);
5695
+ }
4994
5696
  }
5697
+ } else {
5698
+ valueIndexes = headers.map((_, index) => index).filter((index) => index !== labelIndex && isNumericColumn(rows, index));
4995
5699
  }
4996
- if (best && (bestDist <= maxDistance || bestDist <= Math.ceil(input.length * 0.4))) {
4997
- return options?.map ? options.map(best) : best;
5700
+ if (valueIndexes.length === 0) return null;
5701
+ return {
5702
+ labels: rows.map((row) => (row[labelIndex] ?? "").trim()),
5703
+ labelHeader: headers[labelIndex] ?? "",
5704
+ labelNumbers: rows.map((row) => parseCellNumber(row[labelIndex] ?? "")),
5705
+ series: valueIndexes.map((index) => ({
5706
+ name: (headers[index] ?? `Column ${index + 1}`).trim() || `Column ${index + 1}`,
5707
+ values: rows.map((row) => parseCellNumber(row[index] ?? ""))
5708
+ })),
5709
+ warnings
5710
+ };
5711
+ }
5712
+
5713
+ // src/doc/templates/chart/scale.ts
5714
+ function niceNum(range, round4) {
5715
+ const exponent = Math.floor(Math.log10(range));
5716
+ const fraction = range / 10 ** exponent;
5717
+ let niceFraction;
5718
+ if (round4) {
5719
+ if (fraction < 1.5) niceFraction = 1;
5720
+ else if (fraction < 3) niceFraction = 2;
5721
+ else if (fraction < 7) niceFraction = 5;
5722
+ else niceFraction = 10;
5723
+ } else {
5724
+ if (fraction <= 1) niceFraction = 1;
5725
+ else if (fraction <= 2) niceFraction = 2;
5726
+ else if (fraction <= 5) niceFraction = 5;
5727
+ else niceFraction = 10;
5728
+ }
5729
+ return niceFraction * 10 ** exponent;
5730
+ }
5731
+ function niceTicks(min, max, targetCount = 5) {
5732
+ let lo = Number.isFinite(min) ? min : 0;
5733
+ let hi = Number.isFinite(max) ? max : 0;
5734
+ if (lo > hi) [lo, hi] = [hi, lo];
5735
+ if (lo === hi) {
5736
+ const pad = lo === 0 ? 1 : niceNum(Math.abs(lo), true);
5737
+ lo -= pad;
5738
+ hi += pad;
5739
+ if (min >= 0 && lo < 0) lo = 0;
5740
+ }
5741
+ const count = Math.max(2, targetCount);
5742
+ const range = niceNum(hi - lo, false);
5743
+ const step = niceNum(range / (count - 1), true);
5744
+ const niceMin = Math.floor(lo / step) * step;
5745
+ const niceMax = Math.ceil(hi / step) * step;
5746
+ const ticks = [];
5747
+ for (let tick = niceMin; tick <= niceMax + step * 1e-6; tick += step) {
5748
+ ticks.push(Math.abs(tick) < step * 1e-6 ? 0 : Number(tick.toPrecision(12)));
5749
+ }
5750
+ return { ticks, niceMin, niceMax, step };
5751
+ }
5752
+ function formatChartValue(value, unit) {
5753
+ if (!Number.isFinite(value)) return "\u2014";
5754
+ const sign = value < 0 ? "-" : "";
5755
+ const abs = Math.abs(value);
5756
+ let text;
5757
+ if (abs >= 1e6) {
5758
+ text = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
5759
+ } else if (abs >= 1e3) {
5760
+ text = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
5761
+ } else {
5762
+ text = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
4998
5763
  }
4999
- return void 0;
5764
+ return unit ? `${text} ${unit}` : text;
5765
+ }
5766
+ function trimTrailingZero(text) {
5767
+ return text.replace(/\.0$/, "");
5000
5768
  }
5001
5769
 
5002
- // src/doc/templates/inputDescriptors.ts
5003
- var AMBIENT_MOTION_VALUES = ["zoomIn", "zoomOut", "panLeft", "panRight"];
5004
- var CAPTION_POSITION_VALUES = ["bottom", "top", "center"];
5005
- var MAP_STYLE_VALUES = ["terrain", "satellite", "road", "toner", "watercolor"];
5006
- var DATE_EVENT_MOOD_VALUES = ["neutral", "somber", "celebratory"];
5007
- var COLOR_SCHEME_HINT = "theme color-scheme name \u2014 e.g. 'blue', 'green'";
5008
- var BASE_INPUT_DESCRIPTORS = [
5009
- {
5010
- key: "imageTreatment",
5011
- description: "Per-block photographic grade override ('none' opts out)",
5012
- values: ["none", "mono", "duotone", "warm", "cool"]
5013
- },
5014
- {
5015
- key: "useBottomLayer",
5016
- description: "Show the doc's bottom persistent layers",
5017
- coerce: "boolean"
5018
- },
5019
- { key: "useTopLayer", description: "Show the doc's top persistent layers", coerce: "boolean" }
5020
- ];
5021
- var TEMPLATE_INPUT_DESCRIPTORS = {
5022
- title: [
5023
- { key: "title", description: "Main title text" },
5024
- { key: "subtitle", description: "Subtitle or tagline" },
5025
- { key: "backgroundColor", description: "Background color", valueHint: "CSS color" }
5026
- ],
5027
- sectionHeader: [
5028
- { key: "title", description: "Section title" },
5029
- {
5030
- key: "colorScheme",
5031
- description: "Fallback background color scheme",
5032
- valueHint: COLOR_SCHEME_HINT
5033
- },
5034
- { key: "imageSrc", description: "Optional background image path" },
5035
- { key: "imageAlt", description: "Alt text for the background image" },
5036
- { key: "ambientMotion", description: "Slow zoom/pan effect", values: AMBIENT_MOTION_VALUES }
5037
- ],
5038
- content: [{ key: "title", description: "Heading shown above the complete body" }],
5039
- statHighlight: [
5040
- { key: "stat", description: 'The statistic (e.g. "89%", "2x")' },
5041
- { key: "description", description: "What the stat means" },
5042
- { key: "detail", description: "Additional detail or context" },
5043
- { key: "colorScheme", description: "Color scheme for the stat", valueHint: COLOR_SCHEME_HINT }
5044
- ],
5045
- quote: [
5046
- { key: "title", description: "Optional heading above the quote" },
5047
- { key: "quote", description: "The quote text" },
5048
- { key: "attribution", description: "Attribution (author, source)" }
5049
- ],
5050
- pullQuote: [
5051
- { key: "text", description: "Quote text" },
5052
- { key: "attribution", description: "Attribution" }
5053
- ],
5054
- twoColumn: [
5055
- {
5056
- key: "left",
5057
- description: 'Left column, as "label|sublabel"',
5058
- coerce: "labeledPair",
5059
- required: true
5060
- },
5061
- {
5062
- key: "right",
5063
- description: 'Right column, as "label|sublabel"',
5064
- coerce: "labeledPair",
5065
- required: true
5066
- },
5067
- { key: "header", description: "Optional header above the columns" },
5068
- { key: "leftColor", description: "Left column color scheme", valueHint: COLOR_SCHEME_HINT },
5069
- { key: "rightColor", description: "Right column color scheme", valueHint: COLOR_SCHEME_HINT }
5070
- ],
5071
- dateEvent: [
5072
- { key: "date", description: 'The date (e.g. "July 14, 1974")' },
5073
- { key: "description", description: "What happened" },
5074
- { key: "footer", description: "Optional footer text" },
5075
- { key: "mood", description: "Emotional tone", values: DATE_EVENT_MOOD_VALUES }
5076
- ],
5077
- imageWithCaption: [
5078
- { key: "imageSrc", description: "Path to the image file", required: true },
5079
- { key: "imageAlt", description: "Alt text for accessibility" },
5080
- { key: "caption", description: "Caption text" },
5081
- { key: "captionPosition", description: "Caption placement", values: CAPTION_POSITION_VALUES },
5082
- { key: "ambientMotion", description: "Slow zoom/pan effect", values: AMBIENT_MOTION_VALUES },
5083
- { key: "isTitle", description: "Style the caption as a title", coerce: "boolean" },
5084
- { key: "subtitle", description: "Subtitle (only when isTitle)" },
5085
- { key: "imageCredit", description: "Photo credit / artist name" },
5086
- { key: "imageLicense", description: "License identifier" }
5087
- ],
5088
- leftFeature: FEATURE_DESCRIPTORS(),
5089
- rightFeature: FEATURE_DESCRIPTORS(),
5090
- map: [
5091
- {
5092
- key: "center",
5093
- description: 'Map center as "lat,lng"',
5094
- coerce: "latLng",
5095
- required: true
5096
- },
5097
- { key: "zoom", description: "Zoom level (4-16)", coerce: "number" },
5098
- { key: "mapStyle", description: "Map tile style", values: MAP_STYLE_VALUES },
5099
- { key: "title", description: "Optional title overlay" },
5100
- { key: "caption", description: "Optional caption" }
5101
- ],
5102
- photoGrid: [
5103
- { key: "images", description: "Comma-separated image paths", coerce: "stringList" },
5104
- { key: "caption", description: "Optional caption below the grid" },
5105
- {
5106
- key: "ambientMotion",
5107
- description: "Slow zoom/pan on the largest image",
5108
- values: AMBIENT_MOTION_VALUES
5109
- }
5110
- ],
5111
- comparisonBar: [
5112
- { key: "leftLabel", description: "Left bar label", required: true },
5113
- { key: "leftValue", description: "Left bar numeric value", coerce: "number", required: true },
5114
- { key: "rightLabel", description: "Right bar label", required: true },
5115
- { key: "rightValue", description: "Right bar numeric value", coerce: "number", required: true },
5116
- { key: "unit", description: 'Unit label (e.g. "km")' },
5117
- { key: "colorScheme", description: "Color scheme", valueHint: COLOR_SCHEME_HINT }
5118
- ],
5119
- dataTable: [
5120
- { key: "title", description: "Optional title above the table" },
5121
- { key: "headers", description: "Comma-separated header cells", coerce: "stringList" },
5122
- // `rows`/`align` are structured (string[][] / alignment[]) and can't be
5123
- // expressed as a single inline string — declared so they aren't flagged
5124
- // as unknown when supplied via a data fence's overriding params.
5125
- { key: "rows", description: "Table rows (supply via a data fence)" },
5126
- { key: "align", description: "Per-column alignment (supply via a data fence)" },
5127
- { key: "colorScheme", description: "Header color scheme", valueHint: COLOR_SCHEME_HINT }
5128
- ],
5129
- tree: [
5130
- { key: "title", description: "Optional title above the tree" },
5131
- { key: "colorScheme", description: "Folder-row color scheme", valueHint: COLOR_SCHEME_HINT },
5132
- // `items` is a nested structure derived from the tree fence, not an
5133
- // inline string — declared so it isn't flagged as unknown.
5134
- { key: "items", description: "Tree items (derived from the tree fence)" }
5135
- ],
5136
- timeline: [
5137
- { key: "title", description: "Optional title above the timeline" },
5138
- {
5139
- key: "colorScheme",
5140
- description: "Track and marker color scheme",
5141
- valueHint: COLOR_SCHEME_HINT
5142
- },
5143
- // Tracks/events and branch links are nested structures derived from the
5144
- // timeline source, not values that can be expressed as one inline string.
5145
- { key: "tracks", description: "Timeline tracks and events (supply via template data)" },
5146
- { key: "links", description: "Branch links between event ids (supply via template data)" }
5147
- ],
5148
- videoWithCaption: [
5149
- { key: "videoSrc", description: "Path to the video file", required: true },
5150
- { key: "posterSrc", description: "Poster frame path" },
5151
- { key: "videoAlt", description: "Alt text for accessibility" },
5152
- { key: "clipStart", description: "Clip start time (seconds)", coerce: "number" },
5153
- { key: "clipEnd", description: "Clip end time (seconds)", coerce: "number" },
5154
- { key: "caption", description: "Caption text" },
5155
- { key: "captionPosition", description: "Caption placement", values: CAPTION_POSITION_VALUES },
5156
- { key: "videoCredit", description: "Video credit / artist name" },
5157
- { key: "videoLicense", description: "License identifier" }
5158
- ]
5159
- };
5160
- function FEATURE_DESCRIPTORS() {
5770
+ // src/doc/templates/chart/geometry.ts
5771
+ function round3(value) {
5772
+ return Math.round(value * 100) / 100;
5773
+ }
5774
+ function pointOnCircle(cx, cy, r, deg) {
5775
+ const rad = (deg - 90) * Math.PI / 180;
5776
+ return { x: round3(cx + r * Math.cos(rad)), y: round3(cy + r * Math.sin(rad)) };
5777
+ }
5778
+ function arcPath(cx, cy, rOuter, rInner, startDeg, endDeg) {
5779
+ let sweep = endDeg - startDeg;
5780
+ if (sweep <= 0) return "";
5781
+ if (sweep >= 359.9) sweep = 359.9;
5782
+ const end = startDeg + sweep;
5783
+ const largeArc = sweep > 180 ? 1 : 0;
5784
+ const o1 = pointOnCircle(cx, cy, rOuter, startDeg);
5785
+ const o2 = pointOnCircle(cx, cy, rOuter, end);
5786
+ if (rInner <= 0) {
5787
+ return [
5788
+ `M ${round3(cx)} ${round3(cy)}`,
5789
+ `L ${o1.x} ${o1.y}`,
5790
+ `A ${round3(rOuter)} ${round3(rOuter)} 0 ${largeArc} 1 ${o2.x} ${o2.y}`,
5791
+ "Z"
5792
+ ].join(" ");
5793
+ }
5794
+ const i1 = pointOnCircle(cx, cy, rInner, startDeg);
5795
+ const i2 = pointOnCircle(cx, cy, rInner, end);
5161
5796
  return [
5162
- { key: "imageSrc", description: "Path to the feature image", required: true },
5163
- { key: "imageAlt", description: "Alt text for accessibility" },
5164
- { key: "imageWidth", description: "Explicit display width in px", coerce: "number" },
5165
- { key: "imageHeight", description: "Explicit display height in px", coerce: "number" },
5166
- { key: "title", description: "Heading text next to the image" },
5167
- { key: "body", description: "Body text below the title" }
5168
- ];
5797
+ `M ${o1.x} ${o1.y}`,
5798
+ `A ${round3(rOuter)} ${round3(rOuter)} 0 ${largeArc} 1 ${o2.x} ${o2.y}`,
5799
+ `L ${i2.x} ${i2.y}`,
5800
+ `A ${round3(rInner)} ${round3(rInner)} 0 ${largeArc} 0 ${i1.x} ${i1.y}`,
5801
+ "Z"
5802
+ ].join(" ");
5803
+ }
5804
+ function polylinePath(points) {
5805
+ if (points.length === 0) return "";
5806
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round3(p.x)} ${round3(p.y)}`).join(" ");
5807
+ }
5808
+ function areaPath(points, baselineY) {
5809
+ if (points.length === 0) return "";
5810
+ const open = polylinePath(points);
5811
+ const first = points[0];
5812
+ const last = points[points.length - 1];
5813
+ return `${open} L ${round3(last.x)} ${round3(baselineY)} L ${round3(first.x)} ${round3(baselineY)} Z`;
5814
+ }
5815
+
5816
+ // src/doc/templates/chart/layout.ts
5817
+ function computeChartFrame(options) {
5818
+ const W = options.viewportWidth;
5819
+ const H = options.viewportHeight;
5820
+ const marginX = W * 0.06;
5821
+ const marginY = H * 0.06;
5822
+ const innerWidth = W - marginX * 2;
5823
+ let cursor = marginY;
5824
+ let titleBand;
5825
+ if (options.hasTitle) {
5826
+ titleBand = { x: marginX, y: cursor, width: innerWidth, height: options.titleFontSize * 2.2 };
5827
+ cursor = titleBand.y + titleBand.height;
5828
+ }
5829
+ let tableBand;
5830
+ let tableHeight = 0;
5831
+ if (options.tableRowCount > 0) {
5832
+ tableHeight = Math.min(H * 0.4, (options.tableRowCount + 1) * options.tableFontSize * 2.4);
5833
+ }
5834
+ const legendHeight = options.legendRows > 0 ? options.legendRows * options.legendRowHeight : 0;
5835
+ const chartArea = {
5836
+ x: marginX,
5837
+ y: cursor,
5838
+ width: innerWidth,
5839
+ height: Math.max(40, H - marginY - cursor - legendHeight - tableHeight)
5840
+ };
5841
+ cursor = chartArea.y + chartArea.height;
5842
+ let legendBand;
5843
+ if (legendHeight > 0) {
5844
+ legendBand = { x: marginX, y: cursor, width: innerWidth, height: legendHeight };
5845
+ cursor += legendHeight;
5846
+ }
5847
+ if (tableHeight > 0) {
5848
+ tableBand = { x: marginX, y: cursor, width: innerWidth, height: tableHeight };
5849
+ }
5850
+ const plot = {
5851
+ x: chartArea.x + options.leftGutter,
5852
+ y: chartArea.y,
5853
+ width: Math.max(20, chartArea.width - options.leftGutter),
5854
+ height: Math.max(20, chartArea.height - options.bottomGutter)
5855
+ };
5856
+ return { titleBand, chartArea, plot, legendBand, tableBand };
5857
+ }
5858
+ function stackSeries(series) {
5859
+ const rowCount = series.reduce((max, s) => Math.max(max, s.values.length), 0);
5860
+ const positive = new Array(rowCount).fill(0);
5861
+ const negative = new Array(rowCount).fill(0);
5862
+ return series.map(
5863
+ (s) => Array.from({ length: rowCount }, (_, row) => {
5864
+ const value = s.values[row];
5865
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
5866
+ return { y0: positive[row], y1: positive[row] };
5867
+ }
5868
+ if (value >= 0) {
5869
+ const y0 = positive[row];
5870
+ positive[row] += value;
5871
+ return { y0, y1: positive[row] };
5872
+ }
5873
+ const y1 = negative[row];
5874
+ negative[row] += value;
5875
+ return { y0: negative[row], y1 };
5876
+ })
5877
+ );
5878
+ }
5879
+ function layoutLegend(labels, fontSize, maxWidth) {
5880
+ const swatchSize = fontSize * 0.9;
5881
+ const gap = fontSize * 1.6;
5882
+ const rowHeight = fontSize * 2;
5883
+ const widths = labels.map(
5884
+ (label) => swatchSize + fontSize * 0.5 + label.length * fontSize * 0.58
5885
+ );
5886
+ const rows = [];
5887
+ let current = [];
5888
+ let currentWidth = 0;
5889
+ widths.forEach((width, index) => {
5890
+ const extra = current.length > 0 ? gap : 0;
5891
+ if (current.length > 0 && currentWidth + extra + width > maxWidth) {
5892
+ rows.push(current);
5893
+ current = [];
5894
+ currentWidth = 0;
5895
+ }
5896
+ current.push(index);
5897
+ currentWidth += (current.length > 1 ? gap : 0) + width;
5898
+ });
5899
+ if (current.length > 0) rows.push(current);
5900
+ const items = [];
5901
+ rows.forEach((rowIndexes, rowIndex) => {
5902
+ const rowWidth = rowIndexes.reduce(
5903
+ (sum, index, i) => sum + widths[index] + (i > 0 ? gap : 0),
5904
+ 0
5905
+ );
5906
+ let x = Math.max(0, (maxWidth - rowWidth) / 2);
5907
+ rowIndexes.forEach((index) => {
5908
+ items.push({ label: labels[index], index, x, y: rowIndex * rowHeight, swatchSize });
5909
+ x += widths[index] + gap;
5910
+ });
5911
+ });
5912
+ return { items, rows: rows.length, rowHeight };
5913
+ }
5914
+ function categoryLabelStep(categoryCount, axisLength, fontSize) {
5915
+ if (categoryCount <= 1) return 1;
5916
+ const minPitch = fontSize * 6;
5917
+ const pitch = axisLength / categoryCount;
5918
+ return Math.max(1, Math.ceil(minPitch / Math.max(1, pitch)));
5919
+ }
5920
+
5921
+ // src/doc/templates/chart/render.ts
5922
+ var TEMPLATE_ID = {
5923
+ bar: "barChart",
5924
+ column: "columnChart",
5925
+ pie: "pieChart",
5926
+ donut: "donutChart",
5927
+ line: "lineChart",
5928
+ area: "areaChart",
5929
+ scatter: "scatterChart"
5930
+ };
5931
+ function renderChart(kind, input, context) {
5932
+ const table = resolveTable(input, context);
5933
+ const data = table ? buildChartData(table, input) : null;
5934
+ if (!table || !data) return contentFallback(input, context);
5935
+ const { theme, viewport } = context;
5936
+ const templateId = TEMPLATE_ID[kind];
5937
+ const title = (input.title ?? "").trim();
5938
+ const titleFontSize = themedFontSize(44, context, true);
5939
+ const axisFontSize = themedFontSize(20, context, false);
5940
+ const labelFontSize = themedFontSize(22, context, false);
5941
+ const legendFontSize = themedFontSize(22, context, false);
5942
+ const tableFontSize = themedFontSize(20, context, false);
5943
+ const bodyFont = getThemeFont(context, "body");
5944
+ const isRadial = kind === "pie" || kind === "donut";
5945
+ const legendLabels = resolveLegendLabels(kind, input, data);
5946
+ const colors = seriesColors(
5947
+ context,
5948
+ input.colorScheme,
5949
+ isRadial ? data.labels.length : data.series.length
5950
+ );
5951
+ if (isRadial && !data.series[0].values.some((v) => v !== null && v > 0)) {
5952
+ return contentFallback(input, context);
5953
+ }
5954
+ const innerWidth = viewport.width * 0.88;
5955
+ const legend = legendLabels.length > 0 ? layoutLegend(legendLabels, legendFontSize, innerWidth) : null;
5956
+ const gutters = computeGutters(kind, data, input, context, axisFontSize, labelFontSize);
5957
+ const frame = computeChartFrame({
5958
+ viewportWidth: viewport.width,
5959
+ viewportHeight: viewport.height,
5960
+ hasTitle: title.length > 0,
5961
+ titleFontSize,
5962
+ leftGutter: gutters.left,
5963
+ bottomGutter: gutters.bottom,
5964
+ legendRows: legend?.rows ?? 0,
5965
+ legendRowHeight: legendFontSize * 2,
5966
+ tableRowCount: input.showTable === true ? table.rows.length : 0,
5967
+ tableFontSize
5968
+ });
5969
+ const entranceHint = getTemplateHint(context, templateId, "entrance", "staggered");
5970
+ const entrance = (index) => ({
5971
+ type: "fadeIn",
5972
+ duration: 0.6,
5973
+ delay: entranceHint === "subtle" ? 0.1 : Math.min(1.2, 0.2 + index * 0.08)
5974
+ });
5975
+ const paint = {
5976
+ kind,
5977
+ input,
5978
+ context,
5979
+ data,
5980
+ frame,
5981
+ colors,
5982
+ axisFontSize,
5983
+ labelFontSize,
5984
+ bodyFont,
5985
+ gridColor: withAlpha(theme.colors.text, 0.12),
5986
+ axisTextColor: theme.colors.textMuted,
5987
+ entrance
5988
+ };
5989
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
5990
+ if (title && frame.titleBand) {
5991
+ layers.push({
5992
+ type: "text",
5993
+ id: "title",
5994
+ content: {
5995
+ text: title,
5996
+ style: {
5997
+ fontSize: titleFontSize,
5998
+ fontFamily: getThemeFont(context, "title"),
5999
+ fontWeight: "bold",
6000
+ color: theme.colors.text,
6001
+ textAlign: "center",
6002
+ shadow: shouldUseShadow(context)
6003
+ }
6004
+ },
6005
+ position: {
6006
+ x: frame.titleBand.x + frame.titleBand.width / 2,
6007
+ y: frame.titleBand.y + frame.titleBand.height / 2,
6008
+ width: frame.titleBand.width,
6009
+ anchor: "center"
6010
+ },
6011
+ animation: { type: "fadeIn", duration: 0.8 }
6012
+ });
6013
+ }
6014
+ switch (kind) {
6015
+ case "bar":
6016
+ layers.push(...paintBars(paint));
6017
+ break;
6018
+ case "column":
6019
+ layers.push(...paintColumns(paint));
6020
+ break;
6021
+ case "pie":
6022
+ case "donut":
6023
+ layers.push(...paintRadial(paint));
6024
+ break;
6025
+ case "line":
6026
+ case "area":
6027
+ layers.push(...paintLines(paint));
6028
+ break;
6029
+ case "scatter":
6030
+ layers.push(...paintScatter(paint));
6031
+ break;
6032
+ }
6033
+ if (legend && frame.legendBand) {
6034
+ layers.push(...paintLegend(paint, legend, frame.legendBand, legendFontSize));
6035
+ }
6036
+ if (input.showTable === true && frame.tableBand) {
6037
+ layers.push(tableLayer(table, frame.tableBand, tableFontSize, input, context));
6038
+ }
6039
+ return layers;
5169
6040
  }
5170
- function descriptorsFor(canonical) {
5171
- const specific = TEMPLATE_INPUT_DESCRIPTORS[canonical];
5172
- if (!specific) return void 0;
5173
- return [...specific, ...BASE_INPUT_DESCRIPTORS];
6041
+ function resolveTable(input, context) {
6042
+ if (Array.isArray(input.headers) && input.headers.length > 0 && Array.isArray(input.rows) && input.rows.length > 0) {
6043
+ return { headers: input.headers, rows: input.rows };
6044
+ }
6045
+ const contents = input.contents ?? context.block?.contents;
6046
+ return extractTableFromContents(contents);
5174
6047
  }
5175
- function descriptorMap(descriptors) {
5176
- return new Map(descriptors.map((d) => [d.key, d]));
6048
+ function contentFallback(input, context) {
6049
+ const fallbackInput = {
6050
+ ...input,
6051
+ template: "content",
6052
+ title: input.title ?? context.block?.title ?? ""
6053
+ };
6054
+ return contentBlock(fallbackInput, context);
5177
6055
  }
5178
- function coerceTemplateParams(template, params) {
5179
- const input = {};
5180
- const warnings = [];
5181
- const descriptors = template ? descriptorsFor(resolveTemplateName(template)) : void 0;
5182
- const byKey = descriptors ? descriptorMap(descriptors) : void 0;
5183
- for (const [key, raw] of Object.entries(params)) {
5184
- const coerce = byKey?.get(key)?.coerce ?? "string";
5185
- const { value, warning } = coerceValue(coerce, raw);
5186
- if (warning) {
5187
- warnings.push(`"${key}": ${warning}`);
5188
- input[key] = raw;
5189
- } else {
5190
- input[key] = value;
5191
- }
6056
+ function resolveLegendLabels(kind, input, data) {
6057
+ if (input.showLegend === false) return [];
6058
+ if (kind === "pie" || kind === "donut") return data.labels;
6059
+ if (data.series.length > 1 || input.showLegend === true) {
6060
+ return data.series.map((s) => s.name);
5192
6061
  }
5193
- return { input, warnings };
6062
+ return [];
5194
6063
  }
5195
- function coerceValue(coerce, raw) {
5196
- switch (coerce) {
5197
- case "number": {
5198
- const n = parseNumber(raw);
5199
- return n == null ? { value: raw, warning: `not a number (${JSON.stringify(raw)})` } : { value: n };
5200
- }
5201
- case "boolean":
5202
- return coerceBoolean(raw);
5203
- case "latLng":
5204
- return coerceLatLng(raw);
5205
- case "labeledPair":
5206
- return { value: coerceLabeledPair(raw) };
5207
- case "stringList":
5208
- return { value: coerceStringList(raw) };
5209
- case "string":
5210
- default:
5211
- return { value: raw };
6064
+ function seriesColors(context, schemeName, count) {
6065
+ const schemes = context.theme.colorSchemes;
6066
+ const names = Object.keys(schemes);
6067
+ const primary = resolveColorScheme(context, schemeName).accent;
6068
+ const palette = [primary];
6069
+ const start = schemeName && names.includes(schemeName) ? names.indexOf(schemeName) : 0;
6070
+ for (let i = 1; i < names.length && palette.length < count; i++) {
6071
+ const accent = schemes[names[(start + i) % names.length]].accent;
6072
+ if (!palette.some((c) => c.toLowerCase() === accent.toLowerCase())) palette.push(accent);
6073
+ }
6074
+ const distinct = palette.length;
6075
+ for (let i = 0; palette.length < count; i++) {
6076
+ palette.push(withAlpha(palette[i % distinct], 0.55));
6077
+ }
6078
+ return palette.slice(0, Math.max(1, count));
6079
+ }
6080
+ function computeGutters(kind, data, input, context, axisFontSize, labelFontSize) {
6081
+ if (kind === "pie" || kind === "donut") return { left: 0, bottom: 0 };
6082
+ if (kind === "bar") {
6083
+ const longest = data.labels.reduce((max, label) => Math.max(max, label.length), 0);
6084
+ const left = Math.min(
6085
+ context.viewport.width * 0.24,
6086
+ Math.max(labelFontSize * 3, longest * labelFontSize * 0.58 + 16)
6087
+ );
6088
+ return { left, bottom: axisFontSize * 2.4 };
5212
6089
  }
6090
+ const ticks = valueTicks(kind, data, input);
6091
+ const longestTick = ticks.ticks.reduce(
6092
+ (max, tick) => Math.max(max, formatChartValue(tick, input.unit).length),
6093
+ 0
6094
+ );
6095
+ return {
6096
+ left: Math.max(axisFontSize * 2.5, longestTick * axisFontSize * 0.6 + 16),
6097
+ bottom: labelFontSize * 2.6
6098
+ };
5213
6099
  }
5214
- function coerceBoolean(raw) {
5215
- const t = raw.trim().toLowerCase();
5216
- if (t === "true" || t === "") return { value: true };
5217
- if (t === "false") return { value: false };
5218
- return { value: raw, warning: `not a boolean (${JSON.stringify(raw)}) \u2014 expected true or false` };
5219
- }
5220
- function coerceLatLng(raw) {
5221
- const parts = raw.split(",");
5222
- if (parts.length === 2) {
5223
- const lat = parseNumber(parts[0]);
5224
- const lng = parseNumber(parts[1]);
5225
- if (lat != null && lng != null) return { value: { lat, lng } };
6100
+ function valueTicks(kind, data, input) {
6101
+ let values;
6102
+ if ((kind === "bar" || kind === "column") && input.stacked === true) {
6103
+ const extents = stackSeries(data.series).flat();
6104
+ values = extents.flatMap((e) => [e.y0, e.y1]);
6105
+ } else {
6106
+ values = data.series.flatMap((s) => s.values.filter((v) => v !== null));
5226
6107
  }
5227
- return { value: raw, warning: `not a "lat,lng" pair (${JSON.stringify(raw)})` };
6108
+ if (values.length === 0) values = [0, 1];
6109
+ let min = Math.min(...values);
6110
+ let max = Math.max(...values);
6111
+ if (kind !== "line" && kind !== "scatter") {
6112
+ min = Math.min(0, min);
6113
+ max = Math.max(0, max);
6114
+ }
6115
+ return niceTicks(min, max, 5);
5228
6116
  }
5229
- function coerceLabeledPair(raw) {
5230
- const sep = raw.indexOf("|");
5231
- if (sep < 0) return { label: raw.trim() };
5232
- const label = raw.slice(0, sep).trim();
5233
- const sublabel = raw.slice(sep + 1).trim();
5234
- return sublabel ? { label, sublabel } : { label };
6117
+ function paintValueAxisY(paint, scale, yFor) {
6118
+ const { frame, gridColor, axisTextColor, axisFontSize, bodyFont, input } = paint;
6119
+ const layers = [];
6120
+ scale.ticks.forEach((tick, index) => {
6121
+ const y = yFor(tick);
6122
+ layers.push({
6123
+ type: "shape",
6124
+ id: `grid-${index}`,
6125
+ content: {
6126
+ shape: "rect",
6127
+ fill: tick === 0 ? withAlpha(paint.context.theme.colors.text, 0.35) : gridColor
6128
+ },
6129
+ position: {
6130
+ x: frame.plot.x,
6131
+ y: y - (tick === 0 ? 1 : 0.5),
6132
+ width: frame.plot.width,
6133
+ height: tick === 0 ? 2 : 1
6134
+ }
6135
+ });
6136
+ layers.push({
6137
+ type: "text",
6138
+ id: `tick-${index}`,
6139
+ content: {
6140
+ text: formatChartValue(tick, input.unit),
6141
+ style: {
6142
+ fontSize: axisFontSize,
6143
+ fontFamily: bodyFont,
6144
+ color: axisTextColor,
6145
+ textAlign: "center"
6146
+ }
6147
+ },
6148
+ position: {
6149
+ x: frame.chartArea.x + (frame.plot.x - frame.chartArea.x) / 2,
6150
+ y,
6151
+ anchor: "center"
6152
+ }
6153
+ });
6154
+ });
6155
+ return layers;
5235
6156
  }
5236
- function coerceStringList(raw) {
5237
- return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6157
+ function paintCategoryAxisX(paint, xForCenter) {
6158
+ const { frame, data, labelFontSize, axisTextColor, bodyFont } = paint;
6159
+ const step = categoryLabelStep(data.labels.length, frame.plot.width, labelFontSize);
6160
+ const layers = [];
6161
+ data.labels.forEach((label, index) => {
6162
+ if (index % step !== 0 || !label) return;
6163
+ layers.push({
6164
+ type: "text",
6165
+ id: `cat-${index}`,
6166
+ content: {
6167
+ text: label,
6168
+ style: {
6169
+ fontSize: labelFontSize,
6170
+ fontFamily: bodyFont,
6171
+ color: axisTextColor,
6172
+ textAlign: "center"
6173
+ }
6174
+ },
6175
+ position: {
6176
+ x: xForCenter(index),
6177
+ y: frame.plot.y + frame.plot.height + labelFontSize * 1.2,
6178
+ anchor: "center"
6179
+ }
6180
+ });
6181
+ });
6182
+ return layers;
5238
6183
  }
5239
- function lintTemplateParams(template, params) {
5240
- const canonical = resolveTemplateName(template);
5241
- const specific = TEMPLATE_INPUT_DESCRIPTORS[canonical];
5242
- if (!specific) return [];
5243
- const findings = [];
5244
- const allDescriptors = [...specific, ...BASE_INPUT_DESCRIPTORS];
5245
- const byKey = descriptorMap(allDescriptors);
5246
- const knownKeys = /* @__PURE__ */ new Set([...byKey.keys(), ...Object.keys(KNOWN_BLOCK_META_KEYS)]);
5247
- for (const [key, raw] of Object.entries(params)) {
5248
- const descriptor = byKey.get(key);
5249
- if (!descriptor) {
5250
- if (knownKeys.has(key)) continue;
5251
- const suggestion = nearestName(key, knownKeys);
5252
- findings.push({
5253
- kind: "unknown-input",
5254
- key,
5255
- message: `Unknown input "${key}" for template "${canonical}"` + (suggestion ? `. Did you mean "${suggestion}"?` : ""),
5256
- ...suggestion ? { suggestion } : {}
6184
+ function paintColumns(paint) {
6185
+ const { frame, data, colors, input, entrance } = paint;
6186
+ const scale = valueTicks("column", data, input);
6187
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6188
+ const layers = paintValueAxisY(paint, scale, yFor);
6189
+ const n = data.labels.length;
6190
+ const band = frame.plot.width / Math.max(1, n);
6191
+ const innerPad = band * 0.15;
6192
+ layers.push(...paintCategoryAxisX(paint, (i) => frame.plot.x + (i + 0.5) * band));
6193
+ const stacked = input.stacked === true && data.series.length > 1;
6194
+ const extents = stacked ? stackSeries(data.series) : null;
6195
+ const groupWidth = band - innerPad * 2;
6196
+ const barWidth = stacked ? groupWidth : Math.max(2, (groupWidth - 4 * (data.series.length - 1)) / data.series.length);
6197
+ data.series.forEach((series, s) => {
6198
+ series.values.forEach((value, i) => {
6199
+ if (i >= n) return;
6200
+ const extent = extents ? extents[s][i] : value === null ? null : { y0: Math.min(0, value), y1: Math.max(0, value) };
6201
+ if (!extent || extent.y0 === extent.y1) return;
6202
+ const top = yFor(extent.y1);
6203
+ const height = Math.max(1, yFor(extent.y0) - top);
6204
+ const x = stacked ? frame.plot.x + i * band + innerPad : frame.plot.x + i * band + innerPad + s * (barWidth + 4);
6205
+ layers.push({
6206
+ type: "shape",
6207
+ id: `mark-${s}-${i}`,
6208
+ content: { shape: "rect", fill: colors[s], borderRadius: stacked ? 0 : 3 },
6209
+ position: { x, y: top, width: barWidth, height },
6210
+ animation: entrance(i + s)
6211
+ });
6212
+ if (input.showValues === true && value !== null && !stacked) {
6213
+ layers.push(
6214
+ valueLabel(
6215
+ paint,
6216
+ `val-${s}-${i}`,
6217
+ value,
6218
+ x + barWidth / 2,
6219
+ value >= 0 ? top - paint.axisFontSize * 0.9 : top + height + paint.axisFontSize * 0.9
6220
+ )
6221
+ );
6222
+ }
6223
+ });
6224
+ });
6225
+ return layers;
6226
+ }
6227
+ function paintBars(paint) {
6228
+ const {
6229
+ frame,
6230
+ data,
6231
+ colors,
6232
+ input,
6233
+ entrance,
6234
+ labelFontSize,
6235
+ axisFontSize,
6236
+ bodyFont,
6237
+ axisTextColor,
6238
+ gridColor
6239
+ } = paint;
6240
+ const scale = valueTicks("bar", data, input);
6241
+ const labelGap = axisFontSize * 0.6;
6242
+ const labelWidthPx = (value) => formatChartValue(value, input.unit).length * axisFontSize * 0.58;
6243
+ const showValueLabels = input.showValues === true && !(input.stacked === true);
6244
+ const reservedForLabels = showValueLabels ? data.series.flatMap((series) => series.values).filter((v) => v !== null).reduce((max, v) => Math.max(max, labelWidthPx(v)), 0) + labelGap : 0;
6245
+ const axisWidth = Math.max(20, frame.plot.width - reservedForLabels);
6246
+ const xFor = (v) => frame.plot.x + axisWidth * ((v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6247
+ const layers = [];
6248
+ scale.ticks.forEach((tick, index) => {
6249
+ const x = xFor(tick);
6250
+ layers.push({
6251
+ type: "shape",
6252
+ id: `grid-${index}`,
6253
+ content: {
6254
+ shape: "rect",
6255
+ fill: tick === 0 ? withAlpha(paint.context.theme.colors.text, 0.35) : gridColor
6256
+ },
6257
+ position: {
6258
+ x: x - (tick === 0 ? 1 : 0.5),
6259
+ y: frame.plot.y,
6260
+ width: tick === 0 ? 2 : 1,
6261
+ height: frame.plot.height
6262
+ }
6263
+ });
6264
+ layers.push({
6265
+ type: "text",
6266
+ id: `tick-${index}`,
6267
+ content: {
6268
+ text: formatChartValue(tick, input.unit),
6269
+ style: {
6270
+ fontSize: axisFontSize,
6271
+ fontFamily: bodyFont,
6272
+ color: axisTextColor,
6273
+ textAlign: "center"
6274
+ }
6275
+ },
6276
+ position: { x, y: frame.plot.y + frame.plot.height + axisFontSize * 1.2, anchor: "center" }
6277
+ });
6278
+ });
6279
+ const n = data.labels.length;
6280
+ const band = frame.plot.height / Math.max(1, n);
6281
+ const innerPad = band * 0.15;
6282
+ const stacked = input.stacked === true && data.series.length > 1;
6283
+ const extents = stacked ? stackSeries(data.series) : null;
6284
+ const groupHeight = band - innerPad * 2;
6285
+ const barHeight = stacked ? groupHeight : Math.max(2, (groupHeight - 4 * (data.series.length - 1)) / data.series.length);
6286
+ data.labels.forEach((label, i) => {
6287
+ if (!label) return;
6288
+ layers.push({
6289
+ type: "text",
6290
+ id: `cat-${i}`,
6291
+ content: {
6292
+ text: label,
6293
+ style: {
6294
+ fontSize: labelFontSize,
6295
+ fontFamily: bodyFont,
6296
+ color: axisTextColor,
6297
+ textAlign: "center"
6298
+ }
6299
+ },
6300
+ position: {
6301
+ x: frame.chartArea.x + (frame.plot.x - frame.chartArea.x) / 2,
6302
+ y: frame.plot.y + (i + 0.5) * band,
6303
+ anchor: "center"
6304
+ }
6305
+ });
6306
+ });
6307
+ data.series.forEach((series, s) => {
6308
+ series.values.forEach((value, i) => {
6309
+ if (i >= n) return;
6310
+ const extent = extents ? extents[s][i] : value === null ? null : { y0: Math.min(0, value), y1: Math.max(0, value) };
6311
+ if (!extent || extent.y0 === extent.y1) return;
6312
+ const left = xFor(extent.y0);
6313
+ const width = Math.max(1, xFor(extent.y1) - left);
6314
+ const y = stacked ? frame.plot.y + i * band + innerPad : frame.plot.y + i * band + innerPad + s * (barHeight + 4);
6315
+ layers.push({
6316
+ type: "shape",
6317
+ id: `mark-${s}-${i}`,
6318
+ content: { shape: "rect", fill: colors[s], borderRadius: stacked ? 0 : 3 },
6319
+ position: { x: left, y, width, height: barHeight },
6320
+ animation: entrance(i + s)
6321
+ });
6322
+ if (showValueLabels && value !== null) {
6323
+ const half = labelWidthPx(value) / 2;
6324
+ const labelX = value >= 0 ? left + width + labelGap + half : left - labelGap - half;
6325
+ layers.push(valueLabel(paint, `val-${s}-${i}`, value, labelX, y + barHeight / 2));
6326
+ }
6327
+ });
6328
+ });
6329
+ return layers;
6330
+ }
6331
+ function paintRadial(paint) {
6332
+ const { kind, frame, data, colors, input, entrance, context } = paint;
6333
+ const slices = data.labels.map((label, index) => ({ label, value: data.series[0].values[index], index })).filter(
6334
+ (s) => s.value !== null && Number.isFinite(s.value) && s.value > 0
6335
+ );
6336
+ const total = slices.reduce((sum, s) => sum + s.value, 0);
6337
+ const cx = frame.chartArea.x + frame.chartArea.width / 2;
6338
+ const cy = frame.chartArea.y + frame.chartArea.height / 2;
6339
+ const rOuter = Math.min(frame.chartArea.width, frame.chartArea.height) * 0.42;
6340
+ const rInner = kind === "donut" ? rOuter * 0.55 : 0;
6341
+ const layers = [];
6342
+ let startDeg = 0;
6343
+ slices.forEach((slice, order) => {
6344
+ const sweep = slice.value / total * 360;
6345
+ layers.push({
6346
+ type: "path",
6347
+ id: `mark-${slice.index}`,
6348
+ content: {
6349
+ d: arcPath(cx, cy, rOuter, rInner, startDeg, startDeg + sweep),
6350
+ fill: colors[slice.index % colors.length],
6351
+ stroke: context.theme.colors.background,
6352
+ strokeWidth: 2
6353
+ },
6354
+ position: { x: cx - rOuter, y: cy - rOuter, width: rOuter * 2, height: rOuter * 2 },
6355
+ animation: entrance(order)
6356
+ });
6357
+ if (input.showValues === true && sweep >= 14) {
6358
+ const mid = startDeg + sweep / 2;
6359
+ const rad = (mid - 90) * Math.PI / 180;
6360
+ const rLabel = rInner > 0 ? (rInner + rOuter) / 2 : rOuter * 0.68;
6361
+ const pctText = `${Math.round(slice.value / total * 100)}%`;
6362
+ layers.push({
6363
+ type: "text",
6364
+ id: `val-${slice.index}`,
6365
+ content: {
6366
+ text: pctText,
6367
+ style: {
6368
+ fontSize: paint.axisFontSize,
6369
+ fontFamily: paint.bodyFont,
6370
+ fontWeight: "bold",
6371
+ color: pickContrastingText(colors[slice.index % colors.length]),
6372
+ textAlign: "center"
6373
+ }
6374
+ },
6375
+ position: {
6376
+ x: cx + rLabel * Math.cos(rad),
6377
+ y: cy + rLabel * Math.sin(rad),
6378
+ anchor: "center"
6379
+ },
6380
+ animation: entrance(order)
5257
6381
  });
5258
- continue;
5259
6382
  }
5260
- if (descriptor.values) {
5261
- const v = raw.trim();
5262
- if (v !== "" && !descriptor.values.includes(v)) {
5263
- findings.push({
5264
- kind: "invalid-input-value",
5265
- key,
5266
- message: `Invalid value ${JSON.stringify(raw)} for "${key}" \u2014 expected one of: ${descriptor.values.join(", ")}`
6383
+ startDeg += sweep;
6384
+ });
6385
+ return layers;
6386
+ }
6387
+ function paintLines(paint) {
6388
+ const { kind, frame, data, colors, input, entrance } = paint;
6389
+ const scale = valueTicks(kind, data, input);
6390
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6391
+ const n = data.labels.length;
6392
+ const band = frame.plot.width / Math.max(1, n);
6393
+ const xForCenter = (i) => frame.plot.x + (i + 0.5) * band;
6394
+ const layers = paintValueAxisY(paint, scale, yFor);
6395
+ layers.push(...paintCategoryAxisX(paint, xForCenter));
6396
+ const baselineY = yFor(Math.max(scale.niceMin, Math.min(scale.niceMax, 0)));
6397
+ const showPoints = n <= 30;
6398
+ data.series.forEach((series, s) => {
6399
+ const segments = [];
6400
+ let current = [];
6401
+ series.values.forEach((value, i) => {
6402
+ if (i >= n) return;
6403
+ if (value === null) {
6404
+ if (current.length > 0) segments.push(current);
6405
+ current = [];
6406
+ return;
6407
+ }
6408
+ current.push({ x: xForCenter(i), y: yFor(value) });
6409
+ });
6410
+ if (current.length > 0) segments.push(current);
6411
+ segments.forEach((segment, seg) => {
6412
+ if (kind === "area" && segment.length > 1) {
6413
+ layers.push({
6414
+ type: "path",
6415
+ id: `area-${s}-${seg}`,
6416
+ content: {
6417
+ d: areaPath(segment, baselineY),
6418
+ fill: colors[s],
6419
+ fillOpacity: 0.25,
6420
+ stroke: "none",
6421
+ strokeWidth: 0
6422
+ },
6423
+ position: {
6424
+ x: frame.plot.x,
6425
+ y: frame.plot.y,
6426
+ width: frame.plot.width,
6427
+ height: frame.plot.height
6428
+ },
6429
+ animation: entrance(s)
5267
6430
  });
5268
6431
  }
5269
- } else if (descriptor.coerce && descriptor.coerce !== "string") {
5270
- const { warning } = coerceValue(descriptor.coerce, raw);
5271
- if (warning) {
5272
- findings.push({ kind: "invalid-input-value", key, message: `"${key}": ${warning}` });
6432
+ if (segment.length > 1) {
6433
+ layers.push({
6434
+ type: "path",
6435
+ id: `line-${s}-${seg}`,
6436
+ content: { d: polylinePath(segment), stroke: colors[s], strokeWidth: 4, fill: "none" },
6437
+ position: {
6438
+ x: frame.plot.x,
6439
+ y: frame.plot.y,
6440
+ width: frame.plot.width,
6441
+ height: frame.plot.height
6442
+ },
6443
+ animation: entrance(s)
6444
+ });
5273
6445
  }
6446
+ });
6447
+ if (showPoints) {
6448
+ series.values.forEach((value, i) => {
6449
+ if (i >= n || value === null) return;
6450
+ const r = 5;
6451
+ layers.push({
6452
+ type: "shape",
6453
+ id: `pt-${s}-${i}`,
6454
+ content: { shape: "circle", fill: colors[s] },
6455
+ position: { x: xForCenter(i) - r, y: yFor(value) - r, width: r * 2, height: r * 2 },
6456
+ animation: entrance(s)
6457
+ });
6458
+ if (input.showValues === true) {
6459
+ layers.push(
6460
+ valueLabel(
6461
+ paint,
6462
+ `val-${s}-${i}`,
6463
+ value,
6464
+ xForCenter(i),
6465
+ yFor(value) - paint.axisFontSize
6466
+ )
6467
+ );
6468
+ }
6469
+ });
5274
6470
  }
5275
- }
5276
- for (const descriptor of specific) {
5277
- if (descriptor.required && params[descriptor.key] == null) {
5278
- findings.push({
5279
- kind: "missing-input",
5280
- key: descriptor.key,
5281
- message: `Missing required input "${descriptor.key}" for template "${canonical}"`
6471
+ });
6472
+ return layers;
6473
+ }
6474
+ function paintScatter(paint) {
6475
+ const { frame, data, colors, input, entrance } = paint;
6476
+ const scale = valueTicks("scatter", data, input);
6477
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6478
+ const layers = paintValueAxisY(paint, scale, yFor);
6479
+ const numericCount = data.labelNumbers.filter((v) => v !== null).length;
6480
+ const numericX = data.labels.length > 0 && numericCount / data.labels.length >= 0.6;
6481
+ let xFor;
6482
+ if (numericX) {
6483
+ const xs = data.labelNumbers.filter((v) => v !== null);
6484
+ const xScale = niceTicks(Math.min(...xs), Math.max(...xs), 5);
6485
+ const span = xScale.niceMax - xScale.niceMin;
6486
+ xFor = (i) => {
6487
+ const v = data.labelNumbers[i];
6488
+ return v === null ? null : frame.plot.x + frame.plot.width * ((v - xScale.niceMin) / span);
6489
+ };
6490
+ xScale.ticks.forEach((tick, index) => {
6491
+ const x = frame.plot.x + frame.plot.width * ((tick - xScale.niceMin) / span);
6492
+ layers.push({
6493
+ type: "shape",
6494
+ id: `xgrid-${index}`,
6495
+ content: { shape: "rect", fill: paint.gridColor },
6496
+ position: { x: x - 0.5, y: frame.plot.y, width: 1, height: frame.plot.height }
6497
+ });
6498
+ layers.push({
6499
+ type: "text",
6500
+ id: `xtick-${index}`,
6501
+ content: {
6502
+ text: formatChartValue(tick),
6503
+ style: {
6504
+ fontSize: paint.axisFontSize,
6505
+ fontFamily: paint.bodyFont,
6506
+ color: paint.axisTextColor,
6507
+ textAlign: "center"
6508
+ }
6509
+ },
6510
+ position: {
6511
+ x,
6512
+ y: frame.plot.y + frame.plot.height + paint.axisFontSize * 1.2,
6513
+ anchor: "center"
6514
+ }
6515
+ });
6516
+ });
6517
+ } else {
6518
+ const band = frame.plot.width / Math.max(1, data.labels.length);
6519
+ xFor = (i) => frame.plot.x + (i + 0.5) * band;
6520
+ layers.push(...paintCategoryAxisX(paint, (i) => frame.plot.x + (i + 0.5) * band));
6521
+ }
6522
+ const r = 7;
6523
+ data.series.forEach((series, s) => {
6524
+ series.values.forEach((value, i) => {
6525
+ if (value === null) return;
6526
+ const x = xFor(i);
6527
+ if (x === null) return;
6528
+ layers.push({
6529
+ type: "shape",
6530
+ id: `mark-${s}-${i}`,
6531
+ content: { shape: "circle", fill: colors[s], fillOpacity: 0.85 },
6532
+ position: { x: x - r, y: yFor(value) - r, width: r * 2, height: r * 2 },
6533
+ animation: entrance(Math.floor(i / 3) + s)
5282
6534
  });
5283
- }
5284
- }
5285
- return findings;
6535
+ });
6536
+ });
6537
+ return layers;
5286
6538
  }
5287
-
5288
- // src/doc/templates/fallbackBlock.ts
5289
- var MAX_BODY_CHARS = 600;
5290
- function fallbackBlockLayers(block, context, notice) {
5291
- const colors = resolveColorScheme(context, "blue");
5292
- const { layout } = context;
5293
- const title = block.title ?? block.id ?? "";
5294
- const body = extractBodyText(block);
5295
- const layers = [
5296
- {
5297
- type: "shape",
5298
- id: "fallback-bg",
5299
- content: { shape: "rect", fill: colors.bg },
5300
- position: { x: 0, y: 0, width: "100%", height: "100%" }
5301
- }
5302
- ];
5303
- if (title) {
6539
+ function paintLegend(paint, legend, band, fontSize) {
6540
+ const layers = [];
6541
+ legend.items.forEach((item, index) => {
6542
+ const color = paint.colors[item.index % paint.colors.length];
6543
+ const rowCenterY = band.y + item.y + legend.rowHeight / 2;
5304
6544
  layers.push({
5305
- type: "text",
5306
- id: "fallback-title",
5307
- content: {
5308
- text: title,
5309
- style: {
5310
- fontSize: themedFontSize(56, context, true),
5311
- fontFamily: getThemeFont(context, "title"),
5312
- fontWeight: "bold",
5313
- color: colors.text,
5314
- textAlign: "center"
5315
- }
5316
- },
6545
+ type: "shape",
6546
+ id: `legend-swatch-${index}`,
6547
+ content: { shape: "rect", fill: color, borderRadius: 3 },
5317
6548
  position: {
5318
- x: "50%",
5319
- y: body ? "28%" : "50%",
5320
- anchor: "center",
5321
- width: layout.maxTextWidth
6549
+ x: band.x + item.x,
6550
+ y: rowCenterY - item.swatchSize / 2,
6551
+ width: item.swatchSize,
6552
+ height: item.swatchSize
5322
6553
  }
5323
6554
  });
5324
- }
5325
- if (body) {
5326
6555
  layers.push({
5327
6556
  type: "text",
5328
- id: "fallback-body",
6557
+ id: `legend-label-${index}`,
5329
6558
  content: {
5330
- text: body,
5331
- style: {
5332
- fontSize: themedFontSize(28, context),
5333
- fontFamily: getThemeFont(context, "body"),
5334
- color: colors.text,
5335
- textAlign: "center"
5336
- }
6559
+ text: item.label,
6560
+ style: { fontSize, fontFamily: paint.bodyFont, color: paint.context.theme.colors.text }
5337
6561
  },
5338
6562
  position: {
5339
- x: "50%",
5340
- y: "55%",
5341
- anchor: "center",
5342
- width: layout.maxTextWidth
6563
+ x: band.x + item.x + item.swatchSize + fontSize * 0.5,
6564
+ y: rowCenterY - fontSize * 0.62
5343
6565
  }
5344
6566
  });
5345
- }
5346
- layers.push({
6567
+ });
6568
+ return layers;
6569
+ }
6570
+ function valueLabel(paint, id, value, x, y) {
6571
+ return {
5347
6572
  type: "text",
5348
- id: "fallback-notice",
6573
+ id,
5349
6574
  content: {
5350
- text: `\u26A0 ${notice}`,
6575
+ text: formatChartValue(value, paint.input.unit),
5351
6576
  style: {
5352
- fontSize: themedFontSize(18, context),
5353
- fontFamily: getThemeFont(context, "body"),
5354
- color: withAlpha(colors.text, 0.6),
6577
+ fontSize: paint.axisFontSize,
6578
+ fontFamily: paint.bodyFont,
6579
+ fontWeight: "bold",
6580
+ color: paint.context.theme.colors.text,
5355
6581
  textAlign: "center"
5356
6582
  }
5357
6583
  },
5358
- position: { x: "50%", y: "92%", anchor: "center", width: layout.maxTextWidth }
5359
- });
5360
- return layers;
5361
- }
5362
- function extractBodyText(block) {
5363
- if (!block.contents || block.contents.length === 0) return "";
5364
- const text = block.contents.map((node) => extractPlainText(node)).join("\n").trim();
5365
- if (text.length <= MAX_BODY_CHARS) return text;
5366
- return `${text.slice(0, MAX_BODY_CHARS).trimEnd()}\u2026`;
5367
- }
5368
-
5369
- // src/doc/asciiDiagram/types.ts
5370
- var ASCII_CHAR_W = 14;
5371
- var ASCII_CHAR_H = 28;
5372
-
5373
- // src/doc/asciiDiagram/mapping.ts
5374
- function asciiCellToCanvas(col, row) {
5375
- return { x: col * ASCII_CHAR_W, y: row * ASCII_CHAR_H };
5376
- }
5377
- function canvasToAsciiCell(x, y) {
5378
- return {
5379
- col: Math.max(0, Math.round(x / ASCII_CHAR_W)),
5380
- row: Math.max(0, Math.round(y / ASCII_CHAR_H))
6584
+ position: { x, y, anchor: "center" }
5381
6585
  };
5382
6586
  }
5383
- function asciiDiagramToTemplateData(diagram) {
5384
- const nodes = diagram.nodes.map((n) => ({
5385
- id: n.id,
5386
- label: n.label,
5387
- ...asciiCellToCanvas(n.col, n.row),
5388
- w: n.wCols * ASCII_CHAR_W,
5389
- h: n.hRows * ASCII_CHAR_H,
5390
- ...n.containerId ? { container: n.containerId } : {}
5391
- }));
5392
- const anchors = inferEdgeAnchors(nodes, diagram.edges);
5393
- const edges = diagram.edges.map((e, index) => ({
5394
- source: e.source,
5395
- target: e.target,
5396
- ...e.label ? { label: e.label } : {},
5397
- ...e.directed ? {} : { directed: false },
5398
- ...anchors[index],
5399
- routing: "orthogonal"
5400
- }));
5401
- return { nodes, edges };
5402
- }
5403
- function inferEdgeAnchors(nodes, edges) {
5404
- const nodeById = new Map(nodes.map((node) => [node.id, node]));
5405
- const pairs = edges.map((edge) => {
5406
- const source = nodeById.get(edge.source);
5407
- const target = nodeById.get(edge.target);
5408
- if (!source || !target) {
5409
- return {
5410
- sourceAnchor: { side: "right", offset: 0.5 },
5411
- targetAnchor: { side: "left", offset: 0.5 }
5412
- };
5413
- }
5414
- const sourceW = source.w ?? ASCII_CHAR_W * 12;
5415
- const sourceH = source.h ?? ASCII_CHAR_H * 3;
5416
- const targetW = target.w ?? ASCII_CHAR_W * 12;
5417
- const targetH = target.h ?? ASCII_CHAR_H * 3;
5418
- const dx = target.x + targetW / 2 - (source.x + sourceW / 2);
5419
- const dy = target.y + targetH / 2 - (source.y + sourceH / 2);
5420
- const horizontalDistance = Math.abs(dx) / Math.max(1, (sourceW + targetW) / 2);
5421
- const verticalDistance = Math.abs(dy) / Math.max(1, (sourceH + targetH) / 2);
5422
- if (verticalDistance >= horizontalDistance) {
5423
- return dy >= 0 ? {
5424
- sourceAnchor: { side: "bottom", offset: 0.5 },
5425
- targetAnchor: { side: "top", offset: 0.5 }
5426
- } : {
5427
- sourceAnchor: { side: "top", offset: 0.5 },
5428
- targetAnchor: { side: "bottom", offset: 0.5 }
5429
- };
5430
- }
5431
- return dx >= 0 ? {
5432
- sourceAnchor: { side: "right", offset: 0.5 },
5433
- targetAnchor: { side: "left", offset: 0.5 }
5434
- } : {
5435
- sourceAnchor: { side: "left", offset: 0.5 },
5436
- targetAnchor: { side: "right", offset: 0.5 }
5437
- };
5438
- });
5439
- distributeOffsets(pairs, edges, nodes, "source");
5440
- distributeOffsets(pairs, edges, nodes, "target");
5441
- return pairs;
5442
- }
5443
- function distributeOffsets(pairs, edges, nodes, endpoint) {
5444
- const nodeById = new Map(nodes.map((node) => [node.id, node]));
5445
- const groups = /* @__PURE__ */ new Map();
5446
- for (let index = 0; index < edges.length; index++) {
5447
- const edge = edges[index];
5448
- const anchor = endpoint === "source" ? pairs[index].sourceAnchor : pairs[index].targetAnchor;
5449
- const nodeId = endpoint === "source" ? edge.source : edge.target;
5450
- const key = `${nodeId}:${anchor.side}`;
5451
- const group = groups.get(key) ?? [];
5452
- group.push(index);
5453
- groups.set(key, group);
5454
- }
5455
- for (const indexes of groups.values()) {
5456
- if (indexes.length < 2) continue;
5457
- const side = endpoint === "source" ? pairs[indexes[0]].sourceAnchor.side : pairs[indexes[0]].targetAnchor.side;
5458
- indexes.sort((a, b) => {
5459
- const oppositeA = nodeById.get(endpoint === "source" ? edges[a].target : edges[a].source);
5460
- const oppositeB = nodeById.get(endpoint === "source" ? edges[b].target : edges[b].source);
5461
- const axisA = side === "top" || side === "bottom" ? oppositeA?.x ?? 0 : oppositeA?.y ?? 0;
5462
- const axisB = side === "top" || side === "bottom" ? oppositeB?.x ?? 0 : oppositeB?.y ?? 0;
5463
- return axisA - axisB || a - b;
5464
- });
5465
- indexes.forEach((edgeIndex, rank) => {
5466
- const anchor = endpoint === "source" ? pairs[edgeIndex].sourceAnchor : pairs[edgeIndex].targetAnchor;
5467
- anchor.offset = (rank + 1) / (indexes.length + 1);
5468
- });
5469
- }
5470
- }
5471
- function asciiDiagramFromTemplateData(nodes, edges, options = {}) {
5472
- const ids = new Set(nodes.map((n) => n.id));
5473
- const outNodes = nodes.map((n) => {
5474
- const { col, row } = canvasToAsciiCell(n.x, n.y);
5475
- const labelLines = n.label.length > 0 ? n.label.split("\n") : [];
5476
- const innerW = labelLines.reduce((w, l) => Math.max(w, l.length), 0);
5477
- const wCols = typeof n.w === "number" ? Math.max(3, Math.round(n.w / ASCII_CHAR_W)) : innerW + 4;
5478
- const hRows = typeof n.h === "number" ? Math.max(3, Math.round(n.h / ASCII_CHAR_H)) : labelLines.length + 2;
5479
- return {
5480
- id: n.id,
5481
- label: n.label,
5482
- col,
5483
- row,
5484
- wCols: Math.max(wCols, 3),
5485
- hRows: Math.max(hRows, 3),
5486
- ...n.container && ids.has(n.container) && n.container !== n.id ? { containerId: n.container } : {}
5487
- };
5488
- });
6587
+ function tableLayer(table, band, fontSize, input, context) {
6588
+ const { theme } = context;
6589
+ const colors = resolveColorScheme(context, input.colorScheme);
5489
6590
  return {
5490
- nodes: outNodes,
5491
- edges: edges.filter((e) => ids.has(e.source) && ids.has(e.target)).map((e) => ({
5492
- source: e.source,
5493
- target: e.target,
5494
- ...e.label ? { label: e.label } : {},
5495
- directed: e.directed !== false
5496
- })),
5497
- style: options.style ?? "unicode",
5498
- warnings: []
5499
- };
5500
- }
5501
- function asciiDiagramFromBlocks(children, options = {}) {
5502
- const layout = computeDiagramLayout(children);
5503
- const nodes = layout.nodes.map((n) => ({
5504
- id: n.id,
5505
- label: n.label,
5506
- x: n.x,
5507
- y: n.y
5508
- }));
5509
- const edges = layout.edges.map((e) => ({
5510
- source: e.source,
5511
- target: e.target,
5512
- ...e.type ? { label: e.type } : {}
5513
- }));
5514
- const diagram = asciiDiagramFromTemplateData(nodes, edges, options);
5515
- diagram.warnings.push(...layout.warnings);
5516
- return diagram;
5517
- }
5518
-
5519
- // src/doc/asciiTimeline/mapping.ts
5520
- function asciiTimelineToTemplateData(timeline) {
5521
- const globalStart = timeline.tracks.length > 0 ? Math.min(...timeline.tracks.map((track) => track.startColumn)) : 0;
5522
- const globalEnd = timeline.tracks.length > 0 ? Math.max(...timeline.tracks.map((track) => track.endColumn)) : 1;
5523
- const span = Math.max(1, globalEnd - globalStart);
5524
- const tracks = timeline.tracks.map((track) => ({
5525
- id: track.id,
5526
- ...track.label ? { label: track.label } : {},
5527
- ...track.endLabel ? { endLabel: track.endLabel } : {},
5528
- events: track.events.map(
5529
- (event) => ({
5530
- id: event.id,
5531
- label: event.label,
5532
- ...event.description ? { description: event.description } : {},
5533
- position: clamp012((event.column - globalStart) / span),
5534
- ...event.side ? { side: event.side } : {},
5535
- ...event.descriptionSide ? { descriptionSide: event.descriptionSide } : {},
5536
- ...event.callout === false ? { callout: false } : {},
5537
- ...event.marker ? { marker: event.marker } : {}
5538
- })
5539
- )
5540
- }));
5541
- const links = timeline.links.map((link) => ({ ...link }));
5542
- return { tracks, links };
5543
- }
5544
- function clamp012(value) {
5545
- return Math.max(0, Math.min(1, value));
6591
+ type: "table",
6592
+ id: "table",
6593
+ content: {
6594
+ headers: table.headers,
6595
+ rows: table.rows,
6596
+ align: table.align,
6597
+ style: {
6598
+ headerBackground: colors.accent,
6599
+ headerColor: pickContrastingText(colors.accent),
6600
+ cellBackground: withAlpha(theme.colors.text, 0.04),
6601
+ cellColor: theme.colors.text,
6602
+ borderColor: withAlpha(theme.colors.text, 0.15),
6603
+ fontSize,
6604
+ fontFamily: getThemeFont(context, "body"),
6605
+ headerFontFamily: getThemeFont(context, "title"),
6606
+ borderRadius: 8
6607
+ }
6608
+ },
6609
+ position: { x: band.x, y: band.y, width: band.width, height: band.height },
6610
+ animation: { type: "fadeIn", duration: 1, delay: 0.4 }
6611
+ };
5546
6612
  }
5547
6613
 
5548
- // src/doc/treeview/mapping.ts
5549
- function treeToTemplateData(tree) {
5550
- return { items: tree.roots.map(toItem) };
6614
+ // src/doc/templates/chartBlock.ts
6615
+ function barChart(input, context) {
6616
+ return renderChart("bar", input, context);
5551
6617
  }
5552
- function toItem(node) {
5553
- return {
5554
- id: node.id,
5555
- label: node.label,
5556
- ...node.isDir ? { isDir: true } : {},
5557
- ...node.comment ? { comment: node.comment } : {},
5558
- children: node.children.map(toItem)
5559
- };
6618
+ function columnChart(input, context) {
6619
+ return renderChart("column", input, context);
5560
6620
  }
5561
- function treeFromTemplateData(items, options = {}) {
5562
- const roots = items.map(fromItem);
5563
- return { roots, style: options.style ?? "unicode", warnings: [] };
6621
+ function pieChart(input, context) {
6622
+ return renderChart("pie", input, context);
5564
6623
  }
5565
- function fromItem(item) {
5566
- const children = Array.isArray(item.children) ? item.children.map(fromItem) : [];
5567
- return {
5568
- id: typeof item.id === "string" && item.id ? item.id : "node",
5569
- label: typeof item.label === "string" ? item.label : "",
5570
- children,
5571
- isDir: item.isDir === true || children.length > 0,
5572
- ...item.comment ? { comment: item.comment } : {}
5573
- };
6624
+ function donutChart(input, context) {
6625
+ return renderChart("donut", input, context);
5574
6626
  }
5575
- function treeFromMarkdownList(list) {
5576
- const usedIds = /* @__PURE__ */ new Map();
5577
- const slugify2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "node";
5578
- const idFor = (label) => {
5579
- const base = slugify2(label.replace(/\/$/, ""));
5580
- const count = usedIds.get(base) ?? 0;
5581
- usedIds.set(base, count + 1);
5582
- return count === 0 ? base : `${base}-${count + 1}`;
5583
- };
5584
- const itemToNode = (item) => {
5585
- const labelNode = item.children.find((c) => c.type !== "list");
5586
- const label = labelNode ? extractPlainText(labelNode).trim() : "";
5587
- const childLists = item.children.filter((c) => c.type === "list");
5588
- const children = [];
5589
- for (const cl of childLists) for (const ci of cl.children) children.push(itemToNode(ci));
5590
- return {
5591
- id: idFor(label),
5592
- label,
5593
- children,
5594
- isDir: label.endsWith("/") || children.length > 0
5595
- };
5596
- };
5597
- return { roots: list.children.map(itemToNode), style: "unicode", warnings: [] };
6627
+ function lineChart(input, context) {
6628
+ return renderChart("line", input, context);
5598
6629
  }
5599
- function findFirstList(contents) {
5600
- return contents?.find((n) => n.type === "list");
6630
+ function areaChart(input, context) {
6631
+ return renderChart("area", input, context);
5601
6632
  }
5602
-
5603
- // src/doc/templateInputs.ts
5604
- var VIDEO_FILE_RE = /\.(?:webm|mp4|mov|m4v|ogv)(?:[?#].*)?$/i;
5605
- function extractBodyPlainText(contents) {
5606
- if (!contents || contents.length === 0) return "";
5607
- return contents.filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((n) => extractPlainText(n)).join("\n").trim();
6633
+ function scatterChart(input, context) {
6634
+ return renderChart("scatter", input, context);
5608
6635
  }
5609
- function extractListItems(contents) {
5610
- if (!contents) return [];
5611
- const items = [];
5612
- for (const node of contents) {
5613
- if (node.type === "list") {
5614
- for (const item of node.children) {
5615
- const text = extractPlainText(item).trim();
5616
- if (text) items.push(text);
5617
- }
6636
+
6637
+ // src/doc/templates/registry.ts
6638
+ var templateRegistry = {
6639
+ title: titleBlock,
6640
+ sectionHeader,
6641
+ content: contentBlock,
6642
+ statHighlight,
6643
+ quote: quoteBlock,
6644
+ factCard,
6645
+ twoColumn,
6646
+ dateEvent,
6647
+ imageWithCaption,
6648
+ leftFeature,
6649
+ rightFeature,
6650
+ map: mapBlock,
6651
+ fullBleedQuote,
6652
+ list: listBlock,
6653
+ photoGrid,
6654
+ definitionCard,
6655
+ comparisonBar,
6656
+ pullQuote,
6657
+ videoWithCaption,
6658
+ videoPullQuote,
6659
+ dataTable,
6660
+ barChart,
6661
+ columnChart,
6662
+ pieChart,
6663
+ donutChart,
6664
+ lineChart,
6665
+ areaChart,
6666
+ scatterChart,
6667
+ diagram: diagramBlock,
6668
+ tree: treeBlock,
6669
+ timeline: timelineBlock,
6670
+ layout: layoutBlock,
6671
+ drawing: drawingBlock
6672
+ };
6673
+ function buildRegistry(custom) {
6674
+ const merged = {
6675
+ ...templateRegistry
6676
+ };
6677
+ if (custom) {
6678
+ for (const definition of custom) {
6679
+ if (merged[definition.name]) continue;
6680
+ merged[definition.name] = makeCustomTemplateFn(
6681
+ definition
6682
+ );
5618
6683
  }
5619
6684
  }
5620
- return items;
6685
+ return merged;
5621
6686
  }
5622
- function parseDim(raw) {
5623
- if (raw === void 0) return void 0;
5624
- const n = parseFloat(raw);
5625
- return Number.isFinite(n) && n > 0 ? n : void 0;
6687
+
6688
+ // src/doc/utils/applyRenderStyle.ts
6689
+ var AMBIENT_VARIANTS = [
6690
+ { type: "slowZoom", direction: "in" },
6691
+ { type: "slowZoom", direction: "out" },
6692
+ { type: "slowZoom", panDirection: "left" },
6693
+ { type: "slowZoom", panDirection: "right" }
6694
+ ];
6695
+ var MIN_AMBIENT_DURATION_SECONDS = 8;
6696
+ function isZeroish(v) {
6697
+ return v === 0 || v === "0" || v === "0%";
5626
6698
  }
5627
- function extractImages(contents, limit = Infinity) {
5628
- if (!contents || contents.length === 0) return [];
5629
- const found = [];
5630
- function fromHtml(nodes) {
5631
- for (const node of nodes) {
5632
- if (found.length >= limit) return;
5633
- if (!node || typeof node !== "object") continue;
5634
- const n = node;
5635
- if (n.type === "htmlElement" && n.tagName === "img") {
5636
- const attrs = n.attributes;
5637
- if (attrs && typeof attrs.src === "string" && attrs.src) {
5638
- found.push({
5639
- src: attrs.src,
5640
- alt: typeof attrs.alt === "string" ? attrs.alt : "",
5641
- width: parseDim(attrs.width),
5642
- height: parseDim(attrs.height)
5643
- });
5644
- }
5645
- }
5646
- if (Array.isArray(n.children)) fromHtml(n.children);
5647
- }
5648
- }
5649
- function walk(node) {
5650
- if (found.length >= limit) return;
5651
- if (!node || typeof node !== "object") return;
5652
- const n = node;
5653
- if (n.type === "image" && typeof n.url === "string" && n.url) {
5654
- found.push({ src: n.url, alt: typeof n.alt === "string" ? n.alt : "" });
5655
- return;
6699
+ function isFullBleedCoverMedia(layer) {
6700
+ if (layer.type !== "image" && layer.type !== "video") return false;
6701
+ const { position } = layer;
6702
+ if (!isZeroish(position.x) || !isZeroish(position.y)) return false;
6703
+ if (position.width !== "100%" || position.height !== "100%") return false;
6704
+ const fit = layer.content.fit;
6705
+ return fit === void 0 || fit === "cover";
6706
+ }
6707
+ function themeWantsAmbientMotion(theme) {
6708
+ return theme.renderStyle.ambientMotion === true;
6709
+ }
6710
+ function applyRenderStyleToLayers(layers, block, theme) {
6711
+ const speed = theme.style.animationSpeed ?? 1;
6712
+ const ambient = themeWantsAmbientMotion(theme);
6713
+ if (speed === 1 && !ambient) return layers;
6714
+ return layers.map((layer) => {
6715
+ let animation = layer.animation;
6716
+ if (!animation && ambient && isFullBleedCoverMedia(layer)) {
6717
+ const rng = new SeededRandom(hashString(`${block.id}:${layer.id}`));
6718
+ const variant = rng.pickRequired(AMBIENT_VARIANTS);
6719
+ animation = {
6720
+ ...variant,
6721
+ duration: Math.max(MIN_AMBIENT_DURATION_SECONDS, block.duration ?? 0)
6722
+ };
5656
6723
  }
5657
- if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
5658
- fromHtml(n.htmlChildren);
6724
+ if (!animation || animation.type === "none") {
6725
+ return animation === layer.animation ? layer : { ...layer, animation };
5659
6726
  }
5660
- if (Array.isArray(n.children)) {
5661
- for (const child of n.children) walk(child);
6727
+ if (speed !== 1) {
6728
+ animation = {
6729
+ ...animation,
6730
+ ...animation.duration != null ? { duration: animation.duration * speed } : {},
6731
+ ...animation.delay != null ? { delay: animation.delay * speed } : {}
6732
+ };
5662
6733
  }
5663
- }
5664
- for (const node of contents) {
5665
- if (found.length >= limit) break;
5666
- walk(node);
5667
- }
5668
- return found;
5669
- }
5670
- function extractFirstImage(contents) {
5671
- return extractImages(contents, 1)[0] ?? null;
6734
+ return animation === layer.animation ? layer : { ...layer, animation };
6735
+ });
5672
6736
  }
5673
- function extractEmbeddedVideos(contents, limit = Infinity) {
5674
- if (!contents || contents.length === 0) return [];
5675
- const found = [];
5676
- const seen = /* @__PURE__ */ new Set();
5677
- const add = (video) => {
5678
- if (!video.src || found.length >= limit || seen.has(video.src)) return;
5679
- seen.add(video.src);
5680
- found.push(video);
5681
- };
5682
- function nestedSource(children) {
5683
- if (!Array.isArray(children)) return void 0;
5684
- for (const child of children) {
5685
- if (!child || typeof child !== "object") continue;
5686
- const node = child;
5687
- if (node.tagName === "source") {
5688
- const attrs = node.attributes;
5689
- if (typeof attrs?.src === "string" && attrs.src) return attrs.src;
5690
- }
5691
- const nested = nestedSource(node.children);
5692
- if (nested) return nested;
5693
- }
5694
- return void 0;
5695
- }
5696
- function walk(node) {
5697
- if (found.length >= limit || !node || typeof node !== "object") return;
5698
- const n = node;
5699
- if (n.type === "htmlElement" && n.tagName === "video") {
5700
- const attrs = n.attributes;
5701
- const src = attrs?.src || nestedSource(n.children);
5702
- if (src) {
5703
- add({
5704
- src,
5705
- ...attrs?.poster ? { posterSrc: attrs.poster } : {},
5706
- alt: attrs?.["aria-label"] || attrs?.title || attrs?.alt || ""
5707
- });
5708
- }
5709
- } else if ((n.type === "link" || n.type === "image") && typeof n.url === "string" && VIDEO_FILE_RE.test(n.url)) {
5710
- add({
5711
- src: n.url,
5712
- alt: typeof n.alt === "string" ? n.alt : extractPlainText(n).trim()
5713
- });
6737
+
6738
+ // src/doc/utils/nearest.ts
6739
+ function levenshtein(a, b) {
6740
+ if (a === b) return 0;
6741
+ const m = a.length;
6742
+ const n = b.length;
6743
+ if (m === 0) return n;
6744
+ if (n === 0) return m;
6745
+ let prev = new Array(n + 1);
6746
+ let curr = new Array(n + 1);
6747
+ for (let j = 0; j <= n; j++) prev[j] = j;
6748
+ for (let i = 1; i <= m; i++) {
6749
+ curr[0] = i;
6750
+ for (let j = 1; j <= n; j++) {
6751
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
6752
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
5714
6753
  }
5715
- if (Array.isArray(n.children)) n.children.forEach(walk);
5716
- if (Array.isArray(n.htmlChildren)) n.htmlChildren.forEach(walk);
6754
+ [prev, curr] = [curr, prev];
5717
6755
  }
5718
- contents.forEach(walk);
5719
- return found;
5720
- }
5721
- function extractFirstEmbeddedVideo(contents) {
5722
- return extractEmbeddedVideos(contents, 1)[0] ?? null;
6756
+ return prev[n];
5723
6757
  }
5724
- function extractTableFromContents(contents) {
5725
- if (!contents) return null;
5726
- for (const node of contents) {
5727
- if (node.type === "table") {
5728
- const table = node;
5729
- const [headerRow, ...bodyRows] = table.children;
5730
- if (!headerRow) return null;
5731
- const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
5732
- const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
5733
- return { headers, rows, align: table.align };
6758
+ function nearestName(input, candidates, options) {
6759
+ const maxDistance = options?.maxDistance ?? 2;
6760
+ let best;
6761
+ let bestDist = Infinity;
6762
+ const lower = input.toLowerCase();
6763
+ for (const candidate of candidates) {
6764
+ const dist = levenshtein(lower, candidate.toLowerCase());
6765
+ if (dist < bestDist) {
6766
+ bestDist = dist;
6767
+ best = candidate;
5734
6768
  }
5735
6769
  }
5736
- return null;
5737
- }
5738
- function extractBlockquoteText(contents) {
5739
- if (!contents) return "";
5740
- for (const node of contents) {
5741
- if (node.type === "blockquote") return extractPlainText(node).trim();
6770
+ if (best && (bestDist <= maxDistance || bestDist <= Math.ceil(input.length * 0.4))) {
6771
+ return options?.map ? options.map(best) : best;
5742
6772
  }
5743
- return "";
6773
+ return void 0;
5744
6774
  }
5745
- function firstParagraph(contents) {
5746
- if (!contents) return null;
5747
- const index = contents.findIndex((node) => node.type === "paragraph");
5748
- if (index < 0) return null;
5749
- return {
5750
- node: contents[index],
5751
- index
5752
- };
6775
+
6776
+ // src/doc/templates/inputDescriptors.ts
6777
+ var AMBIENT_MOTION_VALUES = ["zoomIn", "zoomOut", "panLeft", "panRight"];
6778
+ var CAPTION_POSITION_VALUES = ["bottom", "top", "center"];
6779
+ var MAP_STYLE_VALUES = ["terrain", "satellite", "road", "toner", "watercolor"];
6780
+ var DATE_EVENT_MOOD_VALUES = ["neutral", "somber", "celebratory"];
6781
+ var COLOR_SCHEME_HINT = "theme color-scheme name \u2014 e.g. 'blue', 'green'";
6782
+ function CHART_DESCRIPTORS(extra = []) {
6783
+ return [
6784
+ { key: "title", description: "Optional title above the chart" },
6785
+ {
6786
+ key: "labelColumn",
6787
+ description: "Category/label column",
6788
+ valueHint: "header name or 0-based index"
6789
+ },
6790
+ {
6791
+ key: "valueColumns",
6792
+ description: "Value columns to plot",
6793
+ coerce: "stringList",
6794
+ valueHint: "comma-separated header names or 0-based indexes"
6795
+ },
6796
+ {
6797
+ key: "showTable",
6798
+ description: "Render the source table beneath the chart",
6799
+ coerce: "boolean"
6800
+ },
6801
+ { key: "showLegend", description: "Show a series legend", coerce: "boolean" },
6802
+ { key: "showValues", description: "Print numeric value labels on marks", coerce: "boolean" },
6803
+ { key: "unit", description: 'Unit suffix for tick/value labels (e.g. "km")' },
6804
+ {
6805
+ key: "colorScheme",
6806
+ description: "Color scheme seeding the series color rotation",
6807
+ valueHint: COLOR_SCHEME_HINT
6808
+ },
6809
+ // `headers`/`rows` are normally derived from the block's markdown table;
6810
+ // declared so data-fence-supplied values aren't flagged as unknown.
6811
+ {
6812
+ key: "headers",
6813
+ description: "Header cells (normally derived from the table)",
6814
+ coerce: "stringList"
6815
+ },
6816
+ { key: "rows", description: "Table rows (supply via a data fence)" },
6817
+ ...extra
6818
+ ];
5753
6819
  }
5754
- function leadingStrongStat(paragraph) {
5755
- for (const child of paragraph.children) {
5756
- if (child.type === "text" && child.value.trim() === "") continue;
5757
- if (child.type !== "strong") return null;
5758
- const text = extractPlainText(child).trim();
5759
- const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
5760
- return match?.[0].trim() || null;
5761
- }
5762
- return null;
6820
+ var STACKED_DESCRIPTOR = {
6821
+ key: "stacked",
6822
+ description: "Stack series instead of grouping them side by side",
6823
+ coerce: "boolean"
6824
+ };
6825
+ var BASE_INPUT_DESCRIPTORS = [
6826
+ {
6827
+ key: "imageTreatment",
6828
+ description: "Per-block photographic grade override ('none' opts out)",
6829
+ values: ["none", "mono", "duotone", "warm", "cool"]
6830
+ },
6831
+ {
6832
+ key: "useBottomLayer",
6833
+ description: "Show the doc's bottom persistent layers",
6834
+ coerce: "boolean"
6835
+ },
6836
+ { key: "useTopLayer", description: "Show the doc's top persistent layers", coerce: "boolean" }
6837
+ ];
6838
+ var TEMPLATE_INPUT_DESCRIPTORS = {
6839
+ title: [
6840
+ { key: "title", description: "Main title text" },
6841
+ { key: "subtitle", description: "Subtitle or tagline" },
6842
+ { key: "backgroundColor", description: "Background color", valueHint: "CSS color" }
6843
+ ],
6844
+ sectionHeader: [
6845
+ { key: "title", description: "Section title" },
6846
+ {
6847
+ key: "colorScheme",
6848
+ description: "Fallback background color scheme",
6849
+ valueHint: COLOR_SCHEME_HINT
6850
+ },
6851
+ { key: "imageSrc", description: "Optional background image path" },
6852
+ { key: "imageAlt", description: "Alt text for the background image" },
6853
+ { key: "ambientMotion", description: "Slow zoom/pan effect", values: AMBIENT_MOTION_VALUES }
6854
+ ],
6855
+ content: [{ key: "title", description: "Heading shown above the complete body" }],
6856
+ statHighlight: [
6857
+ { key: "stat", description: 'The statistic (e.g. "89%", "2x")' },
6858
+ { key: "description", description: "What the stat means" },
6859
+ { key: "detail", description: "Additional detail or context" },
6860
+ { key: "colorScheme", description: "Color scheme for the stat", valueHint: COLOR_SCHEME_HINT }
6861
+ ],
6862
+ quote: [
6863
+ { key: "title", description: "Optional heading above the quote" },
6864
+ { key: "quote", description: "The quote text" },
6865
+ { key: "attribution", description: "Attribution (author, source)" }
6866
+ ],
6867
+ pullQuote: [
6868
+ { key: "text", description: "Quote text" },
6869
+ { key: "attribution", description: "Attribution" }
6870
+ ],
6871
+ twoColumn: [
6872
+ {
6873
+ key: "left",
6874
+ description: 'Left column, as "label|sublabel"',
6875
+ coerce: "labeledPair",
6876
+ required: true
6877
+ },
6878
+ {
6879
+ key: "right",
6880
+ description: 'Right column, as "label|sublabel"',
6881
+ coerce: "labeledPair",
6882
+ required: true
6883
+ },
6884
+ { key: "header", description: "Optional header above the columns" },
6885
+ { key: "leftColor", description: "Left column color scheme", valueHint: COLOR_SCHEME_HINT },
6886
+ { key: "rightColor", description: "Right column color scheme", valueHint: COLOR_SCHEME_HINT }
6887
+ ],
6888
+ dateEvent: [
6889
+ { key: "date", description: 'The date (e.g. "July 14, 1974")' },
6890
+ { key: "description", description: "What happened" },
6891
+ { key: "footer", description: "Optional footer text" },
6892
+ { key: "mood", description: "Emotional tone", values: DATE_EVENT_MOOD_VALUES }
6893
+ ],
6894
+ imageWithCaption: [
6895
+ { key: "imageSrc", description: "Path to the image file", required: true },
6896
+ { key: "imageAlt", description: "Alt text for accessibility" },
6897
+ { key: "caption", description: "Caption text" },
6898
+ { key: "captionPosition", description: "Caption placement", values: CAPTION_POSITION_VALUES },
6899
+ { key: "ambientMotion", description: "Slow zoom/pan effect", values: AMBIENT_MOTION_VALUES },
6900
+ { key: "isTitle", description: "Style the caption as a title", coerce: "boolean" },
6901
+ { key: "subtitle", description: "Subtitle (only when isTitle)" },
6902
+ { key: "imageCredit", description: "Photo credit / artist name" },
6903
+ { key: "imageLicense", description: "License identifier" }
6904
+ ],
6905
+ leftFeature: FEATURE_DESCRIPTORS(),
6906
+ rightFeature: FEATURE_DESCRIPTORS(),
6907
+ map: [
6908
+ {
6909
+ key: "center",
6910
+ description: 'Map center as "lat,lng"',
6911
+ coerce: "latLng",
6912
+ required: true
6913
+ },
6914
+ { key: "zoom", description: "Zoom level (4-16)", coerce: "number" },
6915
+ { key: "mapStyle", description: "Map tile style", values: MAP_STYLE_VALUES },
6916
+ { key: "title", description: "Optional title overlay" },
6917
+ { key: "caption", description: "Optional caption" }
6918
+ ],
6919
+ photoGrid: [
6920
+ { key: "images", description: "Comma-separated image paths", coerce: "stringList" },
6921
+ { key: "caption", description: "Optional caption below the grid" },
6922
+ {
6923
+ key: "ambientMotion",
6924
+ description: "Slow zoom/pan on the largest image",
6925
+ values: AMBIENT_MOTION_VALUES
6926
+ }
6927
+ ],
6928
+ comparisonBar: [
6929
+ { key: "leftLabel", description: "Left bar label", required: true },
6930
+ { key: "leftValue", description: "Left bar numeric value", coerce: "number", required: true },
6931
+ { key: "rightLabel", description: "Right bar label", required: true },
6932
+ { key: "rightValue", description: "Right bar numeric value", coerce: "number", required: true },
6933
+ { key: "unit", description: 'Unit label (e.g. "km")' },
6934
+ { key: "colorScheme", description: "Color scheme", valueHint: COLOR_SCHEME_HINT }
6935
+ ],
6936
+ dataTable: [
6937
+ { key: "title", description: "Optional title above the table" },
6938
+ { key: "headers", description: "Comma-separated header cells", coerce: "stringList" },
6939
+ // `rows`/`align` are structured (string[][] / alignment[]) and can't be
6940
+ // expressed as a single inline string — declared so they aren't flagged
6941
+ // as unknown when supplied via a data fence's overriding params.
6942
+ { key: "rows", description: "Table rows (supply via a data fence)" },
6943
+ { key: "align", description: "Per-column alignment (supply via a data fence)" },
6944
+ { key: "colorScheme", description: "Header color scheme", valueHint: COLOR_SCHEME_HINT }
6945
+ ],
6946
+ barChart: CHART_DESCRIPTORS([STACKED_DESCRIPTOR]),
6947
+ columnChart: CHART_DESCRIPTORS([STACKED_DESCRIPTOR]),
6948
+ pieChart: CHART_DESCRIPTORS(),
6949
+ donutChart: CHART_DESCRIPTORS(),
6950
+ lineChart: CHART_DESCRIPTORS(),
6951
+ areaChart: CHART_DESCRIPTORS(),
6952
+ scatterChart: CHART_DESCRIPTORS(),
6953
+ tree: [
6954
+ { key: "title", description: "Optional title above the tree" },
6955
+ { key: "colorScheme", description: "Folder-row color scheme", valueHint: COLOR_SCHEME_HINT },
6956
+ // `items` is a nested structure derived from the tree fence, not an
6957
+ // inline string — declared so it isn't flagged as unknown.
6958
+ { key: "items", description: "Tree items (derived from the tree fence)" }
6959
+ ],
6960
+ timeline: [
6961
+ { key: "title", description: "Optional title above the timeline" },
6962
+ {
6963
+ key: "colorScheme",
6964
+ description: "Track and marker color scheme",
6965
+ valueHint: COLOR_SCHEME_HINT
6966
+ },
6967
+ // Tracks/events and branch links are nested structures derived from the
6968
+ // timeline source, not values that can be expressed as one inline string.
6969
+ { key: "tracks", description: "Timeline tracks and events (supply via template data)" },
6970
+ { key: "links", description: "Branch links between event ids (supply via template data)" }
6971
+ ],
6972
+ videoWithCaption: [
6973
+ { key: "videoSrc", description: "Path to the video file", required: true },
6974
+ { key: "posterSrc", description: "Poster frame path" },
6975
+ { key: "videoAlt", description: "Alt text for accessibility" },
6976
+ { key: "clipStart", description: "Clip start time (seconds)", coerce: "number" },
6977
+ { key: "clipEnd", description: "Clip end time (seconds)", coerce: "number" },
6978
+ { key: "caption", description: "Caption text" },
6979
+ { key: "captionPosition", description: "Caption placement", values: CAPTION_POSITION_VALUES },
6980
+ { key: "videoCredit", description: "Video credit / artist name" },
6981
+ { key: "videoLicense", description: "License identifier" }
6982
+ ]
6983
+ };
6984
+ function FEATURE_DESCRIPTORS() {
6985
+ return [
6986
+ { key: "imageSrc", description: "Path to the feature image", required: true },
6987
+ { key: "imageAlt", description: "Alt text for accessibility" },
6988
+ { key: "imageWidth", description: "Explicit display width in px", coerce: "number" },
6989
+ { key: "imageHeight", description: "Explicit display height in px", coerce: "number" },
6990
+ { key: "title", description: "Heading text next to the image" },
6991
+ { key: "body", description: "Body text below the title" }
6992
+ ];
5763
6993
  }
5764
- function removeStatPhrase(text, start, end) {
5765
- const before = text.slice(0, start).trim();
5766
- let after = text.slice(end);
5767
- if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, "");
5768
- else after = after.trim();
5769
- return [before, after].filter(Boolean).join(" ").trim();
6994
+ function descriptorsFor(canonical) {
6995
+ const specific = TEMPLATE_INPUT_DESCRIPTORS[canonical];
6996
+ if (!specific) return void 0;
6997
+ return [...specific, ...BASE_INPUT_DESCRIPTORS];
5770
6998
  }
5771
- function bodyWithoutParagraphStat(contents, paragraphIndex, paragraphText, start, end) {
5772
- if (!contents) return "";
5773
- return contents.map(
5774
- (node, index) => index === paragraphIndex ? removeStatPhrase(paragraphText, start, end) : extractPlainText(node).trim()
5775
- ).filter(Boolean).join("\n").trim();
6999
+ function descriptorMap(descriptors) {
7000
+ return new Map(descriptors.map((d) => [d.key, d]));
5776
7001
  }
5777
- function deriveStatHighlightInputs(headingText, contents, bodyText3) {
5778
- const paragraph = firstParagraph(contents);
5779
- if (paragraph) {
5780
- const paragraphText = extractPlainText(paragraph.node).trim();
5781
- const strongStat = leadingStrongStat(paragraph.node);
5782
- if (strongStat) {
5783
- const start = paragraphText.indexOf(strongStat);
5784
- const description = bodyWithoutParagraphStat(
5785
- contents,
5786
- paragraph.index,
5787
- paragraphText,
5788
- start,
5789
- start + strongStat.length
5790
- );
5791
- return {
5792
- stat: strongStat,
5793
- description: description || headingText || strongStat
5794
- };
7002
+ function coerceTemplateParams(template, params) {
7003
+ const input = {};
7004
+ const warnings = [];
7005
+ const descriptors = template ? descriptorsFor(resolveTemplateName(template)) : void 0;
7006
+ const byKey = descriptors ? descriptorMap(descriptors) : void 0;
7007
+ for (const [key, raw] of Object.entries(params)) {
7008
+ const coerce = byKey?.get(key)?.coerce ?? "string";
7009
+ const { value, warning } = coerceValue(coerce, raw);
7010
+ if (warning) {
7011
+ warnings.push(`"${key}": ${warning}`);
7012
+ input[key] = raw;
7013
+ } else {
7014
+ input[key] = value;
5795
7015
  }
5796
7016
  }
5797
- const trimmedHeading = headingText.trim();
5798
- const headingStat = matchNumberHighlight(trimmedHeading);
5799
- if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
5800
- return { stat: headingText, description: bodyText3 || headingText };
5801
- }
5802
- if (paragraph) {
5803
- const paragraphText = extractPlainText(paragraph.node).trim();
5804
- const bodyStat = matchNumberHighlight(paragraphText);
5805
- if (bodyStat) {
5806
- const description = bodyWithoutParagraphStat(
5807
- contents,
5808
- paragraph.index,
5809
- paragraphText,
5810
- bodyStat.index,
5811
- bodyStat.end
5812
- );
5813
- return {
5814
- stat: bodyStat.value,
5815
- description: description || headingText || bodyStat.value
5816
- };
7017
+ return { input, warnings };
7018
+ }
7019
+ function coerceValue(coerce, raw) {
7020
+ switch (coerce) {
7021
+ case "number": {
7022
+ const n = parseNumber(raw);
7023
+ return n == null ? { value: raw, warning: `not a number (${JSON.stringify(raw)})` } : { value: n };
5817
7024
  }
7025
+ case "boolean":
7026
+ return coerceBoolean(raw);
7027
+ case "latLng":
7028
+ return coerceLatLng(raw);
7029
+ case "labeledPair":
7030
+ return { value: coerceLabeledPair(raw) };
7031
+ case "stringList":
7032
+ return { value: coerceStringList(raw) };
7033
+ case "string":
7034
+ default:
7035
+ return { value: raw };
5818
7036
  }
5819
- return { stat: headingText, description: bodyText3 || headingText };
5820
7037
  }
5821
- function normalizeCoverageText(value) {
5822
- return value.replace(/\s+/g, " ").trim();
7038
+ function coerceBoolean(raw) {
7039
+ const t = raw.trim().toLowerCase();
7040
+ if (t === "true" || t === "") return { value: true };
7041
+ if (t === "false") return { value: false };
7042
+ return { value: raw, warning: `not a boolean (${JSON.stringify(raw)}) \u2014 expected true or false` };
5823
7043
  }
5824
- function autoTemplatePreservesContent(templateName, contents) {
5825
- const nodes = contents ?? [];
5826
- switch (templateName) {
5827
- case "quote":
5828
- return nodes.length === 1 && nodes[0]?.type === "blockquote";
5829
- case "dataTable":
5830
- return nodes.length === 1 && nodes[0]?.type === "table";
5831
- case "list":
5832
- return nodes.length === 1 && nodes[0]?.type === "list";
5833
- case "diagram":
5834
- case "timeline":
5835
- return nodes.length === 1 && nodes[0]?.type === "code";
5836
- case "tree":
5837
- return nodes.length === 1 && (nodes[0]?.type === "code" || nodes[0]?.type === "list");
5838
- case "photoGrid": {
5839
- const images = extractImages(nodes);
5840
- if (images.length < 2 || images.length > 4) return false;
5841
- const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
5842
- const representedText = normalizeCoverageText(
5843
- images.map((image) => image.alt).filter(Boolean).join(" ")
5844
- );
5845
- return bodyText3 === "" || bodyText3 === representedText;
5846
- }
5847
- case "videoWithCaption": {
5848
- const videos = extractEmbeddedVideos(nodes);
5849
- if (videos.length !== 1) return false;
5850
- const bodyText3 = normalizeCoverageText(extractBodyPlainText(nodes));
5851
- return bodyText3 === normalizeCoverageText(videos[0]?.alt ?? "");
5852
- }
5853
- case "leftFeature":
5854
- case "rightFeature":
5855
- case "statHighlight":
5856
- return true;
5857
- default:
5858
- return false;
7044
+ function coerceLatLng(raw) {
7045
+ const parts = raw.split(",");
7046
+ if (parts.length === 2) {
7047
+ const lat = parseNumber(parts[0]);
7048
+ const lng = parseNumber(parts[1]);
7049
+ if (lat != null && lng != null) return { value: { lat, lng } };
5859
7050
  }
7051
+ return { value: raw, warning: `not a "lat,lng" pair (${JSON.stringify(raw)})` };
5860
7052
  }
5861
- function deriveTemplateInputs(templateName, headingText, contents, options = {}) {
5862
- const placeholders = options.placeholders === true;
5863
- const bodyText3 = extractBodyPlainText(contents);
5864
- switch (templateName) {
5865
- case "statHighlight": {
5866
- const inputs = deriveStatHighlightInputs(headingText, contents, bodyText3);
5867
- if (options.preserveSourceHeading && headingText && inputs.stat !== headingText && inputs.description !== headingText) {
5868
- return {
5869
- ...inputs,
5870
- description: headingText,
5871
- ...inputs.description ? { detail: inputs.description } : {}
5872
- };
5873
- }
5874
- return inputs;
5875
- }
5876
- case "quote": {
5877
- const quote = extractBlockquoteText(contents) || bodyText3;
5878
- if (quote) return { quote, ...headingText ? { title: headingText } : {} };
5879
- return { quote: headingText };
5880
- }
5881
- case "fullBleedQuote":
5882
- case "pullQuote": {
5883
- const text = extractBlockquoteText(contents) || bodyText3 || headingText;
5884
- return { text };
5885
- }
5886
- case "factCard":
5887
- return { fact: headingText, explanation: bodyText3 || headingText };
5888
- case "comparisonBar":
5889
- return placeholders ? { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 } : null;
5890
- case "list": {
5891
- const items = extractListItems(contents);
5892
- if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
5893
- return placeholders ? { items: ["Item 1", "Item 2", "Item 3"] } : null;
5894
- }
5895
- case "definitionCard":
5896
- return { term: headingText, definition: bodyText3 || headingText };
5897
- case "dateEvent":
5898
- return { date: headingText, description: bodyText3 || headingText };
5899
- case "dataTable": {
5900
- const tableData = extractTableFromContents(contents);
5901
- if (tableData) return { ...tableData, ...headingText ? { title: headingText } : {} };
5902
- return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
5903
- }
5904
- case "imageWithCaption": {
5905
- const img = extractFirstImage(contents);
5906
- if (!img) return placeholders ? { caption: headingText } : null;
5907
- return { imageSrc: img.src, imageAlt: img.alt || headingText, caption: headingText };
5908
- }
5909
- case "photoGrid": {
5910
- const images = extractImages(contents, 4);
5911
- if (images.length < 2) return placeholders ? {} : null;
5912
- return {
5913
- images: images.map((i) => ({ src: i.src, alt: i.alt })),
5914
- caption: headingText
5915
- };
5916
- }
5917
- case "videoWithCaption": {
5918
- const video = extractFirstEmbeddedVideo(contents);
5919
- if (!video) return placeholders ? { caption: headingText } : null;
5920
- return {
5921
- videoSrc: video.src,
5922
- posterSrc: video.posterSrc,
5923
- videoAlt: video.alt || headingText,
5924
- caption: headingText
5925
- };
7053
+ function coerceLabeledPair(raw) {
7054
+ const sep = raw.indexOf("|");
7055
+ if (sep < 0) return { label: raw.trim() };
7056
+ const label = raw.slice(0, sep).trim();
7057
+ const sublabel = raw.slice(sep + 1).trim();
7058
+ return sublabel ? { label, sublabel } : { label };
7059
+ }
7060
+ function coerceStringList(raw) {
7061
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7062
+ }
7063
+ function lintTemplateParams(template, params) {
7064
+ const canonical = resolveTemplateName(template);
7065
+ const specific = TEMPLATE_INPUT_DESCRIPTORS[canonical];
7066
+ if (!specific) return [];
7067
+ const findings = [];
7068
+ const allDescriptors = [...specific, ...BASE_INPUT_DESCRIPTORS];
7069
+ const byKey = descriptorMap(allDescriptors);
7070
+ const knownKeys = /* @__PURE__ */ new Set([...byKey.keys(), ...Object.keys(KNOWN_BLOCK_META_KEYS)]);
7071
+ for (const [key, raw] of Object.entries(params)) {
7072
+ const descriptor = byKey.get(key);
7073
+ if (!descriptor) {
7074
+ if (knownKeys.has(key)) continue;
7075
+ const suggestion = nearestName(key, knownKeys);
7076
+ findings.push({
7077
+ kind: "unknown-input",
7078
+ key,
7079
+ message: `Unknown input "${key}" for template "${canonical}"` + (suggestion ? `. Did you mean "${suggestion}"?` : ""),
7080
+ ...suggestion ? { suggestion } : {}
7081
+ });
7082
+ continue;
5926
7083
  }
5927
- case "leftFeature":
5928
- case "rightFeature": {
5929
- const img = extractFirstImage(contents);
5930
- if (!img && !placeholders) return null;
5931
- return {
5932
- imageSrc: img?.src ?? "",
5933
- imageAlt: img?.alt || headingText,
5934
- imageWidth: img?.width,
5935
- imageHeight: img?.height,
5936
- title: headingText,
5937
- body: bodyText3
5938
- };
7084
+ if (descriptor.values) {
7085
+ const v = raw.trim();
7086
+ if (v !== "" && !descriptor.values.includes(v)) {
7087
+ findings.push({
7088
+ kind: "invalid-input-value",
7089
+ key,
7090
+ message: `Invalid value ${JSON.stringify(raw)} for "${key}" \u2014 expected one of: ${descriptor.values.join(", ")}`
7091
+ });
7092
+ }
7093
+ } else if (descriptor.coerce && descriptor.coerce !== "string") {
7094
+ const { warning } = coerceValue(descriptor.coerce, raw);
7095
+ if (warning) {
7096
+ findings.push({ kind: "invalid-input-value", key, message: `"${key}": ${warning}` });
7097
+ }
5939
7098
  }
5940
- case "diagram": {
5941
- const fences = (contents ?? []).filter((n) => n.type === "code");
5942
- const fence = fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : void 0;
5943
- const detection = fence ? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) }) : void 0;
5944
- if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
5945
- const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
5946
- return { nodes, edges, ...headingText ? { title: headingText } : {} };
7099
+ }
7100
+ for (const descriptor of specific) {
7101
+ if (descriptor.required && params[descriptor.key] == null) {
7102
+ findings.push({
7103
+ kind: "missing-input",
7104
+ key: descriptor.key,
7105
+ message: `Missing required input "${descriptor.key}" for template "${canonical}"`
7106
+ });
5947
7107
  }
5948
- case "timeline": {
5949
- const fences = (contents ?? []).filter((n) => n.type === "code");
5950
- const fence = fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang) ? fences[0] : void 0;
5951
- const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : void 0;
5952
- if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
5953
- const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
5954
- return { tracks, links, ...headingText ? { title: headingText } : {} };
7108
+ }
7109
+ return findings;
7110
+ }
7111
+
7112
+ // src/doc/templates/fallbackBlock.ts
7113
+ var MAX_BODY_CHARS = 600;
7114
+ function fallbackBlockLayers(block, context, notice) {
7115
+ const colors = resolveColorScheme(context, "blue");
7116
+ const { layout } = context;
7117
+ const title = block.title ?? block.id ?? "";
7118
+ const body = extractBodyText(block);
7119
+ const layers = [
7120
+ {
7121
+ type: "shape",
7122
+ id: "fallback-bg",
7123
+ content: { shape: "rect", fill: colors.bg },
7124
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
5955
7125
  }
5956
- case "tree": {
5957
- const fences = (contents ?? []).filter((n) => n.type === "code");
5958
- const fence = fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : void 0;
5959
- if (fence) {
5960
- const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
5961
- if (detection.isTree && detection.tree) {
5962
- const { items } = treeToTemplateData(detection.tree);
5963
- return { items, ...headingText ? { title: headingText } : {} };
7126
+ ];
7127
+ if (title) {
7128
+ layers.push({
7129
+ type: "text",
7130
+ id: "fallback-title",
7131
+ content: {
7132
+ text: title,
7133
+ style: {
7134
+ fontSize: themedFontSize(56, context, true),
7135
+ fontFamily: getThemeFont(context, "title"),
7136
+ fontWeight: "bold",
7137
+ color: colors.text,
7138
+ textAlign: "center"
5964
7139
  }
7140
+ },
7141
+ position: {
7142
+ x: "50%",
7143
+ y: body ? "28%" : "50%",
7144
+ anchor: "center",
7145
+ width: layout.maxTextWidth
5965
7146
  }
5966
- const list = findFirstList(contents);
5967
- if (list) {
5968
- const { items } = treeToTemplateData(treeFromMarkdownList(list));
5969
- if (items.length > 0) return { items, ...headingText ? { title: headingText } : {} };
7147
+ });
7148
+ }
7149
+ if (body) {
7150
+ layers.push({
7151
+ type: "text",
7152
+ id: "fallback-body",
7153
+ content: {
7154
+ text: body,
7155
+ style: {
7156
+ fontSize: themedFontSize(28, context),
7157
+ fontFamily: getThemeFont(context, "body"),
7158
+ color: colors.text,
7159
+ textAlign: "center"
7160
+ }
7161
+ },
7162
+ position: {
7163
+ x: "50%",
7164
+ y: "55%",
7165
+ anchor: "center",
7166
+ width: layout.maxTextWidth
5970
7167
  }
5971
- return placeholders ? {} : null;
5972
- }
5973
- default:
5974
- return placeholders ? {} : null;
7168
+ });
5975
7169
  }
7170
+ layers.push({
7171
+ type: "text",
7172
+ id: "fallback-notice",
7173
+ content: {
7174
+ text: `\u26A0 ${notice}`,
7175
+ style: {
7176
+ fontSize: themedFontSize(18, context),
7177
+ fontFamily: getThemeFont(context, "body"),
7178
+ color: withAlpha(colors.text, 0.6),
7179
+ textAlign: "center"
7180
+ }
7181
+ },
7182
+ position: { x: "50%", y: "92%", anchor: "center", width: layout.maxTextWidth }
7183
+ });
7184
+ return layers;
7185
+ }
7186
+ function extractBodyText(block) {
7187
+ if (!block.contents || block.contents.length === 0) return "";
7188
+ const text = block.contents.map((node) => extractPlainText(node)).join("\n").trim();
7189
+ if (text.length <= MAX_BODY_CHARS) return text;
7190
+ return `${text.slice(0, MAX_BODY_CHARS).trimEnd()}\u2026`;
5976
7191
  }
5977
7192
 
5978
7193
  // src/doc/templates/mediaLayoutPolicy.ts
@@ -6206,6 +7421,55 @@ var BLOCK_MEDIA_LAYOUT_POLICIES = {
6206
7421
  unconsumedMedia: "reserved-slot",
6207
7422
  variants: DENSE_VARIANTS
6208
7423
  },
7424
+ barChart: {
7425
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7426
+ noMedia: "template-default",
7427
+ ownership: "supplemental",
7428
+ unconsumedMedia: "reserved-slot",
7429
+ variants: DENSE_VARIANTS
7430
+ },
7431
+ columnChart: {
7432
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7433
+ noMedia: "template-default",
7434
+ ownership: "supplemental",
7435
+ unconsumedMedia: "reserved-slot",
7436
+ variants: DENSE_VARIANTS
7437
+ },
7438
+ pieChart: {
7439
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7440
+ noMedia: "template-default",
7441
+ ownership: "supplemental",
7442
+ unconsumedMedia: "reserved-slot",
7443
+ variants: DENSE_VARIANTS
7444
+ },
7445
+ donutChart: {
7446
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7447
+ noMedia: "template-default",
7448
+ ownership: "supplemental",
7449
+ unconsumedMedia: "reserved-slot",
7450
+ variants: DENSE_VARIANTS
7451
+ },
7452
+ lineChart: {
7453
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7454
+ noMedia: "template-default",
7455
+ ownership: "supplemental",
7456
+ unconsumedMedia: "reserved-slot",
7457
+ variants: DENSE_VARIANTS
7458
+ },
7459
+ areaChart: {
7460
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7461
+ noMedia: "template-default",
7462
+ ownership: "supplemental",
7463
+ unconsumedMedia: "reserved-slot",
7464
+ variants: DENSE_VARIANTS
7465
+ },
7466
+ scatterChart: {
7467
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7468
+ noMedia: "template-default",
7469
+ ownership: "supplemental",
7470
+ unconsumedMedia: "reserved-slot",
7471
+ variants: DENSE_VARIANTS
7472
+ },
6209
7473
  diagram: {
6210
7474
  summary: "The node-and-edge canvas is intrinsic media and retains its spatial coordinate system.",
6211
7475
  noMedia: "intrinsic-visual",
@@ -7191,6 +8455,34 @@ var TEMPLATE_METADATA = {
7191
8455
  label: "Data Table",
7192
8456
  description: "Renders tabular data in a clean, styled table with a header row."
7193
8457
  },
8458
+ barChart: {
8459
+ label: "Bar Chart",
8460
+ description: "Horizontal bars from the block's markdown table \u2014 one bar per row."
8461
+ },
8462
+ columnChart: {
8463
+ label: "Column Chart",
8464
+ description: "Vertical columns from the block's markdown table \u2014 one column per row."
8465
+ },
8466
+ pieChart: {
8467
+ label: "Pie Chart",
8468
+ description: "A pie of the block's markdown table \u2014 each row becomes a labeled slice."
8469
+ },
8470
+ donutChart: {
8471
+ label: "Donut Chart",
8472
+ description: "A donut of the block's markdown table \u2014 each row becomes a labeled slice."
8473
+ },
8474
+ lineChart: {
8475
+ label: "Line Chart",
8476
+ description: "Lines from the block's markdown table \u2014 one line per value column."
8477
+ },
8478
+ areaChart: {
8479
+ label: "Area Chart",
8480
+ description: "Filled area lines from the block's markdown table \u2014 one area per value column."
8481
+ },
8482
+ scatterChart: {
8483
+ label: "Scatter Chart",
8484
+ description: "An x/y scatter plot from the block's markdown table's numeric columns."
8485
+ },
7194
8486
  diagram: {
7195
8487
  label: "Diagram",
7196
8488
  description: "Renders sub-headings as connected nodes \u2014 edit visually as a node-and-edge diagram."
@@ -7336,6 +8628,48 @@ var TEMPLATE_AUTHORING_METADATA = {
7336
8628
  safeForContentFirst: false,
7337
8629
  placement: "heading"
7338
8630
  },
8631
+ barChart: {
8632
+ role: "data",
8633
+ bodyPolicy: "structured",
8634
+ safeForContentFirst: false,
8635
+ placement: "heading"
8636
+ },
8637
+ columnChart: {
8638
+ role: "data",
8639
+ bodyPolicy: "structured",
8640
+ safeForContentFirst: false,
8641
+ placement: "heading"
8642
+ },
8643
+ pieChart: {
8644
+ role: "data",
8645
+ bodyPolicy: "structured",
8646
+ safeForContentFirst: false,
8647
+ placement: "heading"
8648
+ },
8649
+ donutChart: {
8650
+ role: "data",
8651
+ bodyPolicy: "structured",
8652
+ safeForContentFirst: false,
8653
+ placement: "heading"
8654
+ },
8655
+ lineChart: {
8656
+ role: "data",
8657
+ bodyPolicy: "structured",
8658
+ safeForContentFirst: false,
8659
+ placement: "heading"
8660
+ },
8661
+ areaChart: {
8662
+ role: "data",
8663
+ bodyPolicy: "structured",
8664
+ safeForContentFirst: false,
8665
+ placement: "heading"
8666
+ },
8667
+ scatterChart: {
8668
+ role: "data",
8669
+ bodyPolicy: "structured",
8670
+ safeForContentFirst: false,
8671
+ placement: "heading"
8672
+ },
7339
8673
  diagram: {
7340
8674
  role: "spatial",
7341
8675
  bodyPolicy: "structured",
@@ -8200,6 +9534,18 @@ function time(raw) {
8200
9534
  if (raw == null) return null;
8201
9535
  return parseTimeSeconds(raw);
8202
9536
  }
9537
+ function videoPlacement(params) {
9538
+ const explicit = params.placement ?? params.presentation;
9539
+ if (explicit === "content") return "content";
9540
+ if (explicit === "pip" || explicit === "picture-in-picture") return "picture-in-picture";
9541
+ if (explicit === "overlay" || explicit === "full-frame") return "overlay";
9542
+ if (params.pip === "" || params.pip === "true") return "picture-in-picture";
9543
+ if (params.overlay === "" || params.overlay === "true") return "overlay";
9544
+ return void 0;
9545
+ }
9546
+ function lockToBlock(raw) {
9547
+ return raw !== "false" && raw !== "0";
9548
+ }
8203
9549
  function toMediaClip(parsed, id) {
8204
9550
  const { template, params } = parsed;
8205
9551
  if (!template || !MEDIA_TEMPLATES.has(template)) return null;
@@ -8214,13 +9560,78 @@ function toMediaClip(parsed, id) {
8214
9560
  startAt: time(params.startAt ?? params.startTime) ?? 0,
8215
9561
  anchor
8216
9562
  };
9563
+ const placement = kind === "video" ? videoPlacement(params) : void 0;
9564
+ if (placement && placement !== "content") clip.placement = placement;
9565
+ if (kind === "video" && placement && placement !== "content" && params.lockToBlock != null) {
9566
+ const locked = lockToBlock(params.lockToBlock);
9567
+ clip.lockToBlock = locked;
9568
+ clip.anchor = locked ? "block" : "document";
9569
+ if (locked) clip.startAt = 0;
9570
+ }
8217
9571
  const clipStart = time(params.clipStart);
8218
9572
  const clipEnd = time(params.clipEnd);
8219
9573
  if (clipStart != null) clip.clipStart = clipStart;
8220
9574
  if (clipEnd != null) clip.clipEnd = clipEnd;
9575
+ if (clip.lockToBlock === true) delete clip.clipEnd;
8221
9576
  if (params.spillover === "true" || params.spillover === "") clip.spillover = true;
8222
9577
  return clip;
8223
9578
  }
9579
+ function firstVideoElement(children) {
9580
+ const visit = (nodes) => {
9581
+ for (const child of nodes) {
9582
+ if (child.type !== "htmlElement") continue;
9583
+ if (child.tagName === "video") return child;
9584
+ const nested = visit(child.children);
9585
+ if (nested) return nested;
9586
+ }
9587
+ return null;
9588
+ };
9589
+ return visit(children);
9590
+ }
9591
+ function standaloneHtmlVideo(node) {
9592
+ if (node.type === "htmlBlock") {
9593
+ if (!/^\s*<video\b/i.test(node.rawHtml)) return null;
9594
+ const element = firstVideoElement(node.htmlChildren);
9595
+ return element ? { element, raw: node.rawHtml.trim() } : null;
9596
+ }
9597
+ if (node.type !== "paragraph" || !node.children.every((child) => child.type === "htmlInline")) {
9598
+ return null;
9599
+ }
9600
+ const raw = node.children.map((child) => child.type === "htmlInline" ? child.rawHtml : "").join("");
9601
+ if (!/^\s*<video\b[\s\S]*<\/video>\s*$/i.test(raw)) return null;
9602
+ for (const child of node.children) {
9603
+ if (child.type !== "htmlInline") continue;
9604
+ const element = firstVideoElement(child.htmlChildren);
9605
+ if (element) return { element, raw: raw.trim() };
9606
+ }
9607
+ return null;
9608
+ }
9609
+ function htmlVideoClip(node, id) {
9610
+ const authored = standaloneHtmlVideo(node);
9611
+ if (!authored) return null;
9612
+ const { element } = authored;
9613
+ const placement = videoPlacement({
9614
+ placement: element.attributes["data-squisq-video-placement"] ?? ""
9615
+ });
9616
+ if (!placement || placement === "content") return null;
9617
+ const src = element.attributes.src;
9618
+ if (!src) return null;
9619
+ const locked = lockToBlock(element.attributes["data-squisq-video-lock-to-block"]);
9620
+ const clip = {
9621
+ id,
9622
+ src,
9623
+ kind: "video",
9624
+ placement,
9625
+ lockToBlock: locked,
9626
+ startAt: locked ? 0 : time(element.attributes["data-squisq-video-start-at"]) ?? 0,
9627
+ anchor: locked ? "block" : "document"
9628
+ };
9629
+ const clipStart = time(element.attributes["data-squisq-video-clip-start"]);
9630
+ const clipEnd = time(element.attributes["data-squisq-video-clip-end"]);
9631
+ if (clipStart != null) clip.clipStart = clipStart;
9632
+ if (!locked && clipEnd != null) clip.clipEnd = clipEnd;
9633
+ return clip;
9634
+ }
8224
9635
  function extractMediaFromContents(contents, makeId, blockId) {
8225
9636
  const media = [];
8226
9637
  const documentMedia = [];
@@ -8229,17 +9640,19 @@ function extractMediaFromContents(contents, makeId, blockId) {
8229
9640
  let originalIndex = 0;
8230
9641
  for (const node of contents ?? []) {
8231
9642
  const parsed = parseStandaloneAnnotation(node);
8232
- const clip = parsed ? toMediaClip(parsed, "") : null;
8233
- if (parsed && clip) {
9643
+ const clip = parsed ? toMediaClip(parsed, "") : htmlVideoClip(node, "");
9644
+ if (clip) {
8234
9645
  clip.id = makeId(clip.src, index++);
8235
9646
  const line = node.position?.start.line;
8236
9647
  if (line != null) clip.sourceLine = line;
8237
9648
  if (blockId !== void 0) {
8238
- const rawText = node.type === "paragraph" && node.children?.[0]?.type === "text" ? node.children[0].value.trim() : void 0;
9649
+ const authoredHtml = standaloneHtmlVideo(node);
9650
+ const rawText = node.type === "paragraph" && node.children?.[0]?.type === "text" ? node.children[0].value.trim() : authoredHtml?.raw;
8239
9651
  clip.origin = {
8240
9652
  blockId,
8241
9653
  index: originalIndex,
8242
- ...rawText !== void 0 ? { raw: rawText } : {}
9654
+ ...rawText !== void 0 ? { raw: rawText } : {},
9655
+ ...authoredHtml ? { format: "html" } : {}
8243
9656
  };
8244
9657
  }
8245
9658
  (clip.anchor === "document" ? documentMedia : media).push(clip);
@@ -8576,6 +9989,12 @@ function markdownToDoc(markdownDoc, options) {
8576
9989
  }
8577
9990
  currentTime += block.duration;
8578
9991
  }
9992
+ const blockById = new Map(allBlocks.map((block) => [block.id, block]));
9993
+ for (const clip of documentMedia) {
9994
+ if (clip.lockToBlock === false && clip.origin?.format === "html" && !/\bdata-squisq-video-start-at\s*=/i.test(clip.origin.raw ?? "")) {
9995
+ clip.startAt = blockById.get(clip.origin.blockId)?.startTime ?? 0;
9996
+ }
9997
+ }
8579
9998
  const captions = phrases.length > 0 ? {
8580
9999
  phrases,
8581
10000
  ...options?.captionsGeneratedAt ? { generatedAt: options.captionsGeneratedAt } : {},
@@ -8661,7 +10080,7 @@ function applyStructuredData(block, diagnostics) {
8661
10080
  }
8662
10081
  data = { ...data, ...result.data };
8663
10082
  }
8664
- if (block.template === "dataTable") {
10083
+ if (TABLE_FED_TEMPLATES.has(resolveTemplateName(block.template ?? ""))) {
8665
10084
  const provided = (key) => data && key in data || block.templateOverrides && key in block.templateOverrides;
8666
10085
  if (!provided("headers") && !provided("rows")) {
8667
10086
  const table = findFirstTable(block.contents);
@@ -9187,6 +10606,7 @@ export {
9187
10606
  wrapWithPersistentLayers,
9188
10607
  TEMPLATE_ALIASES,
9189
10608
  resolveTemplateName,
10609
+ TABLE_FED_TEMPLATES,
9190
10610
  CONTAINER_TEMPLATES,
9191
10611
  isContainerTemplate,
9192
10612
  resolveColorScheme,
@@ -9218,6 +10638,30 @@ export {
9218
10638
  rightFeature,
9219
10639
  mapBlock,
9220
10640
  fullBleedQuote,
10641
+ computeDiagramLayout,
10642
+ ASCII_CHAR_W,
10643
+ ASCII_CHAR_H,
10644
+ asciiCellToCanvas,
10645
+ canvasToAsciiCell,
10646
+ asciiDiagramToTemplateData,
10647
+ asciiDiagramFromTemplateData,
10648
+ asciiDiagramFromBlocks,
10649
+ asciiTimelineToTemplateData,
10650
+ treeToTemplateData,
10651
+ treeFromTemplateData,
10652
+ treeFromMarkdownList,
10653
+ findFirstList,
10654
+ extractBodyPlainText,
10655
+ extractListItems,
10656
+ extractRichListItems,
10657
+ extractImages,
10658
+ extractFirstImage,
10659
+ extractEmbeddedVideos,
10660
+ extractFirstEmbeddedVideo,
10661
+ extractTableFromContents,
10662
+ extractBlockquoteText,
10663
+ autoTemplatePreservesContent,
10664
+ deriveTemplateInputs,
9221
10665
  listBlock,
9222
10666
  photoGrid,
9223
10667
  definitionCard,
@@ -9226,12 +10670,12 @@ export {
9226
10670
  videoWithCaption,
9227
10671
  videoPullQuote,
9228
10672
  dataTable,
10673
+ buildChartData,
9229
10674
  DIAGRAM_LABEL_LINE_HEIGHT,
9230
10675
  DIAGRAM_LABEL_HORIZONTAL_PADDING,
9231
10676
  DIAGRAM_LABEL_VERTICAL_PADDING,
9232
10677
  DIAGRAM_LABEL_MIN_FONT_SIZE,
9233
10678
  fitDiagramLabel,
9234
- computeDiagramLayout,
9235
10679
  diagramBlock,
9236
10680
  treeBlock,
9237
10681
  timelineBlock,
@@ -9252,28 +10696,6 @@ export {
9252
10696
  coerceTemplateParams,
9253
10697
  lintTemplateParams,
9254
10698
  fallbackBlockLayers,
9255
- ASCII_CHAR_W,
9256
- ASCII_CHAR_H,
9257
- asciiCellToCanvas,
9258
- canvasToAsciiCell,
9259
- asciiDiagramToTemplateData,
9260
- asciiDiagramFromTemplateData,
9261
- asciiDiagramFromBlocks,
9262
- asciiTimelineToTemplateData,
9263
- treeToTemplateData,
9264
- treeFromTemplateData,
9265
- treeFromMarkdownList,
9266
- findFirstList,
9267
- extractBodyPlainText,
9268
- extractListItems,
9269
- extractImages,
9270
- extractFirstImage,
9271
- extractEmbeddedVideos,
9272
- extractFirstEmbeddedVideo,
9273
- extractTableFromContents,
9274
- extractBlockquoteText,
9275
- autoTemplatePreservesContent,
9276
- deriveTemplateInputs,
9277
10699
  BLOCK_MEDIA_LAYOUT_POLICIES,
9278
10700
  getBlockMediaLayoutPolicy,
9279
10701
  materializeBlockLayers,