mulmocast 2.9.2 → 2.11.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 (178) hide show
  1. package/lib/actions/image_agents.js +2 -2
  2. package/lib/cli/bin.js +0 -0
  3. package/lib/index.browser.d.ts +7 -0
  4. package/lib/index.browser.js +6 -0
  5. package/lib/methods/mulmo_beat_node.d.ts +3 -3
  6. package/lib/types/const.d.ts +7 -0
  7. package/lib/types/const.js +7 -0
  8. package/lib/types/schema.js +6 -2
  9. package/lib/types/type.d.ts +8 -0
  10. package/lib/utils/beat_html/chart.d.ts +15 -0
  11. package/lib/utils/beat_html/chart.js +21 -0
  12. package/lib/utils/beat_html/html_tailwind.d.ts +19 -0
  13. package/lib/utils/beat_html/html_tailwind.js +20 -0
  14. package/lib/utils/beat_html/index.d.ts +33 -0
  15. package/lib/utils/beat_html/index.js +65 -0
  16. package/lib/utils/beat_html/markdown.d.ts +20 -0
  17. package/lib/utils/beat_html/markdown.js +25 -0
  18. package/lib/utils/beat_html/media.d.ts +14 -0
  19. package/lib/utils/beat_html/media.js +19 -0
  20. package/lib/utils/beat_html/mermaid.d.ts +14 -0
  21. package/lib/utils/beat_html/mermaid.js +22 -0
  22. package/lib/utils/beat_html/slide.d.ts +21 -0
  23. package/lib/utils/beat_html/slide.js +33 -0
  24. package/lib/utils/beat_html/text_slide.d.ts +17 -0
  25. package/lib/utils/beat_html/text_slide.js +24 -0
  26. package/lib/utils/beat_html/type.d.ts +73 -0
  27. package/lib/utils/context.d.ts +24 -0
  28. package/lib/utils/context.js +1 -1
  29. package/lib/utils/element_id.d.ts +26 -0
  30. package/lib/utils/element_id.js +31 -0
  31. package/lib/utils/html_escape.d.ts +14 -0
  32. package/lib/utils/html_escape.js +30 -0
  33. package/lib/utils/image_plugins/beat.d.ts +2 -2
  34. package/lib/utils/image_plugins/bg_image_util.d.ts +4 -1
  35. package/lib/utils/image_plugins/bg_image_util.js +2 -1
  36. package/lib/utils/image_plugins/chart.d.ts +3 -3
  37. package/lib/utils/image_plugins/chart.js +6 -30
  38. package/lib/utils/image_plugins/chart_html.d.ts +24 -0
  39. package/lib/utils/image_plugins/chart_html.js +81 -0
  40. package/lib/utils/image_plugins/html_tailwind.d.ts +3 -3
  41. package/lib/utils/image_plugins/html_tailwind.js +2 -5
  42. package/lib/utils/image_plugins/html_tailwind_markup.d.ts +21 -0
  43. package/lib/utils/image_plugins/html_tailwind_markup.js +8 -0
  44. package/lib/utils/image_plugins/image.d.ts +1 -1
  45. package/lib/utils/image_plugins/index.d.ts +7 -7
  46. package/lib/utils/image_plugins/markdown.d.ts +4 -4
  47. package/lib/utils/image_plugins/markdown.js +41 -30
  48. package/lib/utils/image_plugins/markdown_layout.d.ts +46 -4
  49. package/lib/utils/image_plugins/markdown_layout.js +96 -83
  50. package/lib/utils/image_plugins/media_html.d.ts +16 -0
  51. package/lib/utils/image_plugins/media_html.js +39 -0
  52. package/lib/utils/image_plugins/mermaid.d.ts +5 -4
  53. package/lib/utils/image_plugins/mermaid.js +4 -16
  54. package/lib/utils/image_plugins/mermaid_html.d.ts +22 -0
  55. package/lib/utils/image_plugins/mermaid_html.js +36 -0
  56. package/lib/utils/image_plugins/movie.d.ts +4 -4
  57. package/lib/utils/image_plugins/movie.js +2 -15
  58. package/lib/utils/image_plugins/slide.d.ts +3 -3
  59. package/lib/utils/image_plugins/source.d.ts +2 -2
  60. package/lib/utils/image_plugins/text_slide.d.ts +4 -4
  61. package/lib/utils/image_plugins/utils.d.ts +2 -2
  62. package/lib/utils/image_plugins/vision.d.ts +3 -3
  63. package/lib/utils/image_plugins/voice_over.d.ts +2 -2
  64. package/lib/utils/swipe_to_html.d.ts +18 -0
  65. package/lib/utils/swipe_to_html.js +26 -2
  66. package/package.json +15 -13
  67. package/scripts/test/test_captions_2.json~ +278 -0
  68. package/scripts/test/test_en_portrait.json~ +31 -0
  69. package/scripts/test/test_slide_image_ref_gemini_en.json~ +289 -0
  70. package/scripts/test/test_vertexai.json~ +21 -0
  71. package/lib/agents/image_google_agent.d.ts +0 -5
  72. package/lib/agents/image_google_agent.js +0 -86
  73. package/lib/agents/movie_google_agent.d.ts +0 -9
  74. package/lib/agents/movie_google_agent.js +0 -122
  75. package/lib/agents/mulmo_prompts_agent.d.ts +0 -7
  76. package/lib/agents/mulmo_prompts_agent.js +0 -37
  77. package/lib/agents/prompts_data.d.ts +0 -15
  78. package/lib/agents/prompts_data.js +0 -16
  79. package/lib/agents/tts_nijivoice_agent.d.ts +0 -5
  80. package/lib/agents/tts_nijivoice_agent.js +0 -69
  81. package/lib/agents/validate_mulmo_script_agent.d.ts +0 -17
  82. package/lib/agents/validate_mulmo_script_agent.js +0 -34
  83. package/lib/cli/args.d.ts +0 -15
  84. package/lib/cli/args.js +0 -62
  85. package/lib/cli/cli.d.ts +0 -17
  86. package/lib/cli/cli.js +0 -117
  87. package/lib/cli/run.d.ts +0 -2
  88. package/lib/cli/run.js +0 -3
  89. package/lib/cli/tool-args.d.ts +0 -16
  90. package/lib/cli/tool-args.js +0 -64
  91. package/lib/cli/tool-cli.d.ts +0 -2
  92. package/lib/cli/tool-cli.js +0 -32
  93. package/lib/cli/tool_commands/prompt/builder.d.ts +0 -4
  94. package/lib/cli/tool_commands/prompt/builder.js +0 -11
  95. package/lib/cli/tool_commands/prompt/handler.d.ts +0 -4
  96. package/lib/cli/tool_commands/prompt/handler.js +0 -14
  97. package/lib/cli/tool_commands/prompt/index.d.ts +0 -4
  98. package/lib/cli/tool_commands/prompt/index.js +0 -4
  99. package/lib/cli/tool_commands/schema/builder.d.ts +0 -2
  100. package/lib/cli/tool_commands/schema/builder.js +0 -3
  101. package/lib/cli/tool_commands/schema/handler.d.ts +0 -2
  102. package/lib/cli/tool_commands/schema/handler.js +0 -12
  103. package/lib/cli/tool_commands/schema/index.d.ts +0 -4
  104. package/lib/cli/tool_commands/schema/index.js +0 -4
  105. package/lib/cli/tool_commands/scripting/builder.d.ts +0 -20
  106. package/lib/cli/tool_commands/scripting/builder.js +0 -63
  107. package/lib/cli/tool_commands/scripting/handler.d.ts +0 -12
  108. package/lib/cli/tool_commands/scripting/handler.js +0 -36
  109. package/lib/cli/tool_commands/scripting/index.d.ts +0 -4
  110. package/lib/cli/tool_commands/scripting/index.js +0 -4
  111. package/lib/index.d.ts +0 -8
  112. package/lib/index.js +0 -8
  113. package/lib/methods/mulmo_script_template.d.ts +0 -4
  114. package/lib/methods/mulmo_script_template.js +0 -6
  115. package/lib/methods/mulmo_studio.d.ts +0 -8
  116. package/lib/methods/mulmo_studio.js +0 -25
  117. package/lib/slide/blocks.d.ts +0 -13
  118. package/lib/slide/blocks.js +0 -251
  119. package/lib/slide/index.d.ts +0 -6
  120. package/lib/slide/index.js +0 -7
  121. package/lib/slide/layouts/big_quote.d.ts +0 -2
  122. package/lib/slide/layouts/big_quote.js +0 -19
  123. package/lib/slide/layouts/columns.d.ts +0 -2
  124. package/lib/slide/layouts/columns.js +0 -53
  125. package/lib/slide/layouts/comparison.d.ts +0 -2
  126. package/lib/slide/layouts/comparison.js +0 -25
  127. package/lib/slide/layouts/funnel.d.ts +0 -2
  128. package/lib/slide/layouts/funnel.js +0 -25
  129. package/lib/slide/layouts/grid.d.ts +0 -2
  130. package/lib/slide/layouts/grid.js +0 -38
  131. package/lib/slide/layouts/index.d.ts +0 -3
  132. package/lib/slide/layouts/index.js +0 -46
  133. package/lib/slide/layouts/matrix.d.ts +0 -2
  134. package/lib/slide/layouts/matrix.js +0 -53
  135. package/lib/slide/layouts/split.d.ts +0 -2
  136. package/lib/slide/layouts/split.js +0 -51
  137. package/lib/slide/layouts/stats.d.ts +0 -2
  138. package/lib/slide/layouts/stats.js +0 -23
  139. package/lib/slide/layouts/table.d.ts +0 -2
  140. package/lib/slide/layouts/table.js +0 -10
  141. package/lib/slide/layouts/timeline.d.ts +0 -2
  142. package/lib/slide/layouts/timeline.js +0 -27
  143. package/lib/slide/layouts/title.d.ts +0 -2
  144. package/lib/slide/layouts/title.js +0 -19
  145. package/lib/slide/layouts/waterfall.d.ts +0 -2
  146. package/lib/slide/layouts/waterfall.js +0 -63
  147. package/lib/slide/render.d.ts +0 -17
  148. package/lib/slide/render.js +0 -101
  149. package/lib/slide/schema.d.ts +0 -9309
  150. package/lib/slide/schema.js +0 -434
  151. package/lib/slide/utils.d.ts +0 -76
  152. package/lib/slide/utils.js +0 -243
  153. package/lib/tools/prompt.js +0 -18
  154. package/lib/tools/seed.d.ts +0 -3
  155. package/lib/tools/seed.js +0 -211
  156. package/lib/tools/seed_from_url.d.ts +0 -3
  157. package/lib/tools/seed_from_url.js +0 -188
  158. package/lib/types/slide.d.ts +0 -9309
  159. package/lib/types/slide.js +0 -434
  160. package/lib/utils/assets.d.ts +0 -18
  161. package/lib/utils/assets.js +0 -101
  162. package/lib/utils/cli.d.ts +0 -1
  163. package/lib/utils/cli.js +0 -15
  164. package/lib/utils/const.d.ts +0 -12
  165. package/lib/utils/const.js +0 -12
  166. package/lib/utils/image_preprocess.d.ts +0 -14
  167. package/lib/utils/image_preprocess.js +0 -52
  168. package/lib/utils/markdown.d.ts +0 -3
  169. package/lib/utils/markdown.js +0 -42
  170. package/lib/utils/pdf.d.ts +0 -9
  171. package/lib/utils/pdf.js +0 -77
  172. package/lib/utils/preprocess.d.ts +0 -343
  173. package/lib/utils/preprocess.js +0 -58
  174. package/lib/utils/provider2agent.d.ts +0 -141
  175. package/lib/utils/provider2agent.js +0 -267
  176. package/lib/utils/text_hash.d.ts +0 -1
  177. package/lib/utils/text_hash.js +0 -4
  178. /package/lib/{tools/prompt.d.ts → utils/beat_html/type.js} +0 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The rule for element ids that generated markup interpolates. Pure — no Node, no
