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
@@ -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,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) {
@@ -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
+ });
@@ -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,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 };