mulmocast 2.9.2 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/lib/cli/bin.js +0 -0
  2. package/lib/index.browser.d.ts +7 -0
  3. package/lib/index.browser.js +6 -0
  4. package/lib/types/const.d.ts +7 -0
  5. package/lib/types/const.js +7 -0
  6. package/lib/types/schema.js +6 -2
  7. package/lib/utils/beat_html/chart.d.ts +15 -0
  8. package/lib/utils/beat_html/chart.js +21 -0
  9. package/lib/utils/beat_html/html_tailwind.d.ts +19 -0
  10. package/lib/utils/beat_html/html_tailwind.js +20 -0
  11. package/lib/utils/beat_html/index.d.ts +33 -0
  12. package/lib/utils/beat_html/index.js +65 -0
  13. package/lib/utils/beat_html/markdown.d.ts +20 -0
  14. package/lib/utils/beat_html/markdown.js +25 -0
  15. package/lib/utils/beat_html/media.d.ts +14 -0
  16. package/lib/utils/beat_html/media.js +19 -0
  17. package/lib/utils/beat_html/mermaid.d.ts +14 -0
  18. package/lib/utils/beat_html/mermaid.js +22 -0
  19. package/lib/utils/beat_html/slide.d.ts +21 -0
  20. package/lib/utils/beat_html/slide.js +33 -0
  21. package/lib/utils/beat_html/text_slide.d.ts +17 -0
  22. package/lib/utils/beat_html/text_slide.js +24 -0
  23. package/lib/utils/beat_html/type.d.ts +73 -0
  24. package/lib/utils/element_id.d.ts +26 -0
  25. package/lib/utils/element_id.js +31 -0
  26. package/lib/utils/html_escape.d.ts +14 -0
  27. package/lib/utils/html_escape.js +30 -0
  28. package/lib/utils/image_plugins/bg_image_util.js +2 -1
  29. package/lib/utils/image_plugins/chart.js +6 -30
  30. package/lib/utils/image_plugins/chart_html.d.ts +24 -0
  31. package/lib/utils/image_plugins/chart_html.js +81 -0
  32. package/lib/utils/image_plugins/html_tailwind.js +2 -5
  33. package/lib/utils/image_plugins/html_tailwind_markup.d.ts +21 -0
  34. package/lib/utils/image_plugins/html_tailwind_markup.js +8 -0
  35. package/lib/utils/image_plugins/markdown.js +41 -30
  36. package/lib/utils/image_plugins/markdown_layout.d.ts +46 -4
  37. package/lib/utils/image_plugins/markdown_layout.js +96 -83
  38. package/lib/utils/image_plugins/media_html.d.ts +16 -0
  39. package/lib/utils/image_plugins/media_html.js +39 -0
  40. package/lib/utils/image_plugins/mermaid.d.ts +1 -0
  41. package/lib/utils/image_plugins/mermaid.js +4 -16
  42. package/lib/utils/image_plugins/mermaid_html.d.ts +22 -0
  43. package/lib/utils/image_plugins/mermaid_html.js +36 -0
  44. package/lib/utils/image_plugins/movie.js +2 -15
  45. package/lib/utils/sdk_timeout.d.ts +3 -0
  46. package/lib/utils/sdk_timeout.js +7 -0
  47. package/lib/utils/swipe_to_html.d.ts +18 -0
  48. package/lib/utils/swipe_to_html.js +26 -2
  49. package/package.json +11 -10
  50. package/scripts/test/test_captions_2.json~ +278 -0
  51. package/scripts/test/test_en_portrait.json~ +31 -0
  52. package/scripts/test/test_slide_image_ref_gemini_en.json~ +289 -0
  53. package/scripts/test/test_vertexai.json~ +21 -0
  54. package/lib/agents/image_google_agent.d.ts +0 -5
  55. package/lib/agents/image_google_agent.js +0 -86
  56. package/lib/agents/movie_google_agent.d.ts +0 -9
  57. package/lib/agents/movie_google_agent.js +0 -122
  58. package/lib/agents/mulmo_prompts_agent.d.ts +0 -7
  59. package/lib/agents/mulmo_prompts_agent.js +0 -37
  60. package/lib/agents/prompts_data.d.ts +0 -15
  61. package/lib/agents/prompts_data.js +0 -16
  62. package/lib/agents/tts_nijivoice_agent.d.ts +0 -5
  63. package/lib/agents/tts_nijivoice_agent.js +0 -69
  64. package/lib/agents/validate_mulmo_script_agent.d.ts +0 -17
  65. package/lib/agents/validate_mulmo_script_agent.js +0 -34
  66. package/lib/cli/args.d.ts +0 -15
  67. package/lib/cli/args.js +0 -62
  68. package/lib/cli/cli.d.ts +0 -17
  69. package/lib/cli/cli.js +0 -117
  70. package/lib/cli/run.d.ts +0 -2
  71. package/lib/cli/run.js +0 -3
  72. package/lib/cli/tool-args.d.ts +0 -16
  73. package/lib/cli/tool-args.js +0 -64
  74. package/lib/cli/tool-cli.d.ts +0 -2
  75. package/lib/cli/tool-cli.js +0 -32
  76. package/lib/cli/tool_commands/prompt/builder.d.ts +0 -4
  77. package/lib/cli/tool_commands/prompt/builder.js +0 -11
  78. package/lib/cli/tool_commands/prompt/handler.d.ts +0 -4
  79. package/lib/cli/tool_commands/prompt/handler.js +0 -14
  80. package/lib/cli/tool_commands/prompt/index.d.ts +0 -4
  81. package/lib/cli/tool_commands/prompt/index.js +0 -4
  82. package/lib/cli/tool_commands/schema/builder.d.ts +0 -2
  83. package/lib/cli/tool_commands/schema/builder.js +0 -3
  84. package/lib/cli/tool_commands/schema/handler.d.ts +0 -2
  85. package/lib/cli/tool_commands/schema/handler.js +0 -12
  86. package/lib/cli/tool_commands/schema/index.d.ts +0 -4
  87. package/lib/cli/tool_commands/schema/index.js +0 -4
  88. package/lib/cli/tool_commands/scripting/builder.d.ts +0 -20
  89. package/lib/cli/tool_commands/scripting/builder.js +0 -63
  90. package/lib/cli/tool_commands/scripting/handler.d.ts +0 -12
  91. package/lib/cli/tool_commands/scripting/handler.js +0 -36
  92. package/lib/cli/tool_commands/scripting/index.d.ts +0 -4
  93. package/lib/cli/tool_commands/scripting/index.js +0 -4
  94. package/lib/index.d.ts +0 -8
  95. package/lib/index.js +0 -8
  96. package/lib/methods/mulmo_script_template.d.ts +0 -4
  97. package/lib/methods/mulmo_script_template.js +0 -6
  98. package/lib/methods/mulmo_studio.d.ts +0 -8
  99. package/lib/methods/mulmo_studio.js +0 -25
  100. package/lib/slide/blocks.d.ts +0 -13
  101. package/lib/slide/blocks.js +0 -251
  102. package/lib/slide/index.d.ts +0 -6
  103. package/lib/slide/index.js +0 -7
  104. package/lib/slide/layouts/big_quote.d.ts +0 -2
  105. package/lib/slide/layouts/big_quote.js +0 -19
  106. package/lib/slide/layouts/columns.d.ts +0 -2
  107. package/lib/slide/layouts/columns.js +0 -53
  108. package/lib/slide/layouts/comparison.d.ts +0 -2
  109. package/lib/slide/layouts/comparison.js +0 -25
  110. package/lib/slide/layouts/funnel.d.ts +0 -2
  111. package/lib/slide/layouts/funnel.js +0 -25
  112. package/lib/slide/layouts/grid.d.ts +0 -2
  113. package/lib/slide/layouts/grid.js +0 -38
  114. package/lib/slide/layouts/index.d.ts +0 -3
  115. package/lib/slide/layouts/index.js +0 -46
  116. package/lib/slide/layouts/matrix.d.ts +0 -2
  117. package/lib/slide/layouts/matrix.js +0 -53
  118. package/lib/slide/layouts/split.d.ts +0 -2
  119. package/lib/slide/layouts/split.js +0 -51
  120. package/lib/slide/layouts/stats.d.ts +0 -2
  121. package/lib/slide/layouts/stats.js +0 -23
  122. package/lib/slide/layouts/table.d.ts +0 -2
  123. package/lib/slide/layouts/table.js +0 -10
  124. package/lib/slide/layouts/timeline.d.ts +0 -2
  125. package/lib/slide/layouts/timeline.js +0 -27
  126. package/lib/slide/layouts/title.d.ts +0 -2
  127. package/lib/slide/layouts/title.js +0 -19
  128. package/lib/slide/layouts/waterfall.d.ts +0 -2
  129. package/lib/slide/layouts/waterfall.js +0 -63
  130. package/lib/slide/render.d.ts +0 -17
  131. package/lib/slide/render.js +0 -101
  132. package/lib/slide/schema.d.ts +0 -9309
  133. package/lib/slide/schema.js +0 -434
  134. package/lib/slide/utils.d.ts +0 -76
  135. package/lib/slide/utils.js +0 -243
  136. package/lib/tools/prompt.js +0 -18
  137. package/lib/tools/seed.d.ts +0 -3
  138. package/lib/tools/seed.js +0 -211
  139. package/lib/tools/seed_from_url.d.ts +0 -3
  140. package/lib/tools/seed_from_url.js +0 -188
  141. package/lib/types/slide.d.ts +0 -9309
  142. package/lib/types/slide.js +0 -434
  143. package/lib/utils/assets.d.ts +0 -18
  144. package/lib/utils/assets.js +0 -101
  145. package/lib/utils/cli.d.ts +0 -1
  146. package/lib/utils/cli.js +0 -15
  147. package/lib/utils/const.d.ts +0 -12
  148. package/lib/utils/const.js +0 -12
  149. package/lib/utils/image_preprocess.d.ts +0 -14
  150. package/lib/utils/image_preprocess.js +0 -52
  151. package/lib/utils/markdown.d.ts +0 -3
  152. package/lib/utils/markdown.js +0 -42
  153. package/lib/utils/pdf.d.ts +0 -9
  154. package/lib/utils/pdf.js +0 -77
  155. package/lib/utils/preprocess.d.ts +0 -343
  156. package/lib/utils/preprocess.js +0 -58
  157. package/lib/utils/provider2agent.d.ts +0 -141
  158. package/lib/utils/provider2agent.js +0 -267
  159. package/lib/utils/text_hash.d.ts +0 -1
  160. package/lib/utils/text_hash.js +0 -4
  161. /package/lib/{tools/prompt.d.ts → utils/beat_html/type.js} +0 -0
