mulmocast 2.1.40 → 2.2.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.
package/README.md CHANGED
@@ -350,7 +350,7 @@ MulmoCast includes a powerful **Slide DSL** (`type: "slide"`) for creating struc
350
350
  ### Features
351
351
 
352
352
  - **11 Layouts**: title, columns, comparison, grid, bigQuote, stats, timeline, split, matrix, table, funnel
353
- - **7 Content Block Types**: text, bullets, code, callout, metric, divider, image
353
+ - **10 Content Block Types**: text, bullets, code, callout, metric, divider, image, imageRef, chart, mermaid
354
354
  - **13-Color Theme System**: Semantic color palette with dark/light support
355
355
  - **6 Preset Themes**: dark, pop, warm, creative, minimal, corporate
356
356
 
@@ -52,12 +52,13 @@ export declare const imagePreprocessAgent: (namedInputs: {
52
52
  context: MulmoStudioContext;
53
53
  beat: MulmoBeat;
54
54
  index: number;
55
- imageRefs: Record<string, string>;
55
+ imageRefs?: Record<string, string>;
56
56
  }) => Promise<ImagePreprocessAgentResponse>;
57
57
  export declare const imagePluginAgent: (namedInputs: {
58
58
  context: MulmoStudioContext;
59
59
  beat: MulmoBeat;
60
60
  index: number;
61
+ imageRefs?: Record<string, string>;
61
62
  }) => Promise<void>;
62
63
  export declare const htmlImageGeneratorAgent: (namedInputs: {
63
64
  file: string;
@@ -88,18 +88,18 @@ export const imagePreprocessAgent = async (namedInputs) => {
88
88
  return { ...returnValue, imagePath, imageFromMovie: true }; // no image prompt, only movie prompt
89
89
  }
90
90
  // referenceImages for "edit_image", openai agent.
91
- const referenceImages = MulmoBeatMethods.getImageReferenceForImageGenerator(beat, imageRefs);
91
+ const referenceImages = MulmoBeatMethods.getImageReferenceForImageGenerator(beat, imageRefs ?? {});
92
92
  const prompt = imagePrompt(beat, imageAgentInfo.imageParams.style);
93
93
  // ImageGenearalPreprocessAgentResponse
94
94
  return { ...returnValue, imagePath, referenceImageForMovie: imagePath, imageAgentInfo, prompt, referenceImages };
95
95
  };
96
96
  export const imagePluginAgent = async (namedInputs) => {
97
- const { context, beat, index } = namedInputs;
97
+ const { context, beat, index, imageRefs } = namedInputs;
98
98
  const { imagePath } = getBeatPngImagePath(context, index);
99
99
  const plugin = MulmoBeatMethods.getPlugin(beat);
100
100
  try {
101
101
  MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, true);
102
- const processorParams = { beat, context, imagePath, ...htmlStyle(context, beat) };
102
+ const processorParams = { beat, context, imagePath, imageRefs, ...htmlStyle(context, beat) };
103
103
  await plugin.process(processorParams);
104
104
  MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, false);
105
105
  }
@@ -29,7 +29,7 @@ export declare const beat_graph_data: {
29
29
  context: MulmoStudioContext;
30
30
  beat: import("../types/type.js").MulmoBeat;
31
31
  index: number;
32
- imageRefs: Record<string, string>;
32
+ imageRefs?: Record<string, string>;
33
33
  }) => Promise<({
34
34
  imageParams?: MulmoImageParams;
35
35
  movieFile?: string;
@@ -162,11 +162,13 @@ export declare const beat_graph_data: {
162
162
  context: MulmoStudioContext;
163
163
  beat: import("../types/type.js").MulmoBeat;
164
164
  index: number;
165
+ imageRefs?: Record<string, string>;
165
166
  }) => Promise<void>;
166
167
  inputs: {
167
168
  context: string;
168
169
  beat: string;
169
170
  index: string;
171
+ imageRefs: string;
170
172
  onComplete: string[];
171
173
  };
172
174
  };
