mulmocast 2.9.1 → 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 (48) hide show
  1. package/lib/index.browser.d.ts +7 -0
  2. package/lib/index.browser.js +6 -0
  3. package/lib/types/const.d.ts +7 -0
  4. package/lib/types/const.js +7 -0
  5. package/lib/types/schema.js +6 -2
  6. package/lib/utils/beat_html/chart.d.ts +15 -0
  7. package/lib/utils/beat_html/chart.js +21 -0
  8. package/lib/utils/beat_html/html_tailwind.d.ts +19 -0
  9. package/lib/utils/beat_html/html_tailwind.js +20 -0
  10. package/lib/utils/beat_html/index.d.ts +33 -0
  11. package/lib/utils/beat_html/index.js +65 -0
  12. package/lib/utils/beat_html/markdown.d.ts +20 -0
  13. package/lib/utils/beat_html/markdown.js +25 -0
  14. package/lib/utils/beat_html/media.d.ts +14 -0
  15. package/lib/utils/beat_html/media.js +19 -0
  16. package/lib/utils/beat_html/mermaid.d.ts +14 -0
  17. package/lib/utils/beat_html/mermaid.js +22 -0
  18. package/lib/utils/beat_html/slide.d.ts +21 -0
  19. package/lib/utils/beat_html/slide.js +33 -0
  20. package/lib/utils/beat_html/text_slide.d.ts +17 -0
  21. package/lib/utils/beat_html/text_slide.js +24 -0
  22. package/lib/utils/beat_html/type.d.ts +73 -0
  23. package/lib/utils/beat_html/type.js +1 -0
  24. package/lib/utils/element_id.d.ts +26 -0
  25. package/lib/utils/element_id.js +31 -0
  26. package/lib/utils/filters.js +11 -2
  27. package/lib/utils/html_escape.d.ts +14 -0
  28. package/lib/utils/html_escape.js +30 -0
  29. package/lib/utils/image_plugins/bg_image_util.js +2 -1
  30. package/lib/utils/image_plugins/chart.js +6 -30
  31. package/lib/utils/image_plugins/chart_html.d.ts +24 -0
  32. package/lib/utils/image_plugins/chart_html.js +81 -0
  33. package/lib/utils/image_plugins/html_tailwind.js +2 -5
  34. package/lib/utils/image_plugins/html_tailwind_markup.d.ts +21 -0
  35. package/lib/utils/image_plugins/html_tailwind_markup.js +8 -0
  36. package/lib/utils/image_plugins/markdown.js +41 -30
  37. package/lib/utils/image_plugins/markdown_layout.d.ts +46 -4
  38. package/lib/utils/image_plugins/markdown_layout.js +96 -83
  39. package/lib/utils/image_plugins/media_html.d.ts +16 -0
  40. package/lib/utils/image_plugins/media_html.js +39 -0
  41. package/lib/utils/image_plugins/mermaid.d.ts +1 -0
  42. package/lib/utils/image_plugins/mermaid.js +4 -16
  43. package/lib/utils/image_plugins/mermaid_html.d.ts +22 -0
  44. package/lib/utils/image_plugins/mermaid_html.js +36 -0
  45. package/lib/utils/image_plugins/movie.js +2 -15
  46. package/lib/utils/swipe_to_html.d.ts +18 -0
  47. package/lib/utils/swipe_to_html.js +26 -2
  48. package/package.json +11 -10
@@ -1,3 +1,10 @@
1
1
  export * from "./index.common.js";
2
+ /**
3
+ * Beat markup for a browser host. Exported from the browser entry point rather than the
4
+ * common one because `test_browser_safety.ts` guards this module's import graph — a Node
5
+ * import creeping in fails there, not at a user's build.
6
+ */
7
+ export { beatToHtml, supportedBeatTypes } from "./utils/beat_html/index.js";
8
+ export type { BeatHtmlFragment, BeatHtmlOptions, BeatRuntime } from "./utils/beat_html/type.js";
2
9
  import validateSchemaAgent from "./agents/validate_schema_agent.js";
3
10
  export { validateSchemaAgent };
@@ -1,4 +1,10 @@
1
1
  // Entry point for browser code
2
2
  export * from "./index.common.js";
3
+ /**
4
+ * Beat markup for a browser host. Exported from the browser entry point rather than the
5
+ * common one because `test_browser_safety.ts` guards this module's import graph — a Node
6
+ * import creeping in fails there, not at a user's build.
7
+ */
8
+ export { beatToHtml, supportedBeatTypes } from "./utils/beat_html/index.js";
3
9
  import validateSchemaAgent from "./agents/validate_schema_agent.js";
