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.
- package/lib/index.browser.d.ts +7 -0
- package/lib/index.browser.js +6 -0
- package/lib/types/const.d.ts +7 -0
- package/lib/types/const.js +7 -0
- package/lib/types/schema.js +6 -2
- package/lib/utils/beat_html/chart.d.ts +15 -0
- package/lib/utils/beat_html/chart.js +21 -0
- package/lib/utils/beat_html/html_tailwind.d.ts +19 -0
- package/lib/utils/beat_html/html_tailwind.js +20 -0
- package/lib/utils/beat_html/index.d.ts +33 -0
- package/lib/utils/beat_html/index.js +65 -0
- package/lib/utils/beat_html/markdown.d.ts +20 -0
- package/lib/utils/beat_html/markdown.js +25 -0
- package/lib/utils/beat_html/media.d.ts +14 -0
- package/lib/utils/beat_html/media.js +19 -0
- package/lib/utils/beat_html/mermaid.d.ts +14 -0
- package/lib/utils/beat_html/mermaid.js +22 -0
- package/lib/utils/beat_html/slide.d.ts +21 -0
- package/lib/utils/beat_html/slide.js +33 -0
- package/lib/utils/beat_html/text_slide.d.ts +17 -0
- package/lib/utils/beat_html/text_slide.js +24 -0
- package/lib/utils/beat_html/type.d.ts +73 -0
- package/lib/utils/beat_html/type.js +1 -0
- package/lib/utils/element_id.d.ts +26 -0
- package/lib/utils/element_id.js +31 -0
- package/lib/utils/filters.js +11 -2
- package/lib/utils/html_escape.d.ts +14 -0
- package/lib/utils/html_escape.js +30 -0
- package/lib/utils/image_plugins/bg_image_util.js +2 -1
- package/lib/utils/image_plugins/chart.js +6 -30
- package/lib/utils/image_plugins/chart_html.d.ts +24 -0
- package/lib/utils/image_plugins/chart_html.js +81 -0
- package/lib/utils/image_plugins/html_tailwind.js +2 -5
- package/lib/utils/image_plugins/html_tailwind_markup.d.ts +21 -0
- package/lib/utils/image_plugins/html_tailwind_markup.js +8 -0
- package/lib/utils/image_plugins/markdown.js +41 -30
- package/lib/utils/image_plugins/markdown_layout.d.ts +46 -4
- package/lib/utils/image_plugins/markdown_layout.js +96 -83
- package/lib/utils/image_plugins/media_html.d.ts +16 -0
- package/lib/utils/image_plugins/media_html.js +39 -0
- package/lib/utils/image_plugins/mermaid.d.ts +1 -0
- package/lib/utils/image_plugins/mermaid.js +4 -16
- package/lib/utils/image_plugins/mermaid_html.d.ts +22 -0
- package/lib/utils/image_plugins/mermaid_html.js +36 -0
- package/lib/utils/image_plugins/movie.js +2 -15
- package/lib/utils/swipe_to_html.d.ts +18 -0
- package/lib/utils/swipe_to_html.js +26 -2
- package/package.json +11 -10
package/lib/index.browser.d.ts
CHANGED
|
@@ -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 };
|
package/lib/index.browser.js
CHANGED
|
@@ -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 };
|
package/lib/types/const.d.ts
CHANGED
|
@@ -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;
|
package/lib/types/const.js
CHANGED
|
@@ -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_-]*$/;
|
package/lib/types/schema.js
CHANGED
|
@@ -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
|
|
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
|
+
};
|