@@ -76,6 +76,7 @@ export const beat_graph_data = {
76
76
  context: ":context",
77
77
  beat: ":beat",
78
78
  index: ":__mapIndex",
79
+ imageRefs: ":imageRefs",
79
80
  onComplete: [":preprocessor"],
80
81
  },
81
82
  },
@@ -1,5 +1,6 @@
1
1
  import type { MulmoMediaSource, MulmoMediaMermaidSource, MulmoStudioContext, ImageType } from "../types/index.js";
2
2
  export declare const getExtention: (contentType: string | null, url: string) => string;
3
+ export declare const pathToDataUrl: (filePath: string) => string;
3
4
  export declare const MulmoMediaSourceMethods: {
4
5
  getText(mediaSource: MulmoMediaMermaidSource, context: MulmoStudioContext): Promise<string | null>;
5
6
  resolve(mediaSource: MulmoMediaSource | undefined, context: MulmoStudioContext): string | null;
@@ -60,12 +60,26 @@ const urlToDataUrl = async (url, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) => {
60
60
  clearTimeout(timeoutId);
61
61
  }
62
62
  };
63
+ /** Map file extension to MIME type for data URLs */
64
+ const extensionToMimeType = (ext) => {
65
+ const mimeMap = {
66
+ jpg: "image/jpeg",
67
+ jpeg: "image/jpeg",
68
+ png: "image/png",
69
+ gif: "image/gif",
70
+ webp: "image/webp",
71
+ svg: "image/svg+xml",
72
+ bmp: "image/bmp",
73
+ ico: "image/x-icon",
74
+ };
75
+ return mimeMap[ext.toLowerCase()] ?? `image/${ext}`;
76
+ };
63
77
  // Convert local file path to data URL (base64 encoded)
