mviz 1.4.2
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.
- package/README.md +174 -0
- package/dist/charts/area.d.ts +14 -0
- package/dist/charts/area.d.ts.map +1 -0
- package/dist/charts/area.js +137 -0
- package/dist/charts/area.js.map +1 -0
- package/dist/charts/bar.d.ts +14 -0
- package/dist/charts/bar.d.ts.map +1 -0
- package/dist/charts/bar.js +191 -0
- package/dist/charts/bar.js.map +1 -0
- package/dist/charts/boxplot.d.ts +14 -0
- package/dist/charts/boxplot.d.ts.map +1 -0
- package/dist/charts/boxplot.js +79 -0
- package/dist/charts/boxplot.js.map +1 -0
- package/dist/charts/bubble.d.ts +14 -0
- package/dist/charts/bubble.d.ts.map +1 -0
- package/dist/charts/bubble.js +127 -0
- package/dist/charts/bubble.js.map +1 -0
- package/dist/charts/calendar.d.ts +14 -0
- package/dist/charts/calendar.d.ts.map +1 -0
- package/dist/charts/calendar.js +94 -0
- package/dist/charts/calendar.js.map +1 -0
- package/dist/charts/combo.d.ts +14 -0
- package/dist/charts/combo.d.ts.map +1 -0
- package/dist/charts/combo.js +163 -0
- package/dist/charts/combo.js.map +1 -0
- package/dist/charts/dumbbell.d.ts +17 -0
- package/dist/charts/dumbbell.d.ts.map +1 -0
- package/dist/charts/dumbbell.js +368 -0
- package/dist/charts/dumbbell.js.map +1 -0
- package/dist/charts/funnel.d.ts +14 -0
- package/dist/charts/funnel.d.ts.map +1 -0
- package/dist/charts/funnel.js +145 -0
- package/dist/charts/funnel.js.map +1 -0
- package/dist/charts/heatmap.d.ts +14 -0
- package/dist/charts/heatmap.d.ts.map +1 -0
- package/dist/charts/heatmap.js +202 -0
- package/dist/charts/heatmap.js.map +1 -0
- package/dist/charts/histogram.d.ts +14 -0
- package/dist/charts/histogram.d.ts.map +1 -0
- package/dist/charts/histogram.js +103 -0
- package/dist/charts/histogram.js.map +1 -0
- package/dist/charts/index.d.ts +40 -0
- package/dist/charts/index.d.ts.map +1 -0
- package/dist/charts/index.js +42 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/line.d.ts +14 -0
- package/dist/charts/line.d.ts.map +1 -0
- package/dist/charts/line.js +134 -0
- package/dist/charts/line.js.map +1 -0
- package/dist/charts/pie.d.ts +14 -0
- package/dist/charts/pie.d.ts.map +1 -0
- package/dist/charts/pie.js +75 -0
- package/dist/charts/pie.js.map +1 -0
- package/dist/charts/registry.d.ts +36 -0
- package/dist/charts/registry.d.ts.map +1 -0
- package/dist/charts/registry.js +55 -0
- package/dist/charts/registry.js.map +1 -0
- package/dist/charts/sankey.d.ts +14 -0
- package/dist/charts/sankey.d.ts.map +1 -0
- package/dist/charts/sankey.js +74 -0
- package/dist/charts/sankey.js.map +1 -0
- package/dist/charts/scatter.d.ts +14 -0
- package/dist/charts/scatter.d.ts.map +1 -0
- package/dist/charts/scatter.js +130 -0
- package/dist/charts/scatter.js.map +1 -0
- package/dist/charts/sparkline.d.ts +19 -0
- package/dist/charts/sparkline.d.ts.map +1 -0
- package/dist/charts/sparkline.js +154 -0
- package/dist/charts/sparkline.js.map +1 -0
- package/dist/charts/waterfall.d.ts +14 -0
- package/dist/charts/waterfall.d.ts.map +1 -0
- package/dist/charts/waterfall.js +232 -0
- package/dist/charts/waterfall.js.map +1 -0
- package/dist/charts/xmr.d.ts +14 -0
- package/dist/charts/xmr.d.ts.map +1 -0
- package/dist/charts/xmr.js +456 -0
- package/dist/charts/xmr.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +120 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/alert.d.ts +10 -0
- package/dist/components/alert.d.ts.map +1 -0
- package/dist/components/alert.js +65 -0
- package/dist/components/alert.js.map +1 -0
- package/dist/components/big_value.d.ts +10 -0
- package/dist/components/big_value.d.ts.map +1 -0
- package/dist/components/big_value.js +78 -0
- package/dist/components/big_value.js.map +1 -0
- package/dist/components/delta.d.ts +10 -0
- package/dist/components/delta.d.ts.map +1 -0
- package/dist/components/delta.js +83 -0
- package/dist/components/delta.js.map +1 -0
- package/dist/components/empty_space.d.ts +10 -0
- package/dist/components/empty_space.d.ts.map +1 -0
- package/dist/components/empty_space.js +29 -0
- package/dist/components/empty_space.js.map +1 -0
- package/dist/components/index.d.ts +21 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +23 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/note.d.ts +10 -0
- package/dist/components/note.d.ts.map +1 -0
- package/dist/components/note.js +66 -0
- package/dist/components/note.js.map +1 -0
- package/dist/components/registry.d.ts +24 -0
- package/dist/components/registry.d.ts.map +1 -0
- package/dist/components/registry.js +36 -0
- package/dist/components/registry.js.map +1 -0
- package/dist/components/table.d.ts +90 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +610 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/text.d.ts +10 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +46 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/textarea.d.ts +10 -0
- package/dist/components/textarea.d.ts.map +1 -0
- package/dist/components/textarea.js +79 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/core/colors.d.ts +45 -0
- package/dist/core/colors.d.ts.map +1 -0
- package/dist/core/colors.js +93 -0
- package/dist/core/colors.js.map +1 -0
- package/dist/core/css.d.ts +20 -0
- package/dist/core/css.d.ts.map +1 -0
- package/dist/core/css.js +97 -0
- package/dist/core/css.js.map +1 -0
- package/dist/core/exceptions.d.ts +59 -0
- package/dist/core/exceptions.d.ts.map +1 -0
- package/dist/core/exceptions.js +100 -0
- package/dist/core/exceptions.js.map +1 -0
- package/dist/core/formatting.d.ts +53 -0
- package/dist/core/formatting.d.ts.map +1 -0
- package/dist/core/formatting.js +491 -0
- package/dist/core/formatting.js.map +1 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +10 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/serializer.d.ts +29 -0
- package/dist/core/serializer.d.ts.map +1 -0
- package/dist/core/serializer.js +84 -0
- package/dist/core/serializer.js.map +1 -0
- package/dist/core/themes.d.ts +138 -0
- package/dist/core/themes.d.ts.map +1 -0
- package/dist/core/themes.js +484 -0
- package/dist/core/themes.js.map +1 -0
- package/dist/core/version-check.d.ts +23 -0
- package/dist/core/version-check.d.ts.map +1 -0
- package/dist/core/version-check.js +163 -0
- package/dist/core/version-check.js.map +1 -0
- package/dist/generate_test_harness.d.ts +13 -0
- package/dist/generate_test_harness.d.ts.map +1 -0
- package/dist/generate_test_harness.js +35 -0
- package/dist/generate_test_harness.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/converter.d.ts +22 -0
- package/dist/layout/converter.d.ts.map +1 -0
- package/dist/layout/converter.js +46 -0
- package/dist/layout/converter.js.map +1 -0
- package/dist/layout/csv.d.ts +15 -0
- package/dist/layout/csv.d.ts.map +1 -0
- package/dist/layout/csv.js +88 -0
- package/dist/layout/csv.js.map +1 -0
- package/dist/layout/dispatcher.d.ts +13 -0
- package/dist/layout/dispatcher.d.ts.map +1 -0
- package/dist/layout/dispatcher.js +47 -0
- package/dist/layout/dispatcher.js.map +1 -0
- package/dist/layout/index.d.ts +8 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +8 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/parser.d.ts +19 -0
- package/dist/layout/parser.d.ts.map +1 -0
- package/dist/layout/parser.js +888 -0
- package/dist/layout/parser.js.map +1 -0
- package/dist/layout/templates.d.ts +32 -0
- package/dist/layout/templates.d.ts.map +1 -0
- package/dist/layout/templates.js +1016 -0
- package/dist/layout/templates.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +14 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown dashboard parser for mviz
|
|
3
|
+
*
|
|
4
|
+
* Parses markdown files with embedded chart/component specs and generates HTML.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { generateDashboardHtml, generateTestHarnessHtml, generateTestHarnessJs } from './templates.js';
|
|
9
|
+
import { convertColumnarFormat } from './converter.js';
|
|
10
|
+
import { parseCsv } from './csv.js';
|
|
11
|
+
import { getOptionsBuilder } from '../charts/index.js';
|
|
12
|
+
import { serializeOption } from '../core/serializer.js';
|
|
13
|
+
import { formatNumber, inferFormat } from '../core/formatting.js';
|
|
14
|
+
import { COLORS, GRID_TOTAL_COLUMNS, DEFAULT_SIZES, autoSizeChart, getHeatmapColors, getThemeColors, ALERT_ICONS, } from '../core/themes.js';
|
|
15
|
+
import { formatCell, computeHeatmapRanges, computeDumbbellRanges, } from '../components/table.js';
|
|
16
|
+
// Height per row unit (compact for print) - must match Python's ROW_HEIGHT_PX
|
|
17
|
+
const ROW_HEIGHT_PX = 32;
|
|
18
|
+
// Minimum column span for auto-sized items
|
|
19
|
+
const MIN_AUTO_COLUMN_SPAN = 4;
|
|
20
|
+
/**
|
|
21
|
+
* Parse YAML-like frontmatter from markdown
|
|
22
|
+
*/
|
|
23
|
+
function parseFrontmatter(lines, baseTheme) {
|
|
24
|
+
let theme = baseTheme;
|
|
25
|
+
let pageTitle = 'Dashboard';
|
|
26
|
+
let continuous = false;
|
|
27
|
+
if (lines.length === 0 || lines[0] !== '---') {
|
|
28
|
+
return { theme, pageTitle, continuous, remainingLines: lines };
|
|
29
|
+
}
|
|
30
|
+
let frontmatterEnd = -1;
|
|
31
|
+
for (let i = 1; i < lines.length; i++) {
|
|
32
|
+
if (lines[i] === '---') {
|
|
33
|
+
frontmatterEnd = i;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (frontmatterEnd <= 0) {
|
|
38
|
+
return { theme, pageTitle, continuous, remainingLines: lines };
|
|
39
|
+
}
|
|
40
|
+
for (let i = 1; i < frontmatterEnd; i++) {
|
|
41
|
+
const line = lines[i];
|
|
42
|
+
const colonIdx = line.indexOf(':');
|
|
43
|
+
if (colonIdx > 0) {
|
|
44
|
+
const key = line.slice(0, colonIdx).trim();
|
|
45
|
+
let val = line.slice(colonIdx + 1).trim();
|
|
46
|
+
// Strip surrounding quotes
|
|
47
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
48
|
+
val = val.slice(1, -1);
|
|
49
|
+
}
|
|
50
|
+
if (key === 'theme') {
|
|
51
|
+
theme = val;
|
|
52
|
+
}
|
|
53
|
+
else if (key === 'title') {
|
|
54
|
+
pageTitle = val;
|
|
55
|
+
}
|
|
56
|
+
else if (key === 'continuous') {
|
|
57
|
+
continuous = ['true', 'yes', '1'].includes(val.toLowerCase());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { theme, pageTitle, continuous, remainingLines: lines.slice(frontmatterEnd + 1) };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve auto sizes in a row based on row composition
|
|
65
|
+
*/
|
|
66
|
+
function resolveAutoSizes(row) {
|
|
67
|
+
const autoItems = [];
|
|
68
|
+
let fixedCols = 0;
|
|
69
|
+
for (const item of row) {
|
|
70
|
+
if (item.size === 'auto') {
|
|
71
|
+
autoItems.push(item);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const size = item.size ?? DEFAULT_SIZES[item.type] ?? [8, 4];
|
|
75
|
+
fixedCols += size[0];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (autoItems.length === 0)
|
|
79
|
+
return;
|
|
80
|
+
const remainingCols = Math.max(0, GRID_TOTAL_COLUMNS - fixedCols);
|
|
81
|
+
const colsPerAuto = autoItems.length > 0
|
|
82
|
+
? Math.max(MIN_AUTO_COLUMN_SPAN, Math.floor(remainingCols / autoItems.length))
|
|
83
|
+
: MIN_AUTO_COLUMN_SPAN * 2;
|
|
84
|
+
for (const item of autoItems) {
|
|
85
|
+
const [autoCols, autoRows] = autoSizeChart(item.type, item.spec);
|
|
86
|
+
if (autoItems.length === 1) {
|
|
87
|
+
item.size = [autoCols, autoRows];
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
item.size = [Math.min(autoCols, colsPerAuto), autoRows];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generate ECharts initialization script for a chart
|
|
96
|
+
*/
|
|
97
|
+
function generateChartScript(chartId, optionJsonLight, optionJsonDark) {
|
|
98
|
+
const darkOptions = optionJsonDark ?? optionJsonLight;
|
|
99
|
+
return `
|
|
100
|
+
(function() {
|
|
101
|
+
var chart = echarts.init(document.getElementById('${chartId}'), null, {renderer: 'svg'});
|
|
102
|
+
window.chartInstances = window.chartInstances || {};
|
|
103
|
+
window.chartOptions = window.chartOptions || {};
|
|
104
|
+
window.chartInstances['${chartId}'] = chart;
|
|
105
|
+
window.chartOptions['${chartId}'] = {
|
|
106
|
+
light: ${optionJsonLight},
|
|
107
|
+
dark: ${darkOptions}
|
|
108
|
+
};
|
|
109
|
+
var currentTheme = document.body.classList.contains('theme-dark') ? 'dark' : 'light';
|
|
110
|
+
chart.setOption(window.chartOptions['${chartId}'][currentTheme]);
|
|
111
|
+
window.addEventListener('resize', function() { chart.resize(); });
|
|
112
|
+
})();`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Render an ECharts component
|
|
116
|
+
*/
|
|
117
|
+
function renderEchartsComponent(compType, spec, chartId, colSpan, itemHeight, anchorId) {
|
|
118
|
+
const title = spec.title ?? '';
|
|
119
|
+
const titleHtml = title ? `<h3 class="chart-title">${escapeHtml(title)}</h3>` : '';
|
|
120
|
+
const builder = getOptionsBuilder(compType);
|
|
121
|
+
if (!builder) {
|
|
122
|
+
return { html: null, script: null };
|
|
123
|
+
}
|
|
124
|
+
const chartHeight = itemHeight - (title ? 24 : 0);
|
|
125
|
+
// Generate light theme options
|
|
126
|
+
const specLight = { ...spec, theme: 'light', height: chartHeight };
|
|
127
|
+
const optionLight = builder(specLight);
|
|
128
|
+
if (!optionLight) {
|
|
129
|
+
return { html: null, script: null };
|
|
130
|
+
}
|
|
131
|
+
const optionJsonLight = serializeOption(optionLight);
|
|
132
|
+
// Generate dark theme options
|
|
133
|
+
const specDark = { ...spec, theme: 'dark', height: chartHeight };
|
|
134
|
+
const optionDark = builder(specDark);
|
|
135
|
+
const optionJsonDark = optionDark ? serializeOption(optionDark) : null;
|
|
136
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
137
|
+
const html = `
|
|
138
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
139
|
+
${titleHtml}
|
|
140
|
+
<div id="${chartId}" style="width: 100%; height: ${chartHeight}px;"></div>
|
|
141
|
+
</div>`;
|
|
142
|
+
const script = generateChartScript(chartId, optionJsonLight, optionJsonDark);
|
|
143
|
+
return { html, script };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Escape HTML special characters
|
|
147
|
+
*/
|
|
148
|
+
function escapeHtml(text) {
|
|
149
|
+
return text
|
|
150
|
+
.replace(/&/g, '&')
|
|
151
|
+
.replace(/</g, '<')
|
|
152
|
+
.replace(/>/g, '>')
|
|
153
|
+
.replace(/"/g, '"')
|
|
154
|
+
.replace(/'/g, ''');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Simple component renderers
|
|
158
|
+
*/
|
|
159
|
+
function renderBigValue(spec, colSpan, anchorId) {
|
|
160
|
+
const value = typeof spec.value === 'number' ? spec.value : 0;
|
|
161
|
+
const specLabel = spec.label;
|
|
162
|
+
const specTitle = spec.title;
|
|
163
|
+
// If only title is provided (no label), use title as the label below the number
|
|
164
|
+
// If both provided, title becomes H2 header and label goes below
|
|
165
|
+
const label = specLabel ?? specTitle ?? '';
|
|
166
|
+
const showTitleHeader = specTitle && specLabel;
|
|
167
|
+
const fmt = spec.format ?? inferFormat(label, value);
|
|
168
|
+
const comparison = spec.comparison;
|
|
169
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
170
|
+
const displayValue = formatNumber(value, fmt);
|
|
171
|
+
// Build comparison HTML if provided
|
|
172
|
+
let comparisonHtml = '';
|
|
173
|
+
if (comparison) {
|
|
174
|
+
const compVal = comparison.value ?? 0;
|
|
175
|
+
const compLabel = comparison.label ?? '';
|
|
176
|
+
const compFmt = comparison.format;
|
|
177
|
+
const isPositive = compVal >= 0;
|
|
178
|
+
const arrow = isPositive ? '↑' : '↓';
|
|
179
|
+
const compColor = isPositive ? COLORS.POSITIVE_GREEN : COLORS.ERROR_RED;
|
|
180
|
+
const compDisplay = formatNumber(compVal, compFmt, true);
|
|
181
|
+
comparisonHtml = `
|
|
182
|
+
<div style="display: flex; align-items: center; gap: 4px; margin-top: 4px;">
|
|
183
|
+
<span style="color: ${compColor}; font-size: 12px;">${arrow} ${compDisplay}</span>
|
|
184
|
+
<span style="color: var(--text-muted); font-size: 10px;">${escapeHtml(compLabel)}</span>
|
|
185
|
+
</div>`;
|
|
186
|
+
}
|
|
187
|
+
// Build title header HTML if both title and label provided
|
|
188
|
+
const titleHtml = showTitleHeader
|
|
189
|
+
? `<h3 class="chart-title">${escapeHtml(specTitle.toUpperCase())}</h3>`
|
|
190
|
+
: '';
|
|
191
|
+
return `
|
|
192
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
193
|
+
${titleHtml}<div class="big-value">${escapeHtml(displayValue)}</div>
|
|
194
|
+
<div class="label">${escapeHtml(label)}</div>${comparisonHtml}
|
|
195
|
+
</div>`;
|
|
196
|
+
}
|
|
197
|
+
function renderNote(spec, colSpan, anchorId) {
|
|
198
|
+
const content = spec.content ?? '';
|
|
199
|
+
const label = spec.label ?? '';
|
|
200
|
+
// Support both 'noteType' (Python style) and 'variant' for compatibility
|
|
201
|
+
const noteType = spec.noteType ?? spec.variant ?? 'default';
|
|
202
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
203
|
+
// Parse inline markdown in content
|
|
204
|
+
const parsedContent = parseInlineMarkdown(content);
|
|
205
|
+
const contentHtml = label ? `<strong>${escapeHtml(label)}:</strong> ${parsedContent}` : parsedContent;
|
|
206
|
+
return `
|
|
207
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
208
|
+
<div class="note note-${noteType}">
|
|
209
|
+
<div class="note-content">${contentHtml}</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>`;
|
|
212
|
+
}
|
|
213
|
+
function renderText(spec, colSpan, anchorId) {
|
|
214
|
+
const content = spec.content ?? '';
|
|
215
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
216
|
+
return `
|
|
217
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
218
|
+
<div class="text-content">${escapeHtml(content)}</div>
|
|
219
|
+
</div>`;
|
|
220
|
+
}
|
|
221
|
+
function renderEmptySpace(_spec, colSpan, anchorId) {
|
|
222
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
223
|
+
return `
|
|
224
|
+
<div class="grid-item empty-space"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
225
|
+
</div>`;
|
|
226
|
+
}
|
|
227
|
+
function renderInlineHeader(spec, anchorId) {
|
|
228
|
+
const text = spec.text ?? '';
|
|
229
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
230
|
+
return `
|
|
231
|
+
<div class="inline-header"${anchorAttr} style="--col-span: ${GRID_TOTAL_COLUMNS}; grid-column: span ${GRID_TOTAL_COLUMNS};">
|
|
232
|
+
<p class="inline-header-text">${escapeHtml(text)}</p>
|
|
233
|
+
</div>`;
|
|
234
|
+
}
|
|
235
|
+
function renderDelta(spec, colSpan, anchorId) {
|
|
236
|
+
const value = typeof spec.value === 'number' ? spec.value : 0;
|
|
237
|
+
const specLabel = spec.label;
|
|
238
|
+
const specTitle = spec.title;
|
|
239
|
+
// If only title is provided (no label), use title as the label below
|
|
240
|
+
// If both provided, title becomes header and label goes below
|
|
241
|
+
const label = specLabel ?? specTitle ?? '';
|
|
242
|
+
const showTitleHeader = specTitle && specLabel;
|
|
243
|
+
const fmt = spec.format ?? inferFormat(label, value);
|
|
244
|
+
const positiveIsGood = spec.positiveIsGood !== false; // Default true
|
|
245
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
246
|
+
const isPositive = value > 0;
|
|
247
|
+
const isGood = (isPositive && positiveIsGood) || (!isPositive && !positiveIsGood);
|
|
248
|
+
const color = isGood ? COLORS.POSITIVE_GREEN : COLORS.ERROR_RED;
|
|
249
|
+
const arrow = isPositive ? '↑' : '↓';
|
|
250
|
+
const displayValue = formatNumber(value, fmt, true);
|
|
251
|
+
// Build title header HTML if both title and label provided
|
|
252
|
+
const titleHtml = showTitleHeader
|
|
253
|
+
? `<h3 class="chart-title">${escapeHtml(specTitle.toUpperCase())}</h3>`
|
|
254
|
+
: '';
|
|
255
|
+
return `
|
|
256
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
257
|
+
${titleHtml}<div class="delta">
|
|
258
|
+
<span class="arrow" style="color: ${color};">${arrow}</span>
|
|
259
|
+
<span class="value" style="color: ${color};">${escapeHtml(displayValue)}</span>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="label">${escapeHtml(label)}</div>
|
|
262
|
+
</div>`;
|
|
263
|
+
}
|
|
264
|
+
// Alert type configuration
|
|
265
|
+
const ALERT_COLORS = {
|
|
266
|
+
info: COLORS.INFO_BLUE,
|
|
267
|
+
success: COLORS.POSITIVE_GREEN,
|
|
268
|
+
warning: COLORS.WARNING_AMBER,
|
|
269
|
+
error: COLORS.ERROR_RED,
|
|
270
|
+
};
|
|
271
|
+
function renderAlert(spec, colSpan, anchorId) {
|
|
272
|
+
const message = spec.message ?? '';
|
|
273
|
+
const alertType = spec.alertType ?? 'info';
|
|
274
|
+
const alertColor = ALERT_COLORS[alertType] ?? COLORS.INFO_BLUE;
|
|
275
|
+
const icon = ALERT_ICONS[alertType] ?? 'ℹ';
|
|
276
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
277
|
+
// Parse inline markdown (bold, italic)
|
|
278
|
+
const parsedMessage = parseInlineMarkdown(message);
|
|
279
|
+
return `
|
|
280
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
281
|
+
<div class="alert" style="border-left-color: ${alertColor}; background-color: ${alertColor}1a;">
|
|
282
|
+
<span class="icon" style="color: ${alertColor};">${icon}</span>
|
|
283
|
+
<span class="message">${parsedMessage}</span>
|
|
284
|
+
</div>
|
|
285
|
+
</div>`;
|
|
286
|
+
}
|
|
287
|
+
function renderTextarea(spec, colSpan, anchorId) {
|
|
288
|
+
const content = spec.content ?? '';
|
|
289
|
+
const title = spec.title ?? '';
|
|
290
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
291
|
+
const titleHtml = title ? `<h3 class="chart-title">${escapeHtml(title)}</h3>` : '';
|
|
292
|
+
// Escape content for safe embedding in JavaScript
|
|
293
|
+
const escapedContent = content.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
294
|
+
// Generate unique ID based on content hash
|
|
295
|
+
const contentId = `md-${Math.abs(hashString(content)) % 10000000}`;
|
|
296
|
+
return `
|
|
297
|
+
<div class="grid-item textarea-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
298
|
+
${titleHtml}
|
|
299
|
+
<div class="markdown-content" id="${contentId}"></div>
|
|
300
|
+
<script>
|
|
301
|
+
document.getElementById('${contentId}').innerHTML = marked.parse(\`${escapedContent}\`);
|
|
302
|
+
</script>
|
|
303
|
+
</div>`;
|
|
304
|
+
}
|
|
305
|
+
function renderPctBarSparkline(spec, colSpan, anchorId) {
|
|
306
|
+
const title = spec.title ?? '';
|
|
307
|
+
const pctValue = typeof spec.value === 'number' ? spec.value : 0;
|
|
308
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
309
|
+
// Normalize to 0-1 range and clamp
|
|
310
|
+
const pct = pctValue <= 1 ? pctValue : pctValue / 100;
|
|
311
|
+
const clampedPct = Math.max(0, Math.min(1, pct));
|
|
312
|
+
const widthPct = clampedPct * 100;
|
|
313
|
+
const displayValue = `${Math.round(clampedPct * 100)}%`;
|
|
314
|
+
const titleHtml = title ? `<h3 class="chart-title">${escapeHtml(title)}</h3>` : '';
|
|
315
|
+
return `
|
|
316
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
317
|
+
${titleHtml}
|
|
318
|
+
<div class="pct-bar-wrapper">
|
|
319
|
+
<div class="pct-bar-container">
|
|
320
|
+
<div class="pct-bar-fill" style="width: ${widthPct}%;"></div>
|
|
321
|
+
</div>
|
|
322
|
+
<span class="pct-bar-value">${displayValue}</span>
|
|
323
|
+
</div>
|
|
324
|
+
</div>`;
|
|
325
|
+
}
|
|
326
|
+
function renderTable(spec, colSpan, anchorId, baseTheme) {
|
|
327
|
+
const data = spec.data ?? [];
|
|
328
|
+
const title = spec.title ?? '';
|
|
329
|
+
const striped = spec.striped === true;
|
|
330
|
+
const compact = spec.compact === true;
|
|
331
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
332
|
+
// Auto-detect columns from data if not specified
|
|
333
|
+
let columns = spec.columns;
|
|
334
|
+
if (!columns && data.length > 0) {
|
|
335
|
+
const firstRow = data[0];
|
|
336
|
+
if (firstRow) {
|
|
337
|
+
columns = Object.keys(firstRow).map((key) => ({ id: key, title: key }));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!columns)
|
|
341
|
+
columns = [];
|
|
342
|
+
const cellPadding = compact ? '3px 6px' : '5px 8px';
|
|
343
|
+
const headerPadding = compact ? '3px 6px' : '6px 8px';
|
|
344
|
+
const fontSize = compact ? '10px' : '11px';
|
|
345
|
+
const compactClass = compact ? ' table-compact' : '';
|
|
346
|
+
const stripedClass = striped ? ' table-striped' : '';
|
|
347
|
+
// Pre-compute ranges for heatmap and dumbbell
|
|
348
|
+
const defaultHeatmapColors = getHeatmapColors(baseTheme);
|
|
349
|
+
const heatmapRanges = computeHeatmapRanges(columns, data);
|
|
350
|
+
const dumbbellRanges = computeDumbbellRanges(columns, data);
|
|
351
|
+
const themeColors = getThemeColors(baseTheme);
|
|
352
|
+
// Build header
|
|
353
|
+
const headerCells = columns.map((col) => {
|
|
354
|
+
const align = col.align ?? 'left';
|
|
355
|
+
return `<th style="text-align: ${align}; padding: ${headerPadding}; font-size: ${fontSize};">${escapeHtml(col.title ?? col.id)}</th>`;
|
|
356
|
+
}).join('');
|
|
357
|
+
// Build rows
|
|
358
|
+
const bodyRows = data.map((row, _rowIdx) => {
|
|
359
|
+
const cells = columns.map((col) => {
|
|
360
|
+
const cellValue = row[col.id];
|
|
361
|
+
const align = col.align ?? 'left';
|
|
362
|
+
const result = formatCell(cellValue, col, themeColors, heatmapRanges, dumbbellRanges, defaultHeatmapColors);
|
|
363
|
+
const bgStyle = result.bgColor ? `background-color: ${result.bgColor};` : '';
|
|
364
|
+
const textStyle = result.textColor ? `color: ${result.textColor};` : '';
|
|
365
|
+
return `<td style="text-align: ${align}; padding: ${cellPadding}; font-size: ${fontSize}; ${bgStyle} ${textStyle}">${result.content}</td>`;
|
|
366
|
+
}).join('');
|
|
367
|
+
return `<tr>${cells}</tr>`;
|
|
368
|
+
}).join('');
|
|
369
|
+
const titleHtml = title ? `<h3 class="chart-title">${escapeHtml(title)}</h3>` : '';
|
|
370
|
+
return `
|
|
371
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
372
|
+
${titleHtml}
|
|
373
|
+
<table class="data-table${compactClass}${stripedClass}" style="width: 100%; border-collapse: collapse; font-size: ${fontSize};">
|
|
374
|
+
<thead>
|
|
375
|
+
<tr>${headerCells}</tr>
|
|
376
|
+
</thead>
|
|
377
|
+
<tbody>
|
|
378
|
+
${bodyRows}
|
|
379
|
+
</tbody>
|
|
380
|
+
</table>
|
|
381
|
+
</div>`;
|
|
382
|
+
}
|
|
383
|
+
// Helper function to parse inline markdown
|
|
384
|
+
function parseInlineMarkdown(text) {
|
|
385
|
+
// Bold: **text** or __text__
|
|
386
|
+
let result = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
387
|
+
result = result.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
388
|
+
// Italic: *text* or _text_
|
|
389
|
+
result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
390
|
+
result = result.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
391
|
+
// Code: `text`
|
|
392
|
+
result = result.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
// Simple hash function for generating unique IDs
|
|
396
|
+
function hashString(str) {
|
|
397
|
+
let hash = 0;
|
|
398
|
+
for (let i = 0; i < str.length; i++) {
|
|
399
|
+
const char = str.charCodeAt(i);
|
|
400
|
+
hash = ((hash << 5) - hash) + char;
|
|
401
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
402
|
+
}
|
|
403
|
+
return hash;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Parse markdown table syntax into a table spec
|
|
407
|
+
*/
|
|
408
|
+
function parseMarkdownTable(tableLines, theme) {
|
|
409
|
+
if (tableLines.length < 2) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
// Parse header row
|
|
413
|
+
let headerLine = tableLines[0].trim();
|
|
414
|
+
if (!headerLine.startsWith('|') || !headerLine.endsWith('|')) {
|
|
415
|
+
headerLine = '|' + headerLine + '|';
|
|
416
|
+
}
|
|
417
|
+
const headers = headerLine.split('|').slice(1, -1).map(h => h.trim());
|
|
418
|
+
if (headers.length === 0) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
// Parse separator row and extract alignments
|
|
422
|
+
let alignments = [];
|
|
423
|
+
if (tableLines.length > 1) {
|
|
424
|
+
const sepLine = tableLines[1].trim();
|
|
425
|
+
if (!sepLine.includes('-')) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const sepParts = sepLine.split('|').slice(1, -1).map(s => s.trim());
|
|
429
|
+
alignments = sepParts.map(part => {
|
|
430
|
+
if (part.startsWith(':') && part.endsWith(':'))
|
|
431
|
+
return 'center';
|
|
432
|
+
if (part.endsWith(':'))
|
|
433
|
+
return 'right';
|
|
434
|
+
return 'left';
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// Pad alignments to match headers
|
|
438
|
+
while (alignments.length < headers.length) {
|
|
439
|
+
alignments.push('left');
|
|
440
|
+
}
|
|
441
|
+
// Parse data rows
|
|
442
|
+
const data = [];
|
|
443
|
+
for (let i = 2; i < tableLines.length; i++) {
|
|
444
|
+
let line = tableLines[i].trim();
|
|
445
|
+
if (!line || line.startsWith('|---') || /^[|\-:\s]+$/.test(line)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (!line.startsWith('|')) {
|
|
449
|
+
line = '|' + line + '|';
|
|
450
|
+
}
|
|
451
|
+
const cells = line.split('|').slice(1, -1).map(c => c.trim());
|
|
452
|
+
if (cells.length > 0) {
|
|
453
|
+
const row = {};
|
|
454
|
+
for (let j = 0; j < headers.length; j++) {
|
|
455
|
+
const header = headers[j];
|
|
456
|
+
let value = cells[j] ?? '';
|
|
457
|
+
// Try to convert to number
|
|
458
|
+
if (value !== '') {
|
|
459
|
+
if (value.includes('.')) {
|
|
460
|
+
const num = parseFloat(value);
|
|
461
|
+
if (!isNaN(num))
|
|
462
|
+
value = num;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
const num = parseInt(value, 10);
|
|
466
|
+
if (!isNaN(num))
|
|
467
|
+
value = num;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
row[header] = value;
|
|
471
|
+
}
|
|
472
|
+
data.push(row);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (data.length === 0) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
// Build columns spec
|
|
479
|
+
const columns = headers.map((header, i) => ({
|
|
480
|
+
id: header,
|
|
481
|
+
title: header,
|
|
482
|
+
align: alignments[i] ?? 'left',
|
|
483
|
+
}));
|
|
484
|
+
return {
|
|
485
|
+
type: 'table',
|
|
486
|
+
spec: { data, columns, theme },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Parse markdown layout and return HTML
|
|
491
|
+
*/
|
|
492
|
+
export function parseMarkdownToDashboard(markdown, baseTheme = 'light', _baseDir, strict = false, testMode = false) {
|
|
493
|
+
const lines = markdown.trim().split('\n');
|
|
494
|
+
const { theme, pageTitle, continuous, remainingLines } = parseFrontmatter(lines, baseTheme);
|
|
495
|
+
const sections = [];
|
|
496
|
+
let currentSection = { title: '', rows: [] };
|
|
497
|
+
let currentRow = [];
|
|
498
|
+
let inCodeBlock = false;
|
|
499
|
+
let codeType = '';
|
|
500
|
+
let codeContent = [];
|
|
501
|
+
let codeSizeParsed = null;
|
|
502
|
+
let paragraphLines = [];
|
|
503
|
+
let inMarkdownTable = false;
|
|
504
|
+
let markdownTableLines = [];
|
|
505
|
+
const flushParagraph = () => {
|
|
506
|
+
if (paragraphLines.length > 0) {
|
|
507
|
+
const text = paragraphLines.join(' ').trim();
|
|
508
|
+
if (text) {
|
|
509
|
+
currentRow.push({ type: 'text', spec: { content: text, theme } });
|
|
510
|
+
}
|
|
511
|
+
paragraphLines = [];
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const flushMarkdownTable = () => {
|
|
515
|
+
if (markdownTableLines.length > 0) {
|
|
516
|
+
const tableItem = parseMarkdownTable(markdownTableLines, theme);
|
|
517
|
+
if (tableItem) {
|
|
518
|
+
currentRow.push(tableItem);
|
|
519
|
+
}
|
|
520
|
+
markdownTableLines = [];
|
|
521
|
+
}
|
|
522
|
+
inMarkdownTable = false;
|
|
523
|
+
};
|
|
524
|
+
const flushRow = () => {
|
|
525
|
+
if (currentRow.length > 0) {
|
|
526
|
+
currentSection.rows.push(currentRow);
|
|
527
|
+
currentRow = [];
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
let codeFile = null;
|
|
531
|
+
for (const line of remainingLines) {
|
|
532
|
+
// Handle code blocks
|
|
533
|
+
if (line.startsWith('```')) {
|
|
534
|
+
if (!inCodeBlock) {
|
|
535
|
+
flushParagraph();
|
|
536
|
+
let codeHeader = line.slice(3).trim();
|
|
537
|
+
// Parse size=auto or size=[cols,rows]
|
|
538
|
+
let codeSize = null;
|
|
539
|
+
if (/size\s*=\s*auto\b/.test(codeHeader)) {
|
|
540
|
+
codeSize = 'auto';
|
|
541
|
+
codeHeader = codeHeader.replace(/\s*size\s*=\s*auto\b/, '');
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
const sizeMatch = codeHeader.match(/size\s*=\s*\[\s*(\d+)\s*,\s*(\d+)\s*\]/);
|
|
545
|
+
if (sizeMatch) {
|
|
546
|
+
codeSize = [parseInt(sizeMatch[1], 10), parseInt(sizeMatch[2], 10)];
|
|
547
|
+
codeHeader = codeHeader.replace(/\s*size\s*=\s*\[\s*\d+\s*,\s*\d+\s*\]/, '');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Parse file= parameter
|
|
551
|
+
codeFile = null;
|
|
552
|
+
if (codeHeader.includes(' file=')) {
|
|
553
|
+
const parts = codeHeader.split(' file=');
|
|
554
|
+
codeHeader = parts[0].trim();
|
|
555
|
+
codeFile = parts[1]?.trim() ?? null;
|
|
556
|
+
}
|
|
557
|
+
// Parse type (first word after removing size= and file= parameters)
|
|
558
|
+
codeType = codeHeader.split(' ')[0] ?? '';
|
|
559
|
+
codeSizeParsed = codeSize;
|
|
560
|
+
codeContent = [];
|
|
561
|
+
inCodeBlock = true;
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
inCodeBlock = false;
|
|
565
|
+
if (codeType) {
|
|
566
|
+
try {
|
|
567
|
+
let spec = {};
|
|
568
|
+
let fileData = null;
|
|
569
|
+
// Load file data if specified
|
|
570
|
+
if (codeFile && _baseDir) {
|
|
571
|
+
const filePath = resolve(_baseDir, codeFile);
|
|
572
|
+
if (existsSync(filePath)) {
|
|
573
|
+
if (codeFile.endsWith('.csv')) {
|
|
574
|
+
fileData = parseCsv(filePath);
|
|
575
|
+
}
|
|
576
|
+
else if (codeFile.endsWith('.json')) {
|
|
577
|
+
const jsonContent = readFileSync(filePath, 'utf-8');
|
|
578
|
+
const parsed = JSON.parse(jsonContent);
|
|
579
|
+
if (Array.isArray(parsed)) {
|
|
580
|
+
fileData = parsed;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
spec = convertColumnarFormat(parsed);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else if (strict) {
|
|
588
|
+
throw new Error(`File not found: ${filePath}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Parse inline content as options or full spec
|
|
592
|
+
if (codeContent.length > 0) {
|
|
593
|
+
const inlineSpec = JSON.parse(codeContent.join('\n'));
|
|
594
|
+
if (fileData !== null) {
|
|
595
|
+
// File provides data, inline provides options
|
|
596
|
+
spec = { ...inlineSpec, data: fileData };
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
spec = inlineSpec;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else if (fileData !== null) {
|
|
603
|
+
// CSV with no inline options - auto-detect x/y from columns
|
|
604
|
+
const columns = fileData.length > 0 ? Object.keys(fileData[0]) : [];
|
|
605
|
+
spec.data = fileData;
|
|
606
|
+
if (columns.length >= 2) {
|
|
607
|
+
spec.x = columns[0];
|
|
608
|
+
spec.y = columns[1];
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Only add item if we have data or a valid spec
|
|
612
|
+
if (spec.data || Object.keys(spec).length > 0) {
|
|
613
|
+
spec = convertColumnarFormat(spec);
|
|
614
|
+
spec.theme = (spec.theme ?? theme);
|
|
615
|
+
const item = { type: codeType, spec };
|
|
616
|
+
if (codeSizeParsed) {
|
|
617
|
+
item.size = codeSizeParsed;
|
|
618
|
+
}
|
|
619
|
+
currentRow.push(item);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch (e) {
|
|
623
|
+
if (strict)
|
|
624
|
+
throw new Error(`Error in ${codeType} block: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
codeType = '';
|
|
628
|
+
codeContent = [];
|
|
629
|
+
codeSizeParsed = null;
|
|
630
|
+
codeFile = null;
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (inCodeBlock) {
|
|
635
|
+
codeContent.push(line);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
// Handle headings
|
|
639
|
+
// H1: Major section with page break (unless continuous mode)
|
|
640
|
+
if (line.startsWith('# ') && !line.startsWith('## ') && !line.startsWith('### ')) {
|
|
641
|
+
flushParagraph();
|
|
642
|
+
flushRow();
|
|
643
|
+
if (currentSection.rows.length > 0 || currentSection.title) {
|
|
644
|
+
sections.push(currentSection);
|
|
645
|
+
}
|
|
646
|
+
const sectionTitle = line.slice(2).trim();
|
|
647
|
+
// H1 creates a page break (will be suppressed if continuous mode is enabled)
|
|
648
|
+
currentSection = { title: sectionTitle, rows: [], pageBreak: !continuous };
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
// H2: Section title without page break
|
|
652
|
+
if (line.startsWith('## ') && !line.startsWith('### ')) {
|
|
653
|
+
flushParagraph();
|
|
654
|
+
flushRow();
|
|
655
|
+
if (currentSection.rows.length > 0 || currentSection.title) {
|
|
656
|
+
sections.push(currentSection);
|
|
657
|
+
}
|
|
658
|
+
currentSection = { title: line.slice(3).trim(), rows: [] };
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
// H3: Light inline header (subtle, smaller text within current section)
|
|
662
|
+
if (line.startsWith('### ')) {
|
|
663
|
+
flushParagraph();
|
|
664
|
+
if (inMarkdownTable) {
|
|
665
|
+
flushMarkdownTable();
|
|
666
|
+
}
|
|
667
|
+
flushRow();
|
|
668
|
+
const headerText = line.slice(4).trim();
|
|
669
|
+
// Add as an inline header item in the current row
|
|
670
|
+
currentRow.push({
|
|
671
|
+
type: 'inline_header',
|
|
672
|
+
spec: { text: headerText, theme },
|
|
673
|
+
});
|
|
674
|
+
flushRow();
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
// Handle page breaks (=== forces new page when printing)
|
|
678
|
+
if (line.trim() === '===') {
|
|
679
|
+
flushParagraph();
|
|
680
|
+
if (inMarkdownTable) {
|
|
681
|
+
flushMarkdownTable();
|
|
682
|
+
}
|
|
683
|
+
flushRow();
|
|
684
|
+
// Save current section and start a new one with page break flag
|
|
685
|
+
if (currentSection.rows.length > 0 || currentSection.title) {
|
|
686
|
+
sections.push(currentSection);
|
|
687
|
+
}
|
|
688
|
+
currentSection = { title: '', rows: [], pageBreak: true };
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
// Handle horizontal rules (section break - visual divider only)
|
|
692
|
+
if (['---', '***', '___'].includes(line.trim())) {
|
|
693
|
+
flushParagraph();
|
|
694
|
+
if (inMarkdownTable) {
|
|
695
|
+
flushMarkdownTable();
|
|
696
|
+
}
|
|
697
|
+
flushRow();
|
|
698
|
+
// Save current section and start a new one (without title)
|
|
699
|
+
if (currentSection.rows.length > 0 || currentSection.title) {
|
|
700
|
+
sections.push(currentSection);
|
|
701
|
+
}
|
|
702
|
+
currentSection = { title: '', rows: [] };
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
// Empty line = new row
|
|
706
|
+
if (!line.trim()) {
|
|
707
|
+
flushParagraph();
|
|
708
|
+
if (inMarkdownTable) {
|
|
709
|
+
flushMarkdownTable();
|
|
710
|
+
}
|
|
711
|
+
if (currentRow.length > 0) {
|
|
712
|
+
flushRow();
|
|
713
|
+
}
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
// Handle markdown tables (lines starting with |)
|
|
717
|
+
if (line.trim().startsWith('|')) {
|
|
718
|
+
flushParagraph();
|
|
719
|
+
inMarkdownTable = true;
|
|
720
|
+
markdownTableLines.push(line);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
// End markdown table on non-table line
|
|
724
|
+
if (inMarkdownTable) {
|
|
725
|
+
flushMarkdownTable();
|
|
726
|
+
}
|
|
727
|
+
// Regular text
|
|
728
|
+
paragraphLines.push(line);
|
|
729
|
+
}
|
|
730
|
+
// Flush remaining content
|
|
731
|
+
flushParagraph();
|
|
732
|
+
if (inMarkdownTable) {
|
|
733
|
+
flushMarkdownTable();
|
|
734
|
+
}
|
|
735
|
+
flushRow();
|
|
736
|
+
if (currentSection.rows.length > 0 || currentSection.title) {
|
|
737
|
+
sections.push(currentSection);
|
|
738
|
+
}
|
|
739
|
+
// Generate HTML
|
|
740
|
+
const componentsHtml = [];
|
|
741
|
+
const chartScripts = [];
|
|
742
|
+
let chartCounter = 0;
|
|
743
|
+
const seenTypes = new Set();
|
|
744
|
+
const testItems = [];
|
|
745
|
+
for (const section of sections) {
|
|
746
|
+
const sectionHtml = [];
|
|
747
|
+
if (section.title) {
|
|
748
|
+
sectionHtml.push(`<h2 class="section-title">${escapeHtml(section.title.toUpperCase())}</h2>`);
|
|
749
|
+
}
|
|
750
|
+
for (const row of section.rows) {
|
|
751
|
+
resolveAutoSizes(row);
|
|
752
|
+
const rowItems = [];
|
|
753
|
+
for (const item of row) {
|
|
754
|
+
const compType = item.type;
|
|
755
|
+
const spec = item.spec;
|
|
756
|
+
const size = item.size ?? DEFAULT_SIZES[compType] ?? [8, 4];
|
|
757
|
+
const colSpan = size[0];
|
|
758
|
+
const rowSpan = size[1];
|
|
759
|
+
chartCounter++;
|
|
760
|
+
const chartId = `chart_${chartCounter}`;
|
|
761
|
+
const itemHeight = rowSpan * ROW_HEIGHT_PX;
|
|
762
|
+
// Get anchor for navigation (only in test mode, and only for first occurrence)
|
|
763
|
+
const anchorId = testMode && !seenTypes.has(compType) ? compType : '';
|
|
764
|
+
if (anchorId)
|
|
765
|
+
seenTypes.add(compType);
|
|
766
|
+
// Track test items for test harness mode
|
|
767
|
+
if (testMode) {
|
|
768
|
+
const testName = spec.title || spec.label ||
|
|
769
|
+
`${compType.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())} ${chartCounter}`;
|
|
770
|
+
testItems.push({
|
|
771
|
+
id: chartId,
|
|
772
|
+
name: testName,
|
|
773
|
+
type: compType,
|
|
774
|
+
spec,
|
|
775
|
+
section: section.title || compType,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
// Check for pct_bar sparkline (renders as HTML, not ECharts)
|
|
779
|
+
const sparkType = spec.sparkType;
|
|
780
|
+
if (compType === 'sparkline' && (sparkType === 'pct' || sparkType === 'pct_bar')) {
|
|
781
|
+
rowItems.push(renderPctBarSparkline(spec, colSpan, anchorId));
|
|
782
|
+
}
|
|
783
|
+
// Check if it's a chart type with options builder
|
|
784
|
+
else {
|
|
785
|
+
const builder = getOptionsBuilder(compType);
|
|
786
|
+
if (builder) {
|
|
787
|
+
const { html, script } = renderEchartsComponent(compType, spec, chartId, colSpan, itemHeight, anchorId);
|
|
788
|
+
if (html && script) {
|
|
789
|
+
rowItems.push(html);
|
|
790
|
+
chartScripts.push(script);
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
rowItems.push(`<div class="grid-item"><p>Error rendering ${compType}</p></div>`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else if (compType === 'big_value') {
|
|
797
|
+
rowItems.push(renderBigValue(spec, colSpan, anchorId));
|
|
798
|
+
}
|
|
799
|
+
else if (compType === 'delta') {
|
|
800
|
+
rowItems.push(renderDelta(spec, colSpan, anchorId));
|
|
801
|
+
}
|
|
802
|
+
else if (compType === 'alert') {
|
|
803
|
+
rowItems.push(renderAlert(spec, colSpan, anchorId));
|
|
804
|
+
}
|
|
805
|
+
else if (compType === 'note') {
|
|
806
|
+
rowItems.push(renderNote(spec, colSpan, anchorId));
|
|
807
|
+
}
|
|
808
|
+
else if (compType === 'text') {
|
|
809
|
+
rowItems.push(renderText(spec, colSpan, anchorId));
|
|
810
|
+
}
|
|
811
|
+
else if (compType === 'textarea') {
|
|
812
|
+
rowItems.push(renderTextarea(spec, colSpan, anchorId));
|
|
813
|
+
}
|
|
814
|
+
else if (compType === 'table') {
|
|
815
|
+
rowItems.push(renderTable(spec, colSpan, anchorId, theme));
|
|
816
|
+
}
|
|
817
|
+
else if (compType === 'empty_space') {
|
|
818
|
+
rowItems.push(renderEmptySpace(spec, colSpan, anchorId));
|
|
819
|
+
}
|
|
820
|
+
else if (compType === 'inline_header') {
|
|
821
|
+
rowItems.push(renderInlineHeader(spec, anchorId));
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
rowItems.push(`<div class="grid-item"><p>Unknown type: ${compType}</p></div>`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (rowItems.length > 0) {
|
|
829
|
+
sectionHtml.push(`
|
|
830
|
+
<div class="row">
|
|
831
|
+
${rowItems.join('')}
|
|
832
|
+
</div>`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (sectionHtml.length > 0) {
|
|
836
|
+
const sectionClasses = section.pageBreak ? 'dashboard-section page-break' : 'dashboard-section';
|
|
837
|
+
componentsHtml.push(`
|
|
838
|
+
<section class="${sectionClasses}">
|
|
839
|
+
${sectionHtml.join('')}
|
|
840
|
+
</section>`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Test mode: use test harness renderer
|
|
844
|
+
if (testMode) {
|
|
845
|
+
// Count unique types for navigation
|
|
846
|
+
const typesCount = new Map();
|
|
847
|
+
for (const item of testItems) {
|
|
848
|
+
typesCount.set(item.type, (typesCount.get(item.type) || 0) + 1);
|
|
849
|
+
}
|
|
850
|
+
// Generate navigation items sorted alphabetically
|
|
851
|
+
const navItems = [...typesCount.keys()]
|
|
852
|
+
.sort()
|
|
853
|
+
.map(t => `<a href="#${t}" class="nav-item">${t.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}</a>`)
|
|
854
|
+
.join('');
|
|
855
|
+
// Generate test specs JSON for export
|
|
856
|
+
const testSpecsObj = {};
|
|
857
|
+
for (const item of testItems) {
|
|
858
|
+
testSpecsObj[item.id] = item.spec;
|
|
859
|
+
}
|
|
860
|
+
const testSpecsJson = JSON.stringify(testSpecsObj);
|
|
861
|
+
// Generate JavaScript
|
|
862
|
+
const testHarnessJs = generateTestHarnessJs(testItems.length, testSpecsJson);
|
|
863
|
+
return generateTestHarnessHtml(pageTitle, navItems, testItems.length, typesCount.size, componentsHtml.join(''), chartScripts.join(''), testHarnessJs, theme);
|
|
864
|
+
}
|
|
865
|
+
return generateDashboardHtml(pageTitle, componentsHtml, chartScripts, theme, '', continuous);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Parse markdown layout into dashboard spec (legacy API for compatibility)
|
|
869
|
+
*/
|
|
870
|
+
export function parseMarkdownLayout(markdown, _baseDir) {
|
|
871
|
+
const lines = markdown.trim().split('\n');
|
|
872
|
+
const { theme, pageTitle, continuous } = parseFrontmatter(lines, 'light');
|
|
873
|
+
return {
|
|
874
|
+
frontmatter: {
|
|
875
|
+
theme,
|
|
876
|
+
title: pageTitle,
|
|
877
|
+
continuous,
|
|
878
|
+
},
|
|
879
|
+
rows: [],
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Parse markdown into test harness HTML with navigation and summary
|
|
884
|
+
*/
|
|
885
|
+
export function parseMarkdownToTestHarness(markdown, baseTheme = 'light', baseDir, strict = false) {
|
|
886
|
+
return parseMarkdownToDashboard(markdown, baseTheme, baseDir, strict, true);
|
|
887
|
+
}
|
|
888
|
+
//# sourceMappingURL=parser.js.map
|