mulmocast 2.2.2 → 2.2.4

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.
@@ -1,4 +1,10 @@
1
- import type { ContentBlock } from "./schema.js";
1
+ import type { ContentBlock, TableCellValue } from "./schema.js";
2
+ export declare const resolveCellColor: (cellObj: {
3
+ color?: string;
4
+ }, isRowHeader: boolean) => string;
5
+ export declare const renderBadge: (text: string, color: string) => string;
6
+ export declare const renderCellValue: (cell: TableCellValue, isRowHeader: boolean) => string;
7
+ export declare const renderTableCore: (headers: string[] | undefined, rows: TableCellValue[][], rowHeaders?: boolean, striped?: boolean) => string;
2
8
  /** Render a single content block to HTML */
3
9
  export declare const renderContentBlock: (block: ContentBlock) => string;
4
10
  /** Render an array of content blocks to HTML */
@@ -1,4 +1,55 @@
1
- import { escapeHtml, nl2br, c, generateSlideId } from "./utils.js";
1
+ import { escapeHtml, c, generateSlideId, renderInlineMarkup } from "./utils.js";
2
+ // ─── Table cell rendering (shared with layouts/table.ts) ───
3
+ export const resolveCellColor = (cellObj, isRowHeader) => {
4
+ if (cellObj.color)
5
+ return `text-${c(cellObj.color)}`;
6
+ if (isRowHeader)
7
+ return "text-d-text";
8
+ return "text-d-muted";
9
+ };
10
+ export const renderBadge = (text, color) => {
11
+ return `<span class="px-2 py-0.5 rounded-full text-xs font-bold text-white bg-${c(color)}">${renderInlineMarkup(text)}</span>`;
12
+ };
13
+ export const renderCellValue = (cell, isRowHeader) => {
14
+ const cellObj = typeof cell === "object" && cell !== null ? cell : { text: String(cell) };
15
+ if (cellObj.badge && cellObj.color) {
16
+ return `<td class="px-4 py-3 text-sm font-body border-b border-d-alt">${renderBadge(cellObj.text, cellObj.color)}</td>`;
17
+ }
18
+ const colorCls = resolveCellColor(cellObj, isRowHeader);
19
+ const boldCls = cellObj.bold || isRowHeader ? "font-bold" : "";
20
+ return `<td class="px-4 py-3 text-sm ${colorCls} ${boldCls} font-body border-b border-d-alt">${renderInlineMarkup(cellObj.text)}</td>`;
21
+ };
22
+ export const renderTableCore = (headers, rows, rowHeaders, striped) => {
23
+ const parts = [];
24
+ const isStriped = striped !== false;
25
+ parts.push(`<table class="w-full border-collapse">`);
26
+ if (headers && headers.length > 0) {
27
+ parts.push(`<thead>`);
28
+ parts.push(`<tr>`);
29
+ headers.forEach((h) => {
30
+ parts.push(` <th class="text-left px-4 py-3 text-sm font-bold text-d-text font-body border-b-2 border-d-alt">${renderInlineMarkup(h)}</th>`);
31
+ });
32
+ parts.push(`</tr>`);
33
+ parts.push(`</thead>`);
34
+ }
35
+ parts.push(`<tbody>`);
36
+ rows.forEach((row, ri) => {
37
+ const bgCls = isStriped && ri % 2 === 1 ? "bg-d-alt/30" : "";
38
+ parts.push(`<tr class="${bgCls}">`);
39
+ (row || []).forEach((cell, ci) => {
40
+ const isRowHeader = ci === 0 && !!rowHeaders;
41
+ parts.push(` ${renderCellValue(cell, isRowHeader)}`);
42
+ });
43
+ parts.push(`</tr>`);
44
+ });
45
+ parts.push(`</tbody>`);
46
+ parts.push(`</table>`);
47
+ return parts.join("\n");
48
+ };
49
+ const renderTableBlock = (block) => {
50
+ const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${renderInlineMarkup(block.title)}</p>` : "";
51
+ return `<div class="overflow-auto">${titleHtml}${renderTableCore(block.headers, block.rows, block.rowHeaders, block.striped)}</div>`;
52
+ };
2
53
  /** Render a single content block to HTML */
3
54
  export const renderContentBlock = (block) => {
4
55
  switch (block.type) {
@@ -22,6 +73,10 @@ export const renderContentBlock = (block) => {
22
73
  return renderChart(block);
23
74
  case "mermaid":
24
75
  return renderMermaid(block);
76
+ case "section":
77
+ return renderSection(block);
78
+ case "table":
79
+ return renderTableBlock(block);
25
80
  default:
26
81
  return `<p class="text-sm text-d-muted font-body">[unknown block type]</p>`;
27
82
  }
@@ -60,14 +115,31 @@ const renderText = (block) => {
60
115
  const bold = block.bold ? "font-bold" : "";
61
116
  const size = block.fontSize !== undefined && block.fontSize >= 18 ? "text-xl" : "text-[15px]";
62
117
  const alignCls = resolveAlign(block.align);
63
- return `<p class="${size} ${color} ${bold} ${alignCls} font-body leading-relaxed">${nl2br(block.value)}</p>`;
118
+ return `<p class="${size} ${color} ${bold} ${alignCls} font-body leading-relaxed">${renderInlineMarkup(block.value)}</p>`;
119
+ };
120
+ /** Extract text from a bullet item (string or object) */
121
+ const bulletItemText = (item) => {
122
+ return typeof item === "string" ? item : item.text;
123
+ };
124
+ /** Render sub-bullets for a nested bullet item */
125
+ const renderSubBullets = (item) => {
126
+ if (typeof item === "string" || !item.items || item.items.length === 0)
127
+ return "";
128
+ const subs = item.items
129
+ .map((sub) => {
130
+ return ` <li class="flex gap-2 ml-6 text-[14px]"><span class="text-d-dim shrink-0">\u25E6</span><span>${renderInlineMarkup(bulletItemText(sub))}</span></li>`;
131
+ })
132
+ .join("\n");
133
+ return `\n${subs}`;
64
134
  };
65
135
  const renderBullets = (block) => {
66
136
  const tag = block.ordered ? "ol" : "ul";
67
137
  const items = block.items
68
138
  .map((item, i) => {
69
139
  const marker = block.ordered ? `${i + 1}.` : escapeHtml(block.icon || "\u2022");
70
- return ` <li class="flex gap-2"><span class="text-d-dim shrink-0">${marker}</span><span>${escapeHtml(item)}</span></li>`;
140
+ const text = bulletItemText(item);
141
+ const subHtml = renderSubBullets(item);
142
+ return ` <li class="flex flex-col gap-1"><div class="flex gap-2"><span class="text-d-dim shrink-0">${marker}</span><span>${renderInlineMarkup(text)}</span></div>${subHtml}</li>`;
71
143
  })
72
144
  .join("\n");
73
145
  return `<${tag} class="space-y-2 text-[15px] text-d-muted font-body">\n${items}\n</${tag}>`;
@@ -88,15 +160,15 @@ const renderCallout = (block) => {
88
160
  const bg = isQuote ? "bg-d-alt" : "bg-d-card";
89
161
  const textCls = isQuote ? "italic text-d-muted" : "text-d-muted";
90
162
  const content = block.label
91
- ? `<span class="font-bold text-${c(block.color || "warning")}">${escapeHtml(block.label)}:</span> <span class="text-d-muted">${escapeHtml(block.text)}</span>`
92
- : `<span class="${textCls}">${nl2br(block.text)}</span>`;
163
+ ? `<span class="font-bold text-${c(block.color || "warning")}">${renderInlineMarkup(block.label)}:</span> <span class="text-d-muted">${renderInlineMarkup(block.text)}</span>`
164
+ : `<span class="${textCls}">${renderInlineMarkup(block.text)}</span>`;
93
165
  return `<div class="${bg} ${borderCls} p-3 rounded text-sm font-body">${content}</div>`;
94
166
  };
95
167
  const renderMetric = (block) => {
96
168
  const lines = [];
97
169
  lines.push(`<div class="text-center">`);
98
- lines.push(` <p class="text-4xl font-bold text-${c(block.color || "primary")}">${escapeHtml(block.value)}</p>`);
99
- lines.push(` <p class="text-sm text-d-dim mt-1">${escapeHtml(block.label)}</p>`);
170
+ lines.push(` <p class="text-4xl font-bold text-${c(block.color || "primary")}">${renderInlineMarkup(block.value)}</p>`);
171
+ lines.push(` <p class="text-sm text-d-dim mt-1">${renderInlineMarkup(block.label)}</p>`);
100
172
  if (block.change) {
101
173
  const changeColor = block.change.startsWith("+") ? "success" : "danger";
102
174
  lines.push(` <p class="text-sm font-bold text-${c(changeColor)} mt-1">${escapeHtml(block.change)}</p>`);
@@ -119,7 +191,7 @@ const renderImageRefPlaceholder = (block) => {
119
191
  const renderChart = (block) => {
120
192
  const chartId = generateSlideId("chart");
121
193
  const chartData = JSON.stringify(block.chartData);
122
- const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${escapeHtml(block.title)}</p>` : "";
194
+ const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${renderInlineMarkup(block.title)}</p>` : "";
123
195
  return `<div class="flex-1 min-h-0 flex flex-col">
124
196
  ${titleHtml}
125
197
  <div class="flex-1 min-h-0 relative">
@@ -139,7 +211,7 @@ const renderChart = (block) => {
139
211
  };
140
212
  const renderMermaid = (block) => {
141
213
  const mermaidId = generateSlideId("mermaid");
142
- const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${escapeHtml(block.title)}</p>` : "";
214
+ const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${renderInlineMarkup(block.title)}</p>` : "";
143
215
  return `<div class="flex-1 min-h-0 flex flex-col">
144
216
  ${titleHtml}
145
217
  <div class="flex-1 min-h-0 flex justify-center items-center">
@@ -147,3 +219,40 @@ const renderMermaid = (block) => {
147
219
  </div>
148
220
  </div>`;
149
221
  };
222
+ const renderSectionSidebar = (block) => {
223
+ const color = block.color || "primary";
224
+ const chars = block.label
225
+ .split("")
226
+ .map((ch) => escapeHtml(ch))
227
+ .join("<br>");
228
+ const sidebar = `<div class="w-[48px] shrink-0 rounded-l bg-${c(color)} flex items-center justify-center"><span class="text-sm font-bold text-white font-body leading-snug text-center">${chars}</span></div>`;
229
+ const contentParts = [];
230
+ if (block.text) {
231
+ contentParts.push(`<p class="text-[15px] text-d-muted font-body">${renderInlineMarkup(block.text)}</p>`);
232
+ }
233
+ if (block.content) {
234
+ contentParts.push(block.content.map(renderContentBlock).join("\n"));
235
+ }
236
+ return `<div class="flex rounded overflow-hidden bg-d-card">
237
+ ${sidebar}
238
+ <div class="flex-1 space-y-2 p-3">${contentParts.join("\n")}</div>
239
+ </div>`;
240
+ };
241
+ const renderSectionDefault = (block) => {
242
+ const color = block.color || "primary";
243
+ const badge = `<span class="min-w-[80px] px-3 py-1 rounded text-sm font-bold text-white bg-${c(color)} shrink-0">${renderInlineMarkup(block.label)}</span>`;
244
+ const contentParts = [];
245
+ if (block.text) {
246
+ contentParts.push(`<p class="text-[15px] text-d-muted font-body">${renderInlineMarkup(block.text)}</p>`);
247
+ }
248
+ if (block.content) {
249
+ contentParts.push(block.content.map(renderContentBlock).join("\n"));
250
+ }
251
+ return `<div class="flex gap-4 items-start">
252
+ ${badge}
253
+ <div class="flex-1 space-y-2">${contentParts.join("\n")}</div>
254
+ </div>`;
255
+ };
256
+ const renderSection = (block) => {
257
+ return block.sidebar ? renderSectionSidebar(block) : renderSectionDefault(block);
258
+ };
@@ -1,18 +1,18 @@
1
- import { escapeHtml, nl2br, c } from "../utils.js";
1
+ import { renderInlineMarkup, c } from "../utils.js";
2
2
  export const layoutBigQuote = (data) => {
3
3
  const accent = data.accentColor || "primary";
4
4
  const parts = [];
5
5
  parts.push(`<div class="flex flex-col items-center justify-center h-full px-20">`);
6
6
  parts.push(` <div class="h-[3px] w-24 bg-${c(accent)} mb-8"></div>`);
7
7
  parts.push(` <blockquote class="text-[32px] text-d-text font-title italic text-center leading-relaxed">`);
8
- parts.push(` &ldquo;${nl2br(data.quote)}&rdquo;`);
8
+ parts.push(` &ldquo;${renderInlineMarkup(data.quote)}&rdquo;`);
9
9
  parts.push(` </blockquote>`);
10
10
  parts.push(` <div class="h-[3px] w-24 bg-${c(accent)} mt-8 mb-6"></div>`);
11
11
  if (data.author) {
12
- parts.push(` <p class="text-lg text-d-muted font-body">${escapeHtml(data.author)}</p>`);
12
+ parts.push(` <p class="text-lg text-d-muted font-body">${renderInlineMarkup(data.author)}</p>`);
13
13
  }
14
14
  if (data.role) {
15
- parts.push(` <p class="text-sm text-d-dim font-body mt-1">${escapeHtml(data.role)}</p>`);
15
+ parts.push(` <p class="text-sm text-d-dim font-body mt-1">${renderInlineMarkup(data.role)}</p>`);
16
16
  }
17
17
  parts.push(`</div>`);
18
18
  return parts.join("\n");
@@ -1,4 +1,4 @@
1
- import { escapeHtml, c, cardWrap, numBadge, iconSquare, slideHeader, renderCalloutBar } from "../utils.js";
1
+ import { renderInlineMarkup, c, cardWrap, numBadge, iconSquare, slideHeader, renderCalloutBar } from "../utils.js";
2
2
  import { renderCardContentBlocks } from "../blocks.js";
3
3
  const buildColumnCard = (col) => {
4
4
  const accent = col.accentColor || "primary";
@@ -7,19 +7,19 @@ const buildColumnCard = (col) => {
7
7
  inner.push(`<div class="flex flex-col items-center mb-3">`);
8
8
  inner.push(` ${iconSquare(col.icon, accent)}`);
9
9
  inner.push(`</div>`);
10
- inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${escapeHtml(col.title)}</h3>`);
10
+ inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${renderInlineMarkup(col.title)}</h3>`);
11
11
  }
12
12
  else if (col.num != null) {
13
13
  inner.push(`<div class="flex items-center gap-3 mb-1">`);
14
14
  inner.push(` ${numBadge(col.num, accent)}`);
15
- inner.push(` <h3 class="text-lg font-bold text-d-text font-body">${escapeHtml(col.title)}</h3>`);
15
+ inner.push(` <h3 class="text-lg font-bold text-d-text font-body">${renderInlineMarkup(col.title)}</h3>`);
16
16
  inner.push(`</div>`);
17
17
  }
18
18
  else {
19
19
  if (col.label) {
20
- inner.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${escapeHtml(col.label)}</p>`);
20
+ inner.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(col.label)}</p>`);
21
21
  }
22
- inner.push(`<h3 class="text-2xl font-title font-bold text-d-text mt-1">${escapeHtml(col.title)}</h3>`);
22
+ inner.push(`<h3 class="text-2xl font-title font-bold text-d-text mt-1">${renderInlineMarkup(col.title)}</h3>`);
23
23
  }
24
24
  if (col.content) {
25
25
  const centerCls = col.icon ? "text-center" : "";
@@ -28,7 +28,7 @@ const buildColumnCard = (col) => {
28
28
  inner.push(`</div>`);
29
29
  }
30
30
  if (col.footer) {
31
- inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${escapeHtml(col.footer)}</p>`);
31
+ inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${renderInlineMarkup(col.footer)}</p>`);
32
32
  }
33
33
  return cardWrap(accent, inner.join("\n"), "flex-1");
34
34
  };