64
- const pathToDataUrl = (filePath) => {
78
+ export const pathToDataUrl = (filePath) => {
65
79
  assert(fs.existsSync(filePath), `File not found: ${filePath}`, false, mediaSourceFileNotFoundError(filePath));
66
80
  const buffer = fs.readFileSync(filePath);
67
- const extension = getExtention(null, filePath);
68
- const mimeType = extension === "jpg" ? "image/jpeg" : `image/${extension}`;
81
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "png";
82
+ const mimeType = extensionToMimeType(ext);
69
83
  return `data:${mimeType};base64,${buffer.toString("base64")}`;
70
84
  };
71
85
  // Convert base64 string to data URL format
@@ -3,3 +3,5 @@ import type { ContentBlock } from "./schema.js";
3
3
  export declare const renderContentBlock: (block: ContentBlock) => string;
4
4
  /** Render an array of content blocks to HTML */
5
5
  export declare const renderContentBlocks: (blocks: ContentBlock[]) => string;
6
+ /** Render content blocks with fixed aspect-ratio container for image blocks (used in card layouts) */
7
+ export declare const renderCardContentBlocks: (blocks: ContentBlock[]) => string;
@@ -1,4 +1,4 @@
1
- import { escapeHtml, nl2br, c } from "./utils.js";
1
+ import { escapeHtml, nl2br, c, generateSlideId } from "./utils.js";
2
2
  /** Render a single content block to HTML */
3
3
  export const renderContentBlock = (block) => {
4
4
  switch (block.type) {
@@ -16,6 +16,12 @@ export const renderContentBlock = (block) => {
16
16
  return renderDivider(block);
17
17
  case "image":
18
18
  return renderImage(block);
19
+ case "imageRef":
20
+ return renderImageRefPlaceholder(block);
21
+ case "chart":
22
+ return renderChart(block);
23
+ case "mermaid":
24
+ return renderMermaid(block);
19
25
  default:
20
26
  return `<p class="text-sm text-d-muted font-body">[unknown block type]</p>`;
21
27
  }
@@ -24,6 +30,17 @@ export const renderContentBlock = (block) => {
24
30
  export const renderContentBlocks = (blocks) => {
25
31
  return blocks.map(renderContentBlock).join("\n");
26
32
  };
33
+ /** Render content blocks with fixed aspect-ratio container for image blocks (used in card layouts) */
34
+ export const renderCardContentBlocks = (blocks) => {
35
+ return blocks
36
+ .map((block) => {
37
+ if (block.type === "image") {
38
+ return `<div class="aspect-video shrink-0 overflow-hidden">${renderContentBlock(block)}</div>`;
39
+ }
40
+ return renderContentBlock(block);
41
+ })
42
+ .join("\n");
43
+ };
27
44
  const resolveTextColor = (block) => {
28
45
  if (block.color)
29
46
  return `text-${c(block.color)}`;
@@ -93,5 +110,40 @@ const renderDivider = (block) => {
93
110
  };
94
111
  const renderImage = (block) => {
95
112
  const fit = block.fit === "cover" ? "object-cover" : "object-contain";
96
- return `<img src="${escapeHtml(block.src)}" alt="${escapeHtml(block.alt || "")}" class="rounded ${fit} max-h-full w-full" />`;
113
+ return `<div class="min-h-0 flex-1 overflow-hidden flex items-center"><img src="${escapeHtml(block.src)}" alt="${escapeHtml(block.alt || "")}" class="rounded ${fit} w-full h-full" /></div>`;
114
+ };
115
+ /** Placeholder for unresolved imageRef blocks — should be resolved before rendering */
116
+ const renderImageRefPlaceholder = (block) => {
117
+ return `<div class="min-h-0 flex-1 overflow-hidden flex items-center justify-center bg-d-alt rounded"><p class="text-sm text-d-dim font-body">[imageRef: ${escapeHtml(block.ref)}]</p></div>`;
118
+ };
119
+ const renderChart = (block) => {
120
+ const chartId = generateSlideId("chart");
121
+ 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>` : "";
123
+ return `<div class="flex-1 min-h-0 flex flex-col">
124
+ ${titleHtml}
125
+ <div class="flex-1 min-h-0 relative">
126
+ <canvas id="${chartId}" data-chart-ready="false"></canvas>
127
+ </div>
128
+ <script>(function(){
129
+ const ctx=document.getElementById('${chartId}');
130
+ const d=${chartData};
131
+ if(!d.options)d.options={};
132
+ d.options.animation=false;
133
+ d.options.responsive=true;
134
+ d.options.maintainAspectRatio=false;
135
+ new Chart(ctx,d);
136
+ requestAnimationFrame(()=>requestAnimationFrame(()=>{ctx.dataset.chartReady="true"}));
137
+ })()</script>
138
+ </div>`;
139
+ };
140
+ const renderMermaid = (block) => {
141
+ 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>` : "";
143
+ return `<div class="flex-1 min-h-0 flex flex-col">
144
+ ${titleHtml}
145
+ <div class="flex-1 min-h-0 flex justify-center items-center">
146
+ <div id="${mermaidId}" class="mermaid">${escapeHtml(block.code)}</div>
147
+ </div>
148
+ </div>`;
97
149
  };
@@ -1,5 +1,5 @@
1
1
  export { generateSlideHTML } from "./render.js";
2
2
  export { renderSlideContent } from "./layouts/index.js";
3
3
  export { renderContentBlock, renderContentBlocks } from "./blocks.js";
4
- export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, accentColorKeySchema } from "./schema.js";
5
- export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, ContentBlock, AccentColorKey, TitleSlide, ColumnsSlide, ComparisonSlide, GridSlide, BigQuoteSlide, StatsSlide, TimelineSlide, SplitSlide, MatrixSlide, TableSlide, FunnelSlide, Card, CalloutBar, SlideStyle, } from "./schema.js";
4
+ export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, } from "./schema.js";
5
+ export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, ContentBlock, ImageRefBlock, ChartBlock, MermaidBlock, AccentColorKey, TitleSlide, ColumnsSlide, ComparisonSlide, GridSlide, BigQuoteSlide, StatsSlide, TimelineSlide, SplitSlide, MatrixSlide, TableSlide, FunnelSlide, Card, CalloutBar, SlideStyle, } from "./schema.js";
@@ -4,4 +4,4 @@ export { generateSlideHTML } from "./render.js";
4
4
  export { renderSlideContent } from "./layouts/index.js";
5
5
  export { renderContentBlock, renderContentBlocks } from "./blocks.js";
6
6
  // Schemas
7
- export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, accentColorKeySchema } from "./schema.js";
7
+ export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, } from "./schema.js";
@@ -1,5 +1,5 @@
1
1
  import { escapeHtml, c, cardWrap, numBadge, iconSquare, slideHeader, renderCalloutBar } from "../utils.js";