4
10
  export { validateSchemaAgent };
@@ -13,3 +13,10 @@ export declare const storyToScriptGenerateMode: {
13
13
  export declare const bundleTargetLang: string[];
14
14
  export declare const ASPECT_RATIOS: string[];
15
15
  export declare const PRO_ASPECT_RATIOS: string[];
16
+ /**
17
+ * Element ids that generated markup interpolates. Restricting the set — rather than escaping
18
+ * per context — is what makes one id safe in an HTML attribute, a JavaScript string literal
19
+ * inside a `<script>`, and a CSS selector at once. Lives here because `schema.ts` compiles
20
+ * standalone in the types package and can only reach files under `src/types/`.
21
+ */
22
+ export declare const SAFE_ELEMENT_ID: RegExp;
@@ -13,3 +13,10 @@ export const storyToScriptGenerateMode = {
13
13
  export const bundleTargetLang = ["ja", "en"];
14
14
  export const ASPECT_RATIOS = ["1:1", "9:16", "16:9"];
15
15
  export const PRO_ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
16
+ /**
17
+ * Element ids that generated markup interpolates. Restricting the set — rather than escaping
18
+ * per context — is what makes one id safe in an HTML attribute, a JavaScript string literal
19
+ * inside a `<script>`, and a CSS selector at once. Lives here because `schema.ts` compiles
20
+ * standalone in the types package and can only reach files under `src/types/`.
21
+ */
22
+ export const SAFE_ELEMENT_ID = /^[A-Za-z_][A-Za-z0-9_-]*$/;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { htmlLLMProvider, provider2TTSAgent, provider2ImageAgent, provider2MovieAgent, defaultProviders, provider2SoundEffectAgent } from "./provider2agent.js";
3
- import { currentMulmoScriptVersion } from "./const.js";
3
+ import { currentMulmoScriptVersion, SAFE_ELEMENT_ID } from "./const.js";
4
4
  import { mulmoVideoFilterSchema } from "./schema_video_filter.js";
5
5
  import { mulmoSlideMediaSchema, slideThemeSchema, slideBrandingSchema } from "@mulmocast/deck";
6
6
  // Re-export video filter schema
@@ -248,7 +248,11 @@ const swipeShadowSchema = z
248
248
  .strict();
249
249
  export const swipeElementSchema = z.lazy(() => z
250
250
  .object({
251
- id: z.string().optional(),
251
+ id: z
252
+ .string()
253
+ .regex(SAFE_ELEMENT_ID, "must start with a letter or underscore, then letters, digits, hyphens or underscores")
254
+ .optional()
255
+ .describe("Element id. Used as an HTML id and as a CSS selector in the generated animation script, so it must match [A-Za-z_][A-Za-z0-9_-]*."),
252
256
  // Position & size
253
257
  x: swipePositionValue.optional(),
254
258
  y: swipePositionValue.optional(),
@@ -0,0 +1,15 @@
1
+ import type { MulmoChartMedia } from "../../types/index.js";
2
+ import type { BeatHtmlFragment } from "./type.js";
3
+ /**
4
+ * A chart beat as markup a host can inject.
5
+ *
6
+ * The config rides on the canvas as `data-mulmo-chart` rather than in an inline `<script>`:
7
+ * a script injected through `innerHTML` does not execute, and does not survive sanitizing
8
+ * either. The attribute shape matches `@mulmocast/deck`'s, so a host that already drives
9
+ * `[data-mulmo-chart]` for slides drives this too without a second code path.
10
+ *
11
+ * The markup itself comes from `image_plugins/chart_html.ts`, the same module the Node
12
+ * render path uses — a second copy of it would drift, and the two paths have to draw the
13
+ * same chart.
14
+ */
15
+ export declare const chartToHtml: (image: MulmoChartMedia, idPrefix: string) => BeatHtmlFragment;
@@ -0,0 +1,21 @@
1
+ import { chartFragmentHtml, chartPluginCdns, chartTypeOf } from "../image_plugins/chart_html.js";
2
+ /**
3
+ * A chart beat as markup a host can inject.
4
+ *
5
+ * The config rides on the canvas as `data-mulmo-chart` rather than in an inline `<script>`:
6
+ * a script injected through `innerHTML` does not execute, and does not survive sanitizing
7
+ * either. The attribute shape matches `@mulmocast/deck`'s, so a host that already drives
8
+ * `[data-mulmo-chart]` for slides drives this too without a second code path.
9
+ *
10
+ * The markup itself comes from `image_plugins/chart_html.ts`, the same module the Node
11
+ * render path uses — a second copy of it would drift, and the two paths have to draw the
12
+ * same chart.
13
+ */
14
+ export const chartToHtml = (image, idPrefix) => {
15
+ const plugins = chartPluginCdns(chartTypeOf(image.chartData));
16
+ return {
17
+ html: chartFragmentHtml(image.chartData, image.title, `${idPrefix}-chart`),
18
+ requires: ["chart"],
19
+ ...(plugins.length > 0 ? { chartPlugins: plugins } : {}),
20
+ };
21
+ };
@@ -0,0 +1,19 @@
1
+ import type { z } from "zod";
2
+ import type { mulmoHtmlTailwindMediaSchema } from "../../types/schema.js";
3
+ import type { BeatHtmlFragment } from "./type.js";
4
+ /**
5
+ * An html_tailwind beat as markup a host can inject.
6
+ *
7
+ * The beat at rest — the same thing the Node dump path shows. This module generates no
8
+ * `<script>`: the author's `script` FIELD, the swipe animation driver and the frame-based
9
+ * `animation` all need script execution, which a fragment injected through `innerHTML`
10
+ * cannot do and which the dump path has never emitted either. A `<script>` the author wrote
11
+ * inside `html` is markup, and passes through unsanitized — see below. A beat whose only content is an animation therefore shows
12
+ * its elements in their starting positions rather than nothing.
13
+ *
14
+ * The author's html is emitted as written. On this beat type markup IS the content — the
15
+ * schema also accepts `script` — so escaping it would remove the feature rather than a
16
+ * capability the author lacks. `BeatHtmlFragment.html` says the markup is not sanitized;
17
+ * this is the beat type where that matters most.
18
+ */
19
+ export declare const htmlTailwindToHtml: (image: z.infer<typeof mulmoHtmlTailwindMediaSchema>) => BeatHtmlFragment | undefined;
@@ -0,0 +1,20 @@
1
+ import { htmlTailwindMarkup } from "../image_plugins/html_tailwind_markup.js";
2
+ /**
3
+ * An html_tailwind beat as markup a host can inject.
4
+ *
5
+ * The beat at rest — the same thing the Node dump path shows. This module generates no
6
+ * `<script>`: the author's `script` FIELD, the swipe animation driver and the frame-based
7
+ * `animation` all need script execution, which a fragment injected through `innerHTML`
8
+ * cannot do and which the dump path has never emitted either. A `<script>` the author wrote
9
+ * inside `html` is markup, and passes through unsanitized — see below. A beat whose only content is an animation therefore shows
10
+ * its elements in their starting positions rather than nothing.
11
+ *
12
+ * The author's html is emitted as written. On this beat type markup IS the content — the
13
+ * schema also accepts `script` — so escaping it would remove the feature rather than a
14
+ * capability the author lacks. `BeatHtmlFragment.html` says the markup is not sanitized;
15
+ * this is the beat type where that matters most.
16
+ */
17
+ export const htmlTailwindToHtml = (image) => {
18
+ const html = htmlTailwindMarkup(image);
19
+ return html === "" ? undefined : { html };
20
+ };
@@ -0,0 +1,33 @@
1
+ import type { MulmoBeat } from "../../types/index.js";
2
+ import type { BeatHtmlFragment, BeatHtmlOptions } from "./type.js";
3
+ export type { BeatHtmlFragment, BeatHtmlOptions, BeatRuntime } from "./type.js";
4
+ export { textSlideToMarkdown, textSlideToHtml } from "./text_slide.js";
5
+ export { markdownToHtml } from "./markdown.js";
6
+ /**
7
+ * Beat image types this module can turn into markup, extended one type at a time.
8
+ * `test_dispatch.ts` asserts this list and `beatToHtml` agree in both directions, so
9
+ * a type added here without a case — or vice versa — fails rather than silently
10
+ * rendering nothing.
11
+ */
12
+ export declare const supportedBeatTypes: readonly ["textSlide", "markdown", "chart", "mermaid", "image", "movie", "slide", "html_tailwind"];
13
+ /**
14
+ * Render a beat as markup for a browser host.
15
+ *
16
+ * **The markup is not sanitized** — see `BeatHtmlFragment.html`. Sanitize before inserting
17
+ * it into a DOM.
18
+ *
19
+ * `options.idPrefix` is required and must match `[A-Za-z_][A-Za-z0-9_-]*`: fragments generate
20
+ * element ids, only the caller knows which beat this is, and a beat's own `id` comes from
21
+ * the script and is unrestricted. Throws on anything else rather than guessing a repair,
22
+ * because a guess can collapse two beats onto one id. See `BeatHtmlOptions`.
23
+ *
24
+ * Returns `undefined` for a beat this module cannot render — a type not yet supported,
25
+ * or one whose media it cannot reach from a browser (a local file path, say). Callers
26
+ * decide what to show in its place; this module does not invent a placeholder.
27
+ *
28
+ * Everything here must stay browser-safe: no `node:*`, no filesystem, no
29
+ * `MulmoStudioContext`. `test_browser_safety.ts` walks the import graph and fails on
30
+ * anything that would drag Node into a bundle — the failure mode is a runtime error in
31
+ * the browser, not a compile error, so it has to be checked mechanically.
32
+ */
33
+ export declare const beatToHtml: (beat: MulmoBeat, options: BeatHtmlOptions) => BeatHtmlFragment | undefined;
@@ -0,0 +1,65 @@
1
+ import { textSlideToHtml } from "./text_slide.js";
2
+ import { chartToHtml } from "./chart.js";
3
+ import { markdownToHtml } from "./markdown.js";
4
+ import { htmlTailwindToHtml } from "./html_tailwind.js";
5
+ import { imageToHtml, movieToHtml } from "./media.js";
6
+ import { mermaidToHtml } from "./mermaid.js";
7
+ import { slideToHtml } from "./slide.js";
8
+ import { assertSafeElementId } from "../element_id.js";
9
+ export { textSlideToMarkdown, textSlideToHtml } from "./text_slide.js";
10
+ export { markdownToHtml } from "./markdown.js";
11
+ /**
12
+ * Beat image types this module can turn into markup, extended one type at a time.
13
+ * `test_dispatch.ts` asserts this list and `beatToHtml` agree in both directions, so
14
+ * a type added here without a case — or vice versa — fails rather than silently
15
+ * rendering nothing.
16
+ */
17
+ export const supportedBeatTypes = ["textSlide", "markdown", "chart", "mermaid", "image", "movie", "slide", "html_tailwind"];
18
+ /**
19
+ * Render a beat as markup for a browser host.
20
+ *
21
+ * **The markup is not sanitized** — see `BeatHtmlFragment.html`. Sanitize before inserting
22
+ * it into a DOM.
23
+ *
24
+ * `options.idPrefix` is required and must match `[A-Za-z_][A-Za-z0-9_-]*`: fragments generate
25
+ * element ids, only the caller knows which beat this is, and a beat's own `id` comes from
26
+ * the script and is unrestricted. Throws on anything else rather than guessing a repair,
27
+ * because a guess can collapse two beats onto one id. See `BeatHtmlOptions`.
28
+ *
29
+ * Returns `undefined` for a beat this module cannot render — a type not yet supported,
30
+ * or one whose media it cannot reach from a browser (a local file path, say). Callers
31
+ * decide what to show in its place; this module does not invent a placeholder.
32
+ *
33
+ * Everything here must stay browser-safe: no `node:*`, no filesystem, no
34
+ * `MulmoStudioContext`. `test_browser_safety.ts` walks the import graph and fails on
35
+ * anything that would drag Node into a bundle — the failure mode is a runtime error in
36
+ * the browser, not a compile error, so it has to be checked mechanically.
37
+ */
38
+ export const beatToHtml = (beat, options) => {
39
+ const image = beat.image;
40
+ if (!image)
41
+ return undefined;
42
+ assertSafeElementId(options.idPrefix);
43
+ switch (image.type) {
44
+ case "textSlide":
45
+ return textSlideToHtml(image);
46
+ case "markdown":
47
+ return markdownToHtml(image, options.idPrefix);
48
+ case "chart":
49
+ return chartToHtml(image, options.idPrefix);
50
+ case "mermaid":
51
+ return mermaidToHtml(image, options.idPrefix);
52
+ case "image":
53
+ // The only description of the picture this module can reach. A host with more should
54
+ // set its own alt; an <img> with none makes a screen reader read the file name.
55
+ return imageToHtml(image, beat.description ?? beat.text ?? "");
56
+ case "movie":
57
+ return movieToHtml(image);
58
+ case "slide":
59
+ return slideToHtml(image, options);
60
+ case "html_tailwind":
61
+ return htmlTailwindToHtml(image);
62
+ default:
63
+ return undefined;
64
+ }
65
+ };
@@ -0,0 +1,20 @@
1
+ import type { MulmoMarkdownMedia } from "../../types/index.js";
2
+ import type { BeatHtmlFragment } from "./type.js";
3
+ /**
4
+ * A `markdown` beat as markup for a browser host.
5
+ *
6
+ * The rendering itself is `image_plugins/markdown_layout.ts` — the same code the Node
7
+ * render path uses, not a second implementation of it. This file supplies the two things
8
+ * that are actually browser-specific: deterministic element ids, and the `requires`
9
+ * report the host needs.
10
+ *
11
+ * Either markdown shape can embed a mermaid fence, which is why this can come back
12
+ * requiring the mermaid runtime. A markdown beat is not obviously a diagram beat, and a
13
+ * host that only loads mermaid for `mermaid` beats would render the diagram as nothing.
14
+ * `requires` and the markup come from the same decision inside the renderer, so they
15
+ * cannot disagree about what is a diagram.
16
+ *
17
+ * `idPrefix` has no default on purpose: a page-wide default gives two markdown beats the
18
+ * same element ids, which is invalid HTML and sends mermaid at the wrong element.
19
+ */
20
+ export declare const markdownToHtml: (image: MulmoMarkdownMedia, idPrefix: string) => BeatHtmlFragment;
@@ -0,0 +1,25 @@
1
+ import { renderMarkdownContent, renderMarkdownLayout, isMarkdownLayout } from "../image_plugins/markdown_layout.js";
2
+ /**
3
+ * A `markdown` beat as markup for a browser host.
4
+ *
5
+ * The rendering itself is `image_plugins/markdown_layout.ts` — the same code the Node
6
+ * render path uses, not a second implementation of it. This file supplies the two things
7
+ * that are actually browser-specific: deterministic element ids, and the `requires`
8
+ * report the host needs.
9
+ *
10
+ * Either markdown shape can embed a mermaid fence, which is why this can come back
11
+ * requiring the mermaid runtime. A markdown beat is not obviously a diagram beat, and a
12
+ * host that only loads mermaid for `mermaid` beats would render the diagram as nothing.
13
+ * `requires` and the markup come from the same decision inside the renderer, so they
14
+ * cannot disagree about what is a diagram.
15
+ *
16
+ * `idPrefix` has no default on purpose: a page-wide default gives two markdown beats the
17
+ * same element ids, which is invalid HTML and sends mermaid at the wrong element.
18
+ */
19
+ export const markdownToHtml = (image, idPrefix) => {
20
+ let counter = 0;
21
+ const nextId = () => `${idPrefix}-mermaid-${counter++}`;
22
+ const md = image.markdown;
23
+ const { html, hasMermaid } = isMarkdownLayout(md) ? renderMarkdownLayout(md, nextId) : renderMarkdownContent(md, nextId);
24
+ return { html, ...(hasMermaid ? { requires: ["mermaid"] } : {}) };
25
+ };
@@ -0,0 +1,14 @@
1
+ import type { MulmoImageMedia, MulmoMovieMedia } from "../../types/index.js";
2
+ import type { BeatHtmlFragment } from "./type.js";
3
+ /**
4
+ * Image and movie beats as markup a host can inject.
5
+ *
6
+ * The src comes straight from the beat's source: a url is used as written, a path is
7
+ * emitted for the host to resolve against its own page, and a base64 source renders
8
+ * nothing because the schema carries no media type to build a `data:` URI from.
9
+ *
10
+ * Neither needs a runtime, so neither sets `requires` — an `<img>` and a `<video>` are
11
+ * markup the browser already knows.
12
+ */
13
+ export declare const imageToHtml: (image: MulmoImageMedia, alt: string) => BeatHtmlFragment | undefined;
14
+ export declare const movieToHtml: (image: MulmoMovieMedia) => BeatHtmlFragment | undefined;
@@ -0,0 +1,19 @@
1
+ import { imageHtml, mediaSrc, movieHtml } from "../image_plugins/media_html.js";
2
+ /**
3
+ * Image and movie beats as markup a host can inject.
4
+ *
5
+ * The src comes straight from the beat's source: a url is used as written, a path is
6
+ * emitted for the host to resolve against its own page, and a base64 source renders
7
+ * nothing because the schema carries no media type to build a `data:` URI from.
8
+ *
9
+ * Neither needs a runtime, so neither sets `requires` — an `<img>` and a `<video>` are
10
+ * markup the browser already knows.
11
+ */
12
+ export const imageToHtml = (image, alt) => {
13
+ const src = mediaSrc(image.source);
14
+ return src === undefined ? undefined : { html: imageHtml(src, alt) };
15
+ };
16
+ export const movieToHtml = (image) => {
17
+ const src = mediaSrc(image.source);
18
+ return src === undefined ? undefined : { html: movieHtml(src) };
19
+ };
@@ -0,0 +1,14 @@
1
+ import type { MulmoMermaidMedia } from "../../types/index.js";
2
+ import type { BeatHtmlFragment } from "./type.js";
3
+ /**
4
+ * A mermaid beat as markup a host can inject.
5
+ *
6
+ * Only `code.kind === "text"` renders. The other kinds — `url`, `path`, `base64` — are
7
+ * resolved by `MulmoMediaSourceMethods.getText`, which fetches and reads files, so they
8
+ * cannot be reached from here and `beatToHtml` returns undefined rather than inventing a
9
+ * placeholder. `mermaid.ts`'s own markdown dump already draws the same line.
10
+ *
11
+ * The markup comes from `image_plugins/mermaid_html.ts`, the module the Node render path
12
+ * uses, so the browser and the PNG draw the same diagram.
13
+ */
14
+ export declare const mermaidToHtml: (image: MulmoMermaidMedia, idPrefix: string) => BeatHtmlFragment | undefined;
@@ -0,0 +1,22 @@
1
+ import { mermaidHtml } from "../image_plugins/mermaid_html.js";
2
+ /**
3
+ * A mermaid beat as markup a host can inject.
4
+ *
5
+ * Only `code.kind === "text"` renders. The other kinds — `url`, `path`, `base64` — are
6
+ * resolved by `MulmoMediaSourceMethods.getText`, which fetches and reads files, so they
7
+ * cannot be reached from here and `beatToHtml` returns undefined rather than inventing a
8
+ * placeholder. `mermaid.ts`'s own markdown dump already draws the same line.
9
+ *
10
+ * The markup comes from `image_plugins/mermaid_html.ts`, the module the Node render path
11
+ * uses, so the browser and the PNG draw the same diagram.
12
+ */
13
+ export const mermaidToHtml = (image, idPrefix) => {
14
+ if (image.code.kind !== "text")
15
+ return undefined;
16
+ const appendix = image.appendix?.join("\n") ?? "";
17
+ const code = `${image.code.text}\n${appendix}`.trim();
18
+ return {
19
+ html: mermaidHtml(code, `${idPrefix}-mermaid`, image.title || "Diagram"),
20
+ requires: ["mermaid"],
21
+ };
22
+ };
@@ -0,0 +1,21 @@
1
+ import type { MulmoSlideMedia } from "@mulmocast/deck";
2
+ import type { BeatHtmlFragment, BeatHtmlOptions } from "./type.js";
3
+ /**
4
+ * A slide beat as markup a host can inject.
5
+ *
6
+ * `generateSlideFragment` is the body-only counterpart of the `generateSlideHTML` the Node
7
+ * render path uses, so the two draw the same slide from the same layouts — the fragment is
8
+ * not a second renderer.
9
+ *
10
+ * The theme order matches `MulmoPresentationStyleMethods.getResolvedSlideTheme` on the Node
11
+ * side: the beat's own theme, then the deck's, then the built-in corporate one. Repeating
12
+ * the order here rather than calling that method keeps this file off `MulmoPresentationStyle`,
13
+ * which the browser bundle does not need.
14
+ *
15
+ * The scope class is derived from `idPrefix` so a re-render of the same beat keeps it: the
16
+ * css is written against that class and a generated one would change every render, leaving
17
+ * the rules matching nothing. It is not returned separately — deck already puts it on the
18
+ * root of `html`, verified in a browser: injecting `html` and attaching `css`, with no
19
+ * wrapper class of the host's own, resolves `--d-bg` to the beat's theme.
20
+ */
21
+ export declare const slideToHtml: (image: MulmoSlideMedia, options: BeatHtmlOptions) => BeatHtmlFragment;
@@ -0,0 +1,33 @@
1
+ // Deep import: the package barrel bundles every layout plus all of zod — measured 101 inputs
2
+ // and 602kb against 20 and 54kb for this module — and this file is bundled for the browser.
3
+ import { generateSlideFragment } from "@mulmocast/deck/lib/fragment.js";
4
+ import { slideThemes } from "../../data/slideThemes.js";
5
+ /**
6
+ * A slide beat as markup a host can inject.
7
+ *
8
+ * `generateSlideFragment` is the body-only counterpart of the `generateSlideHTML` the Node
9
+ * render path uses, so the two draw the same slide from the same layouts — the fragment is
10
+ * not a second renderer.
11
+ *
12
+ * The theme order matches `MulmoPresentationStyleMethods.getResolvedSlideTheme` on the Node
13
+ * side: the beat's own theme, then the deck's, then the built-in corporate one. Repeating
14
+ * the order here rather than calling that method keeps this file off `MulmoPresentationStyle`,
15
+ * which the browser bundle does not need.
16
+ *
17
+ * The scope class is derived from `idPrefix` so a re-render of the same beat keeps it: the
18
+ * css is written against that class and a generated one would change every render, leaving
19
+ * the rules matching nothing. It is not returned separately — deck already puts it on the
20
+ * root of `html`, verified in a browser: injecting `html` and attaching `css`, with no
21
+ * wrapper class of the host's own, resolves `--d-bg` to the beat's theme.
22
+ */
23
+ export const slideToHtml = (image, options) => {
24
+ const theme = image.theme ?? options.slideTheme ?? slideThemes.corporate;
25
+ const fragment = generateSlideFragment(theme, image.slide, { scopeClass: `${options.idPrefix}-slide` });
26
+ return {
27
+ html: fragment.html,
28
+ css: fragment.css,
29
+ ...(fragment.requires.length > 0 ? { requires: fragment.requires } : {}),
30
+ ...(fragment.chartPlugins.length > 0 ? { chartPlugins: fragment.chartPlugins } : {}),
31
+ ...(fragment.mermaidTheme ? { mermaidTheme: fragment.mermaidTheme } : {}),
32
+ };
33
+ };
@@ -0,0 +1,17 @@
1
+ import type { MulmoTextSlideMedia } from "../../types/index.js";
2
+ import type { BeatHtmlFragment } from "./type.js";
3
+ /**
4
+ * A textSlide as markdown: title, then subtitle, then bullets.
5
+ * Kept separate from the HTML so `mulmo markdown` and `mulmo html` can share one
6
+ * definition of what a textSlide *says*, independently of how it is rendered.
7
+ */
8
+ export declare const textSlideToMarkdown: (image: MulmoTextSlideMedia) => string;
9
+ /**
10
+ * `async: false` keeps `parse` returning a string rather than a promise, so a browser
11
+ * host can render a beat inside a computed property instead of an effect.
12
+ *
13
+ * The result is NOT sanitized: marked renders raw HTML through by design, so a title of
14
+ * `<script>alert(1)</script>` reaches the caller intact. That is the documented contract
15
+ * on `BeatHtmlFragment.html`, and `test_text_slide.ts` pins it in both directions.
16
+ */
17
+ export declare const textSlideToHtml: (image: MulmoTextSlideMedia) => BeatHtmlFragment;
@@ -0,0 +1,24 @@
1
+ import { marked } from "marked";
2
+ /**
3
+ * A textSlide as markdown: title, then subtitle, then bullets.
4
+ * Kept separate from the HTML so `mulmo markdown` and `mulmo html` can share one
5
+ * definition of what a textSlide *says*, independently of how it is rendered.
6
+ */
7
+ export const textSlideToMarkdown = (image) => {
8
+ const { slide } = image;
9
+ const title = slide.title ? `# ${slide.title}\n` : "";
10
+ const subtitle = slide.subtitle ? `## ${slide.subtitle}\n` : "";
11
+ const bullets = (slide.bullets ?? []).map((text) => `- ${text}`).join("\n");
12
+ return `${title}${subtitle}${bullets}`;
13
+ };
14
+ /**
15
+ * `async: false` keeps `parse` returning a string rather than a promise, so a browser
16
+ * host can render a beat inside a computed property instead of an effect.
17
+ *
18
+ * The result is NOT sanitized: marked renders raw HTML through by design, so a title of
19
+ * `<script>alert(1)</script>` reaches the caller intact. That is the documented contract
20
+ * on `BeatHtmlFragment.html`, and `test_text_slide.ts` pins it in both directions.
21
+ */
22
+ export const textSlideToHtml = (image) => ({
23
+ html: marked.parse(textSlideToMarkdown(image), { async: false }),
24
+ });
@@ -0,0 +1,73 @@
1
+ import type { SlideTheme } from "@mulmocast/deck";
2
+ /**
3
+ * A beat rendered as markup for a browser host.
4
+ *
5
+ * Deliberately body-only. Tailwind, Chart.js, mermaid and fonts are shared across
6
+ * beats, so emitting them per beat means N duplicate loads in a list view — which is
7
+ * why `requires` names what the host needs instead of pulling it in. `src/actions/html.ts`
8
+ * already works this way: it puts those CDNs in the document head once.
9
+ */
10
+ export type BeatHtmlFragment = {
11
+ /**
12
+ * Body markup only — no `<html>`, `<head>` or `<style>`, and no `<script>` that this
13
+ * module generated. An author's own script is a different matter: one written into an
14
+ * `html_tailwind` beat's markup, into raw HTML inside a `markdown` beat, or into a
15
+ * `textSlide` field arrives here verbatim, because nothing below sanitizes.
16
+ *
17
+ * **Not sanitized.** A beat is user data and `marked` does not sanitize: raw elements,
18
+ * `onerror=` handlers and `javascript:` URLs written into a beat survive into this
19
+ * string. Sanitize before inserting it into a DOM. Sanitizing here instead would be
20
+ * worse than useless — it would read as a safety guarantee this module cannot make,
21
+ * since it has no DOM and must stay dependency-light to remain bundleable.
22
+ */
23
+ html: string;
24
+ /** Rules the markup needs, scoped by the caller. Absent when the markup needs none. */
25
+ css?: string;
26
+ /** External runtimes the host must load once for the page, not once per beat. */
27
+ requires?: BeatRuntime[];
28
+ /**
29
+ * Extra Chart.js plugin CDNs this beat's chart type needs, on top of Chart.js itself.
30
+ * Same shape as `@mulmocast/deck`'s `SlideFragment.chartPlugins`, so one host can load
31
+ * for both. Absent when the beat needs none.
32
+ */
33
+ chartPlugins?: string[];
34
+ /**
35
+ * Which mermaid theme suits this beat's background. A dark slide with a light diagram is
36
+ * unreadable, and the host initialises mermaid once for the page, so it has to be told.
37
+ * Absent unless the beat both requires mermaid and has an opinion.
38
+ */
39
+ mermaidTheme?: "dark" | "default";
40
+ };
41
+ /** External runtimes a fragment can depend on. */
42
+ export type BeatRuntime = "chart" | "mermaid";
43
+ export type BeatHtmlOptions = {
44
+ /**
45
+ * Prefix for element ids generated inside a fragment (mermaid containers, and chart
46
+ * canvases later). Required, and deliberately so.
47
+ *
48
+ * A page-wide default was the first design, and review found what it does: two markdown
49
+ * beats each start their counter at zero, so both emit `<div id="…-mermaid-0">`. That is
50
+ * invalid HTML and sends mermaid at whichever element it finds first. There is no value
51
+ * a library can pick that is unique per beat AND stable across re-renders — only the
52
+ * caller knows which beat this is, so the caller says.
53
+ *
54
+ * Must match `[A-Za-z_][A-Za-z0-9_-]*`: the id ends up in an HTML attribute and, for some
55
+ * beat types, in a JavaScript string literal inside a `<script>` where HTML entities are
56
+ * not decoded, and it has to stay usable from a CSS selector. Restricting the set is what
57
+ * makes one id safe in all of them — see `element_id.ts`.
58
+ *
59
+ * A beat's `id` comes from the script and is not restricted, so pass something derived
60
+ * from its index — `beat-3`, not `3`, since it must not start with a digit — or sanitize
61
+ * the id yourself. `beatToHtml` throws rather than guessing.
62
+ */
63
+ idPrefix: string;
64
+ /**
65
+ * The deck-level slide theme, for `slide` beats. A beat's own `image.theme` wins over it,
66
+ * and the built-in `corporate` theme is the fallback — the same order
67
+ * `MulmoPresentationStyleMethods.getResolvedSlideTheme` uses on the Node side.
68
+ *
69
+ * Optional because only slide beats read it, and because the host may not have a deck
70
+ * theme to give.
71
+ */
72
+ slideTheme?: SlideTheme;
73
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ };