mulmocast 2.2.1 → 2.2.3

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.
@@ -42,9 +42,19 @@ export const textBlockSchema = z.object({
42
42
  fontSize: z.number().optional(),
43
43
  color: accentColorKeySchema.optional(),
44
44
  });
45
+ /** Sub-bullet item: plain string or object with text */
46
+ const subBulletItemSchema = z.union([z.string(), z.object({ text: z.string() })]);
47
+ /** Bullet item: plain string or object with text and optional sub-items (2 levels max) */
48
+ export const bulletItemSchema = z.union([
49
+ z.string(),
50
+ z.object({
51
+ text: z.string(),
52
+ items: z.array(subBulletItemSchema).optional(),
53
+ }),
54
+ ]);
45
55
  export const bulletsBlockSchema = z.object({
46
56
  type: z.literal("bullets"),
47
- items: z.array(z.string()),
57
+ items: z.array(bulletItemSchema),
48
58
  ordered: z.boolean().optional(),
49
59
  icon: z.string().optional(),
50
60
  });
@@ -93,7 +103,25 @@ export const mermaidBlockSchema = z.object({
93
103
  code: z.string(),
94
104
  title: z.string().optional(),
95
105
  });
96
- export const contentBlockSchema = z.discriminatedUnion("type", [
106
+ export const tableCellValueSchema = z.union([
107
+ z.string(),
108
+ z.object({
109
+ text: z.string(),
110
+ color: accentColorKeySchema.optional(),
111
+ bold: z.boolean().optional(),
112
+ badge: z.boolean().optional(),
113
+ }),
114
+ ]);
115
+ export const tableBlockSchema = z.object({
116
+ type: z.literal("table"),
117
+ title: z.string().optional(),
118
+ headers: z.array(z.string()).optional(),
119
+ rows: z.array(z.array(tableCellValueSchema)),
120
+ rowHeaders: z.boolean().optional(),
121
+ striped: z.boolean().optional(),
122
+ });
123
+ /** Block schemas shared between contentBlockSchema and nonSectionContentBlockSchema */
124
+ const baseBlockSchemas = [
97
125
  textBlockSchema,
98
126
  bulletsBlockSchema,
99
127
  codeBlockSchema,
@@ -104,7 +132,19 @@ export const contentBlockSchema = z.discriminatedUnion("type", [
104
132
  imageRefBlockSchema,
105
133
  chartBlockSchema,
106
134
  mermaidBlockSchema,
107
- ]);
135
+ tableBlockSchema,
136
+ ];
137
+ /** All content block types except section (used inside section to prevent recursion) */
138
+ const nonSectionContentBlockSchema = z.discriminatedUnion("type", [...baseBlockSchemas]);
139
+ export const sectionBlockSchema = z.object({
140
+ type: z.literal("section"),
141
+ label: z.string(),
142
+ color: accentColorKeySchema.optional(),
143
+ content: z.array(nonSectionContentBlockSchema).optional(),
144
+ text: z.string().optional(),
145
+ sidebar: z.boolean().optional(),
146
+ });
147
+ export const contentBlockSchema = z.discriminatedUnion("type", [...baseBlockSchemas, sectionBlockSchema]);
108
148
  // ═══════════════════════════════════════════════════════════
109
149
  // Shared Components
110
150
  // ═══════════════════════════════════════════════════════════
@@ -244,10 +284,12 @@ export const splitPanelSchema = z.object({
244
284
  title: z.string().optional(),
245
285
  subtitle: z.string().optional(),
246
286
  label: z.string().optional(),
287
+ labelBadge: z.boolean().optional(),
247
288
  accentColor: accentColorKeySchema.optional(),
248
289
  content: z.array(contentBlockSchema).optional(),
249
290
  dark: z.boolean().optional(),
250
291
  ratio: z.number().optional(),
292
+ valign: z.enum(["top", "center", "bottom"]).optional(),
251
293
  });
252
294
  export const splitSlideSchema = z.object({
253
295
  layout: z.literal("split"),
@@ -287,14 +329,6 @@ export const matrixSlideSchema = z.object({
287
329
  cells: z.array(matrixCellSchema),
288
330
  });
289
331
  // ─── table ───
290
- export const tableCellValueSchema = z.union([
291
- z.string(),
292
- z.object({
293
- text: z.string(),
294
- color: accentColorKeySchema.optional(),
295
- bold: z.boolean().optional(),
296
- }),
297
- ]);
298
332
  export const tableSlideSchema = z.object({
299
333
  layout: z.literal("table"),
300
334
  ...slideBaseFields,
@@ -345,5 +379,6 @@ export const mulmoSlideMediaSchema = z
345
379
  type: z.literal("slide"),
346
380
  theme: slideThemeSchema.optional(),
347
381
  slide: slideLayoutSchema,
382
+ reference: z.string().optional(),
348
383
  })
349
384
  .strict();
@@ -3,6 +3,12 @@ import type { SlideTheme, SlideLayout } from "./schema.js";
3
3
  export declare const escapeHtml: (s: string) => string;
4
4
  /** Escape HTML and convert newlines to <br> */
5
5
  export declare const nl2br: (s: string) => string;
6
+ /**
7
+ * Render inline markup: escape HTML first, then parse **bold** and {color:text}.
8
+ * Also converts newlines to <br>.
9
+ * Safe: escapeHtml runs before any markup parsing, so XSS is impossible.
10
+ */
11
+ export declare const renderInlineMarkup: (s: string) => string;
6
12
  /** Sanitize a hex color value (hex digits only) */
7
13
  export declare const sanitizeHex: (s: string) => string;
8
14
  /** Accent color key → Tailwind class segment: "primary" → "d-primary" */
@@ -11,6 +11,28 @@ export const escapeHtml = (s) => {
11
11
  export const nl2br = (s) => {
12
12
  return escapeHtml(s).replace(/\n/g, "<br>");
13
13
  };
14
+ /** Valid accent color keys for inline markup */
15
+ const inlineColorKeys = new Set(["primary", "accent", "success", "warning", "danger", "info", "highlight"]);
16
+ /**
17
+ * Render inline markup: escape HTML first, then parse **bold** and {color:text}.
18
+ * Also converts newlines to <br>.
19
+ * Safe: escapeHtml runs before any markup parsing, so XSS is impossible.
20
+ */
21
+ export const renderInlineMarkup = (s) => {
22
+ let result = escapeHtml(s);
23
+ // **bold** → <strong>bold</strong>
24
+ result = result.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
25
+ // {color:text} → <span class="text-d-color">text</span>
26
+ result = result.replace(/\{([a-z]+):(.+?)\}/g, (_match, color, text) => {
27
+ if (inlineColorKeys.has(color)) {
28
+ return `<span class="text-${c(color)}">${text}</span>`;
29
+ }
30
+ return `{${color}:${text}}`;
31
+ });
32
+ // newlines to <br>
33
+ result = result.replace(/\n/g, "<br>");
34
+ return result;
35
+ };
14
36
  /** Sanitize a value for safe use in CSS class names (alphanumeric + hyphens only) */
15
37
  const sanitizeCssClass = (s) => {
16
38
  return s.replace(/[^a-zA-Z0-9-]/g, "");
@@ -87,8 +109,8 @@ export const renderCalloutBar = (obj) => {
87
109
  const leftBar = obj.leftBar ? `<div class="w-1 bg-${c(color)} shrink-0"></div>` : "";
88
110
  const align = obj.align === "center" ? "text-center" : "";
89
111
  const inner = obj.label
90
- ? `<span class="font-bold text-${c(color)}">${escapeHtml(obj.label)}:</span> <span class="text-d-muted">${escapeHtml(obj.text)}</span>`
91
- : `<span class="text-d-muted">${escapeHtml(obj.text)}</span>`;
112
+ ? `<span class="font-bold text-${c(color)}">${renderInlineMarkup(obj.label)}:</span> <span class="text-d-muted">${renderInlineMarkup(obj.text)}</span>`
113
+ : `<span class="text-d-muted">${renderInlineMarkup(obj.text)}</span>`;
92
114
  return `<div class="mx-12 bg-d-card rounded flex overflow-hidden ${align}">
93
115
  ${leftBar}
94
116
  <div class="px-4 py-3 text-sm font-body flex-1">${inner}</div>
@@ -101,11 +123,11 @@ export const slideHeader = (data) => {
101
123
  lines.push(`<div class="h-[3px] bg-${c(accent)} shrink-0"></div>`);
102
124
  lines.push(`<div class="px-12 pt-5 shrink-0">`);
103
125
  if (data.stepLabel) {
104
- lines.push(` <p class="text-sm font-bold text-${c(accent)} font-body">${escapeHtml(data.stepLabel)}</p>`);
126
+ lines.push(` <p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
105
127
  }
106
- lines.push(` <h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${nl2br(data.title)}</h2>`);
128
+ lines.push(` <h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
107
129
  if (data.subtitle) {
108
- lines.push(` <p class="text-[15px] text-d-dim mt-2 font-body">${nl2br(data.subtitle)}</p>`);
130
+ lines.push(` <p class="text-[15px] text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
109
131
  }
110
132
  lines.push(`</div>`);
111
133
  return lines.join("\n");
@@ -159,6 +181,14 @@ export const detectBlockTypes = (slide) => {
159
181
  hasChart = true;
160
182
  if (block.type === "mermaid")
161
183
  hasMermaid = true;
184
+ if (block.type === "section" && block.content) {
185
+ block.content.forEach((inner) => {
186
+ if (inner.type === "chart")
187
+ hasChart = true;
188
+ if (inner.type === "mermaid")
189
+ hasMermaid = true;
190
+ });
191
+ }
162
192
  });
163
193
  });
164
194
  return { hasChart, hasMermaid };