@bendyline/squisq 2.3.3 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{Doc-BrgZC7SE.d.ts → Doc-CSW6K2UF.d.ts} +222 -9
  2. package/dist/{ImageEditDoc-rum0Xb9P.d.ts → ImageEditDoc-x9yoTb4l.d.ts} +1 -1
  3. package/dist/{chunk-GQNH7PLN.js → chunk-2HY2ZA7U.js} +1470 -77
  4. package/dist/{chunk-G3JOS25T.js → chunk-7TJJA2RI.js} +46 -15
  5. package/dist/{chunk-RS5AP3J4.js → chunk-AE3IUKMM.js} +1 -1
  6. package/dist/{chunk-IZLKI3IL.js → chunk-AQF5ZYDI.js} +76 -16
  7. package/dist/{chunk-QWCFK5FN.js → chunk-CUYHFOFL.js} +5 -1
  8. package/dist/{chunk-2VCNTDNZ.js → chunk-D6YTLQCL.js} +71 -0
  9. package/dist/{chunk-32R2RZAO.js → chunk-DAMIRKTO.js} +1 -1
  10. package/dist/{chunk-ABP4LAJS.js → chunk-EMXYZLRH.js} +1 -1
  11. package/dist/{chunk-2ZWIXGAC.js → chunk-GAZKTT4R.js} +28 -4
  12. package/dist/{chunk-SPTY4C6F.js → chunk-GODLNXO4.js} +45 -3
  13. package/dist/chunk-JUAC2QWP.js +983 -0
  14. package/dist/{chunk-BCCXTMN5.js → chunk-O7JILDEF.js} +7 -0
  15. package/dist/{chunk-C33GTPUZ.js → chunk-SBAX4ZPO.js} +438 -0
  16. package/dist/doc/index.d.ts +113 -6
  17. package/dist/doc/index.js +14 -11
  18. package/dist/generate/index.d.ts +1 -1
  19. package/dist/imageEdit/index.d.ts +3 -3
  20. package/dist/index.d.ts +7 -7
  21. package/dist/index.js +41 -17
  22. package/dist/jsonForm/index.d.ts +1 -1
  23. package/dist/jsonForm/index.js +4 -4
  24. package/dist/markdown/index.d.ts +118 -1
  25. package/dist/markdown/index.js +22 -6
  26. package/dist/{materializePageSection-CrZWYnXM.d.ts → materializePageSection-_rrDuOYU.d.ts} +2 -2
  27. package/dist/narration/index.d.ts +1 -1
  28. package/dist/narration/index.js +6 -6
  29. package/dist/recommend/index.js +2 -2
  30. package/dist/schemas/index.d.ts +32 -5
  31. package/dist/schemas/index.js +9 -3
  32. package/dist/storage/index.d.ts +2 -3
  33. package/dist/{themeLibrary-DJt89gyP.d.ts → themeLibrary-DlJzXsin.d.ts} +1 -1
  34. package/dist/timing/index.d.ts +1 -2
  35. package/dist/transform/index.d.ts +2 -2
  36. package/dist/transform/index.js +2 -2
  37. package/package.json +2 -2
  38. package/dist/chunk-UA5DTYAY.js +0 -400
@@ -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,
@@ -30,9 +30,10 @@ import {
30
30
  pickContrastingText,
31
31
  relativeLuminance,
32
32
  resolveFontFamily,
33
+ validateCustomTemplateDefinition,
33
34
  validateTheme,
34
35
  withAlpha
35
- } from "./chunk-C33GTPUZ.js";
36
+ } from "./chunk-SBAX4ZPO.js";
36
37
  import {
37
38
  VIEWPORT_PRESETS,
38
39
  cloneData,
@@ -45,19 +46,22 @@ import {
45
46
  } from "./chunk-BAOV476U.js";
46
47
  import {
47
48
  KNOWN_BLOCK_META_KEYS,
49
+ TABLE_FED_TEMPLATES,
48
50
  coerceAnnotationValues,
51
+ isContainerTemplate,
49
52
  matchTrailingTemplateAnnotation,
50
53
  parseNumber,
51
54
  parseTimeSeconds,
55
+ resolveTemplateName,
52
56
  sanitizeUrl,
53
57
  splitKeyValueToken,
54
58
  tokenizeAttrTokens
55
- } from "./chunk-2VCNTDNZ.js";
59
+ } from "./chunk-D6YTLQCL.js";
56
60
  import {
57
61
  extractPlainText,
58
62
  readFrontmatterThemeId,
59
63
  walkMarkdownTree
60
- } from "./chunk-BCCXTMN5.js";
64
+ } from "./chunk-O7JILDEF.js";
61
65
  import {
62
66
  resolveBlockTransition
63
67
  } from "./chunk-4VOD55SX.js";
@@ -411,23 +415,6 @@ function wrapWithPersistentLayers(layers, block, bottomLayers, topLayers) {
411
415
  ];
412
416
  }
413
417
 
414
- // src/doc/templates/templateNames.ts
415
- var TEMPLATE_ALIASES = Object.freeze({
416
- titleBlock: "title",
417
- quoteBlock: "quote",
418
- mapBlock: "map",
419
- listBlock: "list",
420
- diagramBlock: "diagram",
421
- diagramNode: "diagram"
422
- });
423
- function resolveTemplateName(name) {
424
- return TEMPLATE_ALIASES[name] ?? name;
425
- }
426
- var CONTAINER_TEMPLATES = /* @__PURE__ */ new Set(["diagram", "drawing", "layout"]);
427
- function isContainerTemplate(name) {
428
- return !!name && CONTAINER_TEMPLATES.has(resolveTemplateName(name));
429
- }
430
-
431
418
  // src/doc/utils/themeUtils.ts