2
- import { renderContentBlocks } from "../blocks.js";
2
+ import { renderCardContentBlocks } from "../blocks.js";
3
3
  const buildColumnCard = (col) => {
4
4
  const accent = col.accentColor || "primary";
5
5
  const inner = [];
@@ -23,8 +23,8 @@ const buildColumnCard = (col) => {
23
23
  }
24
24
  if (col.content) {
25
25
  const centerCls = col.icon ? "text-center" : "";
26
- inner.push(`<div class="mt-3 space-y-3 ${centerCls}">`);
27
- inner.push(renderContentBlocks(col.content));
26
+ inner.push(`<div class="mt-3 space-y-3 flex-1 min-h-0 overflow-hidden flex flex-col ${centerCls}">`);
27
+ inner.push(renderCardContentBlocks(col.content));
28
28
  inner.push(`</div>`);
29
29
  }
30
30
  if (col.footer) {
@@ -43,7 +43,7 @@ export const layoutColumns = (data) => {
43
43
  colElements.push(`<div class="flex items-center shrink-0"><span class="text-2xl text-d-dim">\u25B6</span></div>`);
44
44
  }
45
45
  });
46
- parts.push(`<div class="flex gap-4 px-12 mt-5 flex-1">`);
46
+ parts.push(`<div class="flex gap-4 px-12 mt-5 flex-1 min-h-0 items-stretch">`);
47
47
  parts.push(colElements.join("\n"));
48
48
  parts.push(`</div>`);