3
+ * filesystem — because the Node render path and the browser fragment path build the same
4
+ * markup and must agree on it.
5
+ *
6
+ * This enumerates what is PERMITTED rather than escaping what is not. An id drawn from this
7
+ * set is safe in every context the markup uses it in — an HTML attribute, a JavaScript
8
+ * string literal inside a `<script>` where HTML entities are not decoded, a CSS selector, a
9
+ * URL fragment — so a new context cannot introduce a hole the way a per-context escaper can.
10
+ * Escaping would also have to keep the contexts in sync: entity-escaping `<canvas id>` while
11
+ * the `getElementById` beside it stays raw makes the two disagree and the lookup fail.
12
+ *
13
+ * The leading character is restricted because a CSS id selector is the strictest of those
14
+ * contexts. Measured in a browser: `querySelector("#0")`, `"#-"`, `"#0a"` and `"#-0"` all
15
+ * throw, while `getElementById` accepts every one of them — so an id that works today can
16
+ * still be unusable the moment anything reaches for it with a selector. This set is a little
17
+ * narrower than CSS allows (it rejects `-a`, which is valid) in exchange for being stateable
18
+ * in one line.
19
+ */
20
+ export declare const isSafeElementId: (id: string) => boolean;
21
+ /**
22
+ * Throws unless the id is safe in every context. A caller reaching this with a bad id has a
23
+ * bug, not bad user input: values that come from a script have to be turned into an id
24
+ * before they get here.
25
+ */
26
+ export declare const assertSafeElementId: (id: string) => void;
@@ -0,0 +1,31 @@
1
+ import { SAFE_ELEMENT_ID } from "../types/const.js";
2
+ /**
3
+ * The rule for element ids that generated markup interpolates. Pure — no Node, no
4
+ * filesystem — because the Node render path and the browser fragment path build the same
5
+ * markup and must agree on it.
6
+ *
7
+ * This enumerates what is PERMITTED rather than escaping what is not. An id drawn from this
8
+ * set is safe in every context the markup uses it in — an HTML attribute, a JavaScript
9
+ * string literal inside a `<script>` where HTML entities are not decoded, a CSS selector, a
10
+ * URL fragment — so a new context cannot introduce a hole the way a per-context escaper can.
11
+ * Escaping would also have to keep the contexts in sync: entity-escaping `<canvas id>` while
12
+ * the `getElementById` beside it stays raw makes the two disagree and the lookup fail.
13
+ *
14
+ * The leading character is restricted because a CSS id selector is the strictest of those
15
+ * contexts. Measured in a browser: `querySelector("#0")`, `"#-"`, `"#0a"` and `"#-0"` all
16
+ * throw, while `getElementById` accepts every one of them — so an id that works today can
17
+ * still be unusable the moment anything reaches for it with a selector. This set is a little
18
+ * narrower than CSS allows (it rejects `-a`, which is valid) in exchange for being stateable
19
+ * in one line.
20
+ */
21
+ export const isSafeElementId = (id) => SAFE_ELEMENT_ID.test(id);
22
+ /**
23
+ * Throws unless the id is safe in every context. A caller reaching this with a bad id has a
24
+ * bug, not bad user input: values that come from a script have to be turned into an id
25
+ * before they get here.
26
+ */
27
+ export const assertSafeElementId = (id) => {
28
+ if (!isSafeElementId(id)) {
29
+ throw new Error(`element id must match ${SAFE_ELEMENT_ID.source}, got ${JSON.stringify(id)}. Derive it from the beat's index (beat-3, not 3 — it must not start with a digit), or sanitize the beat's id, before passing it.`);
30
+ }
31
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Escaping for JSON embedded in an inline script. Pure — no Node, no filesystem — because the
3
+ * Node render path and the browser fragment path embed the same values and must neutralize
4
+ * them the same way.
5
+ *
6
+ * HTML text and attributes are handled by escapeHtml from @mulmocast/deck; this module exists
7
+ * only for the inline-script context, which that helper does not cover.
8
+ *
9
+ * The lookup goes through a Map rather than an object literal so a value naming an
10
+ * Object.prototype member cannot resolve through the prototype chain.
11
+ */
12
+ /** Neutralize the output of JSON.stringify for embedding inside an inline `<script>`. */
13
+ export declare const escapeJsonForScript: (json: string) => string;
14
+ export declare const neutralizeStyleTerminator: (css: string) => string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Escaping for JSON embedded in an inline script. Pure — no Node, no filesystem — because the
3
+ * Node render path and the browser fragment path embed the same values and must neutralize
4
+ * them the same way.
5
+ *
6
+ * HTML text and attributes are handled by escapeHtml from @mulmocast/deck; this module exists
7
+ * only for the inline-script context, which that helper does not cover.
8
+ *
9
+ * The lookup goes through a Map rather than an object literal so a value naming an
10
+ * Object.prototype member cannot resolve through the prototype chain.
11
+ */
12
+ // JSON.stringify emits these only from inside string literals, so replacing them with their
13
+ // \u escapes leaves the parsed value identical while stopping `</script>` from terminating the
14
+ // block the JSON is embedded in. U+2028 / U+2029 are valid JSON output but were line
15
+ // terminators in JavaScript source before ES2019.
16
+ const SCRIPT_JSON_ESCAPES = new Map([
17
+ ["<", "\\u003c"],
18
+ [">", "\\u003e"],
19
+ ["&", "\\u0026"],
20
+ ["\u2028", "\\u2028"],
21
+ ["\u2029", "\\u2029"],
22
+ ]);
23
+ /** Neutralize the output of JSON.stringify for embedding inside an inline `<script>`. */
24
+ export const escapeJsonForScript = (json) => json.replace(/[<>&\u2028\u2029]/g, (character) => SCRIPT_JSON_ESCAPES.get(character) ?? character);
25
+ // A <style> element holds raw text: nothing inside it is parsed as markup except the closing
26
+ // tag, so `</style` is the only sequence that can end it. Neutralizing just that keeps
27
+ // author-supplied CSS working, which escaping the whole string would not. `\3c ` is the CSS
28
+ // escape for `<`, and the matched text is carried through unchanged so a `</STYLE>` written
29
+ // inside a CSS string keeps its casing as well as its value.
30
+ export const neutralizeStyleTerminator = (css) => css.replace(/<\/style/gi, (terminator) => `\\3c ${terminator.slice(1)}`);
@@ -1,5 +1,5 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatPathParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "beat";
3
3
  export declare const processBeatReference: (__: ImageProcessorParams) => Promise<undefined>;
4
4
  export declare const process: (__: ImageProcessorParams) => Promise<undefined>;
5
- export declare const path: (__: ImageProcessorParams) => undefined;
5
+ export declare const path: (__: BeatPathParams) => undefined;
@@ -5,9 +5,12 @@ import { BackgroundImage, MulmoStudioContext, ImageProcessorParams } from "../..
5
5
  * null explicitly disables background image.
6
6
  */
7
7
  export declare const resolveBackgroundImage: (beatBackgroundImage: BackgroundImage | undefined, globalBackgroundImage: BackgroundImage | undefined) => BackgroundImage | undefined;
8
+ /** The background image comes from the script, the style from the beat; the paths and sizes `process()` needs are not read here. */
9
+ type CombinedStyleParams = Pick<ImageProcessorParams, "context" | "textSlideStyle">;
8
10
  /**
9
11
  * Resolve combined style from background image and custom style.
10
12
  * Common pattern used by markdown, textSlide, mermaid plugins.
11
13
  */
12
- export declare const resolveCombinedStyle: (params: ImageProcessorParams, beatBackgroundImage: BackgroundImage | undefined, beatStyle: string | undefined) => Promise<string>;
14
+ export declare const resolveCombinedStyle: (params: CombinedStyleParams, beatBackgroundImage: BackgroundImage | undefined, beatStyle: string | undefined) => Promise<string>;
13
15
  export declare const backgroundImageToCSS: (backgroundImage: BackgroundImage | undefined, context: MulmoStudioContext) => Promise<string>;
16
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { MulmoMediaSourceMethods } from "../../methods/mulmo_media_source.js";
2
2
  import { resolveStyle } from "./utils.js";
3
3
  import { safeFetch, DEFAULT_FETCH_TIMEOUT_MS } from "../fetch.js";
4
+ import { neutralizeStyleTerminator } from "../html_escape.js";
4
5
  /**
5
6
  * Resolve background image from beat level and global level settings.
6
7
  * Beat level takes precedence over global level.
@@ -50,7 +51,7 @@ export const resolveCombinedStyle = async (params, beatBackgroundImage, beatStyl
50
51
  const resolvedBackgroundImage = resolveBackgroundImage(beatBackgroundImage, globalBackgroundImage);
51
52
  const backgroundCSS = await backgroundImageToCSS(resolvedBackgroundImage, context);
52
53
  const style = resolveStyle(beatStyle, textSlideStyle);
53
- return backgroundCSS + style;
54
+ return neutralizeStyleTerminator(backgroundCSS + style);
54
55
  };
55
56
  export const backgroundImageToCSS = async (backgroundImage, context) => {
56
57
  if (!backgroundImage) {
@@ -1,5 +1,5 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "chart";
3
3
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
- export declare const path: (params: ImageProcessorParams) => string;
5
- export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
4
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
5
+ export declare const html: (params: BeatRenderParams) => Promise<string | undefined>;
@@ -2,19 +2,8 @@ import { getHTMLFile } from "../file.js";
2
2
  import { renderHTMLToImage, interpolate } from "../html_render.js";
3
3
  import { parrotingImagePath, generateUniqueId } from "./utils.js";
4
4
  import { resolveCombinedStyle } from "./bg_image_util.js";
5
+ import { chartHtml, escapedChartTemplateValues, resolveChartPlugins, stringifyChartData } from "./chart_html.js";
5
6
  export const imageType = "chart";
6
- /** Chart.js plugin CDN URLs keyed by chart type */
7
- const CHART_PLUGIN_CDNS = {
8
- sankey: "https://cdn.jsdelivr.net/npm/chartjs-chart-sankey",
9
- treemap: "https://cdn.jsdelivr.net/npm/chartjs-chart-treemap@3",
10
- };
11
- /** Resolve CDN script tags for Chart.js plugins based on chart type */
12
- const resolveChartPlugins = (chartType) => {
13
- const cdn = CHART_PLUGIN_CDNS[chartType];
14
- if (!cdn)
15
- return "";
16
- return `<script src="${cdn}"></script>`;
17
- };
18
7
  const processChart = async (params) => {
19
8
  const { beat, imagePath, canvasSize } = params;
20
9
  if (!beat.image || beat.image.type !== imageType)
@@ -25,10 +14,9 @@ const processChart = async (params) => {
25
14
  const combinedStyle = await resolveCombinedStyle(params, beat.image.backgroundImage, beat.image.style);
26
15
  const template = getHTMLFile("chart");
27
16
  const htmlData = interpolate(template, {
28
- title: beat.image.title,
17
+ ...escapedChartTemplateValues(beat.image.title, beat.image.chartData),
29
18
  style: combinedStyle,
30
19
  chart_width: chart_width.toString(),
31
- chart_data: JSON.stringify(beat.image.chartData),
32
20
  chart_plugins: resolveChartPlugins(chartType),
33
21
  });
34
22
  await renderHTMLToImage(htmlData, imagePath, canvasSize.width, canvasSize.height);
@@ -38,22 +26,10 @@ const dumpHtml = async (params) => {
38
26
  const { beat } = params;
39
27
  if (!beat.image || beat.image.type !== imageType)
40
28
  return;
41
- const chartData = JSON.stringify(beat.image.chartData, null, 2);
42
- const title = beat.image.title || "Chart";
43
- const chartId = generateUniqueId("chart");
44
- return `
45
- <div class="chart-container mb-6">
46
- <h3 class="text-xl font-semibold mb-4">${title}</h3>
47
- <div class="w-full" style="position: relative; height: 400px;">
48
- <canvas id="${chartId}"></canvas>
49
- </div>
50
- <script>
51
- (function() {
52
- const ctx = document.getElementById('${chartId}').getContext('2d');
53
- new Chart(ctx, ${chartData});
54
- })();
55
- </script>
56
- </div>`;
29
+ // main と同じ評価順(stringify → title 読み取り → id 生成)を保つ。引数式は本体より先に
30
+ // 評価されるので、stringify を wrapper 側でやらないと throw / getter の順序が入れ替わる。
31
+ const chartDataJson = stringifyChartData(beat.image.chartData);
32
+ return chartHtml(chartDataJson, beat.image.title, generateUniqueId("chart"));
57
33
  };
58
34
  export const process = processChart;
59
35
  export const path = parrotingImagePath;
@@ -0,0 +1,24 @@
1
+ import type { MulmoChartMedia } from "../../types/index.js";
2
+ /** Resolve CDN script tags for Chart.js plugins based on chart type */
3
+ export declare const resolveChartPlugins: (chartType: string) => string;
4
+ export declare const stringifyChartData: (chartData: MulmoChartMedia["chartData"]) => string;
5
+ /** For a standalone document, where the inline script runs. */
6
+ export declare const chartHtml: (chartDataJson: string, title: string, chartId: string) => string;
7
+ /**
8
+ * For a host that injects the markup into its own page, where an injected `<script>`
9
+ * neither survives sanitizing nor executes via innerHTML. The config rides on the canvas
10
+ * instead, in the shape `@mulmocast/deck` already uses so one host runtime drives both.
11
+ */
12
+ export declare const chartFragmentHtml: (chartData: MulmoChartMedia["chartData"], title: string, chartId: string) => string;
13
+ /** `chartData` is a free record, so the type may be absent or not a string. */
14
+ export declare const chartTypeOf: (chartData: MulmoChartMedia["chartData"]) => string;
15
+ /** The Chart.js plugin CDNs a chart type needs, for a host that loads them once per page. */
16
+ export declare const chartPluginCdns: (chartType: string) => string[];
17
+ /**
18
+ * The two values the PNG/PDF/movie template interpolates from user data, escaped for the
19
+ * contexts it drops them into: `<h1>${title}</h1>` and `const chartData = ${chart_data};`.
20
+ */
21
+ export declare const escapedChartTemplateValues: (title: string, chartData: MulmoChartMedia["chartData"]) => {
22
+ title: string;
23
+ chart_data: string;
24
+ };
@@ -0,0 +1,81 @@
1
+ // Deep import: the package barrel pulls in every layout and all of zod, which measured
2
+ // 551kb against 1.5kb for this file, and the browser fragment path bundles this module.
3
+ import { escapeHtml } from "@mulmocast/deck/lib/utils.js";
4
+ import { escapeJsonForScript } from "../html_escape.js";
5
+ import { assertSafeElementId } from "../element_id.js";
6
+ /**
7
+ * Chart.js markup and plugin resolution. Pure — no Node, no filesystem — because both the
8
+ * Node render path and the browser fragment path draw the same chart and must draw it the
9
+ * same way.
10
+ *
11
+ * The canvas id is a parameter rather than generated here: the Node path renders once to a
12
+ * PNG so a random id is fine, while the browser path re-renders and needs the same beat to
13
+ * produce the same markup, or a host diffing fragments sees every chart change identity on
14
+ * every render.
15
+ */
16
+ /**
17
+ * Chart.js plugin CDN URLs keyed by chart type. A Map rather than an object literal: the
18
+ * chart type comes from the script, so an object lookup resolves `constructor` and
19
+ * `__proto__` through the prototype chain and hands back a function.
20
+ */
21
+ const CHART_PLUGIN_CDNS = new Map([
22
+ ["sankey", "https://cdn.jsdelivr.net/npm/chartjs-chart-sankey"],
23
+ ["treemap", "https://cdn.jsdelivr.net/npm/chartjs-chart-treemap@3"],
24
+ ]);
25
+ /** Resolve CDN script tags for Chart.js plugins based on chart type */
26
+ export const resolveChartPlugins = (chartType) => {
27
+ const cdn = CHART_PLUGIN_CDNS.get(chartType);
28
+ if (!cdn)
29
+ return "";
30
+ return `<script src="${cdn}"></script>`;
31
+ };
32
+ export const stringifyChartData = (chartData) => JSON.stringify(chartData, null, 2);
33
+ /** The card both paths draw: a heading and a fixed-height canvas. `trailing` is where the
34
+ * document path puts its driver script; the fragment path leaves it empty. */
35
+ const chartCard = (title, canvas, trailing) => `
36
+ <div class="chart-container mb-6">
37
+ <h3 class="text-xl font-semibold mb-4">${escapeHtml(title || "Chart")}</h3>
38
+ <div class="w-full" style="position: relative; height: 400px;">
39
+ ${canvas}
40
+ </div>${trailing}
41
+ </div>`;
42
+ /** For a standalone document, where the inline script runs. */
43
+ export const chartHtml = (chartDataJson, title, chartId) => {
44
+ assertSafeElementId(chartId);
45
+ const script = `
46
+ <script>
47
+ (function() {
48
+ const ctx = document.getElementById('${chartId}').getContext('2d');
49
+ new Chart(ctx, ${escapeJsonForScript(chartDataJson)});
50
+ })();
51
+ </script>`;
52
+ return chartCard(title, `<canvas id="${chartId}"></canvas>`, script);
53
+ };
54
+ /**
55
+ * For a host that injects the markup into its own page, where an injected `<script>`
56
+ * neither survives sanitizing nor executes via innerHTML. The config rides on the canvas
57
+ * instead, in the shape `@mulmocast/deck` already uses so one host runtime drives both.
58
+ */
59
+ export const chartFragmentHtml = (chartData, title, chartId) => {
60
+ assertSafeElementId(chartId);
61
+ const config = escapeHtml(JSON.stringify(chartData));
62
+ return chartCard(title, `<canvas id="${chartId}" data-chart-ready="false" data-mulmo-chart="${config}"></canvas>`, "");
63
+ };
64
+ /** `chartData` is a free record, so the type may be absent or not a string. */
65
+ export const chartTypeOf = (chartData) => {
66
+ const type = chartData?.type;
67
+ return typeof type === "string" ? type : "";
68
+ };
69
+ /** The Chart.js plugin CDNs a chart type needs, for a host that loads them once per page. */
70
+ export const chartPluginCdns = (chartType) => {
71
+ const cdn = CHART_PLUGIN_CDNS.get(chartType);
72
+ return cdn ? [cdn] : [];
73
+ };
74
+ /**
75
+ * The two values the PNG/PDF/movie template interpolates from user data, escaped for the
76
+ * contexts it drops them into: `<h1>${title}</h1>` and `const chartData = ${chart_data};`.
77
+ */
78
+ export const escapedChartTemplateValues = (title, chartData) => ({
79
+ title: escapeHtml(title),
80
+ chart_data: escapeJsonForScript(JSON.stringify(chartData)),
81
+ });
@@ -1,4 +1,4 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "html_tailwind";
3
3
  /**
4
4
  * Resolve image:name references to file:// absolute paths using imageRefs.
@@ -21,5 +21,5 @@ export declare const resolveRelativeImagePaths: (html: string, baseDirPath: stri
21
21
  */
22
22
  export declare const resolveRelativeModelPathsInScript: (html: string, baseDirPath: string) => string;
23
23
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
24
- export declare const path: (params: ImageProcessorParams) => string;
25
- export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
24
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
25
+ export declare const html: (params: BeatRenderParams) => Promise<string | undefined>;
@@ -6,6 +6,7 @@ import { renderHTMLToImage, interpolate, renderHTMLToFrames, renderHTMLToVideo,
6
6
  import { framesToVideo } from "../ffmpeg_utils.js";
7
7
  import { parrotingImagePath } from "./utils.js";
8
8
  import { swipeElementsToHtml, swipeElementsToScript } from "../swipe_to_html.js";
9
+ import { htmlTailwindMarkup } from "./html_tailwind_markup.js";
9
10
  export const imageType = "html_tailwind";
10
11
  /**
11
12
  * Resolve image:name references to file:// absolute paths using imageRefs.
@@ -229,11 +230,7 @@ const dumpHtml = async (params) => {
229
230
  const { beat } = params;
230
231
  if (!beat.image || beat.image.type !== imageType)
231
232
  return;
232
- const imageData = beat.image;
233
- if (imageData.elements && Array.isArray(imageData.elements) && imageData.elements.length > 0) {
234
- return swipeElementsToHtml(imageData.elements);
235
- }
236
- return joinHtml(imageData.html ?? "");
233
+ return htmlTailwindMarkup(beat.image);
237
234
  };
238
235
  export const process = processHtmlTailwind;
239
236
  export const path = parrotingImagePath;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The static markup an html_tailwind beat shows. Pure — no Node, no filesystem — because the
3
+ * Node dump path and the browser fragment path show the same thing.
4
+ *
5
+ * Deliberately generates no `<script>`: the author's `script` FIELD, the swipe animation
6
+ * driver and the frame-based `animation` all need script execution, which the dump path has
7
+ * never emitted and which a fragment injected through `innerHTML` could not run anyway.
8
+ * Both paths show the beat's markup at rest.
9
+ *
10
+ * A `<script>` the author wrote inside `html` is another thing entirely: it is markup, and
11
+ * it passes through, because this beat type is raw author markup by design.
12
+ *
13
+ * The author's `html` is emitted as written. It is markup by design on this beat type — the
14
+ * schema also accepts a `script` — so escaping it would break the feature rather than
15
+ * protect anything the author does not already have.
16
+ */
17
+ export type HtmlTailwindMarkupSource = {
18
+ html?: string | string[];
19
+ elements?: unknown;
20
+ };
21
+ export declare const htmlTailwindMarkup: (image: HtmlTailwindMarkupSource) => string;
@@ -0,0 +1,8 @@
1
+ import { isSwipeElements, swipeElementsToHtml } from "../swipe_to_html.js";
2
+ export const htmlTailwindMarkup = (image) => {
3
+ if (isSwipeElements(image.elements) && image.elements.length > 0) {
4
+ return swipeElementsToHtml(image.elements);
5
+ }
6
+ const html = image.html ?? "";
7
+ return Array.isArray(html) ? html.join("\n") : html;
8
+ };
@@ -1,3 +1,3 @@
1
1
  export declare const imageType = "image";
2
2
  export declare const process: (params: import("../../index.common.js").ImageProcessorParams) => Promise<string | undefined>;
3
- export declare const path: (params: import("../../index.common.js").ImageProcessorParams) => string | undefined;
3
+ export declare const path: (params: import("../../index.common.js").BeatPathParams) => string | undefined;
@@ -1,15 +1,15 @@
1
- import { ImageProcessorParams, MulmoBeat } from "../../types/index.js";
1
+ import { BeatPathParams, BeatRenderParams, ImageProcessorParams, MulmoBeat } from "../../types/index.js";
2
2
  export declare const findImagePlugin: (imageType?: string) => {
3
3
  imageType: string;
4
4
  process: (params: ImageProcessorParams) => Promise<string | undefined> | void;
5
- path: (params: ImageProcessorParams) => string | undefined;
6
- markdown?: (params: ImageProcessorParams) => string | undefined;
7
- html?: (params: ImageProcessorParams) => Promise<string | undefined>;
5
+ path: (params: BeatPathParams) => string | undefined;
6
+ markdown?: (params: BeatRenderParams) => string | undefined;
7
+ html?: (params: BeatRenderParams) => Promise<string | undefined>;
8
8
  } | undefined;
9
9
  export declare const getBeatPlugin: (beat: MulmoBeat) => {
10
10
  imageType: string;
11
11
  process: (params: ImageProcessorParams) => Promise<string | undefined> | void;
12
- path: (params: ImageProcessorParams) => string | undefined;
13
- markdown?: (params: ImageProcessorParams) => string | undefined;
14
- html?: (params: ImageProcessorParams) => Promise<string | undefined>;
12
+ path: (params: BeatPathParams) => string | undefined;
13
+ markdown?: (params: BeatRenderParams) => string | undefined;
14
+ html?: (params: BeatRenderParams) => Promise<string | undefined>;
15
15
  };
@@ -1,6 +1,6 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "markdown";
3
3
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
- export declare const path: (params: ImageProcessorParams) => string;
5
- export declare const markdown: (params: ImageProcessorParams) => string | undefined;
6
- export declare const html: (params: ImageProcessorParams) => Promise<string>;
4
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
5
+ export declare const markdown: (params: BeatRenderParams) => string | undefined;
6
+ export declare const html: (params: BeatRenderParams) => Promise<string>;
@@ -1,10 +1,16 @@
1
1
  import { getHTMLFile } from "../file.js";
2
2
  import { renderHTMLToImage, interpolate } from "../html_render.js";
3
- import { parrotingImagePath } from "./utils.js";
3
+ import { parrotingImagePath, generateUniqueId } from "./utils.js";
4
4
  import { resolveCombinedStyle } from "./bg_image_util.js";
5
- import { generateLayoutHtml, layoutToMarkdown, toMarkdownString, parseMarkdown } from "./markdown_layout.js";
5
+ import { renderMarkdownContent, renderMarkdownLayout, layoutToMarkdown, toMarkdownString } from "./markdown_layout.js";
6
6
  import { resolveImageRefs, resolveMovieRefs } from "./html_tailwind.js";
7
7
  import { isObject } from "graphai";
8
+ /**
9
+ * This path renders once to a PNG, so a random element id is fine. The browser path needs
10
+ * a stable one and supplies its own — that generator is the only thing the two callers of
11
+ * the shared markdown renderer differ on.
12
+ */
13
+ const nodeMermaidId = () => generateUniqueId("mermaid");
8
14
  export const imageType = "markdown";
9
15
  // Type guard for object (data) format
10
16
  const isMarkdownLayout = (md) => {
@@ -23,48 +29,54 @@ const dumpMarkdown = (params) => {
23
29
  // object (data) format
24
30
  return layoutToMarkdown(md);
25
31
  };
26
- // Generate full HTML for rendering
32
+ /**
33
+ * Full HTML for rendering, and whether it contains a diagram.
34
+ *
35
+ * The two come back together on purpose. There used to be a second detector —
36
+ * `containsMermaid`, a `text.includes("```mermaid")` over the raw source — deciding
37
+ * whether to load the mermaid runtime, while the renderer decided what to draw. Once the
38
+ * renderer started asking marked's lexer, the two disagreed: a ```MERMAID fence drew a
39
+ * diagram that no runtime was loaded for, so the PNG came out with an empty box, and an
40
+ * example quoted inside an outer ````markdown fence loaded the runtime for nothing.
41
+ *
42
+ * One question, one answer. The renderer already knows, so it says.
43
+ */
27
44
  const generateHtml = async (params) => {
28
45
  const { beat } = params;
29
46
  if (!beat.image || beat.image.type !== imageType)
30
- return "";
47
+ return { html: "", hasMermaid: false };
31
48
  const md = beat.image.markdown;
32
49
  const combinedStyle = await resolveCombinedStyle(params, beat.image.backgroundImage, beat.image.style);
33
50
  if (isMarkdownLayout(md)) {
34
- const htmlBody = await generateLayoutHtml(md);
51
+ const { html: htmlBody, hasMermaid } = renderMarkdownLayout(md, nodeMermaidId);
35
52
  const template = getHTMLFile("tailwind");
36
- return interpolate(template, {
37
- title: "Markdown Layout",
38
- html_body: htmlBody,
39
- custom_style: combinedStyle,
40
- });
53
+ return {
54
+ html: interpolate(template, { title: "Markdown Layout", html_body: htmlBody, custom_style: combinedStyle }),
55
+ hasMermaid,
56
+ };
41
57
  }
42
- const markdown = dumpMarkdown(params) ?? "";
43
- const body = await parseMarkdown(markdown);
44
- // Use tailwind template if mermaid is present to ensure mermaid CDN is loaded
45
- if (containsMermaid(md)) {
58
+ const { html: body, hasMermaid } = renderMarkdownContent(dumpMarkdown(params) ?? "", nodeMermaidId);
59
+ // The tailwind template is what loads the mermaid CDN, so a diagram needs it.
60
+ if (hasMermaid) {
46
61
  const template = getHTMLFile("tailwind");
47
- return interpolate(template, {
48
- title: "Markdown",
49
- html_body: `<div class="prose max-w-none p-6">${body}</div>`,
50
- custom_style: combinedStyle,
51
- });
62
+ return {
63
+ html: interpolate(template, {
64
+ title: "Markdown",
65
+ html_body: `<div class="prose max-w-none p-6">${body}</div>`,
66
+ custom_style: combinedStyle,
67
+ }),
68
+ hasMermaid,
69
+ };
52
70
  }
53
- return `<html><head><style>${combinedStyle}</style></head><body>${body}</body></html>`;
54
- };
55
- // Check if markdown content contains mermaid code blocks
56
- const containsMermaid = (md) => {
57
- const text = isMarkdownLayout(md) ? layoutToMarkdown(md) : toMarkdownString(md);
58
- return text.includes("```mermaid");
71
+ return { html: `<html><head><style>${combinedStyle}</style></head><body>${body}</body></html>`, hasMermaid };
59
72
  };
60
73
  const processMarkdown = async (params) => {
61
74
  const { beat, imagePath, canvasSize } = params;
62
75
  if (!beat.image || beat.image.type !== imageType)
63
76
  return;
64
- const rawHtml = await generateHtml(params);
77
+ const { html: rawHtml, hasMermaid } = await generateHtml(params);
65
78
  const resolvedImages = resolveImageRefs(rawHtml, params.imageRefs ?? {});
66
79
  const html = resolveMovieRefs(resolvedImages, params.movieRefs ?? {});
67
- const hasMermaid = containsMermaid(beat.image.markdown);
68
80
  await renderHTMLToImage(html, imagePath, canvasSize.width, canvasSize.height, hasMermaid);
69
81
  return imagePath;
70
82
  };
@@ -74,11 +86,10 @@ const dumpHtml = async (params) => {
74
86
  return "";
75
87
  const md = beat.image.markdown;
76
88
  if (isMarkdownLayout(md)) {
77
- return await generateLayoutHtml(md);
89
+ return renderMarkdownLayout(md, nodeMermaidId).html;
78
90
  }
79
91
  else {
80
- const markdown = dumpMarkdown(params);
81
- return await parseMarkdown(markdown ?? "");
92
+ return renderMarkdownContent(dumpMarkdown(params) ?? "", nodeMermaidId).html;
82
93
  }
83
94
  };
84
95
  export const process = processMarkdown;
@@ -1,6 +1,48 @@
1
1
  import { type MulmoMarkdownLayout } from "../../types/type.js";
2
- declare const toMarkdownString: (content: string | string[]) => string;
3
- declare const parseMarkdown: (content: string | string[]) => Promise<string>;
4
- export declare const generateLayoutHtml: (md: MulmoMarkdownLayout) => Promise<string>;
2
+ /**
3
+ * The one markdown implementation, shared by the Node render path
4
+ * (`image_plugins/markdown.ts`, which turns it into a PNG) and the browser fragment path
5
+ * (`beat_html/markdown.ts`, which hands it to a host's DOM).
6
+ *
7
+ * Deliberately pure: no `node:*`, no filesystem, no puppeteer. That is what lets the
8
+ * browser side import it rather than keep a second copy — and a second copy is not
9
+ * hypothetical, it existed for one commit and the two immediately disagreed about which
10
+ * fences are diagrams. `test/beat_html/test_browser_safety.ts` bundles the browser entry
11
+ * with esbuild and fails if anything here reaches Node.
12
+ *
13
+ * Element ids come from a caller-supplied generator, because that is the only thing the
14
+ * two callers genuinely need to differ on.
15
+ */
16
+ /** A markdown field is written either as one string or as lines to be joined. */
17
+ export declare const toMarkdownString: (content: string | string[]) => string;
18
+ /**
19
+ * Renders markdown, turning fenced `mermaid` blocks into mermaid markup.
20
+ *
21
+ * Asked of marked's own lexer rather than matched against the raw source. A regex over
22
+ * text was what both copies used, and review found four inputs it got wrong: a CRLF fence
23
+ * and a fence with trailing whitespace after the language were missed, while a mermaid
24
+ * fence quoted INSIDE an outer ````markdown fence, and one inside an indented code block,
25
+ * were converted — turning somebody's example of mermaid syntax into an actual diagram.
26
+ * marked decides what is a code block and what language it is, so all four follow from
27
+ * asking it.
28
+ *
29
+ * A fresh `Marked` instance per call rather than the global `marked.use`, because the
30
+ * renderer closes over this call's mermaid counter — a shared one would leak ids between
31
+ * beats.
32
+ */
33
+ export declare const renderMarkdownContent: (content: string | string[], nextId: () => string) => {
34
+ html: string;
35
+ hasMermaid: boolean;
36
+ };
37
+ /** The object form of `markdown`, as opposed to a plain string or array of lines. */
38
+ export declare const isMarkdownLayout: (md: unknown) => md is MulmoMarkdownLayout;
39
+ /** Lay a markdown layout out in Tailwind: header / sidebar-left / row-2 | 2x2 | content. */
40
+ export declare const renderMarkdownLayout: (md: MulmoMarkdownLayout, nextId: () => string) => {
41
+ html: string;
42
+ hasMermaid: boolean;
43
+ };
44
+ /**
45
+ * A layout flattened back to plain markdown, in slot order. Used by `mulmo markdown`,
46
+ * which wants what the beat SAYS rather than how it is laid out.
47
+ */
5
48
  export declare const layoutToMarkdown: (md: MulmoMarkdownLayout) => string;
6
- export { toMarkdownString, parseMarkdown };