mulmocast 2.20.0 → 2.22.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 (36) hide show
  1. package/assets/schemas/mulmo_script.json +169 -0
  2. package/lib/data/animationRuntime.d.ts +5 -0
  3. package/lib/data/animationRuntime.js +639 -0
  4. package/lib/methods/mulmo_beat.d.ts +2 -7
  5. package/lib/methods/mulmo_beat.js +1 -12
  6. package/lib/methods/mulmo_beat_node.d.ts +2 -8
  7. package/lib/types/schema.d.ts +264 -0
  8. package/lib/utils/animation_config.d.ts +15 -0
  9. package/lib/utils/animation_config.js +17 -0
  10. package/lib/utils/beat_html/chart.d.ts +7 -11
  11. package/lib/utils/beat_html/chart.js +8 -18
  12. package/lib/utils/beat_html/decoration.d.ts +16 -0
  13. package/lib/utils/beat_html/decoration.js +55 -0
  14. package/lib/utils/beat_html/html_tailwind.d.ts +6 -6
  15. package/lib/utils/beat_html/html_tailwind.js +13 -7
  16. package/lib/utils/beat_html/index.js +5 -4
  17. package/lib/utils/beat_html/mermaid.d.ts +3 -6
  18. package/lib/utils/beat_html/mermaid.js +5 -10
  19. package/lib/utils/beat_html/slide.d.ts +13 -6
  20. package/lib/utils/beat_html/slide.js +8 -21
  21. package/lib/utils/beat_html/type.d.ts +47 -4
  22. package/lib/utils/context.d.ts +62 -0
  23. package/lib/utils/html_render.js +1 -1
  24. package/lib/utils/image_plugins/bg_image_css.d.ts +19 -0
  25. package/lib/utils/image_plugins/bg_image_css.js +75 -0
  26. package/lib/utils/image_plugins/bg_image_util.d.ts +0 -6
  27. package/lib/utils/image_plugins/bg_image_util.js +7 -69
  28. package/lib/utils/image_plugins/chart_html.d.ts +0 -10
  29. package/lib/utils/image_plugins/chart_html.js +5 -31
  30. package/lib/utils/image_plugins/html_tailwind.js +2 -39
  31. package/lib/utils/image_plugins/html_tailwind_script.d.ts +41 -0
  32. package/lib/utils/image_plugins/html_tailwind_script.js +111 -0
  33. package/lib/utils/image_plugins/slide.js +2 -5
  34. package/lib/utils/image_plugins/slide_branding.d.ts +7 -0
  35. package/lib/utils/image_plugins/slide_branding.js +10 -0
  36. package/package.json +2 -2
@@ -1,7 +1,8 @@
1
- import type { MulmoSlideMedia } from "@mulmocast/deck";
1
+ import type { MulmoSlideMedia, ResolvedBranding, SlideLayout, SlideTheme } from "@mulmocast/deck";
2
2
  import type { BeatHtmlFragment, BeatHtmlOptions } from "./type.js";
3
3
  /**
4
- * A slide beat as markup a host can inject.
4
+ * A deck slide as markup a host can inject. Shared by the beat types deck draws: `slide`,
5
+ * and `chart` / `mermaid` as a one-block `figure` slide.
5
6
  *
6
7
  * `generateSlideFragment` is the body-only counterpart of the `generateSlideHTML` the Node
7
8
  * render path uses, so the two draw the same slide from the same layouts — the fragment is
@@ -13,9 +14,15 @@ import type { BeatHtmlFragment, BeatHtmlOptions } from "./type.js";
13
14
  * which the browser bundle does not need.
14
15
  *
15
16
  * 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.
17
+ * css is written against that class, and deck derives chart / mermaid ids from it, so a
18
+ * generated one would change every render. It is not returned separately — deck already puts
19
+ * it on the root of `html`, verified in a browser: injecting `html` and attaching `css`, with
20
+ * no wrapper class of the host's own, resolves `--d-bg` to the beat's theme.
20
21
  */