@@ -49,7 +49,7 @@ export const layoutColumns = (data) => {
49
49
  parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
50
50
  }
51
51
  if (data.bottomText) {
52
- parts.push(`<p class="text-center text-sm text-d-dim font-body pb-4">${escapeHtml(data.bottomText)}</p>`);
52
+ parts.push(`<p class="text-center text-sm text-d-dim font-body pb-4">${renderInlineMarkup(data.bottomText)}</p>`);
53
53
  }
54
54
  return parts.join("\n");
55
55
  };
@@ -1,16 +1,16 @@
1
- import { escapeHtml, c, cardWrap, slideHeader, renderCalloutBar } from "../utils.js";
1
+ import { renderInlineMarkup, c, cardWrap, slideHeader, renderCalloutBar } from "../utils.js";
2
2
  import { renderContentBlocks } from "../blocks.js";
3
3
  const buildPanel = (panel) => {
4
4
  const accent = panel.accentColor || "primary";
5
5
  const inner = [];
6
- inner.push(`<h3 class="text-xl font-bold text-${c(accent)} font-body">${escapeHtml(panel.title)}</h3>`);
6
+ inner.push(`<h3 class="text-xl font-bold text-${c(accent)} font-body">${renderInlineMarkup(panel.title)}</h3>`);
7
7
  if (panel.content) {
8
8
  inner.push(`<div class="mt-5 space-y-4 flex-1 min-h-0 overflow-auto flex flex-col">`);
9
9
  inner.push(renderContentBlocks(panel.content));
10
10
  inner.push(`</div>`);
11
11
  }
12
12
  if (panel.footer) {
13
- inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${escapeHtml(panel.footer)}</p>`);
13
+ inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${renderInlineMarkup(panel.footer)}</p>`);
14
14
  }