432
419
  var FALLBACK_COLOR_SCHEME = {
433
420
  bg: "#1a365d",
@@ -949,7 +936,7 @@ function sectionHeader(input, context) {
949
936
 
950
937
  // src/doc/templates/contentBlock.ts
951
938
  function completeBodyText(context) {
952
- return (context.block?.contents ?? []).map((node) => extractPlainText(node)).join("\n").trim();
939
+ return (context.block?.contents ?? []).filter((node) => !(node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid")).map((node) => extractPlainText(node)).join("\n").trim();
953
940
  }
954
941
  function bodyFontSize(body, context) {
955
942
  const base = body.length > 1800 ? 18 : body.length > 1100 ? 21 : body.length > 650 ? 24 : 28;
@@ -2739,6 +2726,32 @@ function deriveTemplateInputs(templateName, headingText, contents, options = {})
2739
2726
  if (tableData) return { ...tableData, ...headingText ? { title: headingText } : {} };
2740
2727
  return placeholders ? { headers: ["Column"], rows: [["Data"]] } : null;
2741
2728
  }
2729
+ case "barChart":
2730
+ case "columnChart":
2731
+ case "pieChart":
2732
+ case "donutChart":
2733
+ case "lineChart":
2734
+ case "areaChart":
2735
+ case "scatterChart": {
2736
+ const tableData = extractTableFromContents(contents);
2737
+ if (tableData) {
2738
+ return {
2739
+ headers: tableData.headers,
2740
+ rows: tableData.rows,
2741
+ ...headingText ? { title: headingText } : {}
2742
+ };
2743
+ }
2744
+ return placeholders && !bodyText3.trim() ? {
2745
+ headers: ["Quarter", "Revenue", "Costs"],
2746
+ rows: [
2747
+ ["Q1", "40", "28"],
2748
+ ["Q2", "55", "31"],
2749
+ ["Q3", "48", "30"],
2750
+ ["Q4", "62", "35"]
2751
+ ],
2752
+ ...headingText ? { title: headingText } : {}
2753
+ } : null;
2754
+ }
2742
2755
  case "imageWithCaption": {
2743
2756
  const img = extractFirstImage(contents);
2744
2757
  if (!img) return placeholders ? { caption: headingText } : null;
@@ -5585,6 +5598,1019 @@ function makeCustomTemplateFn(def) {
5585
5598
  };
5586
5599
  }
5587
5600
 
5601
+ // src/doc/templates/chart/parse.ts
5602
+ var NUMERIC_COLUMN_THRESHOLD = 0.6;
5603
+ function parseCellNumber(raw) {
5604
+ if (typeof raw !== "string") return null;
5605
+ let text = raw.trim();
5606
+ if (!text) return null;
5607
+ let negative = false;
5608
+ const accounting = /^\((.*)\)$/.exec(text);
5609
+ if (accounting) {
5610
+ negative = true;
5611
+ text = accounting[1].trim();
5612
+ }
5613
+ text = text.replace(/^([+-]?)\s*[$€£¥]\s*/, "$1");
5614
+ text = text.replace(/%$/, "").trim();
5615
+ text = text.replace(/,/g, "");
5616
+ if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(text)) {
5617
+ text = text.replace(/ /g, "");
5618
+ }
5619
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text)) return null;
5620
+ const value = Number(text);
5621
+ if (!Number.isFinite(value)) return null;
5622
+ return negative ? -value : value;
5623
+ }
5624
+ function isNumericColumn(rows, colIndex) {
5625
+ let nonEmpty = 0;
5626
+ let numeric = 0;
5627
+ for (const row of rows) {
5628
+ const cell = (row[colIndex] ?? "").trim();
5629
+ if (!cell) continue;
5630
+ nonEmpty += 1;
5631
+ if (parseCellNumber(cell) !== null) numeric += 1;
5632
+ }
5633
+ return numeric > 0 && numeric / Math.max(1, nonEmpty) >= NUMERIC_COLUMN_THRESHOLD;
5634
+ }
5635
+ function resolveColumnRef(ref, headers) {
5636
+ const text = String(ref).trim();
5637
+ if (!text) return null;
5638
+ const lower = text.toLowerCase();
5639
+ const byName = headers.findIndex((h) => h.trim().toLowerCase() === lower);
5640
+ if (byName >= 0) return byName;
5641
+ if (/^\d+$/.test(text)) {
5642
+ const index = Number(text);
5643
+ if (index < headers.length) return index;
5644
+ }
5645
+ return null;
5646
+ }
5647
+ function buildChartData(table, options = {}) {
5648
+ const headers = Array.isArray(table.headers) ? table.headers : [];
5649
+ const rows = Array.isArray(table.rows) ? table.rows : [];
5650
+ if (headers.length === 0 || rows.length === 0) return null;
5651
+ const warnings = [];
5652
+ let labelIndex = 0;
5653
+ if (options.labelColumn !== void 0 && String(options.labelColumn).trim() !== "") {
5654
+ const resolved = resolveColumnRef(String(options.labelColumn), headers);
5655
+ if (resolved === null) {
5656
+ warnings.push(
5657
+ `labelColumn "${options.labelColumn}" matches no column; using the first column`
5658
+ );
5659
+ } else {
5660
+ labelIndex = resolved;
5661
+ }
5662
+ }
5663
+ let valueIndexes = [];
5664
+ const requested = (options.valueColumns ?? []).map((ref) => String(ref)).filter((ref) => ref.trim() !== "");
5665
+ if (requested.length > 0) {
5666
+ for (const ref of requested) {
5667
+ const resolved = resolveColumnRef(ref, headers);
5668
+ if (resolved === null) {
5669
+ warnings.push(`valueColumns entry "${ref}" matches no column; skipped`);
5670
+ } else if (!valueIndexes.includes(resolved)) {
5671
+ valueIndexes.push(resolved);
5672
+ }
5673
+ }
5674
+ } else {
5675
+ valueIndexes = headers.map((_, index) => index).filter((index) => index !== labelIndex && isNumericColumn(rows, index));
5676
+ }
5677
+ if (valueIndexes.length === 0) return null;
5678
+ return {
5679
+ labels: rows.map((row) => (row[labelIndex] ?? "").trim()),
5680
+ labelHeader: headers[labelIndex] ?? "",
5681
+ labelNumbers: rows.map((row) => parseCellNumber(row[labelIndex] ?? "")),
5682
+ series: valueIndexes.map((index) => ({
5683
+ name: (headers[index] ?? `Column ${index + 1}`).trim() || `Column ${index + 1}`,
5684
+ values: rows.map((row) => parseCellNumber(row[index] ?? ""))
5685
+ })),
5686
+ warnings
5687
+ };
5688
+ }
5689
+
5690
+ // src/doc/templates/chart/scale.ts
5691
+ function niceNum(range, round4) {
5692
+ const exponent = Math.floor(Math.log10(range));
5693
+ const fraction = range / 10 ** exponent;
5694
+ let niceFraction;
5695
+ if (round4) {
5696
+ if (fraction < 1.5) niceFraction = 1;
5697
+ else if (fraction < 3) niceFraction = 2;
5698
+ else if (fraction < 7) niceFraction = 5;
5699
+ else niceFraction = 10;
5700
+ } else {
5701
+ if (fraction <= 1) niceFraction = 1;
5702
+ else if (fraction <= 2) niceFraction = 2;
5703
+ else if (fraction <= 5) niceFraction = 5;
5704
+ else niceFraction = 10;
5705
+ }
5706
+ return niceFraction * 10 ** exponent;
5707
+ }
5708
+ function niceTicks(min, max, targetCount = 5) {
5709
+ let lo = Number.isFinite(min) ? min : 0;
5710
+ let hi = Number.isFinite(max) ? max : 0;
5711
+ if (lo > hi) [lo, hi] = [hi, lo];
5712
+ if (lo === hi) {
5713
+ const pad = lo === 0 ? 1 : niceNum(Math.abs(lo), true);
5714
+ lo -= pad;
5715
+ hi += pad;
5716
+ if (min >= 0 && lo < 0) lo = 0;
5717
+ }
5718
+ const count = Math.max(2, targetCount);
5719
+ const range = niceNum(hi - lo, false);
5720
+ const step = niceNum(range / (count - 1), true);
5721
+ const niceMin = Math.floor(lo / step) * step;
5722
+ const niceMax = Math.ceil(hi / step) * step;
5723
+ const ticks = [];
5724
+ for (let tick = niceMin; tick <= niceMax + step * 1e-6; tick += step) {
5725
+ ticks.push(Math.abs(tick) < step * 1e-6 ? 0 : Number(tick.toPrecision(12)));
5726
+ }
5727
+ return { ticks, niceMin, niceMax, step };
5728
+ }
5729
+ function formatChartValue(value, unit) {
5730
+ if (!Number.isFinite(value)) return "\u2014";
5731
+ const sign = value < 0 ? "-" : "";
5732
+ const abs = Math.abs(value);
5733
+ let text;
5734
+ if (abs >= 1e6) {
5735
+ text = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
5736
+ } else if (abs >= 1e3) {
5737
+ text = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
5738
+ } else {
5739
+ text = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5740
+ }
5741
+ return unit ? `${text} ${unit}` : text;
5742
+ }
5743
+ function trimTrailingZero(text) {
5744
+ return text.replace(/\.0$/, "");
5745
+ }
5746
+
5747
+ // src/doc/templates/chart/geometry.ts
5748
+ function round3(value) {
5749
+ return Math.round(value * 100) / 100;
5750
+ }
5751
+ function pointOnCircle(cx, cy, r, deg) {
5752
+ const rad = (deg - 90) * Math.PI / 180;
5753
+ return { x: round3(cx + r * Math.cos(rad)), y: round3(cy + r * Math.sin(rad)) };
5754
+ }
5755
+ function arcPath(cx, cy, rOuter, rInner, startDeg, endDeg) {
5756
+ let sweep = endDeg - startDeg;
5757
+ if (sweep <= 0) return "";
5758
+ if (sweep >= 359.9) sweep = 359.9;
5759
+ const end = startDeg + sweep;
5760
+ const largeArc = sweep > 180 ? 1 : 0;
5761
+ const o1 = pointOnCircle(cx, cy, rOuter, startDeg);
5762
+ const o2 = pointOnCircle(cx, cy, rOuter, end);
5763
+ if (rInner <= 0) {
5764
+ return [
5765
+ `M ${round3(cx)} ${round3(cy)}`,
5766
+ `L ${o1.x} ${o1.y}`,
5767
+ `A ${round3(rOuter)} ${round3(rOuter)} 0 ${largeArc} 1 ${o2.x} ${o2.y}`,
5768
+ "Z"
5769
+ ].join(" ");
5770
+ }
5771
+ const i1 = pointOnCircle(cx, cy, rInner, startDeg);
5772
+ const i2 = pointOnCircle(cx, cy, rInner, end);
5773
+ return [
5774
+ `M ${o1.x} ${o1.y}`,
5775
+ `A ${round3(rOuter)} ${round3(rOuter)} 0 ${largeArc} 1 ${o2.x} ${o2.y}`,
5776
+ `L ${i2.x} ${i2.y}`,
5777
+ `A ${round3(rInner)} ${round3(rInner)} 0 ${largeArc} 0 ${i1.x} ${i1.y}`,
5778
+ "Z"
5779
+ ].join(" ");
5780
+ }
5781
+ function polylinePath(points) {
5782
+ if (points.length === 0) return "";
5783
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round3(p.x)} ${round3(p.y)}`).join(" ");
5784
+ }
5785
+ function areaPath(points, baselineY) {
5786
+ if (points.length === 0) return "";
5787
+ const open = polylinePath(points);
5788
+ const first = points[0];
5789
+ const last = points[points.length - 1];
5790
+ return `${open} L ${round3(last.x)} ${round3(baselineY)} L ${round3(first.x)} ${round3(baselineY)} Z`;
5791
+ }
5792
+
5793
+ // src/doc/templates/chart/layout.ts
5794
+ function computeChartFrame(options) {
5795
+ const W = options.viewportWidth;
5796
+ const H = options.viewportHeight;
5797
+ const marginX = W * 0.06;
5798
+ const marginY = H * 0.06;
5799
+ const innerWidth = W - marginX * 2;
5800
+ let cursor = marginY;
5801
+ let titleBand;
5802
+ if (options.hasTitle) {
5803
+ titleBand = { x: marginX, y: cursor, width: innerWidth, height: options.titleFontSize * 2.2 };
5804
+ cursor = titleBand.y + titleBand.height;
5805
+ }
5806
+ let tableBand;
5807
+ let tableHeight = 0;
5808
+ if (options.tableRowCount > 0) {
5809
+ tableHeight = Math.min(H * 0.4, (options.tableRowCount + 1) * options.tableFontSize * 2.4);
5810
+ }
5811
+ const legendHeight = options.legendRows > 0 ? options.legendRows * options.legendRowHeight : 0;
5812
+ const chartArea = {
5813
+ x: marginX,
5814
+ y: cursor,
5815
+ width: innerWidth,
5816
+ height: Math.max(40, H - marginY - cursor - legendHeight - tableHeight)
5817
+ };
5818
+ cursor = chartArea.y + chartArea.height;
5819
+ let legendBand;
5820
+ if (legendHeight > 0) {
5821
+ legendBand = { x: marginX, y: cursor, width: innerWidth, height: legendHeight };
5822
+ cursor += legendHeight;
5823
+ }
5824
+ if (tableHeight > 0) {
5825
+ tableBand = { x: marginX, y: cursor, width: innerWidth, height: tableHeight };
5826
+ }
5827
+ const plot = {
5828
+ x: chartArea.x + options.leftGutter,
5829
+ y: chartArea.y,
5830
+ width: Math.max(20, chartArea.width - options.leftGutter),
5831
+ height: Math.max(20, chartArea.height - options.bottomGutter)
5832
+ };
5833
+ return { titleBand, chartArea, plot, legendBand, tableBand };
5834
+ }
5835
+ function stackSeries(series) {
5836
+ const rowCount = series.reduce((max, s) => Math.max(max, s.values.length), 0);
5837
+ const positive = new Array(rowCount).fill(0);
5838
+ const negative = new Array(rowCount).fill(0);
5839
+ return series.map(
5840
+ (s) => Array.from({ length: rowCount }, (_, row) => {
5841
+ const value = s.values[row];
5842
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
5843
+ return { y0: positive[row], y1: positive[row] };
5844
+ }
5845
+ if (value >= 0) {
5846
+ const y0 = positive[row];
5847
+ positive[row] += value;
5848
+ return { y0, y1: positive[row] };
5849
+ }
5850
+ const y1 = negative[row];
5851
+ negative[row] += value;
5852
+ return { y0: negative[row], y1 };
5853
+ })
5854
+ );
5855
+ }
5856
+ function layoutLegend(labels, fontSize, maxWidth) {
5857
+ const swatchSize = fontSize * 0.9;
5858
+ const gap = fontSize * 1.6;
5859
+ const rowHeight = fontSize * 2;
5860
+ const widths = labels.map(
5861
+ (label) => swatchSize + fontSize * 0.5 + label.length * fontSize * 0.58
5862
+ );
5863
+ const rows = [];
5864
+ let current = [];
5865
+ let currentWidth = 0;
5866
+ widths.forEach((width, index) => {
5867
+ const extra = current.length > 0 ? gap : 0;
5868
+ if (current.length > 0 && currentWidth + extra + width > maxWidth) {
5869
+ rows.push(current);
5870
+ current = [];
5871
+ currentWidth = 0;
5872
+ }
5873
+ current.push(index);
5874
+ currentWidth += (current.length > 1 ? gap : 0) + width;
5875
+ });
5876
+ if (current.length > 0) rows.push(current);
5877
+ const items = [];
5878
+ rows.forEach((rowIndexes, rowIndex) => {
5879
+ const rowWidth = rowIndexes.reduce(
5880
+ (sum, index, i) => sum + widths[index] + (i > 0 ? gap : 0),
5881
+ 0
5882
+ );
5883
+ let x = Math.max(0, (maxWidth - rowWidth) / 2);
5884
+ rowIndexes.forEach((index) => {
5885
+ items.push({ label: labels[index], index, x, y: rowIndex * rowHeight, swatchSize });
5886
+ x += widths[index] + gap;
5887
+ });
5888
+ });
5889
+ return { items, rows: rows.length, rowHeight };
5890
+ }
5891
+ function categoryLabelStep(categoryCount, axisLength, fontSize) {
5892
+ if (categoryCount <= 1) return 1;
5893
+ const minPitch = fontSize * 6;
5894
+ const pitch = axisLength / categoryCount;
5895
+ return Math.max(1, Math.ceil(minPitch / Math.max(1, pitch)));
5896
+ }
5897
+
5898
+ // src/doc/templates/chart/render.ts
5899
+ var TEMPLATE_ID = {
5900
+ bar: "barChart",
5901
+ column: "columnChart",
5902
+ pie: "pieChart",
5903
+ donut: "donutChart",
5904
+ line: "lineChart",
5905
+ area: "areaChart",
5906
+ scatter: "scatterChart"
5907
+ };
5908
+ function renderChart(kind, input, context) {
5909
+ const table = resolveTable(input, context);
5910
+ const data = table ? buildChartData(table, input) : null;
5911
+ if (!table || !data) return contentFallback(input, context);
5912
+ const { theme, viewport } = context;
5913
+ const templateId = TEMPLATE_ID[kind];
5914
+ const title = (input.title ?? "").trim();
5915
+ const titleFontSize = themedFontSize(44, context, true);
5916
+ const axisFontSize = themedFontSize(20, context, false);
5917
+ const labelFontSize = themedFontSize(22, context, false);
5918
+ const legendFontSize = themedFontSize(22, context, false);
5919
+ const tableFontSize = themedFontSize(20, context, false);
5920
+ const bodyFont = getThemeFont(context, "body");
5921
+ const isRadial = kind === "pie" || kind === "donut";
5922
+ const legendLabels = resolveLegendLabels(kind, input, data);
5923
+ const colors = seriesColors(
5924
+ context,
5925
+ input.colorScheme,
5926
+ isRadial ? data.labels.length : data.series.length
5927
+ );
5928
+ if (isRadial && !data.series[0].values.some((v) => v !== null && v > 0)) {
5929
+ return contentFallback(input, context);
5930
+ }
5931
+ const innerWidth = viewport.width * 0.88;
5932
+ const legend = legendLabels.length > 0 ? layoutLegend(legendLabels, legendFontSize, innerWidth) : null;
5933
+ const gutters = computeGutters(kind, data, input, context, axisFontSize, labelFontSize);
5934
+ const frame = computeChartFrame({
5935
+ viewportWidth: viewport.width,
5936
+ viewportHeight: viewport.height,
5937
+ hasTitle: title.length > 0,
5938
+ titleFontSize,
5939
+ leftGutter: gutters.left,
5940
+ bottomGutter: gutters.bottom,
5941
+ legendRows: legend?.rows ?? 0,
5942
+ legendRowHeight: legendFontSize * 2,
5943
+ tableRowCount: input.showTable === true ? table.rows.length : 0,
5944
+ tableFontSize
5945
+ });
5946
+ const entranceHint = getTemplateHint(context, templateId, "entrance", "staggered");
5947
+ const entrance = (index) => ({
5948
+ type: "fadeIn",
5949
+ duration: 0.6,
5950
+ delay: entranceHint === "subtle" ? 0.1 : Math.min(1.2, 0.2 + index * 0.08)
5951
+ });
5952
+ const paint = {
5953
+ kind,
5954
+ input,
5955
+ context,
5956
+ data,
5957
+ frame,
5958
+ colors,
5959
+ axisFontSize,
5960
+ labelFontSize,
5961
+ bodyFont,
5962
+ gridColor: withAlpha(theme.colors.text, 0.12),
5963
+ axisTextColor: theme.colors.textMuted,
5964
+ entrance
5965
+ };
5966
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
5967
+ if (title && frame.titleBand) {
5968
+ layers.push({
5969
+ type: "text",
5970
+ id: "title",
5971
+ content: {
5972
+ text: title,
5973
+ style: {
5974
+ fontSize: titleFontSize,
5975
+ fontFamily: getThemeFont(context, "title"),
5976
+ fontWeight: "bold",
5977
+ color: theme.colors.text,
5978
+ textAlign: "center",
5979
+ shadow: shouldUseShadow(context)
5980
+ }
5981
+ },
5982
+ position: {
5983
+ x: frame.titleBand.x + frame.titleBand.width / 2,
5984
+ y: frame.titleBand.y + frame.titleBand.height / 2,
5985
+ width: frame.titleBand.width,
5986
+ anchor: "center"
5987
+ },
5988
+ animation: { type: "fadeIn", duration: 0.8 }
5989
+ });
5990
+ }
5991
+ switch (kind) {
5992
+ case "bar":
5993
+ layers.push(...paintBars(paint));
5994
+ break;
5995
+ case "column":
5996
+ layers.push(...paintColumns(paint));
5997
+ break;
5998
+ case "pie":
5999
+ case "donut":
6000
+ layers.push(...paintRadial(paint));
6001
+ break;
6002
+ case "line":
6003
+ case "area":
6004
+ layers.push(...paintLines(paint));
6005
+ break;
6006
+ case "scatter":
6007
+ layers.push(...paintScatter(paint));
6008
+ break;
6009
+ }
6010
+ if (legend && frame.legendBand) {
6011
+ layers.push(...paintLegend(paint, legend, frame.legendBand, legendFontSize));
6012
+ }
6013
+ if (input.showTable === true && frame.tableBand) {
6014
+ layers.push(tableLayer(table, frame.tableBand, tableFontSize, input, context));
6015
+ }
6016
+ return layers;
6017
+ }
6018
+ function resolveTable(input, context) {
6019
+ if (Array.isArray(input.headers) && input.headers.length > 0 && Array.isArray(input.rows) && input.rows.length > 0) {
6020
+ return { headers: input.headers, rows: input.rows };
6021
+ }
6022
+ const contents = input.contents ?? context.block?.contents;
6023
+ return extractTableFromContents(contents);
6024
+ }
6025
+ function contentFallback(input, context) {
6026
+ const fallbackInput = {
6027
+ ...input,
6028
+ template: "content",
6029
+ title: input.title ?? context.block?.title ?? ""
6030
+ };
6031
+ return contentBlock(fallbackInput, context);
6032
+ }
6033
+ function resolveLegendLabels(kind, input, data) {
6034
+ if (input.showLegend === false) return [];
6035
+ if (kind === "pie" || kind === "donut") return data.labels;
6036
+ if (data.series.length > 1 || input.showLegend === true) {
6037
+ return data.series.map((s) => s.name);
6038
+ }
6039
+ return [];
6040
+ }
6041
+ function seriesColors(context, schemeName, count) {
6042
+ const schemes = context.theme.colorSchemes;
6043
+ const names = Object.keys(schemes);
6044
+ const primary = resolveColorScheme(context, schemeName).accent;
6045
+ const palette = [primary];
6046
+ const start = schemeName && names.includes(schemeName) ? names.indexOf(schemeName) : 0;
6047
+ for (let i = 1; i < names.length && palette.length < count; i++) {
6048
+ const accent = schemes[names[(start + i) % names.length]].accent;
6049
+ if (!palette.some((c) => c.toLowerCase() === accent.toLowerCase())) palette.push(accent);
6050
+ }
6051
+ const distinct = palette.length;
6052
+ for (let i = 0; palette.length < count; i++) {
6053
+ palette.push(withAlpha(palette[i % distinct], 0.55));
6054
+ }
6055
+ return palette.slice(0, Math.max(1, count));
6056
+ }
6057
+ function computeGutters(kind, data, input, context, axisFontSize, labelFontSize) {
6058
+ if (kind === "pie" || kind === "donut") return { left: 0, bottom: 0 };
6059
+ if (kind === "bar") {
6060
+ const longest = data.labels.reduce((max, label) => Math.max(max, label.length), 0);
6061
+ const left = Math.min(
6062
+ context.viewport.width * 0.24,
6063
+ Math.max(labelFontSize * 3, longest * labelFontSize * 0.58 + 16)
6064
+ );
6065
+ return { left, bottom: axisFontSize * 2.4 };
6066
+ }
6067
+ const ticks = valueTicks(kind, data, input);
6068
+ const longestTick = ticks.ticks.reduce(
6069
+ (max, tick) => Math.max(max, formatChartValue(tick, input.unit).length),
6070
+ 0
6071
+ );
6072
+ return {
6073
+ left: Math.max(axisFontSize * 2.5, longestTick * axisFontSize * 0.6 + 16),
6074
+ bottom: labelFontSize * 2.6
6075
+ };
6076
+ }
6077
+ function valueTicks(kind, data, input) {
6078
+ let values;
6079
+ if ((kind === "bar" || kind === "column") && input.stacked === true) {
6080
+ const extents = stackSeries(data.series).flat();
6081
+ values = extents.flatMap((e) => [e.y0, e.y1]);
6082
+ } else {
6083
+ values = data.series.flatMap((s) => s.values.filter((v) => v !== null));
6084
+ }
6085
+ if (values.length === 0) values = [0, 1];
6086
+ let min = Math.min(...values);
6087
+ let max = Math.max(...values);
6088
+ if (kind !== "line" && kind !== "scatter") {
6089
+ min = Math.min(0, min);
6090
+ max = Math.max(0, max);
6091
+ }
6092
+ return niceTicks(min, max, 5);
6093
+ }
6094
+ function paintValueAxisY(paint, scale, yFor) {
6095
+ const { frame, gridColor, axisTextColor, axisFontSize, bodyFont, input } = paint;
6096
+ const layers = [];
6097
+ scale.ticks.forEach((tick, index) => {
6098
+ const y = yFor(tick);
6099
+ layers.push({
6100
+ type: "shape",
6101
+ id: `grid-${index}`,
6102
+ content: {
6103
+ shape: "rect",
6104
+ fill: tick === 0 ? withAlpha(paint.context.theme.colors.text, 0.35) : gridColor
6105
+ },
6106
+ position: {
6107
+ x: frame.plot.x,
6108
+ y: y - (tick === 0 ? 1 : 0.5),
6109
+ width: frame.plot.width,
6110
+ height: tick === 0 ? 2 : 1
6111
+ }
6112
+ });
6113
+ layers.push({
6114
+ type: "text",
6115
+ id: `tick-${index}`,
6116
+ content: {
6117
+ text: formatChartValue(tick, input.unit),
6118
+ style: {
6119
+ fontSize: axisFontSize,
6120
+ fontFamily: bodyFont,
6121
+ color: axisTextColor,
6122
+ textAlign: "center"
6123
+ }
6124
+ },
6125
+ position: {
6126
+ x: frame.chartArea.x + (frame.plot.x - frame.chartArea.x) / 2,
6127
+ y,
6128
+ anchor: "center"
6129
+ }
6130
+ });
6131
+ });
6132
+ return layers;
6133
+ }
6134
+ function paintCategoryAxisX(paint, xForCenter) {
6135
+ const { frame, data, labelFontSize, axisTextColor, bodyFont } = paint;
6136
+ const step = categoryLabelStep(data.labels.length, frame.plot.width, labelFontSize);
6137
+ const layers = [];
6138
+ data.labels.forEach((label, index) => {
6139
+ if (index % step !== 0 || !label) return;
6140
+ layers.push({
6141
+ type: "text",
6142
+ id: `cat-${index}`,
6143
+ content: {
6144
+ text: label,
6145
+ style: {
6146
+ fontSize: labelFontSize,
6147
+ fontFamily: bodyFont,
6148
+ color: axisTextColor,
6149
+ textAlign: "center"
6150
+ }
6151
+ },
6152
+ position: {
6153
+ x: xForCenter(index),
6154
+ y: frame.plot.y + frame.plot.height + labelFontSize * 1.2,
6155
+ anchor: "center"
6156
+ }
6157
+ });
6158
+ });
6159
+ return layers;
6160
+ }
6161
+ function paintColumns(paint) {
6162
+ const { frame, data, colors, input, entrance } = paint;
6163
+ const scale = valueTicks("column", data, input);
6164
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6165
+ const layers = paintValueAxisY(paint, scale, yFor);
6166
+ const n = data.labels.length;
6167
+ const band = frame.plot.width / Math.max(1, n);
6168
+ const innerPad = band * 0.15;
6169
+ layers.push(...paintCategoryAxisX(paint, (i) => frame.plot.x + (i + 0.5) * band));
6170
+ const stacked = input.stacked === true && data.series.length > 1;
6171
+ const extents = stacked ? stackSeries(data.series) : null;
6172
+ const groupWidth = band - innerPad * 2;
6173
+ const barWidth = stacked ? groupWidth : Math.max(2, (groupWidth - 4 * (data.series.length - 1)) / data.series.length);
6174
+ data.series.forEach((series, s) => {
6175
+ series.values.forEach((value, i) => {
6176
+ if (i >= n) return;
6177
+ const extent = extents ? extents[s][i] : value === null ? null : { y0: Math.min(0, value), y1: Math.max(0, value) };
6178
+ if (!extent || extent.y0 === extent.y1) return;
6179
+ const top = yFor(extent.y1);
6180
+ const height = Math.max(1, yFor(extent.y0) - top);
6181
+ const x = stacked ? frame.plot.x + i * band + innerPad : frame.plot.x + i * band + innerPad + s * (barWidth + 4);
6182
+ layers.push({
6183
+ type: "shape",
6184
+ id: `mark-${s}-${i}`,
6185
+ content: { shape: "rect", fill: colors[s], borderRadius: stacked ? 0 : 3 },
6186
+ position: { x, y: top, width: barWidth, height },
6187
+ animation: entrance(i + s)
6188
+ });
6189
+ if (input.showValues === true && value !== null && !stacked) {
6190
+ layers.push(
6191
+ valueLabel(
6192
+ paint,
6193
+ `val-${s}-${i}`,
6194
+ value,
6195
+ x + barWidth / 2,
6196
+ value >= 0 ? top - paint.axisFontSize * 0.9 : top + height + paint.axisFontSize * 0.9
6197
+ )
6198
+ );
6199
+ }
6200
+ });
6201
+ });
6202
+ return layers;
6203
+ }
6204
+ function paintBars(paint) {
6205
+ const {
6206
+ frame,
6207
+ data,
6208
+ colors,
6209
+ input,
6210
+ entrance,
6211
+ labelFontSize,
6212
+ axisFontSize,
6213
+ bodyFont,
6214
+ axisTextColor,
6215
+ gridColor
6216
+ } = paint;
6217
+ const scale = valueTicks("bar", data, input);
6218
+ const labelGap = axisFontSize * 0.6;
6219
+ const labelWidthPx = (value) => formatChartValue(value, input.unit).length * axisFontSize * 0.58;
6220
+ const showValueLabels = input.showValues === true && !(input.stacked === true);
6221
+ const reservedForLabels = showValueLabels ? data.series.flatMap((series) => series.values).filter((v) => v !== null).reduce((max, v) => Math.max(max, labelWidthPx(v)), 0) + labelGap : 0;
6222
+ const axisWidth = Math.max(20, frame.plot.width - reservedForLabels);
6223
+ const xFor = (v) => frame.plot.x + axisWidth * ((v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6224
+ const layers = [];
6225
+ scale.ticks.forEach((tick, index) => {
6226
+ const x = xFor(tick);
6227
+ layers.push({
6228
+ type: "shape",
6229
+ id: `grid-${index}`,
6230
+ content: {
6231
+ shape: "rect",
6232
+ fill: tick === 0 ? withAlpha(paint.context.theme.colors.text, 0.35) : gridColor
6233
+ },
6234
+ position: {
6235
+ x: x - (tick === 0 ? 1 : 0.5),
6236
+ y: frame.plot.y,
6237
+ width: tick === 0 ? 2 : 1,
6238
+ height: frame.plot.height
6239
+ }
6240
+ });
6241
+ layers.push({
6242
+ type: "text",
6243
+ id: `tick-${index}`,
6244
+ content: {
6245
+ text: formatChartValue(tick, input.unit),
6246
+ style: {
6247
+ fontSize: axisFontSize,
6248
+ fontFamily: bodyFont,
6249
+ color: axisTextColor,
6250
+ textAlign: "center"
6251
+ }
6252
+ },
6253
+ position: { x, y: frame.plot.y + frame.plot.height + axisFontSize * 1.2, anchor: "center" }
6254
+ });
6255
+ });
6256
+ const n = data.labels.length;
6257
+ const band = frame.plot.height / Math.max(1, n);
6258
+ const innerPad = band * 0.15;
6259
+ const stacked = input.stacked === true && data.series.length > 1;
6260
+ const extents = stacked ? stackSeries(data.series) : null;
6261
+ const groupHeight = band - innerPad * 2;
6262
+ const barHeight = stacked ? groupHeight : Math.max(2, (groupHeight - 4 * (data.series.length - 1)) / data.series.length);
6263
+ data.labels.forEach((label, i) => {
6264
+ if (!label) return;
6265
+ layers.push({
6266
+ type: "text",
6267
+ id: `cat-${i}`,
6268
+ content: {
6269
+ text: label,
6270
+ style: {
6271
+ fontSize: labelFontSize,
6272
+ fontFamily: bodyFont,
6273
+ color: axisTextColor,
6274
+ textAlign: "center"
6275
+ }
6276
+ },
6277
+ position: {
6278
+ x: frame.chartArea.x + (frame.plot.x - frame.chartArea.x) / 2,
6279
+ y: frame.plot.y + (i + 0.5) * band,
6280
+ anchor: "center"
6281
+ }
6282
+ });
6283
+ });
6284
+ data.series.forEach((series, s) => {
6285
+ series.values.forEach((value, i) => {
6286
+ if (i >= n) return;
6287
+ const extent = extents ? extents[s][i] : value === null ? null : { y0: Math.min(0, value), y1: Math.max(0, value) };
6288
+ if (!extent || extent.y0 === extent.y1) return;
6289
+ const left = xFor(extent.y0);
6290
+ const width = Math.max(1, xFor(extent.y1) - left);
6291
+ const y = stacked ? frame.plot.y + i * band + innerPad : frame.plot.y + i * band + innerPad + s * (barHeight + 4);
6292
+ layers.push({
6293
+ type: "shape",
6294
+ id: `mark-${s}-${i}`,
6295
+ content: { shape: "rect", fill: colors[s], borderRadius: stacked ? 0 : 3 },
6296
+ position: { x: left, y, width, height: barHeight },
6297
+ animation: entrance(i + s)
6298
+ });
6299
+ if (showValueLabels && value !== null) {
6300
+ const half = labelWidthPx(value) / 2;
6301
+ const labelX = value >= 0 ? left + width + labelGap + half : left - labelGap - half;
6302
+ layers.push(valueLabel(paint, `val-${s}-${i}`, value, labelX, y + barHeight / 2));
6303
+ }
6304
+ });
6305
+ });
6306
+ return layers;
6307
+ }
6308
+ function paintRadial(paint) {
6309
+ const { kind, frame, data, colors, input, entrance, context } = paint;
6310
+ const slices = data.labels.map((label, index) => ({ label, value: data.series[0].values[index], index })).filter(
6311
+ (s) => s.value !== null && Number.isFinite(s.value) && s.value > 0
6312
+ );
6313
+ const total = slices.reduce((sum, s) => sum + s.value, 0);
6314
+ const cx = frame.chartArea.x + frame.chartArea.width / 2;
6315
+ const cy = frame.chartArea.y + frame.chartArea.height / 2;
6316
+ const rOuter = Math.min(frame.chartArea.width, frame.chartArea.height) * 0.42;
6317
+ const rInner = kind === "donut" ? rOuter * 0.55 : 0;
6318
+ const layers = [];
6319
+ let startDeg = 0;
6320
+ slices.forEach((slice, order) => {
6321
+ const sweep = slice.value / total * 360;
6322
+ layers.push({
6323
+ type: "path",
6324
+ id: `mark-${slice.index}`,
6325
+ content: {
6326
+ d: arcPath(cx, cy, rOuter, rInner, startDeg, startDeg + sweep),
6327
+ fill: colors[slice.index % colors.length],
6328
+ stroke: context.theme.colors.background,
6329
+ strokeWidth: 2
6330
+ },
6331
+ position: { x: cx - rOuter, y: cy - rOuter, width: rOuter * 2, height: rOuter * 2 },
6332
+ animation: entrance(order)
6333
+ });
6334
+ if (input.showValues === true && sweep >= 14) {
6335
+ const mid = startDeg + sweep / 2;
6336
+ const rad = (mid - 90) * Math.PI / 180;
6337
+ const rLabel = rInner > 0 ? (rInner + rOuter) / 2 : rOuter * 0.68;
6338
+ const pctText = `${Math.round(slice.value / total * 100)}%`;
6339
+ layers.push({
6340
+ type: "text",
6341
+ id: `val-${slice.index}`,
6342
+ content: {
6343
+ text: pctText,
6344
+ style: {
6345
+ fontSize: paint.axisFontSize,
6346
+ fontFamily: paint.bodyFont,
6347
+ fontWeight: "bold",
6348
+ color: pickContrastingText(colors[slice.index % colors.length]),
6349
+ textAlign: "center"
6350
+ }
6351
+ },
6352
+ position: {
6353
+ x: cx + rLabel * Math.cos(rad),
6354
+ y: cy + rLabel * Math.sin(rad),
6355
+ anchor: "center"
6356
+ },
6357
+ animation: entrance(order)
6358
+ });
6359
+ }
6360
+ startDeg += sweep;
6361
+ });
6362
+ return layers;
6363
+ }
6364
+ function paintLines(paint) {
6365
+ const { kind, frame, data, colors, input, entrance } = paint;
6366
+ const scale = valueTicks(kind, data, input);
6367
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6368
+ const n = data.labels.length;
6369
+ const band = frame.plot.width / Math.max(1, n);
6370
+ const xForCenter = (i) => frame.plot.x + (i + 0.5) * band;
6371
+ const layers = paintValueAxisY(paint, scale, yFor);
6372
+ layers.push(...paintCategoryAxisX(paint, xForCenter));
6373
+ const baselineY = yFor(Math.max(scale.niceMin, Math.min(scale.niceMax, 0)));
6374
+ const showPoints = n <= 30;
6375
+ data.series.forEach((series, s) => {
6376
+ const segments = [];
6377
+ let current = [];
6378
+ series.values.forEach((value, i) => {
6379
+ if (i >= n) return;
6380
+ if (value === null) {
6381
+ if (current.length > 0) segments.push(current);
6382
+ current = [];
6383
+ return;
6384
+ }
6385
+ current.push({ x: xForCenter(i), y: yFor(value) });
6386
+ });
6387
+ if (current.length > 0) segments.push(current);
6388
+ segments.forEach((segment, seg) => {
6389
+ if (kind === "area" && segment.length > 1) {
6390
+ layers.push({
6391
+ type: "path",
6392
+ id: `area-${s}-${seg}`,
6393
+ content: {
6394
+ d: areaPath(segment, baselineY),
6395
+ fill: colors[s],
6396
+ fillOpacity: 0.25,
6397
+ stroke: "none",
6398
+ strokeWidth: 0
6399
+ },
6400
+ position: {
6401
+ x: frame.plot.x,
6402
+ y: frame.plot.y,
6403
+ width: frame.plot.width,
6404
+ height: frame.plot.height
6405
+ },
6406
+ animation: entrance(s)
6407
+ });
6408
+ }
6409
+ if (segment.length > 1) {
6410
+ layers.push({
6411
+ type: "path",
6412
+ id: `line-${s}-${seg}`,
6413
+ content: { d: polylinePath(segment), stroke: colors[s], strokeWidth: 4, fill: "none" },
6414
+ position: {
6415
+ x: frame.plot.x,
6416
+ y: frame.plot.y,
6417
+ width: frame.plot.width,
6418
+ height: frame.plot.height
6419
+ },
6420
+ animation: entrance(s)
6421
+ });
6422
+ }
6423
+ });
6424
+ if (showPoints) {
6425
+ series.values.forEach((value, i) => {
6426
+ if (i >= n || value === null) return;
6427
+ const r = 5;
6428
+ layers.push({
6429
+ type: "shape",
6430
+ id: `pt-${s}-${i}`,
6431
+ content: { shape: "circle", fill: colors[s] },
6432
+ position: { x: xForCenter(i) - r, y: yFor(value) - r, width: r * 2, height: r * 2 },
6433
+ animation: entrance(s)
6434
+ });
6435
+ if (input.showValues === true) {
6436
+ layers.push(
6437
+ valueLabel(
6438
+ paint,
6439
+ `val-${s}-${i}`,
6440
+ value,
6441
+ xForCenter(i),
6442
+ yFor(value) - paint.axisFontSize
6443
+ )
6444
+ );
6445
+ }
6446
+ });
6447
+ }
6448
+ });
6449
+ return layers;
6450
+ }
6451
+ function paintScatter(paint) {
6452
+ const { frame, data, colors, input, entrance } = paint;
6453
+ const scale = valueTicks("scatter", data, input);
6454
+ const yFor = (v) => frame.plot.y + frame.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
6455
+ const layers = paintValueAxisY(paint, scale, yFor);
6456
+ const numericCount = data.labelNumbers.filter((v) => v !== null).length;
6457
+ const numericX = data.labels.length > 0 && numericCount / data.labels.length >= 0.6;
6458
+ let xFor;
6459
+ if (numericX) {
6460
+ const xs = data.labelNumbers.filter((v) => v !== null);
6461
+ const xScale = niceTicks(Math.min(...xs), Math.max(...xs), 5);
6462
+ const span = xScale.niceMax - xScale.niceMin;
6463
+ xFor = (i) => {
6464
+ const v = data.labelNumbers[i];
6465
+ return v === null ? null : frame.plot.x + frame.plot.width * ((v - xScale.niceMin) / span);
6466
+ };
6467
+ xScale.ticks.forEach((tick, index) => {
6468
+ const x = frame.plot.x + frame.plot.width * ((tick - xScale.niceMin) / span);
6469
+ layers.push({
6470
+ type: "shape",
6471
+ id: `xgrid-${index}`,
6472
+ content: { shape: "rect", fill: paint.gridColor },
6473
+ position: { x: x - 0.5, y: frame.plot.y, width: 1, height: frame.plot.height }
6474
+ });
6475
+ layers.push({
6476
+ type: "text",
6477
+ id: `xtick-${index}`,
6478
+ content: {
6479
+ text: formatChartValue(tick),
6480
+ style: {
6481
+ fontSize: paint.axisFontSize,
6482
+ fontFamily: paint.bodyFont,
6483
+ color: paint.axisTextColor,
6484
+ textAlign: "center"
6485
+ }
6486
+ },
6487
+ position: {
6488
+ x,
6489
+ y: frame.plot.y + frame.plot.height + paint.axisFontSize * 1.2,
6490
+ anchor: "center"
6491
+ }
6492
+ });
6493
+ });
6494
+ } else {
6495
+ const band = frame.plot.width / Math.max(1, data.labels.length);
6496
+ xFor = (i) => frame.plot.x + (i + 0.5) * band;
6497
+ layers.push(...paintCategoryAxisX(paint, (i) => frame.plot.x + (i + 0.5) * band));
6498
+ }
6499
+ const r = 7;
6500
+ data.series.forEach((series, s) => {
6501
+ series.values.forEach((value, i) => {
6502
+ if (value === null) return;
6503
+ const x = xFor(i);
6504
+ if (x === null) return;
6505
+ layers.push({
6506
+ type: "shape",
6507
+ id: `mark-${s}-${i}`,
6508
+ content: { shape: "circle", fill: colors[s], fillOpacity: 0.85 },
6509
+ position: { x: x - r, y: yFor(value) - r, width: r * 2, height: r * 2 },
6510
+ animation: entrance(Math.floor(i / 3) + s)
6511
+ });
6512
+ });
6513
+ });
6514
+ return layers;
6515
+ }
6516
+ function paintLegend(paint, legend, band, fontSize) {
6517
+ const layers = [];
6518
+ legend.items.forEach((item, index) => {
6519
+ const color = paint.colors[item.index % paint.colors.length];
6520
+ const rowCenterY = band.y + item.y + legend.rowHeight / 2;
6521
+ layers.push({
6522
+ type: "shape",
6523
+ id: `legend-swatch-${index}`,
6524
+ content: { shape: "rect", fill: color, borderRadius: 3 },
6525
+ position: {
6526
+ x: band.x + item.x,
6527
+ y: rowCenterY - item.swatchSize / 2,
6528
+ width: item.swatchSize,
6529
+ height: item.swatchSize
6530
+ }
6531
+ });
6532
+ layers.push({
6533
+ type: "text",
6534
+ id: `legend-label-${index}`,
6535
+ content: {
6536
+ text: item.label,
6537
+ style: { fontSize, fontFamily: paint.bodyFont, color: paint.context.theme.colors.text }
6538
+ },
6539
+ position: {
6540
+ x: band.x + item.x + item.swatchSize + fontSize * 0.5,
6541
+ y: rowCenterY - fontSize * 0.62
6542
+ }
6543
+ });
6544
+ });
6545
+ return layers;
6546
+ }
6547
+ function valueLabel(paint, id, value, x, y) {
6548
+ return {
6549
+ type: "text",
6550
+ id,
6551
+ content: {
6552
+ text: formatChartValue(value, paint.input.unit),
6553
+ style: {
6554
+ fontSize: paint.axisFontSize,
6555
+ fontFamily: paint.bodyFont,
6556
+ fontWeight: "bold",
6557
+ color: paint.context.theme.colors.text,
6558
+ textAlign: "center"
6559
+ }
6560
+ },
6561
+ position: { x, y, anchor: "center" }
6562
+ };
6563
+ }
6564
+ function tableLayer(table, band, fontSize, input, context) {
6565
+ const { theme } = context;
6566
+ const colors = resolveColorScheme(context, input.colorScheme);
6567
+ return {
6568
+ type: "table",
6569
+ id: "table",
6570
+ content: {
6571
+ headers: table.headers,
6572
+ rows: table.rows,
6573
+ align: table.align,
6574
+ style: {
6575
+ headerBackground: colors.accent,
6576
+ headerColor: pickContrastingText(colors.accent),
6577
+ cellBackground: withAlpha(theme.colors.text, 0.04),
6578
+ cellColor: theme.colors.text,
6579
+ borderColor: withAlpha(theme.colors.text, 0.15),
6580
+ fontSize,
6581
+ fontFamily: getThemeFont(context, "body"),
6582
+ headerFontFamily: getThemeFont(context, "title"),
6583
+ borderRadius: 8
6584
+ }
6585
+ },
6586
+ position: { x: band.x, y: band.y, width: band.width, height: band.height },
6587
+ animation: { type: "fadeIn", duration: 1, delay: 0.4 }
6588
+ };
6589
+ }
6590
+
6591
+ // src/doc/templates/chartBlock.ts
6592
+ function barChart(input, context) {
6593
+ return renderChart("bar", input, context);
6594
+ }
6595
+ function columnChart(input, context) {
6596
+ return renderChart("column", input, context);
6597
+ }
6598
+ function pieChart(input, context) {
6599
+ return renderChart("pie", input, context);
6600
+ }
6601
+ function donutChart(input, context) {
6602
+ return renderChart("donut", input, context);
6603
+ }
6604
+ function lineChart(input, context) {
6605
+ return renderChart("line", input, context);
6606
+ }
6607
+ function areaChart(input, context) {
6608
+ return renderChart("area", input, context);
6609
+ }
6610
+ function scatterChart(input, context) {
6611
+ return renderChart("scatter", input, context);
6612
+ }
6613
+
5588
6614
  // src/doc/templates/registry.ts
5589
6615
  var templateRegistry = {
5590
6616
  title: titleBlock,
@@ -5608,6 +6634,13 @@ var templateRegistry = {
5608
6634
  videoWithCaption,
5609
6635
  videoPullQuote,
5610
6636
  dataTable,
6637
+ barChart,
6638
+ columnChart,
6639
+ pieChart,
6640
+ donutChart,
6641
+ lineChart,
6642
+ areaChart,
6643
+ scatterChart,
5611
6644
  diagram: diagramBlock,
5612
6645
  tree: treeBlock,
5613
6646
  timeline: timelineBlock,
@@ -5723,6 +6756,49 @@ var CAPTION_POSITION_VALUES = ["bottom", "top", "center"];
5723
6756
  var MAP_STYLE_VALUES = ["terrain", "satellite", "road", "toner", "watercolor"];
5724
6757
  var DATE_EVENT_MOOD_VALUES = ["neutral", "somber", "celebratory"];
5725
6758
  var COLOR_SCHEME_HINT = "theme color-scheme name \u2014 e.g. 'blue', 'green'";
6759
+ function CHART_DESCRIPTORS(extra = []) {
6760
+ return [
6761
+ { key: "title", description: "Optional title above the chart" },
6762
+ {
6763
+ key: "labelColumn",
6764
+ description: "Category/label column",
6765
+ valueHint: "header name or 0-based index"
6766
+ },
6767
+ {
6768
+ key: "valueColumns",
6769
+ description: "Value columns to plot",
6770
+ coerce: "stringList",
6771
+ valueHint: "comma-separated header names or 0-based indexes"
6772
+ },
6773
+ {
6774
+ key: "showTable",
6775
+ description: "Render the source table beneath the chart",
6776
+ coerce: "boolean"
6777
+ },
6778
+ { key: "showLegend", description: "Show a series legend", coerce: "boolean" },
6779
+ { key: "showValues", description: "Print numeric value labels on marks", coerce: "boolean" },
6780
+ { key: "unit", description: 'Unit suffix for tick/value labels (e.g. "km")' },
6781
+ {
6782
+ key: "colorScheme",
6783
+ description: "Color scheme seeding the series color rotation",
6784
+ valueHint: COLOR_SCHEME_HINT
6785
+ },
6786
+ // `headers`/`rows` are normally derived from the block's markdown table;
6787
+ // declared so data-fence-supplied values aren't flagged as unknown.
6788
+ {
6789
+ key: "headers",
6790
+ description: "Header cells (normally derived from the table)",
6791
+ coerce: "stringList"
6792
+ },
6793
+ { key: "rows", description: "Table rows (supply via a data fence)" },
6794
+ ...extra
6795
+ ];
6796
+ }
6797
+ var STACKED_DESCRIPTOR = {
6798
+ key: "stacked",
6799
+ description: "Stack series instead of grouping them side by side",
6800
+ coerce: "boolean"
6801
+ };
5726
6802
  var BASE_INPUT_DESCRIPTORS = [
5727
6803
  {
5728
6804
  key: "imageTreatment",
@@ -5844,6 +6920,13 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
5844
6920
  { key: "align", description: "Per-column alignment (supply via a data fence)" },
5845
6921
  { key: "colorScheme", description: "Header color scheme", valueHint: COLOR_SCHEME_HINT }
5846
6922
  ],
6923
+ barChart: CHART_DESCRIPTORS([STACKED_DESCRIPTOR]),
6924
+ columnChart: CHART_DESCRIPTORS([STACKED_DESCRIPTOR]),
6925
+ pieChart: CHART_DESCRIPTORS(),
6926
+ donutChart: CHART_DESCRIPTORS(),
6927
+ lineChart: CHART_DESCRIPTORS(),
6928
+ areaChart: CHART_DESCRIPTORS(),
6929
+ scatterChart: CHART_DESCRIPTORS(),
5847
6930
  tree: [
5848
6931
  { key: "title", description: "Optional title above the tree" },
5849
6932
  { key: "colorScheme", description: "Folder-row color scheme", valueHint: COLOR_SCHEME_HINT },
@@ -6315,6 +7398,55 @@ var BLOCK_MEDIA_LAYOUT_POLICIES = {
6315
7398
  unconsumedMedia: "reserved-slot",
6316
7399
  variants: DENSE_VARIANTS
6317
7400
  },
7401
+ barChart: {
7402
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7403
+ noMedia: "template-default",
7404
+ ownership: "supplemental",
7405
+ unconsumedMedia: "reserved-slot",
7406
+ variants: DENSE_VARIANTS
7407
+ },
7408
+ columnChart: {
7409
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7410
+ noMedia: "template-default",
7411
+ ownership: "supplemental",
7412
+ unconsumedMedia: "reserved-slot",
7413
+ variants: DENSE_VARIANTS
7414
+ },
7415
+ pieChart: {
7416
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7417
+ noMedia: "template-default",
7418
+ ownership: "supplemental",
7419
+ unconsumedMedia: "reserved-slot",
7420
+ variants: DENSE_VARIANTS
7421
+ },
7422
+ donutChart: {
7423
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7424
+ noMedia: "template-default",
7425
+ ownership: "supplemental",
7426
+ unconsumedMedia: "reserved-slot",
7427
+ variants: DENSE_VARIANTS
7428
+ },
7429
+ lineChart: {
7430
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7431
+ noMedia: "template-default",
7432
+ ownership: "supplemental",
7433
+ unconsumedMedia: "reserved-slot",
7434
+ variants: DENSE_VARIANTS
7435
+ },
7436
+ areaChart: {
7437
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7438
+ noMedia: "template-default",
7439
+ ownership: "supplemental",
7440
+ unconsumedMedia: "reserved-slot",
7441
+ variants: DENSE_VARIANTS
7442
+ },
7443
+ scatterChart: {
7444
+ summary: "The chart remains wide; supplemental media stacks below the dense data region.",
7445
+ noMedia: "template-default",
7446
+ ownership: "supplemental",
7447
+ unconsumedMedia: "reserved-slot",
7448
+ variants: DENSE_VARIANTS
7449
+ },
6318
7450
  diagram: {
6319
7451
  summary: "The node-and-edge canvas is intrinsic media and retains its spatial coordinate system.",
6320
7452
  noMedia: "intrinsic-visual",
@@ -7300,6 +8432,34 @@ var TEMPLATE_METADATA = {
7300
8432
  label: "Data Table",
7301
8433
  description: "Renders tabular data in a clean, styled table with a header row."
7302
8434
  },
8435
+ barChart: {
8436
+ label: "Bar Chart",
8437
+ description: "Horizontal bars from the block's markdown table \u2014 one bar per row."
8438
+ },
8439
+ columnChart: {
8440
+ label: "Column Chart",
8441
+ description: "Vertical columns from the block's markdown table \u2014 one column per row."
8442
+ },
8443
+ pieChart: {
8444
+ label: "Pie Chart",
8445
+ description: "A pie of the block's markdown table \u2014 each row becomes a labeled slice."
8446
+ },
8447
+ donutChart: {
8448
+ label: "Donut Chart",
8449
+ description: "A donut of the block's markdown table \u2014 each row becomes a labeled slice."
8450
+ },
8451
+ lineChart: {
8452
+ label: "Line Chart",
8453
+ description: "Lines from the block's markdown table \u2014 one line per value column."
8454
+ },
8455
+ areaChart: {
8456
+ label: "Area Chart",
8457
+ description: "Filled area lines from the block's markdown table \u2014 one area per value column."
8458
+ },
8459
+ scatterChart: {
8460
+ label: "Scatter Chart",
8461
+ description: "An x/y scatter plot from the block's markdown table's numeric columns."
8462
+ },
7303
8463
  diagram: {
7304
8464
  label: "Diagram",
7305
8465
  description: "Renders sub-headings as connected nodes \u2014 edit visually as a node-and-edge diagram."
@@ -7445,6 +8605,48 @@ var TEMPLATE_AUTHORING_METADATA = {
7445
8605
  safeForContentFirst: false,
7446
8606
  placement: "heading"
7447
8607
  },
8608
+ barChart: {
8609
+ role: "data",
8610
+ bodyPolicy: "structured",
8611
+ safeForContentFirst: false,
8612
+ placement: "heading"
8613
+ },
8614
+ columnChart: {
8615
+ role: "data",
8616
+ bodyPolicy: "structured",
8617
+ safeForContentFirst: false,
8618
+ placement: "heading"
8619
+ },
8620
+ pieChart: {
8621
+ role: "data",
8622
+ bodyPolicy: "structured",
8623
+ safeForContentFirst: false,
8624
+ placement: "heading"
8625
+ },
8626
+ donutChart: {
8627
+ role: "data",
8628
+ bodyPolicy: "structured",
8629
+ safeForContentFirst: false,
8630
+ placement: "heading"
8631
+ },
8632
+ lineChart: {
8633
+ role: "data",
8634
+ bodyPolicy: "structured",
8635
+ safeForContentFirst: false,
8636
+ placement: "heading"
8637
+ },
8638
+ areaChart: {
8639
+ role: "data",
8640
+ bodyPolicy: "structured",
8641
+ safeForContentFirst: false,
8642
+ placement: "heading"
8643
+ },
8644
+ scatterChart: {
8645
+ role: "data",
8646
+ bodyPolicy: "structured",
8647
+ safeForContentFirst: false,
8648
+ placement: "heading"
8649
+ },
7448
8650
  diagram: {
7449
8651
  role: "spatial",
7450
8652
  bodyPolicy: "structured",
@@ -7694,7 +8896,8 @@ function expandDocBlocks(blocks, options = {}) {
7694
8896
  audioSegments,
7695
8897
  customTemplates,
7696
8898
  failureMode = "fallback",
7697
- onDiagnostic
8899
+ onDiagnostic,
8900
+ splitLongBlocks = true
7698
8901
  } = opts;
7699
8902
  const totalBlocks = blocks.length;
7700
8903
  const registry = customTemplates && customTemplates.length > 0 ? buildRegistry(customTemplates) : templateRegistry;
@@ -7885,27 +9088,29 @@ function expandDocBlocks(blocks, options = {}) {
7885
9088
  }
7886
9089
  }
7887
9090
  const MAX_BLOCK_DURATION = 20;
7888
- for (let i = 0; i < segmentExpandedBlocks.length; i++) {
7889
- const block = segmentExpandedBlocks[i];
7890
- if (block.duration > MAX_BLOCK_DURATION) {
7891
- const numParts = Math.ceil(block.duration / MAX_BLOCK_DURATION);
7892
- const partDuration = block.duration / numParts;
7893
- if (partDuration >= MIN_TRANSITION_GAP) {
7894
- block.duration = partDuration;
7895
- for (let p = 1; p < numParts; p++) {
7896
- const splitBlock = {
7897
- id: `${block.id}-split-${p}`,
7898
- startTime: block.startTime + p * partDuration,
7899
- duration: partDuration,
7900
- audioSegment: block.audioSegment,
7901
- layers: (block.layers ?? []).map((layer) => ({
7902
- ...cloneData(layer),
7903
- id: `${layer.id}-split-${p}`
7904
- })),
7905
- transition: { type: "dissolve", duration: 1 },
7906
- template: block.template
7907
- };
7908
- expandedBlocks.push(splitBlock);
9091
+ if (splitLongBlocks) {
9092
+ for (let i = 0; i < segmentExpandedBlocks.length; i++) {
9093
+ const block = segmentExpandedBlocks[i];
9094
+ if (block.duration > MAX_BLOCK_DURATION) {
9095
+ const numParts = Math.ceil(block.duration / MAX_BLOCK_DURATION);
9096
+ const partDuration = block.duration / numParts;
9097
+ if (partDuration >= MIN_TRANSITION_GAP) {
9098
+ block.duration = partDuration;
9099
+ for (let p = 1; p < numParts; p++) {
9100
+ const splitBlock = {
9101
+ id: `${block.id}-split-${p}`,
9102
+ startTime: block.startTime + p * partDuration,
9103
+ duration: partDuration,
9104
+ audioSegment: block.audioSegment,
9105
+ layers: (block.layers ?? []).map((layer) => ({
9106
+ ...cloneData(layer),
9107
+ id: `${layer.id}-split-${p}`
9108
+ })),
9109
+ transition: { type: "dissolve", duration: 1 },
9110
+ template: block.template
9111
+ };
9112
+ expandedBlocks.push(splitBlock);
9113
+ }
7909
9114
  }
7910
9115
  }
7911
9116
  }
@@ -8005,16 +9210,15 @@ function readCustomTemplatesFromFrontmatter(frontmatter) {
8005
9210
  for (const entry of candidates) {
8006
9211
  if (!entry || typeof entry !== "object") continue;
8007
9212
  const e = entry;
8008
- if (typeof e.name !== "string" || typeof e.label !== "string") continue;
8009
- if (!Array.isArray(e.layers)) continue;
8010
- const def = {
9213
+ const candidate = {
8011
9214
  name: e.name,
8012
9215
  label: e.label,
8013
- viewport: readViewport(e.viewport),
9216
+ viewport: e.viewport ?? { ...DEFAULT_VIEWPORT },
8014
9217
  layers: e.layers
8015
9218
  };
8016
- if (typeof e.description === "string") def.description = e.description;
8017
- out.push(def);
9219
+ if (e.description !== void 0) candidate.description = e.description;
9220
+ const result = validateCustomTemplateDefinition(candidate);
9221
+ if (result.template) out.push(result.template);
8018
9222
  }
8019
9223
  return out.length > 0 ? out : void 0;
8020
9224
  }
@@ -8030,15 +9234,6 @@ function writeCustomTemplatesToFrontmatter(templates, options) {
8030
9234
  }
8031
9235
  return JSON.stringify(map, null, options?.pretty ? 2 : void 0);
8032
9236
  }
8033
- function readViewport(raw) {
8034
- if (raw && typeof raw === "object") {
8035
- const v = raw;
8036
- const w = typeof v.width === "number" ? v.width : DEFAULT_VIEWPORT.width;
8037
- const h = typeof v.height === "number" ? v.height : DEFAULT_VIEWPORT.height;
8038
- return { width: w, height: h };
8039
- }
8040
- return DEFAULT_VIEWPORT;
8041
- }
8042
9237
  function expandCompactMap(map) {
8043
9238
  return Object.entries(map).map(([name, raw]) => {
8044
9239
  const e = raw && typeof raw === "object" ? raw : {};
@@ -8281,14 +9476,8 @@ function extractTableData(table) {
8281
9476
  }
8282
9477
 
8283
9478
  // src/doc/standaloneAnnotation.ts
8284
- function parseStandaloneAnnotation(node) {
8285
- if (node.type !== "paragraph") return null;
8286
- const children = node.children ?? [];
8287
- if (children.length !== 1 || children[0].type !== "text") return null;
8288
- const text = children[0].value.trim();
8289
- const match = matchTrailingTemplateAnnotation(text);
8290
- if (!match || match.index !== 0) return null;
8291
- const tokens = tokenizeAttrTokens(match.inner.trim());
9479
+ function parseAnnotationInner(inner) {
9480
+ const tokens = tokenizeAttrTokens(inner.trim());
8292
9481
  const firstIsParam = tokens.length > 0 && tokens[0].indexOf("=") > 0;
8293
9482
  const template = firstIsParam || tokens.length === 0 ? void 0 : tokens[0];
8294
9483
  const startIdx = firstIsParam ? 0 : 1;
@@ -8299,6 +9488,15 @@ function parseStandaloneAnnotation(node) {
8299
9488
  }
8300
9489
  return { template, params };
8301
9490
  }
9491
+ function parseStandaloneAnnotation(node) {
9492
+ if (node.type !== "paragraph") return null;
9493
+ const children = node.children ?? [];
9494
+ if (children.length !== 1 || children[0].type !== "text") return null;
9495
+ const text = children[0].value.trim();
9496
+ const match = matchTrailingTemplateAnnotation(text);
9497
+ if (!match || match.index !== 0) return null;
9498
+ return parseAnnotationInner(match.inner);
9499
+ }
8302
9500
 
8303
9501
  // src/doc/mediaAnnotations.ts
8304
9502
  var MEDIA_TEMPLATES = /* @__PURE__ */ new Set(["audio", "video", "media"]);
@@ -8309,6 +9507,27 @@ function time(raw) {
8309
9507
  if (raw == null) return null;
8310
9508
  return parseTimeSeconds(raw);
8311
9509
  }
9510
+ function videoPlacement(params) {
9511
+ const explicit = params.placement ?? params.presentation;
9512
+ if (explicit === "content") return "content";
9513
+ if (explicit === "pip" || explicit === "picture-in-picture") return "picture-in-picture";
9514
+ if (explicit === "overlay" || explicit === "full-frame") return "overlay";
9515
+ if (params.pip === "" || params.pip === "true") return "picture-in-picture";
9516
+ if (params.overlay === "" || params.overlay === "true") return "overlay";
9517
+ return void 0;
9518
+ }
9519
+ function lockToBlock(raw) {
9520
+ return raw !== "false" && raw !== "0";
9521
+ }
9522
+ function videoPipSize(raw) {
9523
+ return raw === "small" || raw === "large" ? raw : void 0;
9524
+ }
9525
+ function videoPipShape(raw) {
9526
+ return raw === "square" || raw === "wide" ? raw : void 0;
9527
+ }
9528
+ function videoPipPosition(raw) {
9529
+ return raw === "top-left" || raw === "top-right" || raw === "bottom-left" || raw === "bottom-right" ? raw : void 0;
9530
+ }
8312
9531
  function toMediaClip(parsed, id) {
8313
9532
  const { template, params } = parsed;
8314
9533
  if (!template || !MEDIA_TEMPLATES.has(template)) return null;
@@ -8323,13 +9542,90 @@ function toMediaClip(parsed, id) {
8323
9542
  startAt: time(params.startAt ?? params.startTime) ?? 0,
8324
9543
  anchor
8325
9544
  };
9545
+ const placement = kind === "video" ? videoPlacement(params) : void 0;
9546
+ if (placement && placement !== "content") clip.placement = placement;
9547
+ const pipSize = videoPipSize(params.pipSize);
9548
+ const pipShape = videoPipShape(params.pipShape);
9549
+ const pipPosition = videoPipPosition(params.pipPosition);
9550
+ if (pipSize) clip.pipSize = pipSize;
9551
+ if (pipShape) clip.pipShape = pipShape;
9552
+ if (pipPosition) clip.pipPosition = pipPosition;
9553
+ if (kind === "video" && placement && placement !== "content" && params.lockToBlock != null) {
9554
+ const locked = lockToBlock(params.lockToBlock);
9555
+ clip.lockToBlock = locked;
9556
+ clip.anchor = locked ? "block" : "document";
9557
+ if (locked) clip.startAt = 0;
9558
+ }
8326
9559
  const clipStart = time(params.clipStart);
8327
9560
  const clipEnd = time(params.clipEnd);
8328
9561
  if (clipStart != null) clip.clipStart = clipStart;
8329
9562
  if (clipEnd != null) clip.clipEnd = clipEnd;
9563
+ if (clip.lockToBlock === true) delete clip.clipEnd;
8330
9564
  if (params.spillover === "true" || params.spillover === "") clip.spillover = true;
8331
9565
  return clip;
8332
9566
  }
9567
+ function firstVideoElement(children) {
9568
+ const visit = (nodes) => {
9569
+ for (const child of nodes) {
9570
+ if (child.type !== "htmlElement") continue;
9571
+ if (child.tagName === "video") return child;
9572
+ const nested = visit(child.children);
9573
+ if (nested) return nested;
9574
+ }
9575
+ return null;
9576
+ };
9577
+ return visit(children);
9578
+ }
9579
+ function standaloneHtmlVideo(node) {
9580
+ if (node.type === "htmlBlock") {
9581
+ if (!/^\s*<video\b/i.test(node.rawHtml)) return null;
9582
+ const element = firstVideoElement(node.htmlChildren);
9583
+ return element ? { element, raw: node.rawHtml.trim() } : null;
9584
+ }
9585
+ if (node.type !== "paragraph" || !node.children.every((child) => child.type === "htmlInline")) {
9586
+ return null;
9587
+ }
9588
+ const raw = node.children.map((child) => child.type === "htmlInline" ? child.rawHtml : "").join("");
9589
+ if (!/^\s*<video\b[\s\S]*<\/video>\s*$/i.test(raw)) return null;
9590
+ for (const child of node.children) {
9591
+ if (child.type !== "htmlInline") continue;
9592
+ const element = firstVideoElement(child.htmlChildren);
9593
+ if (element) return { element, raw: raw.trim() };
9594
+ }
9595
+ return null;
9596
+ }
9597
+ function htmlVideoClip(node, id) {
9598
+ const authored = standaloneHtmlVideo(node);
9599
+ if (!authored) return null;
9600
+ const { element } = authored;
9601
+ const placement = videoPlacement({
9602
+ placement: element.attributes["data-squisq-video-placement"] ?? ""
9603
+ });
9604
+ if (!placement || placement === "content") return null;
9605
+ const src = element.attributes.src;
9606
+ if (!src) return null;
9607
+ const locked = lockToBlock(element.attributes["data-squisq-video-lock-to-block"]);
9608
+ const clip = {
9609
+ id,
9610
+ src,
9611
+ kind: "video",
9612
+ placement,
9613
+ lockToBlock: locked,
9614
+ startAt: locked ? 0 : time(element.attributes["data-squisq-video-start-at"]) ?? 0,
9615
+ anchor: locked ? "block" : "document"
9616
+ };
9617
+ const pipSize = videoPipSize(element.attributes["data-squisq-video-pip-size"]);
9618
+ const pipShape = videoPipShape(element.attributes["data-squisq-video-pip-shape"]);
9619
+ const pipPosition = videoPipPosition(element.attributes["data-squisq-video-pip-position"]);
9620
+ if (pipSize) clip.pipSize = pipSize;
9621
+ if (pipShape) clip.pipShape = pipShape;
9622
+ if (pipPosition) clip.pipPosition = pipPosition;
9623
+ const clipStart = time(element.attributes["data-squisq-video-clip-start"]);
9624
+ const clipEnd = time(element.attributes["data-squisq-video-clip-end"]);
9625
+ if (clipStart != null) clip.clipStart = clipStart;
9626
+ if (!locked && clipEnd != null) clip.clipEnd = clipEnd;
9627
+ return clip;
9628
+ }
8333
9629
  function extractMediaFromContents(contents, makeId, blockId) {
8334
9630
  const media = [];
8335
9631
  const documentMedia = [];
@@ -8338,17 +9634,19 @@ function extractMediaFromContents(contents, makeId, blockId) {
8338
9634
  let originalIndex = 0;
8339
9635
  for (const node of contents ?? []) {
8340
9636
  const parsed = parseStandaloneAnnotation(node);
8341
- const clip = parsed ? toMediaClip(parsed, "") : null;
8342
- if (parsed && clip) {
9637
+ const clip = parsed ? toMediaClip(parsed, "") : htmlVideoClip(node, "");
9638
+ if (clip) {
8343
9639
  clip.id = makeId(clip.src, index++);
8344
9640
  const line = node.position?.start.line;
8345
9641
  if (line != null) clip.sourceLine = line;
8346
9642
  if (blockId !== void 0) {
8347
- const rawText = node.type === "paragraph" && node.children?.[0]?.type === "text" ? node.children[0].value.trim() : void 0;
9643
+ const authoredHtml = standaloneHtmlVideo(node);
9644
+ const rawText = node.type === "paragraph" && node.children?.[0]?.type === "text" ? node.children[0].value.trim() : authoredHtml?.raw;
8348
9645
  clip.origin = {
8349
9646
  blockId,
8350
9647
  index: originalIndex,
8351
- ...rawText !== void 0 ? { raw: rawText } : {}
9648
+ ...rawText !== void 0 ? { raw: rawText } : {},
9649
+ ...authoredHtml ? { format: "html" } : {}
8352
9650
  };
8353
9651
  }
8354
9652
  (clip.anchor === "document" ? documentMedia : media).push(clip);
@@ -8412,6 +9710,78 @@ function extractTemplateBlocksFromContents(contents, options) {
8412
9710
  return { leading, blocks };
8413
9711
  }
8414
9712
 
9713
+ // src/doc/promoteBodyAnnotation.ts
9714
+ function lastTextChildValue(node) {
9715
+ if (node.type !== "paragraph") return null;
9716
+ const children = node.children;
9717
+ if (children.length === 0) return null;
9718
+ const last = children[children.length - 1];
9719
+ return last.type === "text" ? last.value : null;
9720
+ }
9721
+ function isBodyAnnotationNode(node) {
9722
+ if (parseStandaloneAnnotation(node)?.template) return true;
9723
+ const value = lastTextChildValue(node);
9724
+ if (value == null) return false;
9725
+ const match = matchTrailingTemplateAnnotation(value);
9726
+ return !!match && match.index > 0 && !!parseAnnotationInner(match.inner).template;
9727
+ }
9728
+ function stripTrailingToken(contents, index, cut) {
9729
+ const out = [...contents];
9730
+ const para = out[index];
9731
+ if (para.type !== "paragraph") return out;
9732
+ const children = [...para.children];
9733
+ const last = children[children.length - 1];
9734
+ if (last.type !== "text") return out;
9735
+ const kept = last.value.slice(0, cut);
9736
+ if (kept.length === 0 && children.length === 1) {
9737
+ out.splice(index, 1);
9738
+ } else {
9739
+ children[children.length - 1] = { ...last, value: kept };
9740
+ out[index] = { ...para, children };
9741
+ }
9742
+ return out;
9743
+ }
9744
+ function detectPromotableBodyAnnotation(contents) {
9745
+ if (!contents || contents.length === 0) return null;
9746
+ const lastIndex = contents.length - 1;
9747
+ const last = contents[lastIndex];
9748
+ let data = null;
9749
+ let strippedContents = null;
9750
+ const whole = parseStandaloneAnnotation(last);
9751
+ if (whole?.template && last.type === "paragraph") {
9752
+ const first = last.children[0];
9753
+ data = {
9754
+ template: whole.template,
9755
+ params: whole.params,
9756
+ origin: {
9757
+ kind: "paragraph",
9758
+ index: lastIndex,
9759
+ raw: first && first.type === "text" ? first.value.trim() : ""
9760
+ }
9761
+ };
9762
+ strippedContents = contents.slice(0, lastIndex);
9763
+ } else {
9764
+ const value = lastTextChildValue(last);
9765
+ const match = value != null ? matchTrailingTemplateAnnotation(value) : null;
9766
+ if (value != null && match && match.index > 0) {
9767
+ const parsed = parseAnnotationInner(match.inner);
9768
+ if (parsed.template) {
9769
+ data = {
9770
+ template: parsed.template,
9771
+ params: parsed.params,
9772
+ origin: { kind: "trailing", index: lastIndex, suffix: value.slice(match.index) }
9773
+ };
9774
+ strippedContents = stripTrailingToken(contents, lastIndex, match.index);
9775
+ }
9776
+ }
9777
+ }
9778
+ if (!data || !strippedContents) return null;
9779
+ for (let i = 0; i < lastIndex; i++) {
9780
+ if (isBodyAnnotationNode(contents[i])) return null;
9781
+ }
9782
+ return { data, strippedContents };
9783
+ }
9784
+
8415
9785
  // src/doc/markdownToDoc.ts
8416
9786
  var SLUG_NON_WORD_RE = /[^\w\s-]/g;
8417
9787
  var SLUG_SPACES_RE = /[\s_]+/g;
@@ -8457,6 +9827,9 @@ function pinnedHeadingMeta(heading) {
8457
9827
  return { ...fromAnnotation, ...fromPandoc };
8458
9828
  }
8459
9829
  function pinnedMeta(block) {
9830
+ if (block.promotedBodyAnnotation?.params) {
9831
+ return coerceAnnotationValues(block.promotedBodyAnnotation.params).blockMeta;
9832
+ }
8460
9833
  if (block.sourceHeading) return pinnedHeadingMeta(block.sourceHeading);
8461
9834
  if (block.sourceAnnotation?.params) {
8462
9835
  return coerceAnnotationValues(block.sourceAnnotation.params).blockMeta;
@@ -8466,6 +9839,22 @@ function pinnedMeta(block) {
8466
9839
  function getPinnedBlockMeta(block) {
8467
9840
  return pinnedMeta(block);
8468
9841
  }
9842
+ function promoteBodyAnnotations(blocks) {
9843
+ for (const block of blocks) {
9844
+ if (!block.sourceHeading || block.sourceHeading.templateAnnotation?.template) continue;
9845
+ const found = detectPromotableBodyAnnotation(block.contents);
9846
+ if (!found) continue;
9847
+ const { template, params } = found.data;
9848
+ if (isMediaAnnotationName(template) || !hasTemplate(template)) continue;
9849
+ block.template = resolveTemplateName(template);
9850
+ if (Object.keys(params).length > 0) {
9851
+ block.templateOverrides = params;
9852
+ applyBlockMeta2(block, coerceAnnotationValues(params).blockMeta);
9853
+ }
9854
+ block.contents = found.strippedContents;
9855
+ block.promotedBodyAnnotation = found.data;
9856
+ }
9857
+ }
8469
9858
  function expandStandaloneAnnotations(blocks, makeId, defaultDuration) {
8470
9859
  const out = [];
8471
9860
  for (const block of blocks) {
@@ -8602,6 +9991,7 @@ function markdownToDoc(markdownDoc, options) {
8602
9991
  if (rootBlocks.length > 0 && !rootBlocks[0].sourceHeading && (rootBlocks[0].contents?.length ?? 0) === 0 && (rootBlocks[0].children?.length ?? 0) === 0 && !rootBlocks[0].media) {
8603
9992
  rootBlocks.shift();
8604
9993
  }
9994
+ promoteBodyAnnotations(flattenBlocks(rootBlocks));
8605
9995
  const makeStandaloneId = (parsed) => {
8606
9996
  const base = parsed.params.title ?? parsed.template ?? "block";
8607
9997
  return idGenerator ? idGenerator.fromText(base) : slugify(base);
@@ -8685,6 +10075,12 @@ function markdownToDoc(markdownDoc, options) {
8685
10075
  }
8686
10076
  currentTime += block.duration;
8687
10077
  }
10078
+ const blockById = new Map(allBlocks.map((block) => [block.id, block]));
10079
+ for (const clip of documentMedia) {
10080
+ if (clip.lockToBlock === false && clip.origin?.format === "html" && !/\bdata-squisq-video-start-at\s*=/i.test(clip.origin.raw ?? "")) {
10081
+ clip.startAt = blockById.get(clip.origin.blockId)?.startTime ?? 0;
10082
+ }
10083
+ }
8688
10084
  const captions = phrases.length > 0 ? {
8689
10085
  phrases,
8690
10086
  ...options?.captionsGeneratedAt ? { generatedAt: options.captionsGeneratedAt } : {},
@@ -8770,7 +10166,7 @@ function applyStructuredData(block, diagnostics) {
8770
10166
  }
8771
10167
  data = { ...data, ...result.data };
8772
10168
  }
8773
- if (block.template === "dataTable") {
10169
+ if (TABLE_FED_TEMPLATES.has(resolveTemplateName(block.template ?? ""))) {
8774
10170
  const provided = (key) => data && key in data || block.templateOverrides && key in block.templateOverrides;
8775
10171
  if (!provided("headers") && !provided("rows")) {
8776
10172
  const table = findFirstTable(block.contents);
@@ -8855,7 +10251,7 @@ function frontmatterDisablesAutoTemplates(markdownDoc) {
8855
10251
  }
8856
10252
  function applyAutoTemplates(blocks, resolvedDefault, state) {
8857
10253
  for (const block of blocks) {
8858
- const annotated = !!block.sourceHeading?.templateAnnotation?.template;
10254
+ const annotated = !!block.sourceHeading?.templateAnnotation?.template || !!block.promotedBodyAnnotation;
8859
10255
  if (block.sourceHeading && !annotated && block.template === resolvedDefault) {
8860
10256
  const profile = profileBlockContents(block.contents ?? []);
8861
10257
  const picked = pickAutoTemplate(profile, state.featureIndex);
@@ -9294,10 +10690,6 @@ export {
9294
10690
  getPersistentLayersFromTheme,
9295
10691
  resolvePersistentLayers,
9296
10692
  wrapWithPersistentLayers,
9297
- TEMPLATE_ALIASES,
9298
- resolveTemplateName,
9299
- CONTAINER_TEMPLATES,
9300
- isContainerTemplate,
9301
10693
  resolveColorScheme,
9302
10694
  getThemeFont,
9303
10695
  themedFontSize,
@@ -9359,6 +10751,7 @@ export {
9359
10751
  videoWithCaption,
9360
10752
  videoPullQuote,
9361
10753
  dataTable,
10754
+ buildChartData,
9362
10755
  DIAGRAM_LABEL_LINE_HEIGHT,
9363
10756
  DIAGRAM_LABEL_HORIZONTAL_PADDING,
9364
10757
  DIAGRAM_LABEL_VERTICAL_PADDING,