22
+ /** What the PNG render draws around a slide's layout: the branding layers and the reference line. */
23
+ export type SlideDecoration = {
24
+ branding?: ResolvedBranding;
25
+ reference?: string;
26
+ };
27
+ export declare const deckSlideToHtml: (slide: SlideLayout, beatTheme: SlideTheme | undefined, options: BeatHtmlOptions, decoration?: SlideDecoration) => BeatHtmlFragment;
21
28
  export declare const slideToHtml: (image: MulmoSlideMedia, options: BeatHtmlOptions) => BeatHtmlFragment;
@@ -2,27 +2,10 @@
2
2
  // and 602kb against 20 and 54kb for this module — and this file is bundled for the browser.
3
3
  import { generateSlideFragment } from "@mulmocast/deck/lib/fragment.js";
4
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` });
5
+ import { slideBranding } from "./decoration.js";
6
+ export const deckSlideToHtml = (slide, beatTheme, options, decoration = {}) => {
7
+ const theme = beatTheme ?? options.slideTheme ?? slideThemes.corporate;
8
+ const fragment = generateSlideFragment(theme, slide, { scopeClass: `${options.idPrefix}-slide`, ...decoration });
26
9
  return {
27
10
  html: fragment.html,
28
11
  css: fragment.css,
@@ -31,3 +14,7 @@ export const slideToHtml = (image, options) => {
31
14
  ...(fragment.mermaidTheme ? { mermaidTheme: fragment.mermaidTheme } : {}),
32
15
  };
33
16
  };
17
+ export const slideToHtml = (image, options) => {
18
+ const branding = slideBranding(image.branding, options);
19
+ return deckSlideToHtml(image.slide, image.theme, options, { ...(branding ? { branding } : {}), ...(image.reference ? { reference: image.reference } : {}) });
20
+ };
@@ -1,4 +1,5 @@
1
- import type { SlideTheme } from "@mulmocast/deck";
1
+ import type { SlideBranding, SlideTheme } from "@mulmocast/deck";
2
+ import type { BackgroundImage, MulmoMediaSource } from "../../types/index.js";
2
3
  /**
3
4
  * A beat rendered as markup for a browser host.
4
5
  *
@@ -30,6 +31,27 @@ export type BeatHtmlFragment = {
30
31
  * and a host that does not gets an empty box, silently.
31
32
  */
32
33
  css?: string;
34
+ /**
35
+ * Rules for the whole document, as the PNG render applies them: a textSlide / markdown beat's
36
+ * background image on `html` / `body`, or an animated html_tailwind beat's rules pausing CSS
37
+ * animations and transitions. Absent when the beat needs neither.
38
+ *
39
+ * Apply it only where the beat owns the document (an iframe, a print page). Injected into a
40
+ * page shared with anything else it restyles that page: it cannot be scoped to the markup,
41
+ * because a text beat's markup has no root of its own and the PNG paints the background on
42
+ * `body`, behind the body's margin as well.
43
+ */
44
+ documentCss?: string;
45
+ /**
46
+ * `<script>` elements for the end of the beat's own document: an `html_tailwind` beat's author
47
+ * `script`, swipe driver and animation runtime, which bring it to what the PNG shows — for an
48
+ * animation, the final frame. Absent when the beat runs none.
49
+ *
50
+ * Only for a host that gives the beat a document of its own and lets it run scripts (an iframe
51
+ * with `allow-scripts`); `html` alone is the beat at rest. Not sanitized: the author script is
52
+ * the beat's content, as on the PNG path.
53
+ */
54
+ documentScript?: string;
33
55
  /** External runtimes the host must load once for the page, not once per beat. */
34
56
  requires?: BeatRuntime[];
35
57
  /**
@@ -69,12 +91,33 @@ export type BeatHtmlOptions = {
69
91
  */
70
92
  idPrefix: string;
71
93
  /**
72
- * The deck-level slide theme, for `slide` beats. A beat's own `image.theme` wins over it,
73
- * and the built-in `corporate` theme is the fallback — the same order
94
+ * The deck-level slide theme, for the beats deck draws: `slide`, `chart` and `mermaid`.
95
+ * A slide beat's own `image.theme` wins over it, and the built-in `corporate` theme is the
96
+ * fallback — the same order
74
97
  * `MulmoPresentationStyleMethods.getResolvedSlideTheme` uses on the Node side.
75
98
  *
76
- * Optional because only slide beats read it, and because the host may not have a deck
99
+ * Optional because only those beats read it, and because the host may not have a deck
77
100
  * theme to give.
78
101
  */
79
102
  slideTheme?: SlideTheme;
103
+ /**
104
+ * The deck's `imageParams.backgroundImage`. A beat's own `backgroundImage` wins over it and a
105
+ * beat-level `null` switches it off — the order `resolveBackgroundImage` uses on the Node side.
106
+ * textSlide and markdown beats get it as `documentCss`; chart and mermaid beats, which deck
107
+ * draws as a figure slide, as that slide's background layer.
108
+ */
109
+ backgroundImage?: BackgroundImage;
110
+ /**
111
+ * The deck's `slideParams.branding` (logo and background) for slide beats. A beat's own
112
+ * `branding` wins over it and a beat-level `null` switches it off, as on the Node side.
113
+ */
114
+ branding?: SlideBranding;
115
+ /**
116
+ * Turns a background or branding image into a URL the host's page can load. The default uses
117
+ * a url as written and a path as written, and skips a base64 source — the schema carries no
118
+ * media type to build a `data:` URI from. A host whose scripts sit next to local files
119
+ * resolves paths here. What it returns is percent-encoded where it would otherwise break out
120
+ * of a CSS string or an HTML attribute.
121
+ */
122
+ resolveMediaUrl?: (source: MulmoMediaSource) => string | undefined;
80
123
  };
@@ -2132,6 +2132,37 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
2132
2132
  subtitleSize?: "default" | "big" | "lead" | undefined;
2133
2133
  intro?: "fade" | "fade-up" | "fade-down" | "slide-left" | "slide-right" | "zoom-in" | undefined;
2134
2134
  staggerMs?: number | undefined;
2135
+ } | {
2136
+ title: string;
2137
+ figure: {
2138
+ type: "chart";
2139
+ chartData: Record<string, unknown>;
2140
+ title?: string | undefined;
2141
+ } | {
2142
+ type: "mermaid";
2143
+ code: string;
2144
+ title?: string | undefined;
2145
+ };
2146
+ layout: "figure";
2147
+ stepLabel?: string | undefined;
2148
+ subtitle?: string | undefined;
2149
+ accentColor?: "success" | "info" | "primary" | "accent" | "warning" | "danger" | "highlight" | undefined;
2150
+ style?: {
2151
+ bgColor?: string | undefined;
2152
+ bgGradient?: string | undefined;
2153
+ decorations?: boolean | undefined;
2154
+ bgOpacity?: number | undefined;
2155
+ footer?: string | undefined;
2156
+ } | undefined;
2157
+ eyebrow?: {
2158
+ label: string;
2159
+ color?: "success" | "info" | "primary" | "accent" | "warning" | "danger" | "highlight" | undefined;
2160
+ } | undefined;
2161
+ density?: "default" | "compact" | undefined;
2162
+ titleSize?: "default" | "small" | "large" | "hero" | undefined;
2163
+ subtitleSize?: "default" | "big" | "lead" | undefined;
2164
+ intro?: "fade" | "fade-up" | "fade-down" | "slide-left" | "slide-right" | "zoom-in" | undefined;
2165
+ staggerMs?: number | undefined;
2135
2166
  };
2136
2167
  theme?: {
2137
2168
  colors: {
@@ -4787,6 +4818,37 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
4787
4818
  subtitleSize?: "default" | "big" | "lead" | undefined;
4788
4819
  intro?: "fade" | "fade-up" | "fade-down" | "slide-left" | "slide-right" | "zoom-in" | undefined;
4789
4820
  staggerMs?: number | undefined;
4821
+ } | {
4822
+ title: string;
4823
+ figure: {
4824
+ type: "chart";
4825
+ chartData: Record<string, unknown>;
4826
+ title?: string | undefined;
4827
+ } | {
4828
+ type: "mermaid";
4829
+ code: string;
4830
+ title?: string | undefined;
4831
+ };
4832
+ layout: "figure";
4833
+ stepLabel?: string | undefined;
4834
+ subtitle?: string | undefined;
4835
+ accentColor?: "success" | "info" | "primary" | "accent" | "warning" | "danger" | "highlight" | undefined;
4836
+ style?: {
4837
+ bgColor?: string | undefined;
4838
+ bgGradient?: string | undefined;
4839
+ decorations?: boolean | undefined;
4840
+ bgOpacity?: number | undefined;
4841
+ footer?: string | undefined;
4842
+ } | undefined;
4843
+ eyebrow?: {
4844
+ label: string;
4845
+ color?: "success" | "info" | "primary" | "accent" | "warning" | "danger" | "highlight" | undefined;
4846
+ } | undefined;
4847
+ density?: "default" | "compact" | undefined;
4848
+ titleSize?: "default" | "small" | "large" | "hero" | undefined;
4849
+ subtitleSize?: "default" | "big" | "lead" | undefined;
4850
+ intro?: "fade" | "fade-up" | "fade-down" | "slide-left" | "slide-right" | "zoom-in" | undefined;
4851
+ staggerMs?: number | undefined;
4790
4852
  };
4791
4853
  theme?: {
4792
4854
  colors: {
@@ -7,8 +7,8 @@ import puppeteer from "puppeteer";
7
7
  import { guardRenderPage } from "./render_network_guard.js";
8
8
  import { strictNetworkLaunchArgs, withRenderContentSecurityPolicy } from "./render_network_policy.js";
9
9
  import { GraphAILogger } from "graphai";
10
+ import { VIDEO_LOAD_TIMEOUT_MS } from "./image_plugins/html_tailwind_script.js";
10
11
  const isCI = process.env.CI === "true";
11
- const VIDEO_LOAD_TIMEOUT_MS = 15000;
12
12
  const VIDEO_SEEK_TIMEOUT_MS = 3000;
13
13
  /** Wait for all <video> elements on the page to be ready for playback */
14
14
  const waitForVideosReady = async (page) => {
@@ -0,0 +1,19 @@
1
+ import type { BackgroundImage } from "../../types/index.js";
2
+ /**
3
+ * Background-image rules, without fetching or reading anything. Pure — no Node, no filesystem —
4
+ * because the PNG render and the browser fragment path paint the same background and must
5
+ * resolve and write it the same way.
6
+ */
7
+ /**
8
+ * Resolve background image from beat level and global level settings.
9
+ * Beat level takes precedence over global level.
10
+ * null explicitly disables background image.
11
+ */
12
+ export declare const resolveBackgroundImage: (beatBackgroundImage: BackgroundImage | undefined, globalBackgroundImage: BackgroundImage | undefined) => BackgroundImage | undefined;
13
+ /** How a resolved background is laid out: a plain url string is always `cover` at full opacity. */
14
+ export declare const backgroundImageLayout: (backgroundImage: NonNullable<BackgroundImage>) => {
15
+ size: string;
16
+ opacity: number;
17
+ };
18
+ /** Rules painting `imageUrl` behind the whole document (`html` / `body`), as the PNG render does. */
19
+ export declare const backgroundImageCss: (imageUrl: string, size: string, opacity: number) => string;
@@ -0,0 +1,75 @@
1
+ import { escapeCssString } from "../html_escape.js";
2
+ /**
3
+ * Background-image rules, without fetching or reading anything. Pure — no Node, no filesystem —
4
+ * because the PNG render and the browser fragment path paint the same background and must
5
+ * resolve and write it the same way.
6
+ */
7
+ /**
8
+ * Resolve background image from beat level and global level settings.
9
+ * Beat level takes precedence over global level.
10
+ * null explicitly disables background image.
11
+ */
12
+ export const resolveBackgroundImage = (beatBackgroundImage, globalBackgroundImage) => {
13
+ // null means explicitly disabled
14
+ if (beatBackgroundImage === null) {
15
+ return undefined;
16
+ }
17
+ // Beat level takes precedence
18
+ if (beatBackgroundImage !== undefined) {
19
+ return beatBackgroundImage;
20
+ }
21
+ // Fall back to global
22
+ if (globalBackgroundImage === null) {
23
+ return undefined;
24
+ }
25
+ return globalBackgroundImage;
26
+ };
27
+ /** How a resolved background is laid out: a plain url string is always `cover` at full opacity. */
28
+ export const backgroundImageLayout = (backgroundImage) => {
29
+ if (typeof backgroundImage === "string")
30
+ return { size: "cover", opacity: 1 };
31
+ return { size: backgroundImage.size ?? "cover", opacity: backgroundImage.opacity ?? 1 };
32
+ };
33
+ // Convert size option to CSS background-size value
34
+ const sizeToCSS = (size) => {
35
+ return size === "fill" ? "100% 100%" : size;
36
+ };
37
+ /** Rules painting `imageUrl` behind the whole document (`html` / `body`), as the PNG render does. */
38
+ export const backgroundImageCss = (imageUrl, size, opacity) => {
39
+ const cssSize = sizeToCSS(size);
40
+ // Use pseudo-element for opacity to not affect content
41
+ if (opacity < 1) {
42
+ return `
43
+ html, body {
44
+ height: 100%;
45
+ margin: 0;
46
+ }
47
+ body::before {
48
+ content: '';
49
+ position: fixed;
50
+ top: 0;
51
+ left: 0;
52
+ right: 0;
53
+ bottom: 0;
54
+ background-image: url('${escapeCssString(imageUrl)}');
55
+ background-size: ${cssSize};
56
+ background-position: center;
57
+ background-repeat: no-repeat;
58
+ opacity: ${opacity};
59
+ z-index: -1;
60
+ }
61
+ `;
62
+ }
63
+ return `
64
+ html, body {
65
+ height: 100%;
66
+ margin: 0;
67
+ }
68
+ body {
69
+ background-image: url('${escapeCssString(imageUrl)}');
70
+ background-size: ${cssSize};
71
+ background-position: center;
72
+ background-repeat: no-repeat;
73
+ }
74
+ `;
75
+ };
@@ -1,10 +1,4 @@
1
1
  import { BackgroundImage, MulmoStudioContext, ImageProcessorParams } from "../../types/index.js";
2
- /**
3
- * Resolve background image from beat level and global level settings.
4
- * Beat level takes precedence over global level.
5
- * null explicitly disables background image.
6
- */
7
- export declare const resolveBackgroundImage: (beatBackgroundImage: BackgroundImage | undefined, globalBackgroundImage: BackgroundImage | undefined) => BackgroundImage | undefined;
8
2
  /** The background image comes from the script, the style from the beat; the paths and sizes `process()` needs are not read here. */
9
3
  type CombinedStyleParams = Pick<ImageProcessorParams, "context" | "textSlideStyle">;
10
4
  /**
@@ -1,27 +1,8 @@
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 { escapeCssString, neutralizeStyleTerminator } from "../html_escape.js";
5
- /**
6
- * Resolve background image from beat level and global level settings.
7
- * Beat level takes precedence over global level.
8
- * null explicitly disables background image.
9
- */
10
- export const resolveBackgroundImage = (beatBackgroundImage, globalBackgroundImage) => {
11
- // null means explicitly disabled
12
- if (beatBackgroundImage === null) {
13
- return undefined;
14
- }
15
- // Beat level takes precedence
16
- if (beatBackgroundImage !== undefined) {
17
- return beatBackgroundImage;
18
- }
19
- // Fall back to global
20
- if (globalBackgroundImage === null) {
21
- return undefined;
22
- }
23
- return globalBackgroundImage;
24
- };
4
+ import { neutralizeStyleTerminator } from "../html_escape.js";
5
+ import { backgroundImageCss, backgroundImageLayout, resolveBackgroundImage } from "./bg_image_css.js";
25
6
  /**
26
7
  * Fetch URL and convert to data URL with timeout
27
8
  */
@@ -34,13 +15,6 @@ const fetchUrlAsDataUrl = async (url, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) => {
34
15
  const contentType = response.headers.get("content-type") || "image/png";
35
16
  return `data:${contentType};base64,${buffer.toString("base64")}`;
36
17
  };
37
- /**
38
- * Convert BackgroundImage to CSS string
39
- */
40
- // Convert size option to CSS background-size value
41
- const sizeToCSS = (size) => {
42
- return size === "fill" ? "100% 100%" : size;
43
- };
44
18
  /**
45
19
  * Resolve combined style from background image and custom style.
46
20
  * Common pattern used by markdown, textSlide, mermaid plugins.
@@ -57,45 +31,9 @@ export const backgroundImageToCSS = async (backgroundImage, context) => {
57
31
  if (!backgroundImage) {
58
32
  return "";
59
33
  }
60
- const isSimpleUrl = typeof backgroundImage === "string";
61
- // Escaped at each use rather than here: the base64 body cannot carry a quote, but the
62
- // content-type in front of it is whatever the remote server's response header said.
63
- const imageUrl = isSimpleUrl ? await fetchUrlAsDataUrl(backgroundImage) : await MulmoMediaSourceMethods.toDataUrl(backgroundImage.source, context);
64
- const size = sizeToCSS(isSimpleUrl ? "cover" : (backgroundImage.size ?? "cover"));
65
- const opacity = isSimpleUrl ? 1 : (backgroundImage.opacity ?? 1);
66
- // Use pseudo-element for opacity to not affect content
67
- if (opacity < 1) {
68
- return `
69
- html, body {
70
- height: 100%;
71
- margin: 0;
72
- }
73
- body::before {
74
- content: '';
75
- position: fixed;
76
- top: 0;
77
- left: 0;
78
- right: 0;
79
- bottom: 0;
80
- background-image: url('${escapeCssString(imageUrl)}');
81
- background-size: ${size};
82
- background-position: center;
83
- background-repeat: no-repeat;
84
- opacity: ${opacity};
85
- z-index: -1;
86
- }
87
- `;
88
- }
89
- return `
90
- html, body {
91
- height: 100%;
92
- margin: 0;
93
- }
94
- body {
95
- background-image: url('${escapeCssString(imageUrl)}');
96
- background-size: ${size};
97
- background-position: center;
98
- background-repeat: no-repeat;
99
- }
100
- `;
34
+ // Escaped where it is written (backgroundImageCss) rather than here: the base64 body cannot carry a quote, but
35
+ // the content-type in front of it is whatever the remote server's response header said.
36
+ const imageUrl = typeof backgroundImage === "string" ? await fetchUrlAsDataUrl(backgroundImage) : await MulmoMediaSourceMethods.toDataUrl(backgroundImage.source, context);
37
+ const { size, opacity } = backgroundImageLayout(backgroundImage);
38
+ return backgroundImageCss(imageUrl, size, opacity);
101
39
  };
@@ -4,16 +4,6 @@ export declare const resolveChartPlugins: (chartType: string) => string;
4
4
  export declare const stringifyChartData: (chartData: MulmoChartMedia["chartData"]) => string;
5
5
  /** For a standalone document, where the inline script runs. */
6
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
7
  /**
18
8
  * The two values the PNG/PDF/movie template interpolates from user data, escaped for the
19
9
  * contexts it drops them into: `<h1>${title}</h1>` and `const chartData = ${chart_data};`.
@@ -1,17 +1,12 @@
1
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.
2
+ // 551kb against 1.5kb for this file.
3
3
  import { escapeHtml } from "@mulmocast/deck/lib/utils.js";
4
4
  import { escapeJsonForScript } from "../html_escape.js";
5
5
  import { assertSafeElementId } from "../element_id.js";
6
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.
7
+ * Chart.js markup and plugin resolution for the Node render path. Pure — no Node, no
8
+ * filesystem — so it can be tested without either. The browser path draws charts through
9
+ * `@mulmocast/deck` instead (`beat_html/chart.ts`).
15
10
  */
16
11
  /**
17
12
  * Chart.js plugin CDN URLs keyed by chart type. A Map rather than an object literal: the
@@ -30,8 +25,7 @@ export const resolveChartPlugins = (chartType) => {
30
25
  return `<script src="${cdn}"></script>`;
31
26
  };
32
27
  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. */
28
+ /** A heading and a fixed-height canvas, followed by `trailing` — the driver script. */
35
29
  const chartCard = (title, canvas, trailing) => `