15
15
  return cardWrap(accent, inner.join("\n"), "flex-1");
16
16
  };
@@ -1,4 +1,4 @@
1
- import { escapeHtml, c, slideHeader, renderCalloutBar } from "../utils.js";
1
+ import { renderInlineMarkup, c, slideHeader, renderCalloutBar } from "../utils.js";
2
2
  export const layoutFunnel = (data) => {
3
3
  const parts = [slideHeader(data)];
4
4
  const stages = data.stages || [];
@@ -9,13 +9,13 @@ export const layoutFunnel = (data) => {
9
9
  const widthPct = 100 - (i / Math.max(total - 1, 1)) * 55;
10
10
  parts.push(`<div class="bg-${c(color)} rounded-lg flex items-center justify-between px-6 py-4" style="width: ${widthPct}%">`);
11
11
  parts.push(` <div class="flex items-center gap-3">`);
12
- parts.push(` <span class="text-base font-bold text-white font-body">${escapeHtml(stage.label)}</span>`);
12
+ parts.push(` <span class="text-base font-bold text-white font-body">${renderInlineMarkup(stage.label)}</span>`);
13
13
  if (stage.description) {
14
- parts.push(` <span class="text-sm text-white/70 font-body">${escapeHtml(stage.description)}</span>`);
14
+ parts.push(` <span class="text-sm text-white/70 font-body">${renderInlineMarkup(stage.description)}</span>`);
15
15
  }
16
16
  parts.push(` </div>`);
17
17
  if (stage.value) {
18
- parts.push(` <span class="text-lg font-bold text-white font-body">${escapeHtml(stage.value)}</span>`);
18
+ parts.push(` <span class="text-lg font-bold text-white font-body">${renderInlineMarkup(stage.value)}</span>`);
19
19
  }
20
20
  parts.push(`</div>`);
21
21
  });
