@oml/markdown 0.7.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 (112) hide show
  1. package/README.md +39 -0
  2. package/out/index.d.ts +2 -0
  3. package/out/index.js +4 -0
  4. package/out/index.js.map +1 -0
  5. package/out/md/index.d.ts +6 -0
  6. package/out/md/index.js +8 -0
  7. package/out/md/index.js.map +1 -0
  8. package/out/md/md-execution.d.ts +33 -0
  9. package/out/md/md-execution.js +3 -0
  10. package/out/md/md-execution.js.map +1 -0
  11. package/out/md/md-executor.d.ts +21 -0
  12. package/out/md/md-executor.js +498 -0
  13. package/out/md/md-executor.js.map +1 -0
  14. package/out/md/md-frontmatter.d.ts +4 -0
  15. package/out/md/md-frontmatter.js +48 -0
  16. package/out/md/md-frontmatter.js.map +1 -0
  17. package/out/md/md-registry.d.ts +7 -0
  18. package/out/md/md-registry.js +19 -0
  19. package/out/md/md-registry.js.map +1 -0
  20. package/out/md/md-runtime.d.ts +10 -0
  21. package/out/md/md-runtime.js +166 -0
  22. package/out/md/md-runtime.js.map +1 -0
  23. package/out/md/md-types.d.ts +40 -0
  24. package/out/md/md-types.js +3 -0
  25. package/out/md/md-types.js.map +1 -0
  26. package/out/md/md-yaml.d.ts +1 -0
  27. package/out/md/md-yaml.js +15 -0
  28. package/out/md/md-yaml.js.map +1 -0
  29. package/out/renderers/chart-renderer.d.ts +6 -0
  30. package/out/renderers/chart-renderer.js +392 -0
  31. package/out/renderers/chart-renderer.js.map +1 -0
  32. package/out/renderers/diagram-renderer.d.ts +7 -0
  33. package/out/renderers/diagram-renderer.js +2354 -0
  34. package/out/renderers/diagram-renderer.js.map +1 -0
  35. package/out/renderers/graph-renderer.d.ts +6 -0
  36. package/out/renderers/graph-renderer.js +1384 -0
  37. package/out/renderers/graph-renderer.js.map +1 -0
  38. package/out/renderers/index.d.ts +14 -0
  39. package/out/renderers/index.js +16 -0
  40. package/out/renderers/index.js.map +1 -0
  41. package/out/renderers/list-renderer.d.ts +6 -0
  42. package/out/renderers/list-renderer.js +252 -0
  43. package/out/renderers/list-renderer.js.map +1 -0
  44. package/out/renderers/matrix-renderer.d.ts +14 -0
  45. package/out/renderers/matrix-renderer.js +498 -0
  46. package/out/renderers/matrix-renderer.js.map +1 -0
  47. package/out/renderers/message-renderer.d.ts +6 -0
  48. package/out/renderers/message-renderer.js +14 -0
  49. package/out/renderers/message-renderer.js.map +1 -0
  50. package/out/renderers/registry.d.ts +9 -0
  51. package/out/renderers/registry.js +41 -0
  52. package/out/renderers/registry.js.map +1 -0
  53. package/out/renderers/renderer.d.ts +28 -0
  54. package/out/renderers/renderer.js +61 -0
  55. package/out/renderers/renderer.js.map +1 -0
  56. package/out/renderers/table-editor-renderer.d.ts +4 -0
  57. package/out/renderers/table-editor-renderer.js +9 -0
  58. package/out/renderers/table-editor-renderer.js.map +1 -0
  59. package/out/renderers/table-renderer.d.ts +95 -0
  60. package/out/renderers/table-renderer.js +1571 -0
  61. package/out/renderers/table-renderer.js.map +1 -0
  62. package/out/renderers/text-renderer.d.ts +7 -0
  63. package/out/renderers/text-renderer.js +219 -0
  64. package/out/renderers/text-renderer.js.map +1 -0
  65. package/out/renderers/tree-renderer.d.ts +4 -0
  66. package/out/renderers/tree-renderer.js +9 -0
  67. package/out/renderers/tree-renderer.js.map +1 -0
  68. package/out/renderers/types.d.ts +18 -0
  69. package/out/renderers/types.js +3 -0
  70. package/out/renderers/types.js.map +1 -0
  71. package/out/renderers/wikilink-utils.d.ts +6 -0
  72. package/out/renderers/wikilink-utils.js +100 -0
  73. package/out/renderers/wikilink-utils.js.map +1 -0
  74. package/out/static/browser-runtime.bundle.js +74155 -0
  75. package/out/static/browser-runtime.bundle.js.map +7 -0
  76. package/out/static/browser-runtime.d.ts +1 -0
  77. package/out/static/browser-runtime.js +218 -0
  78. package/out/static/browser-runtime.js.map +1 -0
  79. package/out/static/index.d.ts +1 -0
  80. package/out/static/index.js +3 -0
  81. package/out/static/index.js.map +1 -0
  82. package/out/static/runtime-assets.d.ts +2 -0
  83. package/out/static/runtime-assets.js +174 -0
  84. package/out/static/runtime-assets.js.map +1 -0
  85. package/package.json +74 -0
  86. package/src/index.ts +4 -0
  87. package/src/md/index.ts +8 -0
  88. package/src/md/md-execution.ts +51 -0
  89. package/src/md/md-executor.ts +598 -0
  90. package/src/md/md-frontmatter.ts +53 -0
  91. package/src/md/md-registry.ts +22 -0
  92. package/src/md/md-runtime.ts +191 -0
  93. package/src/md/md-types.ts +48 -0
  94. package/src/md/md-yaml.ts +17 -0
  95. package/src/renderers/chart-renderer.ts +473 -0
  96. package/src/renderers/diagram-renderer.ts +2520 -0
  97. package/src/renderers/graph-renderer.ts +1653 -0
  98. package/src/renderers/index.ts +16 -0
  99. package/src/renderers/list-renderer.ts +289 -0
  100. package/src/renderers/matrix-renderer.ts +616 -0
  101. package/src/renderers/message-renderer.ts +18 -0
  102. package/src/renderers/registry.ts +45 -0
  103. package/src/renderers/renderer.ts +84 -0
  104. package/src/renderers/table-editor-renderer.ts +8 -0
  105. package/src/renderers/table-renderer.ts +1868 -0
  106. package/src/renderers/text-renderer.ts +252 -0
  107. package/src/renderers/tree-renderer.ts +7 -0
  108. package/src/renderers/types.ts +22 -0
  109. package/src/renderers/wikilink-utils.ts +108 -0
  110. package/src/static/browser-runtime.ts +249 -0
  111. package/src/static/index.ts +3 -0
  112. package/src/static/runtime-assets.ts +175 -0