36
30
  <div class="chart-container mb-6">
37
31
  <h3 class="text-xl font-semibold mb-4">${escapeHtml(title || "Chart")}</h3>
@@ -51,26 +45,6 @@ export const chartHtml = (chartDataJson, title, chartId) => {
51
45
  </script>`;
52
46
  return chartCard(title, `<canvas id="${chartId}"></canvas>`, script);
53
47
  };
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
48
  /**
75
49
  * The two values the PNG/PDF/movie template interpolates from user data, escaped for the
76
50
  * contexts it drops them into: `<h1>${title}</h1>` and `const chartData = ${chart_data};`.
@@ -5,8 +5,8 @@ import { getHTMLFile, getJSFile } from "../file.js";
5
5
  import { renderHTMLToImage, interpolate, renderHTMLToFrames, renderHTMLToVideo, renderHTMLToFinalFrame } from "../html_render.js";
6
6
  import { framesToVideo } from "../ffmpeg_utils.js";
7
7
  import { parrotingImagePath } from "./utils.js";
8
- import { swipeElementsToHtml, swipeElementsToScript } from "../swipe_to_html.js";
9
8
  import { htmlTailwindMarkup } from "./html_tailwind_markup.js";
9
+ import { FINAL_FRAME_TOTAL, animationFps, buildUserScript, resolveHtmlAndScript } from "./html_tailwind_script.js";
10
10
  import { MulmoStudioContextMethods } from "../../methods/mulmo_studio_context.js";
11
11
  export const imageType = "html_tailwind";
12
12
  /**
@@ -53,22 +53,6 @@ export const resolveRelativeImagePaths = (html, baseDirPath) => {
53
53
  return `${prefix}${quote}file://${absolutePath}${quote}`;
54
54
  });