@@ -1,4 +1,4 @@
1
- import { escapeHtml, nl2br, c, cardWrap, numBadge, iconSquare } from "../utils.js";
1
+ import { renderInlineMarkup, c, cardWrap, numBadge, iconSquare } from "../utils.js";
2
2
  import { renderCardContentBlocks } from "../blocks.js";
3
3
  export const layoutGrid = (data) => {
4
4
  const accent = data.accentColor || "primary";
@@ -6,7 +6,7 @@ export const layoutGrid = (data) => {
6
6
  const parts = [];
7
7
  parts.push(`<div class="h-[3px] bg-${c(accent)} shrink-0"></div>`);
8
8
  parts.push(`<div class="px-12 pt-5 shrink-0">`);
9
- parts.push(` <h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${nl2br(data.title)}</h2>`);
9
+ parts.push(` <h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
10
10
  parts.push(`</div>`);
11
11
  parts.push(`<div class="grid grid-cols-${nCols} gap-4 px-12 mt-5 flex-1 min-h-0 overflow-hidden content-center">`);
12
12
  (data.items || []).forEach((item) => {
@@ -16,19 +16,19 @@ export const layoutGrid = (data) => {
16
16
  inner.push(`<div class="flex flex-col items-center mb-2">`);
17
17
  inner.push(` ${iconSquare(item.icon, itemAccent)}`);
18
18
  inner.push(`</div>`);
19
- inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${escapeHtml(item.title)}</h3>`);
19
+ inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${renderInlineMarkup(item.title)}</h3>`);
20
20
  }
21
21
  else if (item.num != null) {
22
22
  inner.push(`<div class="flex items-center gap-3">`);
23
23
  inner.push(` ${numBadge(item.num, itemAccent)}`);
24
- inner.push(` <h3 class="text-sm font-bold text-d-text font-body">${escapeHtml(item.title)}</h3>`);
24
+ inner.push(` <h3 class="text-sm font-bold text-d-text font-body">${renderInlineMarkup(item.title)}</h3>`);
25
25
  inner.push(`</div>`);
26
26
  }
27
27
  else {
28
- inner.push(`<h3 class="text-lg font-bold text-d-text font-body">${escapeHtml(item.title)}</h3>`);
28
+ inner.push(`<h3 class="text-lg font-bold text-d-text font-body">${renderInlineMarkup(item.title)}</h3>`);
29
29
  }
30
30
  if (item.description) {
31
- inner.push(`<p class="text-sm text-d-muted font-body mt-3">${escapeHtml(item.description)}</p>`);
31
+ inner.push(`<p class="text-sm text-d-muted font-body mt-3">${renderInlineMarkup(item.description)}</p>`);
32
32
  }
33
33
  if (item.content) {
34
34
  inner.push(`<div class="mt-3 space-y-3 flex-1 min-h-0 overflow-hidden flex flex-col">${renderCardContentBlocks(item.content)}</div>`);
@@ -37,7 +37,7 @@ export const layoutGrid = (data) => {
37
37
  });
38
38
  parts.push(`</div>`);
39
39
  if (data.footer) {
40
- parts.push(`<p class="text-xs text-d-dim font-body px-12 pb-3">${escapeHtml(data.footer)}</p>`);
40
+ parts.push(`<p class="text-xs text-d-dim font-body px-12 pb-3">${renderInlineMarkup(data.footer)}</p>`);
41
41
  }
42
42
  return parts.join("\n");
43
43
  };
@@ -1,4 +1,4 @@
1
- import { escapeHtml, c, cardWrap, slideHeader } from "../utils.js";
1
+ import { renderInlineMarkup, c, cardWrap, slideHeader } from "../utils.js";
2
2
  import { renderContentBlocks } from "../blocks.js";
3
3
  export const layoutMatrix = (data) => {
4
4
  const parts = [slideHeader(data)];
@@ -8,11 +8,11 @@ export const layoutMatrix = (data) => {
8
8
  parts.push(`<div class="flex flex-1 px-12 mt-4 gap-2">`);
9
9
  if (data.yAxis) {
10
10
  parts.push(`<div class="flex flex-col justify-between items-center w-6 shrink-0 py-4">`);
11
- parts.push(` <span class="text-xs text-d-dim font-body [writing-mode:vertical-lr] rotate-180">${escapeHtml(data.yAxis.high || "")}</span>`);
11
+ parts.push(` <span class="text-xs text-d-dim font-body [writing-mode:vertical-lr] rotate-180">${renderInlineMarkup(data.yAxis.high || "")}</span>`);
12
12
  if (data.yAxis.label) {
13
- parts.push(` <span class="text-xs font-bold text-d-muted font-body [writing-mode:vertical-lr] rotate-180">${escapeHtml(data.yAxis.label)}</span>`);
13
+ parts.push(` <span class="text-xs font-bold text-d-muted font-body [writing-mode:vertical-lr] rotate-180">${renderInlineMarkup(data.yAxis.label)}</span>`);
14
14
  }
15
- parts.push(` <span class="text-xs text-d-dim font-body [writing-mode:vertical-lr] rotate-180">${escapeHtml(data.yAxis.low || "")}</span>`);
15
+ parts.push(` <span class="text-xs text-d-dim font-body [writing-mode:vertical-lr] rotate-180">${renderInlineMarkup(data.yAxis.low || "")}</span>`);
16
16
  parts.push(`</div>`);
17
17
  }
18
18
  parts.push(`<div class="flex-1 flex flex-col gap-3">`);
@@ -23,11 +23,11 @@ export const layoutMatrix = (data) => {
23
23
  const cell = cells[idx] || { label: "" };
24
24
  const accent = cell.accentColor || "primary";
25
25
  const inner = [];
26
- inner.push(`<h3 class="text-lg font-bold text-${c(accent)} font-body">${escapeHtml(cell.label)}</h3>`);
26
+ inner.push(`<h3 class="text-lg font-bold text-${c(accent)} font-body">${renderInlineMarkup(cell.label)}</h3>`);
27
27
  if (cell.items) {
28
28
  inner.push(`<ul class="mt-2 space-y-1 text-sm text-d-muted font-body">`);
29
29
  cell.items.forEach((item) => {
30
- inner.push(` <li class="flex gap-2"><span class="text-d-dim shrink-0">&bull;</span><span>${escapeHtml(item)}</span></li>`);
30
+ inner.push(` <li class="flex gap-2"><span class="text-d-dim shrink-0">&bull;</span><span>${renderInlineMarkup(item)}</span></li>`);
31
31
  });
32
32
  inner.push(`</ul>`);
33
33
  }
@@ -40,11 +40,11 @@ export const layoutMatrix = (data) => {
40
40
  });
41
41
  if (data.xAxis) {
42
42
  parts.push(`<div class="flex justify-between px-2 mt-1">`);
43
- parts.push(` <span class="text-xs text-d-dim font-body">${escapeHtml(data.xAxis.low || "")}</span>`);
43
+ parts.push(` <span class="text-xs text-d-dim font-body">${renderInlineMarkup(data.xAxis.low || "")}</span>`);
44
44
  if (data.xAxis.label) {
45
- parts.push(` <span class="text-xs font-bold text-d-muted font-body">${escapeHtml(data.xAxis.label)}</span>`);
45
+ parts.push(` <span class="text-xs font-bold text-d-muted font-body">${renderInlineMarkup(data.xAxis.label)}</span>`);
46
46
  }
47
- parts.push(` <span class="text-xs text-d-dim font-body">${escapeHtml(data.xAxis.high || "")}</span>`);
47
+ parts.push(` <span class="text-xs text-d-dim font-body">${renderInlineMarkup(data.xAxis.high || "")}</span>`);
48
48
  parts.push(`</div>`);
49
49
  }
50
50
  parts.push(`</div>`);
@@ -1,18 +1,31 @@
1
- import { escapeHtml, nl2br, c } from "../utils.js";
1
+ import { renderInlineMarkup, c } from "../utils.js";
2
2
  import { renderContentBlocks } from "../blocks.js";
3
+ const resolveValign = (valign) => {
4
+ if (valign === "top")
5
+ return "justify-start";
6
+ if (valign === "bottom")
7
+ return "justify-end";
8
+ return "justify-center";
9
+ };
3
10
  const buildSplitPanel = (panel, fallbackAccent, ratio) => {
4
11
  const accent = panel.accentColor || fallbackAccent;
5
12
  const bg = panel.dark ? "bg-d-card" : "";
13
+ const vCls = resolveValign(panel.valign);
6
14
  const lines = [];
7
- lines.push(`<div class="${bg} flex flex-col justify-center px-10 py-8" style="flex: ${ratio}">`);
15
+ lines.push(`<div class="${bg} flex flex-col ${vCls} px-10 py-8" style="flex: ${ratio}">`);
8
16
  if (panel.label) {
9
- lines.push(` <p class="text-sm font-bold text-${c(accent)} font-body mb-2">${escapeHtml(panel.label)}</p>`);
17
+ if (panel.labelBadge) {
18
+ lines.push(` <span class="inline-block self-start px-6 py-2.5 rounded-lg bg-${c(accent)} text-lg font-bold text-white font-title mb-4">${renderInlineMarkup(panel.label)}</span>`);
19
+ }
20
+ else {
21
+ lines.push(` <p class="text-sm font-bold text-${c(accent)} font-body mb-2">${renderInlineMarkup(panel.label)}</p>`);
22
+ }
10
23
  }
11
24
  if (panel.title) {
12
- lines.push(` <h2 class="text-[36px] leading-tight font-title font-bold text-d-text">${nl2br(panel.title)}</h2>`);
25
+ lines.push(` <h2 class="text-[36px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(panel.title)}</h2>`);
13
26
  }
14
27
  if (panel.subtitle) {
15
- lines.push(` <p class="text-base text-d-dim font-body mt-3">${nl2br(panel.subtitle)}</p>`);
28
+ lines.push(` <p class="text-base text-d-dim font-body mt-3">${renderInlineMarkup(panel.subtitle)}</p>`);
16
29
  }
17
30
  if (panel.content) {
18
31
  lines.push(` <div class="mt-6 space-y-3">${renderContentBlocks(panel.content)}</div>`);
@@ -1,4 +1,4 @@
1
- import { escapeHtml, nl2br, c, renderCalloutBar } from "../utils.js";
1
+ import { renderInlineMarkup, c, renderCalloutBar } from "../utils.js";
2
2
  export const layoutStats = (data) => {
3
3
  const accent = data.accentColor || "primary";
4
4
  const stats = data.stats || [];
@@ -7,11 +7,11 @@ export const layoutStats = (data) => {
7
7
  parts.push(`<div class="flex-1 flex flex-col justify-center px-12 min-h-0">`);
8
8
  // Header inside centering wrapper
9
9
  if (data.stepLabel) {
10
- parts.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${escapeHtml(data.stepLabel)}</p>`);
10
+ parts.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
11
11
  }
12
- parts.push(`<h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${nl2br(data.title)}</h2>`);
12
+ parts.push(`<h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
13
13
  if (data.subtitle) {
14
- parts.push(`<p class="text-[15px] text-d-dim mt-2 font-body">${nl2br(data.subtitle)}</p>`);
14
+ parts.push(`<p class="text-[15px] text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
15
15
  }
16
16
  // Stats cards
17
17
  parts.push(`<div class="flex gap-6 mt-10">`);
@@ -19,11 +19,11 @@ export const layoutStats = (data) => {
19
19
  const color = stat.color || data.accentColor || "primary";
20
20
  parts.push(`<div class="flex-1 bg-d-card rounded-lg shadow-lg p-10 text-center">`);
21
21
  parts.push(` <div class="h-[3px] bg-${c(color)} rounded-full w-12 mx-auto mb-6"></div>`);
22
- parts.push(` <p class="text-[52px] font-bold text-${c(color)} font-body leading-none">${escapeHtml(stat.value)}</p>`);
23
- parts.push(` <p class="text-lg text-d-muted font-body mt-4">${escapeHtml(stat.label)}</p>`);
22
+ parts.push(` <p class="text-[52px] font-bold text-${c(color)} font-body leading-none">${renderInlineMarkup(stat.value)}</p>`);
23
+ parts.push(` <p class="text-lg text-d-muted font-body mt-4">${renderInlineMarkup(stat.label)}</p>`);
24
24
  if (stat.change) {
25
- const changeColor = stat.change.startsWith("+") ? "success" : "danger";
26
- parts.push(` <p class="text-base font-bold text-${c(changeColor)} font-body mt-3">${escapeHtml(stat.change)}</p>`);
25
+ const changeColor = /\+/.test(stat.change) ? "success" : "danger";
26
+ parts.push(` <p class="text-base font-bold text-${c(changeColor)} font-body mt-3">${renderInlineMarkup(stat.change)}</p>`);
27
27
  }
28
28
  parts.push(`</div>`);
29
29
  });
@@ -1,43 +1,9 @@
1
- import { escapeHtml, c, slideHeader, renderCalloutBar } from "../utils.js";
2
- const resolveCellColor = (cellObj, isRowHeader) => {
3
- if (cellObj.color)
4
- return `text-${c(cellObj.color)}`;
5
- if (isRowHeader)
6
- return "text-d-text";
7
- return "text-d-muted";
8
- };
9
- const renderCellValue = (cell, isRowHeader) => {
10
- const cellObj = typeof cell === "object" && cell !== null ? cell : { text: String(cell) };
11
- const colorCls = resolveCellColor(cellObj, isRowHeader);
12
- const boldCls = cellObj.bold || isRowHeader ? "font-bold" : "";
13
- return `<td class="px-4 py-3 text-sm ${colorCls} ${boldCls} font-body border-b border-d-alt">${escapeHtml(cellObj.text)}</td>`;
14
- };
1
+ import { slideHeader, renderCalloutBar } from "../utils.js";
2
+ import { renderTableCore } from "../blocks.js";
15
3
  export const layoutTable = (data) => {
16
4
  const parts = [slideHeader(data)];
17
- const headers = data.headers || [];
18
- const rows = data.rows || [];
19
- const striped = data.striped !== false;
20
5
  parts.push(`<div class="px-12 mt-5 flex-1 overflow-auto">`);
21
- parts.push(`<table class="w-full border-collapse">`);
22
- parts.push(`<thead>`);
23
- parts.push(`<tr>`);
24
- headers.forEach((h) => {
25
- parts.push(` <th class="text-left px-4 py-3 text-sm font-bold text-d-text font-body border-b-2 border-d-alt">${escapeHtml(h)}</th>`);
26
- });
27
- parts.push(`</tr>`);
28
- parts.push(`</thead>`);
29
- parts.push(`<tbody>`);
30
- rows.forEach((row, ri) => {
31
- const bgCls = striped && ri % 2 === 1 ? "bg-d-alt/30" : "";
32
- parts.push(`<tr class="${bgCls}">`);
33
- (row || []).forEach((cell, ci) => {
34
- const isRowHeader = ci === 0 && !!data.rowHeaders;
35
- parts.push(` ${renderCellValue(cell, isRowHeader)}`);
36
- });
37
- parts.push(`</tr>`);
38
- });
39
- parts.push(`</tbody>`);
40
- parts.push(`</table>`);
6
+ parts.push(renderTableCore(data.headers, data.rows, data.rowHeaders, data.striped));
41
7
  parts.push(`</div>`);
42
8
  if (data.callout) {
43
9
  parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
@@ -1,4 +1,4 @@
1
- import { escapeHtml, nl2br, c } from "../utils.js";
1
+ import { renderInlineMarkup, c } from "../utils.js";
2
2
  export const layoutTimeline = (data) => {
3
3
  const accent = data.accentColor || "primary";
4
4
  const parts = [];
@@ -7,11 +7,11 @@ export const layoutTimeline = (data) => {
7
7
  parts.push(`<div class="flex-1 flex flex-col justify-center px-12 min-h-0">`);
8
8
  // Header inside centering wrapper
9
9
  if (data.stepLabel) {
10
- parts.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${escapeHtml(data.stepLabel)}</p>`);
10
+ parts.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
11
11
  }
12
- parts.push(`<h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${nl2br(data.title)}</h2>`);
12
+ parts.push(`<h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
13
13
  if (data.subtitle) {
14
- parts.push(`<p class="text-[15px] text-d-dim mt-2 font-body">${nl2br(data.subtitle)}</p>`);
14
+ parts.push(`<p class="text-[15px] text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
15
15
  }
16
16
  // Timeline items
17
17
  parts.push(`<div class="flex items-start mt-10 relative">`);
@@ -24,10 +24,10 @@ export const layoutTimeline = (data) => {
24
24
  parts.push(` <div class="w-10 h-10 rounded-full ${dotBorder} flex items-center justify-center shadow-lg">`);
25
25
  parts.push(` <div class="w-4 h-4 rounded-full ${dotInner}"></div>`);
26
26
  parts.push(` </div>`);
27
- parts.push(` <p class="text-sm font-bold text-${c(color)} font-body mt-4">${escapeHtml(item.date)}</p>`);
28
- parts.push(` <p class="text-base font-bold text-d-text font-body mt-2">${escapeHtml(item.title)}</p>`);
27
+ parts.push(` <p class="text-sm font-bold text-${c(color)} font-body mt-4">${renderInlineMarkup(item.date)}</p>`);
28
+ parts.push(` <p class="text-base font-bold text-d-text font-body mt-2">${renderInlineMarkup(item.title)}</p>`);
29
29
  if (item.description) {
30
- parts.push(` <p class="text-sm text-d-muted font-body mt-1 px-3">${nl2br(item.description)}</p>`);
30
+ parts.push(` <p class="text-sm text-d-muted font-body mt-1 px-3">${renderInlineMarkup(item.description)}</p>`);
31
31
  }
32
32
  parts.push(`</div>`);
33
33
  });
@@ -1,14 +1,16 @@
1
- import { nl2br } from "../utils.js";
1
+ import { renderInlineMarkup } from "../utils.js";
2
2
  export const layoutTitle = (data) => {
3
3
  return [
4
4
  `<div class="h-[3px] bg-d-primary"></div>`,
5
5
  `<div class="absolute -top-20 -right-8 w-[360px] h-[360px] rounded-full bg-d-primary opacity-10"></div>`,
6
6
  `<div class="absolute -bottom-12 -left-16 w-[280px] h-[280px] rounded-full bg-d-accent opacity-10"></div>`,
7
7
  `<div class="flex flex-col justify-center h-full px-16 relative z-10">`,
8
- ` <h1 class="text-[60px] leading-tight font-title font-bold text-d-text">${nl2br(data.title)}</h1>`,
9
- data.subtitle ? ` <p class="text-2xl text-d-muted mt-6 font-body">${nl2br(data.subtitle)}</p>` : "",
10
- data.author ? ` <p class="text-lg text-d-dim mt-10 font-body">${nl2br(data.author)}</p>` : "",
11
- data.note ? ` <div class="bg-d-card px-4 py-2 mt-6 inline-block rounded"><p class="text-sm text-d-dim font-body">${nl2br(data.note)}</p></div>` : "",
8
+ ` <h1 class="text-[60px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h1>`,
9
+ data.subtitle ? ` <p class="text-2xl text-d-muted mt-6 font-body">${renderInlineMarkup(data.subtitle)}</p>` : "",
10
+ data.author ? ` <p class="text-lg text-d-dim mt-10 font-body">${renderInlineMarkup(data.author)}</p>` : "",
11
+ data.note
12
+ ? ` <div class="bg-d-card px-4 py-2 mt-6 inline-block rounded"><p class="text-sm text-d-dim font-body">${renderInlineMarkup(data.note)}</p></div>`
13
+ : "",
12
14
  `</div>`,
13
15
  `<div class="absolute bottom-0 left-0 right-0 h-[3px] bg-d-accent"></div>`,
14
16
  ]