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.
- package/assets/schemas/mulmo_script.json +169 -0
- package/lib/data/animationRuntime.d.ts +5 -0
- package/lib/data/animationRuntime.js +639 -0
- package/lib/methods/mulmo_beat.d.ts +2 -7
- package/lib/methods/mulmo_beat.js +1 -12
- package/lib/methods/mulmo_beat_node.d.ts +2 -8
- package/lib/types/schema.d.ts +264 -0
- package/lib/utils/animation_config.d.ts +15 -0
- package/lib/utils/animation_config.js +17 -0
- package/lib/utils/beat_html/chart.d.ts +7 -11
- package/lib/utils/beat_html/chart.js +8 -18
- package/lib/utils/beat_html/decoration.d.ts +16 -0
- package/lib/utils/beat_html/decoration.js +55 -0
- package/lib/utils/beat_html/html_tailwind.d.ts +6 -6
- package/lib/utils/beat_html/html_tailwind.js +13 -7
- package/lib/utils/beat_html/index.js +5 -4
- package/lib/utils/beat_html/mermaid.d.ts +3 -6
- package/lib/utils/beat_html/mermaid.js +5 -10
- package/lib/utils/beat_html/slide.d.ts +13 -6
- package/lib/utils/beat_html/slide.js +8 -21
- package/lib/utils/beat_html/type.d.ts +47 -4
- package/lib/utils/context.d.ts +62 -0
- package/lib/utils/html_render.js +1 -1
- package/lib/utils/image_plugins/bg_image_css.d.ts +19 -0
- package/lib/utils/image_plugins/bg_image_css.js +75 -0
- package/lib/utils/image_plugins/bg_image_util.d.ts +0 -6
- package/lib/utils/image_plugins/bg_image_util.js +7 -69
- package/lib/utils/image_plugins/chart_html.d.ts +0 -10
- package/lib/utils/image_plugins/chart_html.js +5 -31
- package/lib/utils/image_plugins/html_tailwind.js +2 -39
- package/lib/utils/image_plugins/html_tailwind_script.d.ts +41 -0
- package/lib/utils/image_plugins/html_tailwind_script.js +111 -0
- package/lib/utils/image_plugins/slide.js +2 -5
- package/lib/utils/image_plugins/slide_branding.d.ts +7 -0
- package/lib/utils/image_plugins/slide_branding.js +10 -0
- 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
|
|
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
|
|
17
|
-
*
|
|
18
|
-
* root of `html`, verified in a browser: injecting `html` and attaching `css`, with
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
73
|
-
* and the built-in `corporate` theme is the
|
|
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
|
|
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
|
};
|
package/lib/utils/context.d.ts
CHANGED
|
@@ -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: {
|
package/lib/utils/html_render.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
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
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
/**
|
|
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 =
|
|
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;
|