@@ -0,0 +1,473 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import { QueryMarkdownBlockRenderer } from './renderer.js';
4
+ import type { MdBlockExecutionResult } from './types.js';
5
+
6
+ // --- Module-level Chart.js cache ---
7
+ let chartClass: (new (canvas: HTMLCanvasElement, config: unknown) => any) | undefined;
8
+
9
+ // --- Types ---
10
+
11
+ type ChartThemePalette = {
12
+ foreground: string;
13
+ background: string;
14
+ border: string;
15
+ muted: string;
16
+ accent: string;
17
+ accentForeground: string;
18
+ fontFamily: string;
19
+ isDark: boolean;
20
+ };
21
+
22
+ type DatasetDef = {
23
+ label: string;
24
+ dataColumn: string;
25
+ extra: Record<string, unknown>;
26
+ hasExplicitColors: boolean;
27
+ };
28
+
29
+ type ParsedChartOptions = {
30
+ type: string;
31
+ labelColumn: string;
32
+ datasets: DatasetDef[];
33
+ chartOptions: Record<string, unknown>;
34
+ canvasHeight: string;
35
+ canvasMinHeight: string;
36
+ };
37
+
38
+ // --- Renderer ---
39
+
40
+ export class ChartMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
41
+ canRender(result: MdBlockExecutionResult): boolean {
42
+ return result.status === 'ok'
43
+ && result.format === 'table'
44
+ && result.kind === 'chart';
45
+ }
46
+
47
+ render(result: MdBlockExecutionResult): HTMLElement {
48
+ const container = this.createResultContainer(result.status);
49
+ const payload = result.payload;
50
+ if (!payload) {
51
+ container.appendChild(this.createMessageContainer('Chart renderer requires tabular payload.'));
52
+ return container;
53
+ }
54
+
55
+ const parsed = parseChartOptions(result.options);
56
+ if (!parsed.labelColumn) {
57
+ container.appendChild(this.createMessageContainer(
58
+ "Chart renderer requires 'data.labels' to be set to a column name."
59
+ ));
60
+ return container;
61
+ }
62
+ if (parsed.datasets.length === 0) {
63
+ container.appendChild(this.createMessageContainer(
64
+ "Chart renderer requires at least one entry in 'data.datasets'."
65
+ ));
66
+ return container;
67
+ }
68
+
69
+ const chartRoot = document.createElement('div');
70
+ chartRoot.className = 'graph-canvas-root';
71
+ chartRoot.style.height = parsed.canvasHeight;
72
+ chartRoot.style.minHeight = parsed.canvasMinHeight;
73
+ chartRoot.style.position = 'relative';
74
+ container.appendChild(chartRoot);
75
+
76
+ const canvasEl = document.createElement('canvas');
77
+ chartRoot.appendChild(canvasEl);
78
+
79
+ initializeChartWhenReady(chartRoot, canvasEl, payload.columns, payload.rows, parsed, container);
80
+
81
+ return container;
82
+ }
83
+ }
84
+
85
+ // --- Initialization ---
86
+
87
+ function initializeChartWhenReady(
88
+ chartRoot: HTMLElement,
89
+ canvasEl: HTMLCanvasElement,
90
+ columns: string[],
91
+ rows: string[][],
92
+ parsed: ParsedChartOptions,
93
+ messageContainer: HTMLElement,
94
+ ): void {
95
+ const maxAttempts = 20;
96
+ let attempts = 0;
97
+ const tryInit = (): void => {
98
+ attempts += 1;
99
+ if (!chartRoot.isConnected || chartRoot.clientWidth === 0 || chartRoot.clientHeight === 0) {
100
+ if (attempts < maxAttempts) requestAnimationFrame(tryInit);
101
+ return;
102
+ }
103
+ void doChartInit(chartRoot, canvasEl, columns, rows, parsed).catch((err) => {
104
+ const detail = err instanceof Error ? err.message : String(err);
105
+ const msg = document.createElement('div');
106
+ msg.className = 'oml-md-result-message';
107
+ msg.textContent = `Chart rendering failed: ${detail}`;
108
+ messageContainer.appendChild(msg);
109
+ });
110
+ };
111
+ requestAnimationFrame(tryInit);
112
+ }
113
+
114
+ async function doChartInit(
115
+ chartRoot: HTMLElement,
116
+ canvasEl: HTMLCanvasElement,
117
+ columns: string[],
118
+ rows: string[][],
119
+ parsed: ParsedChartOptions,
120
+ ): Promise<void> {
121
+ const ChartCtor = await loadChartJs();
122
+ let chart: any = null;
123
+
124
+ const buildAndRender = (): void => {
125
+ const theme = resolveThemePalette();
126
+ const config = buildChartConfig(columns, rows, parsed, theme);
127
+ if (chart) {
128
+ chart.destroy();
129
+ }
130
+ chart = new ChartCtor(canvasEl, config);
131
+ };
132
+
133
+ buildAndRender();
134
+
135
+ // Resize the canvas whenever the host element resizes.
136
+ const resizeObserver = new ResizeObserver(() => {
137
+ if (!chartRoot.isConnected) return;
138
+ chart?.resize();
139
+ });
140
+ resizeObserver.observe(chartRoot);
141
+
142
+ // Also watch the result container so the chart fills width when page grows.
143
+ const resultContainer = chartRoot.closest('.oml-md-result');
144
+ let resultResizeObserver: ResizeObserver | undefined;
145
+ if (resultContainer instanceof HTMLElement) {
146
+ resultResizeObserver = new ResizeObserver(() => {
147
+ const nextWidth = Math.max(0, Math.floor(resultContainer.clientWidth));
148
+ chartRoot.style.width = `${nextWidth}px`;
149
+ chart?.resize();
150
+ });
151
+ resultResizeObserver.observe(resultContainer);
152
+ }
153
+
154
+ chartRoot.addEventListener('DOMNodeRemovedFromDocument', () => {
155
+ resizeObserver.disconnect();
156
+ resultResizeObserver?.disconnect();
157
+ chart?.destroy();
158
+ chart = null;
159
+ }, { once: true });
160
+
161
+ // Rebuild chart on VS Code theme change so colors stay consistent.
162
+ const themeObserver = new MutationObserver(() => {
163
+ if (!chartRoot.isConnected) return;
164
+ buildAndRender();
165
+ });
166
+ themeObserver.observe(document.body, {
167
+ attributes: true,
168
+ attributeFilter: ['class', 'style', 'data-vscode-theme-kind'],
169
+ });
170
+ themeObserver.observe(document.documentElement, {
171
+ attributes: true,
172
+ attributeFilter: ['class', 'style', 'data-vscode-theme-kind'],
173
+ });
174
+ const colorScheme = typeof window.matchMedia === 'function'
175
+ ? window.matchMedia('(prefers-color-scheme: dark)')
176
+ : undefined;
177
+ const onColorSchemeChange = (): void => {
178
+ if (!chartRoot.isConnected) return;
179
+ buildAndRender();
180
+ };
181
+ colorScheme?.addEventListener?.('change', onColorSchemeChange);
182
+ chartRoot.addEventListener('DOMNodeRemovedFromDocument', () => {
183
+ themeObserver.disconnect();
184
+ colorScheme?.removeEventListener?.('change', onColorSchemeChange);
185
+ }, { once: true });
186
+
187
+ // Drag-to-resize handle (same pattern as other canvas renderers).
188
+ const parsedMinHeight = parseInt(chartRoot.style.minHeight, 10);
189
+ const canvasMinHeight = Number.isFinite(parsedMinHeight) ? Math.max(120, parsedMinHeight) : 200;
190
+ const resizeHandle = document.createElement('div');
191
+ resizeHandle.className = 'canvas-resize-handle';
192
+ chartRoot.appendChild(resizeHandle);
193
+ let canvasResize: { pointerId: number; startY: number; startHeight: number } | undefined;
194
+ const onResizePointerDown = (event: PointerEvent): void => {
195
+ event.preventDefault();
196
+ event.stopPropagation();
197
+ if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
198
+ canvasResize = { pointerId: event.pointerId, startY: event.clientY, startHeight: chartRoot.clientHeight };
199
+ resizeHandle.setPointerCapture(event.pointerId);
200
+ };
201
+ const onResizePointerMove = (event: PointerEvent): void => {
202
+ if (!canvasResize || event.pointerId !== canvasResize.pointerId) return;
203
+ const delta = event.clientY - canvasResize.startY;
204
+ const newHeight = Math.max(canvasMinHeight, canvasResize.startHeight + delta);
205
+ chartRoot.style.height = `${Math.ceil(newHeight)}px`;
206
+ chart?.resize();
207
+ };
208
+ const onResizePointerEnd = (event: PointerEvent): void => {
209
+ if (!canvasResize || event.pointerId !== canvasResize.pointerId) return;
210
+ canvasResize = undefined;
211
+ };
212
+ resizeHandle.addEventListener('pointerdown', onResizePointerDown);
213
+ resizeHandle.addEventListener('pointermove', onResizePointerMove);
214
+ resizeHandle.addEventListener('pointerup', onResizePointerEnd);
215
+ resizeHandle.addEventListener('pointercancel', onResizePointerEnd);
216
+ }
217
+
218
+ // --- Chart.js loader ---
219
+
220
+ async function loadChartJs(): Promise<new (canvas: HTMLCanvasElement, config: unknown) => any> {
221
+ if (chartClass) return chartClass;
222
+ const mod = await import('chart.js/auto');
223
+ const ctor = (mod as any).default ?? (mod as any).Chart;
224
+ if (typeof ctor !== 'function') {
225
+ throw new Error('Chart constructor is unavailable from chart.js/auto');
226
+ }
227
+ chartClass = ctor as new (canvas: HTMLCanvasElement, config: unknown) => any;
228
+ return chartClass;
229
+ }
230
+
231
+ // --- Config builder ---
232
+
233
+ function buildChartConfig(
234
+ columns: string[],
235
+ rows: string[][],
236
+ parsed: ParsedChartOptions,
237
+ theme: ChartThemePalette,
238
+ ): Record<string, unknown> {
239
+ const labelColIdx = columns.indexOf(parsed.labelColumn);
240
+ const labels = labelColIdx >= 0
241
+ ? rows.map((row) => String(row[labelColIdx] ?? ''))
242
+ : [];
243
+
244
+ // Polar types get one auto-color per label; cartesian types get one per dataset.
245
+ const isPolar = ['pie', 'doughnut', 'polarArea'].includes(parsed.type);
246
+ const autoColorCount = isPolar ? labels.length : parsed.datasets.length;
247
+ const autoColors = generateAutoColors(autoColorCount, theme.isDark);
248
+
249
+ const datasets = parsed.datasets.map((def, datasetIdx) => {
250
+ const dataColIdx = columns.indexOf(def.dataColumn);
251
+ const data = dataColIdx >= 0
252
+ ? rows.map((row) => {
253
+ const v = row[dataColIdx];
254
+ const n = Number(v);
255
+ return Number.isFinite(n) ? n : (v ?? null);
256
+ })
257
+ : [];
258
+
259
+ const resolvedExtra = deepResolveThemeTokens(def.extra, theme) as Record<string, unknown>;
260
+ const ds: Record<string, unknown> = { label: def.label, data, ...resolvedExtra };
261
+
262
+ if (!def.hasExplicitColors) {
263
+ if (isPolar) {
264
+ ds.backgroundColor = autoColors;
265
+ ds.hoverOffset = 6;
266
+ } else {
267
+ const base = autoColors[datasetIdx % autoColors.length];
268
+ ds.backgroundColor = withAlpha(base, 0.75);
269
+ ds.borderColor = base;
270
+ ds.borderWidth = 1;
271
+ }
272
+ }
273
+
274
+ return ds;
275
+ });
276
+
277
+ // Base theme-aware Chart.js options.
278
+ const baseOptions: Record<string, unknown> = {
279
+ responsive: true,
280
+ maintainAspectRatio: false,
281
+ animation: { duration: 300 },
282
+ color: theme.foreground,
283
+ font: { family: theme.fontFamily },
284
+ plugins: {
285
+ title: {
286
+ color: theme.foreground,
287
+ font: { family: theme.fontFamily },
288
+ },
289
+ legend: {
290
+ labels: {
291
+ color: theme.foreground,
292
+ font: { family: theme.fontFamily },
293
+ },
294
+ },
295
+ tooltip: {
296
+ bodyColor: theme.foreground,
297
+ titleColor: theme.foreground,
298
+ backgroundColor: theme.background,
299
+ borderColor: theme.border,
300
+ borderWidth: 1,
301
+ bodyFont: { family: theme.fontFamily },
302
+ titleFont: { family: theme.fontFamily },
303
+ },
304
+ },
305
+ };
306
+
307
+ if (!isPolar) {
308
+ (baseOptions as any).scales = {
309
+ x: {
310
+ ticks: { color: theme.foreground, font: { family: theme.fontFamily } },
311
+ title: { color: theme.foreground, font: { family: theme.fontFamily } },
312
+ grid: { color: theme.border },
313
+ },
314
+ y: {
315
+ ticks: { color: theme.foreground, font: { family: theme.fontFamily } },
316
+ title: { color: theme.foreground, font: { family: theme.fontFamily } },
317
+ grid: { color: theme.border },
318
+ },
319
+ };
320
+ }
321
+
322
+ const userOptions = deepResolveThemeTokens(parsed.chartOptions, theme) as Record<string, unknown>;
323
+ const mergedOptions = deepMerge(baseOptions, userOptions);
324
+
325
+ return {
326
+ type: parsed.type,
327
+ data: { labels, datasets },
328
+ options: mergedOptions,
329
+ };
330
+ }
331
+
332
+ // --- Auto-color generation ---
333
+
334
+ function generateAutoColors(count: number, isDark: boolean): string[] {
335
+ if (count === 0) return [];
336
+ const saturation = isDark ? 65 : 58;
337
+ const lightness = isDark ? 58 : 48;
338
+ return Array.from({ length: count }, (_, i) => {
339
+ const hue = Math.round((i * 360) / count);
340
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
341
+ });
342
+ }
343
+
344
+ function withAlpha(hslColor: string, alpha: number): string {
345
+ const match = /^hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)$/.exec(hslColor);
346
+ if (match) {
347
+ return `hsla(${match[1]}, ${match[2]}%, ${match[3]}%, ${alpha})`;
348
+ }
349
+ return hslColor;
350
+ }
351
+
352
+ // --- Theme ---
353
+
354
+ function resolveThemePalette(): ChartThemePalette {
355
+ const styles = getComputedStyle(document.documentElement);
356
+ const foreground = readCssVarFirst(styles, ['--vscode-editor-foreground', '--oml-static-foreground'], '#24292f');
357
+ const background = readCssVarFirst(styles, ['--vscode-editor-background', '--oml-static-background'], '#ffffff');
358
+ const border = readCssVarFirst(styles, ['--vscode-editorWidget-border', '--oml-static-border'], '#d0d7de');
359
+ const muted = readCssVarFirst(styles, ['--vscode-descriptionForeground', '--oml-static-muted'], '#57606a');
360
+ const link = readCssVarFirst(styles, ['--vscode-textLink-foreground', '--oml-static-link'], '#0969da');
361
+ const accent = readCssVarFirst(styles, ['--vscode-button-background'], link);
362
+ const accentForeground = readCssVarFirst(styles, ['--vscode-button-foreground'], '#ffffff');
363
+ const fontFamily = readCssVarFirst(
364
+ styles,
365
+ ['--vscode-editor-font-family'],
366
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
367
+ );
368
+ const kind = document.body.getAttribute('data-vscode-theme-kind') ?? '';
369
+ const isDark = kind
370
+ ? !kind.includes('light')
371
+ : Boolean(window.matchMedia?.('(prefers-color-scheme: dark)').matches);
372
+ return { foreground, background, border, muted, accent, accentForeground, fontFamily, isDark };
373
+ }
374
+
375
+ function readCssVarFirst(styles: CSSStyleDeclaration, names: string[], fallback: string): string {
376
+ for (const name of names) {
377
+ const value = styles.getPropertyValue(name).trim();
378
+ if (value.length > 0) {
379
+ return value;
380
+ }
381
+ }
382
+ return fallback;
383
+ }
384
+
385
+ // --- Token resolution ---
386
+
387
+ function resolveThemeToken(value: string, theme: ChartThemePalette): string {
388
+ const t = value.trim();
389
+ if (t === 'theme.foreground') return theme.foreground;
390
+ if (t === 'theme.background') return theme.background;
391
+ if (t === 'theme.border') return theme.border;
392
+ if (t === 'theme.muted') return theme.muted;
393
+ if (t === 'theme.accent') return theme.accent;
394
+ if (t === 'theme.accentForeground') return theme.accentForeground;
395
+ return t;
396
+ }
397
+
398
+ function deepResolveThemeTokens(obj: unknown, theme: ChartThemePalette): unknown {
399
+ if (typeof obj === 'string') return resolveThemeToken(obj, theme);
400
+ if (Array.isArray(obj)) return obj.map((item) => deepResolveThemeTokens(item, theme));
401
+ if (isRecord(obj)) {
402
+ const result: Record<string, unknown> = {};
403
+ for (const [key, val] of Object.entries(obj)) {
404
+ result[key] = deepResolveThemeTokens(val, theme);
405
+ }
406
+ return result;
407
+ }
408
+ return obj;
409
+ }
410
+
411
+ // --- Option parsing ---
412
+
413
+ function parseChartOptions(options: Record<string, unknown> | undefined): ParsedChartOptions {
414
+ const type = typeof options?.type === 'string' ? options.type.trim() : 'pie';
415
+ const rawData = isRecord(options?.data) ? options.data : {};
416
+ const labelColumn = typeof rawData.labels === 'string' ? rawData.labels.trim() : '';
417
+ const rawDatasets = Array.isArray(rawData.datasets) ? rawData.datasets : [];
418
+
419
+ const datasets: DatasetDef[] = [];
420
+ for (const rawDs of rawDatasets) {
421
+ if (!isRecord(rawDs)) continue;
422
+ const dataColumn = typeof rawDs.data === 'string' ? rawDs.data.trim() : '';
423
+ if (!dataColumn) continue;
424
+ const label = typeof rawDs.label === 'string' ? rawDs.label : dataColumn;
425
+ // Separate column-mapping keys from pass-through Chart.js dataset props.
426
+ const { data: _d, label: _l, ...extra } = rawDs as Record<string, unknown>;
427
+ const hasExplicitColors = 'backgroundColor' in extra || 'borderColor' in extra;
428
+ datasets.push({ label, dataColumn, extra, hasExplicitColors });
429
+ }
430
+
431
+ const chartOptions = isRecord(options?.options) ? options.options : {};
432
+ const canvas = isRecord(options?.canvas) ? options.canvas : {};
433
+ const heightRaw = canvas.height ?? options?.height;
434
+ const minHeightRaw = canvas.minHeight ?? options?.minHeight;
435
+
436
+ return {
437
+ type,
438
+ labelColumn,
439
+ datasets,
440
+ chartOptions,
441
+ canvasHeight: resolveHeight(heightRaw, '380px'),
442
+ canvasMinHeight: resolveHeight(minHeightRaw, '200px'),
443
+ };
444
+ }
445
+
446
+ function resolveHeight(raw: unknown, fallback: string): string {
447
+ if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) return `${raw}px`;
448
+ if (typeof raw === 'string') {
449
+ const t = raw.trim();
450
+ if (/^\d+(\.\d+)?(px|vh|%)$/.test(t)) return t;
451
+ }
452
+ return fallback;
453
+ }
454
+
455
+ // --- Utilities ---
456
+
457
+ function deepMerge(base: Record<string, unknown>, override: unknown): Record<string, unknown> {
458
+ if (!isRecord(override)) return base;
459
+ const result: Record<string, unknown> = { ...base };
460
+ for (const [key, val] of Object.entries(override)) {
461
+ if (val === undefined) continue;
462
+ if (isRecord(val) && isRecord(result[key])) {
463
+ result[key] = deepMerge(result[key] as Record<string, unknown>, val);
464
+ } else {
465
+ result[key] = val;
466
+ }
467
+ }
468
+ return result;
469
+ }
470
+
471
+ function isRecord(value: unknown): value is Record<string, unknown> {
472
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
473
+ }