@@ -1,118 +1,132 @@
1
- import { marked } from "marked";
2
- import { generateMermaidHtml } from "./mermaid.js";
3
- // Regex to match mermaid code blocks
4
- const mermaidBlockRegex = /```mermaid\n([\s\S]*?)```/g;
5
- // Convert string or string array to markdown string
6
- const toMarkdownString = (content) => {
7
- if (Array.isArray(content)) {
8
- return content.join("\n");
9
- }
10
- return content;
11
- };
12
- // Replace mermaid code blocks with rendered HTML
13
- const convertMermaidBlocks = (text) => {
14
- return text.replace(mermaidBlockRegex, (_match, code) => {
15
- return generateMermaidHtml(code.trim());
1
+ import { Marked } from "marked";
2
+ import { mermaidHtml } from "./mermaid_html.js";
3
+ /**
4
+ * The one markdown implementation, shared by the Node render path
5
+ * (`image_plugins/markdown.ts`, which turns it into a PNG) and the browser fragment path
6
+ * (`beat_html/markdown.ts`, which hands it to a host's DOM).
7
+ *
8
+ * Deliberately pure: no `node:*`, no filesystem, no puppeteer. That is what lets the
9
+ * browser side import it rather than keep a second copy — and a second copy is not
10
+ * hypothetical, it existed for one commit and the two immediately disagreed about which
11
+ * fences are diagrams. `test/beat_html/test_browser_safety.ts` bundles the browser entry
12
+ * with esbuild and fails if anything here reaches Node.
13
+ *
14
+ * Element ids come from a caller-supplied generator, because that is the only thing the
15
+ * two callers genuinely need to differ on.
16
+ */
17
+ /** A markdown field is written either as one string or as lines to be joined. */
18
+ export const toMarkdownString = (content) => (Array.isArray(content) ? content.join("\n") : content);
19
+ /**
20
+ * A fence's info string can carry more than the language (```mermaid title="x"), and
21
+ * marked hands it over verbatim. The language is its first word.
22
+ */
23
+ const languageOf = (info) => (info ?? "").trim().split(/\s+/)[0].toLowerCase();
24
+ /**
25
+ * Renders markdown, turning fenced `mermaid` blocks into mermaid markup.
26
+ *
27
+ * Asked of marked's own lexer rather than matched against the raw source. A regex over
28
+ * text was what both copies used, and review found four inputs it got wrong: a CRLF fence
29
+ * and a fence with trailing whitespace after the language were missed, while a mermaid
30
+ * fence quoted INSIDE an outer ````markdown fence, and one inside an indented code block,
31
+ * were converted — turning somebody's example of mermaid syntax into an actual diagram.
32
+ * marked decides what is a code block and what language it is, so all four follow from
33
+ * asking it.
34
+ *
35
+ * A fresh `Marked` instance per call rather than the global `marked.use`, because the
36
+ * renderer closes over this call's mermaid counter — a shared one would leak ids between
37
+ * beats.
38
+ */
39
+ export const renderMarkdownContent = (content, nextId) => {
40
+ let hasMermaid = false;
41
+ const md = new Marked({
42
+ async: false,
43
+ renderer: {
44
+ code({ text, lang }) {
45
+ if (languageOf(lang) !== "mermaid")
46
+ return false;
47
+ hasMermaid = true;
48
+ return mermaidHtml(text, nextId());
49
+ },
50
+ },
16
51
  });
52
+ return { html: md.parse(toMarkdownString(content), { async: false }), hasMermaid };
17
53
  };
18
- // Parse markdown content to HTML (with mermaid support)
19
- const parseMarkdown = async (content) => {
20
- const text = toMarkdownString(content);
21
- const textWithMermaidHtml = convertMermaidBlocks(text);
22
- return await marked.parse(textWithMermaidHtml);
23
- };
24
- // Generate header HTML
25
- const generateHeaderHtml = async (data) => {
26
- const headerHtml = await parseMarkdown(data);
27
- return `
54
+ /** The object form of `markdown`, as opposed to a plain string or array of lines. */
55
+ export const isMarkdownLayout = (md) => typeof md === "object" && md !== null && !Array.isArray(md);
56
+ /**
57
+ * The slot markup is main's, verbatim including its indentation. Reformatting it would
58
+ * change every rendered markdown beat's HTML for no reason — semantically inert, but this
59
+ * feeds Puppeteer and the `mulmo html` export, and "no behaviour change" should mean the
60
+ * bytes too.
61
+ */
62
+ const HEADER_HTML = (inner) => `
28
63
  <div class="shrink-0 px-8 py-4 border-b border-gray-200 bg-gray-50">
29
- <div class="prose prose-lg max-w-none">${headerHtml}</div>
64
+ <div class="prose prose-lg max-w-none">${inner}</div>
30
65
  </div>
31
66
  `;
32
- };
33
- // Generate sidebar HTML
34
- const generateSidebarHtml = async (data) => {
35
- const sidebarHtml = await parseMarkdown(data);
36
- return `
67
+ const SIDEBAR_HTML = (inner) => `
37
68
  <div class="shrink-0 w-56 px-4 py-4 border-r border-gray-200 bg-gray-100 overflow-auto">
38
- <div class="prose prose-sm max-w-none">${sidebarHtml}</div>
69
+ <div class="prose prose-sm max-w-none">${inner}</div>
39
70
  </div>
40
71
  `;
41
- };
42
- // Generate row-2 layout HTML (two columns)
43
- const generateRow2Html = async (data) => {
44
- const [left, right] = data;
45
- const leftHtml = await parseMarkdown(left);
46
- const rightHtml = await parseMarkdown(right);
47
- return `
72
+ const ROW2_HTML = (left, right) => `
48
73
  <div class="h-full flex gap-6">
49
74
  <div class="flex-1 overflow-auto">
50
- <div class="prose max-w-none">${leftHtml}</div>
75
+ <div class="prose max-w-none">${left}</div>
51
76
  </div>
52
77
  <div class="flex-1 overflow-auto">
53
- <div class="prose max-w-none">${rightHtml}</div>
78
+ <div class="prose max-w-none">${right}</div>
54
79
  </div>
55
80
  </div>
56
81
  `;
57
- };
58
- // Generate 2x2 grid layout HTML
59
- const generate2x2Html = async (data) => {
60
- const [tl, tr, bl, br] = data;
61
- const [tlHtml, trHtml, blHtml, brHtml] = await Promise.all([parseMarkdown(tl), parseMarkdown(tr), parseMarkdown(bl), parseMarkdown(br)]);
62
- return `
82
+ const GRID_2X2_HTML = (cells) => `
63
83
  <div class="h-full grid grid-cols-2 grid-rows-2 gap-4">
64
- <div class="overflow-auto p-4 bg-gray-50 rounded-lg">
65
- <div class="prose prose-sm max-w-none">${tlHtml}</div>
66
- </div>
67
- <div class="overflow-auto p-4 bg-gray-50 rounded-lg">
68
- <div class="prose prose-sm max-w-none">${trHtml}</div>
69
- </div>
70
- <div class="overflow-auto p-4 bg-gray-50 rounded-lg">
71
- <div class="prose prose-sm max-w-none">${blHtml}</div>
72
- </div>
73
- <div class="overflow-auto p-4 bg-gray-50 rounded-lg">
74
- <div class="prose prose-sm max-w-none">${brHtml}</div>
75
- </div>
84
+ ${cells
85
+ .map((cell) => ` <div class="overflow-auto p-4 bg-gray-50 rounded-lg">
86
+ <div class="prose prose-sm max-w-none">${cell}</div>
87
+ </div>`)
88
+ .join("\n")}
76
89
  </div>
77
90
  `;
78
- };
79
- // Generate content HTML (single column)
80
- const generateContentHtml = async (data) => {
81
- const contentHtml = await parseMarkdown(data);
82
- return `<div class="prose max-w-none">${contentHtml}</div>`;
83
- };
84
- // Generate Tailwind HTML for layout
85
- export const generateLayoutHtml = async (md) => {
91
+ const CONTENT_HTML = (inner) => `<div class="prose max-w-none">${inner}</div>`;
92
+ /** Lay a markdown layout out in Tailwind: header / sidebar-left / row-2 | 2x2 | content. */
93
+ export const renderMarkdownLayout = (md, nextId) => {
94
+ let hasMermaid = false;
95
+ const render = (content) => {
96
+ const rendered = renderMarkdownContent(content, nextId);
97
+ hasMermaid = hasMermaid || rendered.hasMermaid;
98
+ return rendered.html;
99
+ };
86
100
  const parts = ['<div class="w-full h-full flex flex-col overflow-hidden">'];
87
- if (md.header) {
88
- parts.push(await generateHeaderHtml(md.header));
89
- }
101
+ if (md.header)
102
+ parts.push(HEADER_HTML(render(md.header)));
90
103
  parts.push('<div class="flex-1 flex min-h-0 overflow-hidden">');
91
- if (md["sidebar-left"]) {
92
- parts.push(await generateSidebarHtml(md["sidebar-left"]));
93
- }
104
+ if (md["sidebar-left"])
105
+ parts.push(SIDEBAR_HTML(render(md["sidebar-left"])));
94
106
  parts.push('<div class="flex-1 p-6 overflow-auto">');
95
107
  if ("row-2" in md) {
96
- parts.push(await generateRow2Html(md["row-2"]));
108
+ const [left, right] = md["row-2"];
109
+ parts.push(ROW2_HTML(render(left), render(right)));
97
110
  }
98
111
  else if ("2x2" in md) {
99
- parts.push(await generate2x2Html(md["2x2"]));
112
+ parts.push(GRID_2X2_HTML(md["2x2"].map(render)));
100
113
  }
101
114
  else if ("content" in md) {
102
- parts.push(await generateContentHtml(md.content));
115
+ parts.push(CONTENT_HTML(render(md.content)));
103
116
  }
104
117
  parts.push("</div>", "</div>", "</div>");
105
- return parts.join("");
118
+ return { html: parts.join(""), hasMermaid };
106
119
  };
107
- // Convert layout to plain markdown string
120
+ /**
121
+ * A layout flattened back to plain markdown, in slot order. Used by `mulmo markdown`,
122
+ * which wants what the beat SAYS rather than how it is laid out.
123
+ */
108
124
  export const layoutToMarkdown = (md) => {
109
125
  const parts = [];
110
- if (md.header) {
126
+ if (md.header)
111
127
  parts.push(toMarkdownString(md.header));
112
- }
113
- if (md["sidebar-left"]) {
128
+ if (md["sidebar-left"])
114
129
  parts.push(toMarkdownString(md["sidebar-left"]));
115
- }
116
130
  if ("row-2" in md) {
117
131
  parts.push(...md["row-2"].map(toMarkdownString));
118
132
  }
@@ -124,4 +138,3 @@ export const layoutToMarkdown = (md) => {
124
138
  }
125
139
  return parts.join("\n\n");
126
140
  };
127
- export { toMarkdownString, parseMarkdown };
@@ -0,0 +1,16 @@
1
+ import type { MulmoMediaSource } from "../../types/index.js";
2
+ /**
3
+ * Markup for image and movie media. Pure — no Node, no filesystem — because the Node dump
4
+ * path and the browser fragment path show the same media and must show it the same way.
5
+ *
6
+ * `src` is escaped: `z.url()` accepts a quote, so a url from a script can otherwise close
7
+ * the attribute. Measured — `https://e.com/a.mp4" onerror="pwn()` parses clean.
8
+ */
9
+ /**
10
+ * The src a browser can load from a media source. A `base64` source has no media type in
11
+ * the schema, so there is nothing to build a `data:` URI from; a `path` is emitted as
12
+ * written and left for the host to resolve against its own page.
13
+ */
14
+ export declare const mediaSrc: (source: MulmoMediaSource) => string | undefined;
15
+ export declare const imageHtml: (src: string, alt: string) => string;
16
+ export declare const movieHtml: (src: string) => string;
@@ -0,0 +1,39 @@
1
+ import { escapeHtml } from "@mulmocast/deck/lib/utils.js";
2
+ /**
3
+ * Markup for image and movie media. Pure — no Node, no filesystem — because the Node dump
4
+ * path and the browser fragment path show the same media and must show it the same way.
5
+ *
6
+ * `src` is escaped: `z.url()` accepts a quote, so a url from a script can otherwise close
7
+ * the attribute. Measured — `https://e.com/a.mp4" onerror="pwn()` parses clean.
8
+ */
9
+ /**
10
+ * The src a browser can load from a media source. A `base64` source has no media type in
11
+ * the schema, so there is nothing to build a `data:` URI from; a `path` is emitted as
12
+ * written and left for the host to resolve against its own page.
13
+ */
14
+ export const mediaSrc = (source) => {
15
+ if (source.kind === "url")
16
+ return source.url;
17
+ if (source.kind === "path")
18
+ return source.path;
19
+ return undefined;
20
+ };
21
+ export const imageHtml = (src, alt) => `\n<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="w-full rounded-lg shadow-lg" />`;
22
+ export const movieHtml = (src) => {
23
+ const url = escapeHtml(src);
24
+ return `
25
+ <div class="movie-container mb-6">
26
+ <div class="relative w-full" style="padding-bottom: 56.25%; /* 16:9 aspect ratio */">
27
+ <video
28
+ class="absolute top-0 left-0 w-full h-full rounded-lg shadow-lg"
29
+ controls
30
+ preload="metadata"
31
+ >
32
+ <source src="${url}" type="video/mp4">
33
+ <source src="${url}" type="video/webm">
34
+ <source src="${url}" type="video/ogg">
35
+ Your browser does not support the video tag.
36
+ </video>
37
+ </div>
38
+ </div>`;
39
+ };
@@ -1,5 +1,6 @@
1
1
  import { ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "mermaid";
3
+ /** Mermaid markup for this path. The markup itself is shared with the browser path. */
3
4
  export declare const generateMermaidHtml: (code: string, title?: string) => string;
4
5
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
5
6
  export declare const path: (params: ImageProcessorParams) => string;
@@ -2,23 +2,12 @@ import { MulmoMediaSourceMethods } from "../../methods/index.js";
2
2
  import { getHTMLFile } from "../file.js";
3
3
  import { renderHTMLToImage, interpolate } from "../html_render.js";
4
4
  import { parrotingImagePath, generateUniqueId } from "./utils.js";
5
+ import { escapedMermaidTemplateValues, mermaidHtml } from "./mermaid_html.js";
5
6
  import { resolveCombinedStyle } from "./bg_image_util.js";
6
7
  import { resolveImageRefs, resolveMovieRefs, resolveRelativeImagePaths } from "./html_tailwind.js";
7
8
  export const imageType = "mermaid";
8
- // Generate mermaid HTML from code string (shared utility)
9
- export const generateMermaidHtml = (code, title) => {
10
- const diagramId = generateUniqueId("mermaid");
11
- const titleHtml = title ? `<h3 class="text-xl font-semibold mb-4">${title}</h3>` : "";
12
- return `
13
- <div class="mermaid-container mb-6">
14
- ${titleHtml}
15
- <div class="flex justify-center">
16
- <div id="${diagramId}" class="mermaid">
17
- ${code.trim()}
18
- </div>
19
- </div>
20
- </div>`;
21
- };
9
+ /** Mermaid markup for this path. The markup itself is shared with the browser path. */
10
+ export const generateMermaidHtml = (code, title) => mermaidHtml(code, generateUniqueId("mermaid"), title);
22
11
  const processMermaid = async (params) => {
23
12
  const { beat, imagePath, canvasSize, context } = params;
24
13
  if (!beat?.image || beat.image.type !== imageType)
@@ -28,9 +17,8 @@ const processMermaid = async (params) => {
28
17
  if (diagram_code) {
29
18
  const combinedStyle = await resolveCombinedStyle(params, beat.image.backgroundImage, beat.image.style);
30
19
  const rawHtml = interpolate(template, {
31
- title: beat.image.title,
20
+ ...escapedMermaidTemplateValues(beat.image.title, `${diagram_code}\n${beat.image.appendix?.join("\n") ?? ""}`),
32
21
  style: combinedStyle,
33
- diagram_code: `${diagram_code}\n${beat.image.appendix?.join("\n") ?? ""}`,
34
22
  });
35
23
  const resolvedImageRefs = resolveImageRefs(rawHtml, params.imageRefs ?? {});
36
24
  const resolvedAllRefs = resolveMovieRefs(resolvedImageRefs, params.movieRefs ?? {});
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Mermaid markup. Pure — no Node, no filesystem — because both the Node render path and
3
+ * the browser fragment path draw the same diagram and must draw it the same way.
4
+ *
5
+ * The element id is a parameter rather than generated here: the Node path renders once to
6
+ * a PNG so a random id is fine, while the browser path re-renders and needs the same beat
7
+ * to produce the same markup, or a host diffing fragments sees every diagram change
8
+ * identity on every render. That parameter is caller-supplied — the markdown path builds it
9
+ * from an idPrefix, and a beat's `id` comes from the script — so it is validated rather than
10
+ * escaped: see element_id.ts for why the permitted-set rule beats a per-context escaper.
11
+ *
12
+ * Both values are escaped. Mermaid reads the element's innerHTML and entity-decodes it
13
+ * before parsing (mermaid 11.17.0: `o = u.innerHTML; o = entityDecode(o)`), so it receives
14
+ * the original characters either way — measured across ten diagram shapes including `<br/>`
15
+ * in labels, `#quot;`, raw tags and an init directive, the rendered SVG is byte-identical.
16
+ */
17
+ export declare const mermaidHtml: (code: string, id: string, title?: string) => string;
18
+ /** The two values assets/html/mermaid.html interpolates from user data, escaped for its `<h1>` and `.mermaid` text contexts. */
19
+ export declare const escapedMermaidTemplateValues: (title: string, diagramCode: string) => {
20
+ title: string;
21
+ diagram_code: string;
22
+ };
@@ -0,0 +1,36 @@
1
+ import { escapeHtml } from "@mulmocast/deck/lib/utils.js";
2
+ import { assertSafeElementId } from "../element_id.js";
3
+ /**
4
+ * Mermaid markup. Pure — no Node, no filesystem — because both the Node render path and
5
+ * the browser fragment path draw the same diagram and must draw it the same way.
6
+ *
7
+ * The element id is a parameter rather than generated here: the Node path renders once to
8
+ * a PNG so a random id is fine, while the browser path re-renders and needs the same beat
9
+ * to produce the same markup, or a host diffing fragments sees every diagram change
10
+ * identity on every render. That parameter is caller-supplied — the markdown path builds it
11
+ * from an idPrefix, and a beat's `id` comes from the script — so it is validated rather than
12
+ * escaped: see element_id.ts for why the permitted-set rule beats a per-context escaper.
13
+ *
14
+ * Both values are escaped. Mermaid reads the element's innerHTML and entity-decodes it
15
+ * before parsing (mermaid 11.17.0: `o = u.innerHTML; o = entityDecode(o)`), so it receives
16
+ * the original characters either way — measured across ten diagram shapes including `<br/>`
17
+ * in labels, `#quot;`, raw tags and an init directive, the rendered SVG is byte-identical.
18
+ */
19
+ export const mermaidHtml = (code, id, title) => {
20
+ assertSafeElementId(id);
21
+ const titleHtml = title ? `<h3 class="text-xl font-semibold mb-4">${escapeHtml(title)}</h3>` : "";
22
+ return `
23
+ <div class="mermaid-container mb-6">
24
+ ${titleHtml}
25
+ <div class="flex justify-center">
26
+ <div id="${id}" class="mermaid">
27
+ ${escapeHtml(code.trim())}
28
+ </div>
29
+ </div>
30
+ </div>`;
31
+ };
32
+ /** The two values assets/html/mermaid.html interpolates from user data, escaped for its `<h1>` and `.mermaid` text contexts. */
33
+ export const escapedMermaidTemplateValues = (title, diagramCode) => ({
34
+ title: escapeHtml(title),
35
+ diagram_code: escapeHtml(diagramCode),
36
+ });
@@ -1,5 +1,6 @@
1
1
  import { processSource, pathSource } from "./source.js";
2
2
  import { MulmoMediaSourceMethods } from "../../methods/mulmo_media_source.js";
3
+ import { movieHtml } from "./media_html.js";
3
4
  export const imageType = "movie";
4
5
  const dumpHtml = async (params) => {
5
6
  const { beat, context } = params;
@@ -8,21 +9,7 @@ const dumpHtml = async (params) => {
8
9
  const moviePathOrUrl = MulmoMediaSourceMethods.resolve(beat.image.source, context);
9
10
  if (!moviePathOrUrl)
10
11
  return;
11
- return `
12
- <div class="movie-container mb-6">
13
- <div class="relative w-full" style="padding-bottom: 56.25%; /* 16:9 aspect ratio */">
14
- <video
15
- class="absolute top-0 left-0 w-full h-full rounded-lg shadow-lg"
16
- controls
17
- preload="metadata"
18
- >
19
- <source src="${moviePathOrUrl}" type="video/mp4">
20
- <source src="${moviePathOrUrl}" type="video/webm">
21
- <source src="${moviePathOrUrl}" type="video/ogg">
22
- Your browser does not support the video tag.
23
- </video>
24
- </div>
25
- </div>`;
12
+ return movieHtml(moviePathOrUrl);
26
13
  };
27
14
  export const process = processSource(imageType);
28
15
  export const path = pathSource(imageType);
@@ -0,0 +1,3 @@
1
+ export declare const GENAI_REQUEST_TIMEOUT_MS = 120000;
2
+ export declare const REPLICATE_RUN_TIMEOUT_MS = 600000;
3
+ export declare const VIDEO_POLL_TIMEOUT_MS = 1200000;
@@ -0,0 +1,7 @@
1
+ // Timeout budgets for provider SDK calls (OpenAI has its own in openai_client.ts).
2
+ // Generous safety nets against a stalled connection or a never-completing
3
+ // operation — NOT tight deadlines. A stall becomes a rejection so the caller's
4
+ // GraphAI `retry` can recover instead of hanging forever.
5
+ export const GENAI_REQUEST_TIMEOUT_MS = 120_000; // per @google/genai API call (kick-off / each poll)
6
+ export const REPLICATE_RUN_TIMEOUT_MS = 600_000; // whole replicate.run() job (video models run minutes)
7
+ export const VIDEO_POLL_TIMEOUT_MS = 1_200_000; // wall-clock cap for a long-running Veo video operation
@@ -49,6 +49,24 @@ export interface SwipeElement {
49
49
  loop?: SwipeLoop;
50
50
  elements?: SwipeElement[];
51
51
  }
52
+ /**
53
+ * The element id both the markup and the generated animation script use. It reaches an HTML
54
+ * attribute AND a raw CSS selector inside a JavaScript string literal
55
+ * (`animation.animate('#id')`), so it is validated rather than escaped — escaping one
56
+ * context leaves the other wrong, and a quote in an id silently produces broken JavaScript
57
+ * instead of a diagnosable error.
58
+ *
59
+ * Not a privilege boundary: `html_tailwind` accepts an author's own `script` by design, so
60
+ * an author already has that capability. This is here so the two contexts agree.
61
+ */
62
+ /**
63
+ * Bridges a TypeScript gap, not a validation gap: `swipeElementSchema` is declared
64
+ * `z.ZodType` because it is recursive through `z.lazy`, so `z.array(...)` infers `unknown[]`
65
+ * and the shape zod already checked at parse time is lost to the compiler. Every field of
66
+ * SwipeElement is optional, so any object satisfies it structurally — which is what makes
67
+ * this a sound narrowing rather than a cast wearing a guard's clothes.
68
+ */
69
+ export declare const isSwipeElements: (value: unknown) => value is SwipeElement[];
52
70
  /** Generate HTML from Swipe elements */
53
71
  export declare const swipeElementsToHtml: (elements: SwipeElement[]) => string;
54
72
  /** Generate render() script from Swipe element animations */
@@ -1,6 +1,30 @@
1
+ import { assertSafeElementId } from "./element_id.js";
1
2
  const escapeHtml = (str) => {
2
3
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3
4
  };
5
+ /**
6
+ * The element id both the markup and the generated animation script use. It reaches an HTML
7
+ * attribute AND a raw CSS selector inside a JavaScript string literal
8
+ * (`animation.animate('#id')`), so it is validated rather than escaped — escaping one
9
+ * context leaves the other wrong, and a quote in an id silently produces broken JavaScript
10
+ * instead of a diagnosable error.
11
+ *
12
+ * Not a privilege boundary: `html_tailwind` accepts an author's own `script` by design, so
13
+ * an author already has that capability. This is here so the two contexts agree.
14
+ */
15
+ /**
16
+ * Bridges a TypeScript gap, not a validation gap: `swipeElementSchema` is declared
17
+ * `z.ZodType` because it is recursive through `z.lazy`, so `z.array(...)` infers `unknown[]`
18
+ * and the shape zod already checked at parse time is lost to the compiler. Every field of
19
+ * SwipeElement is optional, so any object satisfies it structurally — which is what makes
20
+ * this a sound narrowing rather than a cast wearing a guard's clothes.
21
+ */
22
+ export const isSwipeElements = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "object" && entry !== null);
23
+ const elementId = (el, index) => {
24
+ const id = el.id ?? `swipe_el_${index}`;
25
+ assertSafeElementId(id);
26
+ return id;
27
+ };
4
28
  const toCssValue = (value) => {
5
29
  return typeof value === "number" ? `${value}px` : value;
6
30
  };
@@ -69,7 +93,7 @@ const buildTextStyle = (el) => {
69
93
  return styles.join(" ");
70
94
  };
71
95
  const elementToHtml = (el, index) => {
72
- const id = el.id ?? `swipe_el_${index}`;
96
+ const id = elementId(el, index);
73
97
  const style = buildElementStyle(el);
74
98
  const textStyle = buildTextStyle(el);
75
99
  const lines = [];
@@ -96,7 +120,7 @@ export const swipeElementsToHtml = (elements) => {
96
120
  };
97
121
  const collectAnimations = (elements, entries, indexBase = 0) => {
98
122
  elements.forEach((el, i) => {
99
- const id = el.id ?? `swipe_el_${indexBase + i}`;
123
+ const id = elementId(el, indexBase + i);
100
124
  if (el.to || el.loop) {
101
125
  entries.push({ id, to: el.to, loop: el.loop });
102
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.9.2",
3
+ "version": "2.10.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "resolutions": {
27
- "minimatch": "^10.2.5",
27
+ "minimatch": "^10.2.6",
28
28
  "tar": "7.5.22",
29
29
  "yauzl": "^3.4.0"
30
30
  },
@@ -89,7 +89,7 @@
89
89
  "homepage": "https://github.com/receptron/mulmocast-cli#readme",
90
90
  "dependencies": {
91
91
  "@google-cloud/text-to-speech": "^6.4.1",
92
- "@google/genai": "^2.13.0",
92
+ "@google/genai": "^2.15.0",
93
93
  "@graphai/anthropic_agent": "^2.0.12",
94
94
  "@graphai/browserless_agent": "^2.0.3",
95
95
  "@graphai/gemini_agent": "^2.0.5",
@@ -102,9 +102,9 @@
102
102
  "@inquirer/input": "^5.1.2",
103
103
  "@inquirer/select": "^5.2.1",
104
104
  "@modelcontextprotocol/sdk": "^1.29.0",
105
- "@modernized/fluent-ffmpeg": "^1.0.2",
105
+ "@modernized/fluent-ffmpeg": "^1.0.3",
106
106
  "@mozilla/readability": "^0.6.0",
107
- "@mulmocast/deck": "^1.1.0",
107
+ "@mulmocast/deck": "^2.0.0",
108
108
  "@tavily/core": "^0.7.6",
109
109
  "archiver": "^8.0.0",
110
110
  "clipboardy": "^5.3.2",
@@ -115,10 +115,10 @@
115
115
  "marked": "^18.0.7",
116
116
  "mulmocast-vision": "^1.0.12",
117
117
  "ora": "^9.4.1",
118
- "puppeteer": "^25.3.0",
118
+ "puppeteer": "^25.4.0",
119
119
  "replicate": "^1.4.0",
120
120
  "yaml": "^2.9.0",
121
- "yargs": "^18.0.0",
121
+ "yargs": "^18.1.0",
122
122
  "zod": "^4.4.3"
123
123
  },
124
124
  "devDependencies": {
@@ -128,16 +128,17 @@
128
128
  "@types/jsdom": "^28.0.3",
129
129
  "@types/yargs": "^17.0.35",
130
130
  "cross-env": "^10.1.0",
131
+ "esbuild": "^0.28.2",
131
132
  "eslint": "^10.8.0",
132
133
  "eslint-config-prettier": "^10.1.8",
133
134
  "eslint-plugin-import": "^2.32.0",
134
135
  "eslint-plugin-prettier": "^5.5.6",
135
136
  "eslint-plugin-sonarjs": "^4.2.0",
136
- "globals": "^17.7.0",
137
+ "globals": "^17.9.0",
137
138
  "prettier": "^3.9.6",
138
- "tsx": "^4.23.1",
139
+ "tsx": "^4.23.5",
139
140
  "typescript": "6.0.3",
140
- "typescript-eslint": "^8.65.0"
141
+ "typescript-eslint": "^8.66.0"
141
142
  },
142
143
  "engines": {
143
144
  "node": ">=22.0.0"