55
55
  };
56
- const DEFAULT_ANIMATION_FPS = 30;
57
- /** Join html field into a single string (handles both string and string[]) */
58
- const joinHtml = (html) => {
59
- return Array.isArray(html) ? html.join("\n") : html;
60
- };
61
- const buildUserScript = (script) => {
62
- if (!script)
63
- return "";
64
- const code = Array.isArray(script) ? script.join("\n") : script;
65
- // If user script contains ESM import/export, emit module script so imports work.
66
- const isModule = code.split("\n").some((line) => {
67
- const trimmed = line.trimStart();
68
- return trimmed.startsWith("import ") || trimmed.startsWith("export ");
69
- });
70
- return isModule ? `<script type="module">\n${code}\n</script>` : `<script>\n${code}\n</script>`;
71
- };
72
56
  /**
73
57
  * Resolve relative modelUrl assignment in user scripts to file:// absolute paths.
74
58
  * e.g. const modelUrl = "models/a.glb" -> const modelUrl = "file:///abs/models/a.glb"
@@ -105,25 +89,6 @@ export const resolveRelativeModelPathsInScript = (html, baseDirPath) => {
105
89
  })
106
90
  .join("\n");
107
91
  };
108
- /**
109
- * Resolve HTML and script from beat image data.
110
- * If `elements` (Swipe-style) is provided, convert to HTML + script.
111
- * Otherwise, use raw `html` and `script` fields.
112
- */
113
- const resolveHtmlAndScript = (imageData) => {
114
- if (imageData.elements && Array.isArray(imageData.elements) && imageData.elements.length > 0) {
115
- const html = swipeElementsToHtml(imageData.elements);
116
- const generatedScript = swipeElementsToScript(imageData.elements);
117
- // Merge with user-provided script if any
118
- const userScript = imageData.script ? joinHtml(imageData.script) : "";
119
- const combinedScript = [generatedScript, userScript].filter(Boolean).join("\n");
120
- return { html, script: combinedScript || undefined };
121
- }
122
- return {
123
- html: joinHtml(imageData.html ?? ""),
124
- script: imageData.script,
125
- };
126
- };
127
92
  const getAnimationConfig = (params) => {
128
93
  const { beat } = params;
129
94
  if (!beat.image || beat.image.type !== imageType)
@@ -131,12 +96,10 @@ const getAnimationConfig = (params) => {
131
96
  const animation = beat.image.animation;
132
97
  if (!MulmoBeatMethods.isAnimationEnabled(animation))
133
98
  return null;
134
- const fps = MulmoBeatMethods.isAnimationObject(animation) ? (animation.fps ?? DEFAULT_ANIMATION_FPS) : DEFAULT_ANIMATION_FPS;
99
+ const fps = animationFps(animation);
135
100
  const movie = MulmoBeatMethods.isMovieMode(animation);
136
101
  return { fps, movie };
137
102
  };
138
- /** Large frame count to ensure all animations reach their end state when exact duration is unknown */
139
- const FINAL_FRAME_TOTAL = 9000;
140
103
  /**
141
104
  * Build the animated HTML string from beat data and template.
142
105
  */
@@ -0,0 +1,41 @@
1
+ import { type SwipeElement } from "../swipe_to_html.js";
2
+ /** How long the final frame waits for `<video>` elements to be playable, as the PNG render does. */
3
+ export declare const VIDEO_LOAD_TIMEOUT_MS = 15000;
4
+ /** `tailwind_animated.html`'s rules: CSS animations and transitions stay still, so frames come from `render` alone. */
5
+ export declare const ANIMATED_DOCUMENT_CSS = "*, *::before, *::after {\n animation-play-state: paused !important;\n transition: none !important;\n}";
6
+ /** Large frame count to ensure all animations reach their end state when exact duration is unknown */
7
+ export declare const FINAL_FRAME_TOTAL = 9000;
8
+ /** Frames per second of an enabled `animation` config. */
9
+ export declare const animationFps: (animation: unknown) => number;
10
+ export declare const buildUserScript: (script: string | string[] | undefined) => string;
11
+ /**
12
+ * Resolve HTML and script from beat image data.
13
+ * If `elements` (Swipe-style) is provided, convert to HTML + script.
14
+ * Otherwise, use raw `html` and `script` fields.
15
+ */
16
+ export declare const resolveHtmlAndScript: (imageData: {
17
+ html?: string | string[];
18
+ script?: string | string[];
19
+ elements?: SwipeElement[];
20
+ }) => {
21
+ html: string;
22
+ script: string | string[] | undefined;
23
+ };
24
+ export type HtmlTailwindScriptSource = {
25
+ script?: string | string[];
26
+ elements?: unknown;
27
+ animation?: unknown;
28
+ };
29
+ /**
30
+ * The scripts that bring an html_tailwind beat to the state its PNG shows, as `<script>` elements
31
+ * for the end of the beat's own document; undefined when it runs none.
32
+ *
33
+ * Without `animation` that is the author script and swipe driver, as `tailwind.html` runs them.
34
+ * With it, the runtime, frame state and registration `tailwind_animated.html` loads, then the
35
+ * final frame — what `renderHTMLToFinalFrame` captures. The final frame is drawn on `load`, after
36
+ * a module author script (deferred) has defined its `render`, as Puppeteer calls it after load;
37
+ * scripts a host adds to an already-loaded document draw it once `render` exists (bounded wait).
38
+ */
39
+ export declare const htmlTailwindDocumentScript: (image: HtmlTailwindScriptSource) => string | undefined;
40
+ /** `documentCss` for an html_tailwind beat: the animated template's pause rules, when it animates. */
41
+ export declare const htmlTailwindDocumentCss: (image: HtmlTailwindScriptSource) => string | undefined;