49
49
  if (data.callout) {
@@ -5,19 +5,20 @@ const buildPanel = (panel) => {
5
5
  const inner = [];
6
6
  inner.push(`<h3 class="text-xl font-bold text-${c(accent)} font-body">${escapeHtml(panel.title)}</h3>`);
7
7
  if (panel.content) {
8
- inner.push(`<div class="mt-4 space-y-3">`);
8
+ inner.push(`<div class="mt-4 space-y-3 flex-1 min-h-0 overflow-hidden 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(`<div class="flex-1"></div>`);
13
+ if (!panel.content)
14
+ inner.push(`<div class="flex-1"></div>`);
14
15
  inner.push(`<p class="text-sm text-d-dim font-body mt-3">${escapeHtml(panel.footer)}</p>`);
15
16
  }
16
17
  return cardWrap(accent, inner.join("\n"), "flex-1");
17
18
  };
18
19
  export const layoutComparison = (data) => {
19
20
  const parts = [slideHeader(data)];
20
- parts.push(`<div class="flex gap-5 px-12 mt-5 flex-1">`);
21
+ parts.push(`<div class="flex gap-5 px-12 mt-5 flex-1 min-h-0 items-stretch">`);
21
22
  parts.push(buildPanel(data.left));
22
23
  parts.push(buildPanel(data.right));
23
24
  parts.push(`</div>`);
@@ -1,5 +1,5 @@
1
1
  import { escapeHtml, nl2br, c, cardWrap, numBadge, iconSquare } from "../utils.js";
2
- import { renderContentBlocks } from "../blocks.js";
2
+ import { renderCardContentBlocks } from "../blocks.js";
3
3
  export const layoutGrid = (data) => {
4
4
  const accent = data.accentColor || "primary";
5
5
  const nCols = data.gridColumns || 3;
@@ -8,7 +8,7 @@ export const layoutGrid = (data) => {
8
8
  parts.push(`<div class="px-12 pt-5 shrink-0">`);
9
9
  parts.push(` <h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${nl2br(data.title)}</h2>`);
10
10
  parts.push(`</div>`);
11
- parts.push(`<div class="grid grid-cols-${nCols} gap-4 px-12 mt-5 flex-1 content-start">`);
11
+ parts.push(`<div class="grid grid-cols-${nCols} gap-4 px-12 mt-5 flex-1 min-h-0 overflow-hidden content-start">`);
12
12
  (data.items || []).forEach((item) => {
13
13
  const itemAccent = item.accentColor || "primary";
14
14
  const inner = [];
@@ -31,7 +31,7 @@ export const layoutGrid = (data) => {
31
31
  inner.push(`<p class="text-sm text-d-muted font-body mt-3">${escapeHtml(item.description)}</p>`);
32
32
  }
33
33
  if (item.content) {
34
- inner.push(`<div class="mt-3 space-y-3">${renderContentBlocks(item.content)}</div>`);
34
+ inner.push(`<div class="mt-3 space-y-3 flex-1 min-h-0 overflow-hidden flex flex-col">${renderCardContentBlocks(item.content)}</div>`);
35
35
  }
36
36
  parts.push(cardWrap(itemAccent, inner.join("\n")));
37
37
  });
@@ -1,9 +1,31 @@
1
- import { escapeHtml, buildTailwindConfig, sanitizeHex } from "./utils.js";
1
+ import { escapeHtml, buildTailwindConfig, sanitizeHex, detectBlockTypes } from "./utils.js";
2
2
  import { renderSlideContent } from "./layouts/index.js";
3
+ /** Determine if a hex color is dark (luminance < 128) */
4
+ const isDarkBg = (hex) => {
5
+ const r = parseInt(hex.slice(0, 2), 16);
6
+ const g = parseInt(hex.slice(2, 4), 16);
7
+ const b = parseInt(hex.slice(4, 6), 16);
8
+ return (r * 299 + g * 587 + b * 114) / 1000 < 128;
9
+ };
10
+ /** Build CDN script tags for chart/mermaid when needed */
11
+ const buildCdnScripts = (theme, slide) => {
12
+ const { hasChart, hasMermaid } = detectBlockTypes(slide);
13
+ const scripts = [];
14
+ if (hasChart) {
15
+ scripts.push('<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>');
16
+ }
17
+ if (hasMermaid) {
18
+ const mermaidTheme = isDarkBg(theme.colors.bg) ? "dark" : "default";
19
+ scripts.push(`<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
20
+ <script>mermaid.initialize({startOnLoad:true,theme:'${mermaidTheme}'})</script>`);
21
+ }
22
+ return scripts.join("\n");
23
+ };
3
24
  /** Generate a complete HTML document for a single slide */
4
25
  export const generateSlideHTML = (theme, slide) => {
5
26
  const content = renderSlideContent(slide);
6
27
  const twConfig = buildTailwindConfig(theme);
28
+ const cdnScripts = buildCdnScripts(theme, slide);
7
29
  const slideStyle = slide.style;
8
30
  const bgCls = slideStyle?.bgColor ? "" : "bg-d-bg";
9
31
  const inlineStyle = slideStyle?.bgColor ? ` style="background-color:#${sanitizeHex(slideStyle.bgColor)}"` : "";
@@ -15,6 +37,7 @@ export const generateSlideHTML = (theme, slide) => {
15
37
  <meta name="viewport" content="width=1280">
16
38
  <script src="https://cdn.tailwindcss.com"></script>
17
39
  <script>tailwind.config = ${twConfig}</script>
40
+ ${cdnScripts}
18
41
  <style>
19
42
  html, body { height: 100%; margin: 0; padding: 0; overflow: hidden; }
20
43
  </style>