mulmocast 2.1.25 → 2.1.27
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/README.md +63 -3
- package/assets/html/chart.html +20 -8
- package/lib/cli/bin.js +0 -0
- package/lib/cli/commands/tool/complete/builder.d.ts +2 -0
- package/lib/cli/commands/tool/complete/builder.js +7 -1
- package/lib/cli/commands/tool/complete/handler.d.ts +1 -0
- package/lib/cli/commands/tool/complete/handler.js +11 -5
- package/lib/mcp/server.js +0 -0
- package/lib/tools/complete_script.d.ts +35 -4
- package/lib/tools/complete_script.js +86 -15
- package/lib/utils/markdown.js +106 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -285,9 +285,38 @@ mulmo tool scripting -i
|
|
|
285
285
|
```
|
|
286
286
|
|
|
287
287
|
Note:
|
|
288
|
+
|
|
288
289
|
- When -i is specified, --input-file value will be ignored
|
|
289
290
|
- When --input-file is specified, -u value will be ignored
|
|
290
291
|
|
|
292
|
+
### Complete MulmoScript from minimal beats
|
|
293
|
+
|
|
294
|
+
You can create a minimal JSON with just beats and complete it with `mulmo tool complete`:
|
|
295
|
+
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"beats": [
|
|
299
|
+
{ "text": "Hello, welcome to MulmoCast!" },
|
|
300
|
+
{ "text": "This is a simple example." }
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Save this as `my_beats.json`, then complete it with a style:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
# Complete with built-in style
|
|
309
|
+
mulmo tool complete my_beats.json -s ani
|
|
310
|
+
|
|
311
|
+
# Complete with custom style file
|
|
312
|
+
mulmo tool complete my_beats.json -s ./my_style.json
|
|
313
|
+
|
|
314
|
+
# Output to specific file
|
|
315
|
+
mulmo tool complete my_beats.json -s ani -o my_script.json
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
This generates a complete MulmoScript with all required fields (canvasSize, speechParams, imageParams, etc.) from the style.
|
|
319
|
+
|
|
291
320
|
|
|
292
321
|
## Generate content from MulmoScript
|
|
293
322
|
|
|
@@ -536,9 +565,10 @@ mulmo tool <command>
|
|
|
536
565
|
Generate Mulmo script and other tools
|
|
537
566
|
|
|
538
567
|
Commands:
|
|
539
|
-
mulmo tool scripting
|
|
540
|
-
mulmo tool
|
|
541
|
-
mulmo tool
|
|
568
|
+
mulmo tool scripting Generate mulmocast script
|
|
569
|
+
mulmo tool complete <file> Complete partial MulmoScript with defaults
|
|
570
|
+
mulmo tool prompt Dump prompt from template
|
|
571
|
+
mulmo tool schema Dump mulmocast schema
|
|
542
572
|
|
|
543
573
|
Options:
|
|
544
574
|
--version Show version number [boolean]
|
|
@@ -626,6 +656,36 @@ Options:
|
|
|
626
656
|
-h, --help Show help [boolean]
|
|
627
657
|
```
|
|
628
658
|
|
|
659
|
+
```
|
|
660
|
+
mulmo tool complete <file>
|
|
661
|
+
|
|
662
|
+
Complete partial MulmoScript with schema defaults and optional style/template
|
|
663
|
+
|
|
664
|
+
Positionals:
|
|
665
|
+
file Input beats file path (JSON) [string] [required]
|
|
666
|
+
|
|
667
|
+
Options:
|
|
668
|
+
--version Show version number [boolean]
|
|
669
|
+
-v, --verbose verbose log [boolean] [required] [default: false]
|
|
670
|
+
-h, --help Show help [boolean]
|
|
671
|
+
-o, --output Output file path (default: <file>_completed.json) [string]
|
|
672
|
+
-t, --template Template name to apply [string]
|
|
673
|
+
-s, --style Style name or file path (.json) [string]
|
|
674
|
+
|
|
675
|
+
Examples:
|
|
676
|
+
# Complete minimal script with schema defaults
|
|
677
|
+
mulmo tool complete input.json
|
|
678
|
+
|
|
679
|
+
# Apply built-in style
|
|
680
|
+
mulmo tool complete input.json -s ani
|
|
681
|
+
|
|
682
|
+
# Apply custom style file
|
|
683
|
+
mulmo tool complete input.json -s ./my_style.json
|
|
684
|
+
|
|
685
|
+
# Apply template
|
|
686
|
+
mulmo tool complete input.json -t children_book
|
|
687
|
+
```
|
|
688
|
+
|
|
629
689
|
|
|
630
690
|
|
|
631
691
|
## Contributing
|
package/assets/html/chart.html
CHANGED
|
@@ -17,21 +17,33 @@
|
|
|
17
17
|
<body>
|
|
18
18
|
<h1>${title}</h1>
|
|
19
19
|
<div class="chart-container">
|
|
20
|
-
<canvas id="myChart"></canvas>
|
|
20
|
+
<canvas id="myChart" data-chart-ready="false"></canvas>
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<!-- Plain JavaScript instead of TypeScript -->
|
|
24
24
|
<script>
|
|
25
|
-
// Wait for DOM to be
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Wait for DOM and Chart.js to be ready, then render.
|
|
26
|
+
function initChart() {
|
|
27
|
+
if (!window.Chart) return false;
|
|
28
28
|
const ctx = document.getElementById('myChart');
|
|
29
|
-
|
|
30
|
-
// Create the data object (no TypeScript interfaces)
|
|
29
|
+
if (!ctx) return false;
|
|
31
30
|
const chartData = ${chart_data};
|
|
32
|
-
|
|
33
|
-
// Initialize the chart
|
|
34
31
|
new Chart(ctx, chartData);
|
|
32
|
+
requestAnimationFrame(() => {
|
|
33
|
+
requestAnimationFrame(() => {
|
|
34
|
+
ctx.dataset.chartReady = "true";
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function waitForChart() {
|
|
41
|
+
if (initChart()) return;
|
|
42
|
+
setTimeout(waitForChart, 50);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
46
|
+
waitForChart();
|
|
35
47
|
});
|
|
36
48
|
</script>
|
|
37
49
|
</body>
|
package/lib/cli/bin.js
CHANGED
|
File without changes
|
|
@@ -10,10 +10,16 @@ export const builder = (yargs) => {
|
|
|
10
10
|
})
|
|
11
11
|
.option("t", {
|
|
12
12
|
alias: "template",
|
|
13
|
-
description: "Template
|
|
13
|
+
description: "Template name to apply",
|
|
14
14
|
demandOption: false,
|
|
15
15
|
choices: availableTemplateNames,
|
|
16
16
|
type: "string",
|
|
17
|
+
})
|
|
18
|
+
.option("s", {
|
|
19
|
+
alias: "style",
|
|
20
|
+
description: "Style name or file path (.json)",
|
|
21
|
+
demandOption: false,
|
|
22
|
+
type: "string",
|
|
17
23
|
})
|
|
18
24
|
.positional("file", {
|
|
19
25
|
description: "Input beats file path (JSON)",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { GraphAILogger } from "graphai";
|
|
4
|
-
import { completeScript, templateExists } from "../../../../tools/complete_script.js";
|
|
4
|
+
import { completeScript, templateExists, styleExists } from "../../../../tools/complete_script.js";
|
|
5
5
|
export const handler = async (argv) => {
|
|
6
|
-
const { file, o: outputPath, t: templateName, v: verbose } = argv;
|
|
6
|
+
const { file, o: outputPath, t: templateName, s: styleName, v: verbose } = argv;
|
|
7
7
|
if (!file) {
|
|
8
8
|
GraphAILogger.error("Error: Input file is required");
|
|
9
9
|
process.exit(1);
|
|
@@ -23,7 +23,10 @@ export const handler = async (argv) => {
|
|
|
23
23
|
if (templateName && !templateExists(templateName)) {
|
|
24
24
|
GraphAILogger.warn(`Warning: Template '${templateName}' not found`);
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
if (styleName && !styleExists(styleName)) {
|
|
27
|
+
GraphAILogger.warn(`Warning: Style '${styleName}' not found`);
|
|
28
|
+
}
|
|
29
|
+
const result = completeScript(inputData, { templateName, styleName });
|
|
27
30
|
if (!result.success) {
|
|
28
31
|
GraphAILogger.error("Validation errors:");
|
|
29
32
|
result.error.issues.forEach((issue) => {
|
|
@@ -31,8 +34,11 @@ export const handler = async (argv) => {
|
|
|
31
34
|
});
|
|
32
35
|
process.exit(1);
|
|
33
36
|
}
|
|
34
|
-
if (verbose
|
|
35
|
-
|
|
37
|
+
if (verbose) {
|
|
38
|
+
if (styleName)
|
|
39
|
+
GraphAILogger.info(`Applied style: ${styleName}`);
|
|
40
|
+
if (templateName)
|
|
41
|
+
GraphAILogger.info(`Applied template: ${templateName}`);
|
|
36
42
|
}
|
|
37
43
|
const outputFilePath = outputPath ? path.resolve(outputPath) : inputPath.replace(/\.json$/, "_completed.json");
|
|
38
44
|
writeFileSync(outputFilePath, JSON.stringify(result.data, null, 2));
|
package/lib/mcp/server.js
CHANGED
|
File without changes
|
|
@@ -6,16 +6,47 @@ type PartialMulmoScript = Record<string, unknown>;
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const addMulmocastVersion: (data: PartialMulmoScript) => PartialMulmoScript;
|
|
8
8
|
/**
|
|
9
|
-
* Merge
|
|
9
|
+
* Merge base with override (override takes precedence)
|
|
10
10
|
*/
|
|
11
|
-
export declare const
|
|
11
|
+
export declare const mergeScripts: (base: PartialMulmoScript, override: PartialMulmoScript) => PartialMulmoScript;
|
|
12
|
+
/**
|
|
13
|
+
* Get style by name or file path
|
|
14
|
+
*/
|
|
15
|
+
export declare const getStyle: (style: string) => PartialMulmoScript | undefined;
|
|
12
16
|
export type CompleteScriptResult = ZodSafeParseResult<MulmoScript>;
|
|
17
|
+
type CompleteScriptOptions = {
|
|
18
|
+
templateName?: string;
|
|
19
|
+
styleName?: string;
|
|
20
|
+
};
|
|
13
21
|
/**
|
|
14
|
-
* Complete a partial MulmoScript with schema defaults
|
|
22
|
+
* Complete a partial MulmoScript with schema defaults, optional style or template
|
|
23
|
+
*
|
|
24
|
+
* @param data - Partial MulmoScript to complete (highest precedence)
|
|
25
|
+
* @param options - Optional template or style to use as base
|
|
26
|
+
* @param options.templateName - Template name (e.g., "children_book"). Mutually exclusive with styleName.
|
|
27
|
+
* @param options.styleName - Style name or file path. Mutually exclusive with templateName.
|
|
28
|
+
* @returns Zod safe parse result with completed MulmoScript or validation errors
|
|
29
|
+
* @throws Error if both templateName and styleName are specified
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // With template
|
|
33
|
+
* completeScript(data, { templateName: "children_book" })
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // With style
|
|
37
|
+
* completeScript(data, { styleName: "ghibli_comic" })
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // With style from file
|
|
41
|
+
* completeScript(data, { styleName: "./my-style.json" })
|
|
15
42
|
*/
|
|
16
|
-
export declare const completeScript: (data: PartialMulmoScript,
|
|
43
|
+
export declare const completeScript: (data: PartialMulmoScript, options?: CompleteScriptOptions) => CompleteScriptResult;
|
|
17
44
|
/**
|
|
18
45
|
* Check if template exists
|
|
19
46
|
*/
|
|
20
47
|
export declare const templateExists: (templateName: string) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Check if style exists (by name or file path)
|
|
50
|
+
*/
|
|
51
|
+
export declare const styleExists: (style: string) => boolean;
|
|
21
52
|
export {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
1
3
|
import { mulmoScriptSchema } from "../types/schema.js";
|
|
2
4
|
import { getScriptFromPromptTemplate } from "../utils/file.js";
|
|
3
5
|
import { currentMulmoScriptVersion } from "../types/const.js";
|
|
6
|
+
import { promptTemplates } from "../data/index.js";
|
|
4
7
|
/**
|
|
5
8
|
* Add $mulmocast version if not present
|
|
6
9
|
*/
|
|
@@ -15,29 +18,91 @@ export const addMulmocastVersion = (data) => {
|
|
|
15
18
|
};
|
|
16
19
|
const deepMergeKeys = ["speechParams", "imageParams", "movieParams", "audioParams"];
|
|
17
20
|
/**
|
|
18
|
-
* Merge
|
|
21
|
+
* Merge base with override (override takes precedence)
|
|
19
22
|
*/
|
|
20
|
-
export const
|
|
21
|
-
const merged = { ...
|
|
23
|
+
export const mergeScripts = (base, override) => {
|
|
24
|
+
const merged = { ...base, ...override };
|
|
22
25
|
deepMergeKeys.forEach((key) => {
|
|
23
|
-
if (
|
|
24
|
-
merged[key] = { ...
|
|
26
|
+
if (base[key] && override[key]) {
|
|
27
|
+
merged[key] = { ...base[key], ...override[key] };
|
|
25
28
|
}
|
|
26
29
|
});
|
|
27
30
|
return merged;
|
|
28
31
|
};
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
33
|
+
* Check if style specifier is a file path
|
|
31
34
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return
|
|
35
|
+
const isFilePath = (style) => {
|
|
36
|
+
return style.endsWith(".json") || style.includes("/") || style.includes("\\");
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Get style by name from promptTemplates
|
|
40
|
+
*/
|
|
41
|
+
const getStyleByName = (styleName) => {
|
|
42
|
+
const template = promptTemplates.find((t) => t.filename === styleName);
|
|
43
|
+
return template?.presentationStyle;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Get style from file path
|
|
47
|
+
*/
|
|
48
|
+
const getStyleFromFile = (filePath) => {
|
|
49
|
+
const resolvedPath = path.resolve(filePath);
|
|
50
|
+
if (!existsSync(resolvedPath)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const content = readFileSync(resolvedPath, "utf-8");
|
|
54
|
+
return JSON.parse(content);
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Get style by name or file path
|
|
58
|
+
*/
|
|
59
|
+
export const getStyle = (style) => {
|
|
60
|
+
return isFilePath(style) ? getStyleFromFile(style) : getStyleByName(style);
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Complete a partial MulmoScript with schema defaults, optional style or template
|
|
64
|
+
*
|
|
65
|
+
* @param data - Partial MulmoScript to complete (highest precedence)
|
|
66
|
+
* @param options - Optional template or style to use as base
|
|
67
|
+
* @param options.templateName - Template name (e.g., "children_book"). Mutually exclusive with styleName.
|
|
68
|
+
* @param options.styleName - Style name or file path. Mutually exclusive with templateName.
|
|
69
|
+
* @returns Zod safe parse result with completed MulmoScript or validation errors
|
|
70
|
+
* @throws Error if both templateName and styleName are specified
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // With template
|
|
74
|
+
* completeScript(data, { templateName: "children_book" })
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // With style
|
|
78
|
+
* completeScript(data, { styleName: "ghibli_comic" })
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // With style from file
|
|
82
|
+
* completeScript(data, { styleName: "./my-style.json" })
|
|
83
|
+
*/
|
|
84
|
+
export const completeScript = (data, options = {}) => {
|
|
85
|
+
const { templateName, styleName } = options;
|
|
86
|
+
// template and style are mutually exclusive
|
|
87
|
+
if (templateName && styleName) {
|
|
88
|
+
throw new Error("Cannot specify both templateName and styleName. They are mutually exclusive.");
|
|
89
|
+
}
|
|
90
|
+
// Get base config from template or style
|
|
91
|
+
const getBase = () => {
|
|
92
|
+
if (templateName) {
|
|
93
|
+
return getScriptFromPromptTemplate(templateName);
|
|
94
|
+
}
|
|
95
|
+
if (styleName) {
|
|
96
|
+
return getStyle(styleName);
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
};
|
|
100
|
+
const base = getBase();
|
|
101
|
+
// Merge base with input data (input data has highest precedence)
|
|
102
|
+
const merged = base ? mergeScripts(base, data) : data;
|
|
103
|
+
// Add version if not present
|
|
104
|
+
const withVersion = addMulmocastVersion(merged);
|
|
105
|
+
return mulmoScriptSchema.safeParse(withVersion);
|
|
41
106
|
};
|
|
42
107
|
/**
|
|
43
108
|
* Check if template exists
|
|
@@ -45,3 +110,9 @@ export const completeScript = (data, templateName) => {
|
|
|
45
110
|
export const templateExists = (templateName) => {
|
|
46
111
|
return getScriptFromPromptTemplate(templateName) !== undefined;
|
|
47
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* Check if style exists (by name or file path)
|
|
115
|
+
*/
|
|
116
|
+
export const styleExists = (style) => {
|
|
117
|
+
return getStyle(style) !== undefined;
|
|
118
|
+
};
|
package/lib/utils/markdown.js
CHANGED
|
@@ -1,35 +1,113 @@
|
|
|
1
1
|
import { marked } from "marked";
|
|
2
2
|
import puppeteer from "puppeteer";
|
|
3
3
|
const isCI = process.env.CI === "true";
|
|
4
|
+
const reuseBrowser = process.env.MULMO_PUPPETEER_REUSE !== "0";
|
|
5
|
+
const browserLaunchArgs = isCI ? ["--no-sandbox"] : [];
|
|
6
|
+
// Shared browser to avoid spawning a new Chromium per render.
|
|
7
|
+
let sharedBrowserPromise = null;
|
|
8
|
+
let sharedBrowserRefs = 0;
|
|
9
|
+
let sharedBrowserCloseTimer = null;
|
|
10
|
+
// Acquire a browser instance; reuse a shared one when enabled.
|
|
11
|
+
const acquireBrowser = async () => {
|
|
12
|
+
if (!reuseBrowser) {
|
|
13
|
+
return await puppeteer.launch({ args: browserLaunchArgs });
|
|
14
|
+
}
|
|
15
|
+
sharedBrowserRefs += 1;
|
|
16
|
+
if (sharedBrowserCloseTimer) {
|
|
17
|
+
clearTimeout(sharedBrowserCloseTimer);
|
|
18
|
+
sharedBrowserCloseTimer = null;
|
|
19
|
+
}
|
|
20
|
+
if (!sharedBrowserPromise) {
|
|
21
|
+
sharedBrowserPromise = puppeteer.launch({ args: browserLaunchArgs });
|
|
22
|
+
}
|
|
23
|
+
const currentPromise = sharedBrowserPromise;
|
|
24
|
+
try {
|
|
25
|
+
return await currentPromise;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (sharedBrowserPromise === currentPromise) {
|
|
29
|
+
sharedBrowserPromise = null;
|
|
30
|
+
}
|
|
31
|
+
sharedBrowserRefs = Math.max(0, sharedBrowserRefs - 1);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
// Release the browser; close only after a short idle window.
|
|
36
|
+
const releaseBrowser = async (browser) => {
|
|
37
|
+
if (!reuseBrowser) {
|
|
38
|
+
await browser.close().catch(() => { });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
sharedBrowserRefs = Math.max(0, sharedBrowserRefs - 1);
|
|
42
|
+
if (sharedBrowserRefs > 0 || !sharedBrowserPromise) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Delay close to allow back-to-back renders to reuse the browser.
|
|
46
|
+
sharedBrowserCloseTimer = setTimeout(async () => {
|
|
47
|
+
const current = sharedBrowserPromise;
|
|
48
|
+
sharedBrowserPromise = null;
|
|
49
|
+
sharedBrowserCloseTimer = null;
|
|
50
|
+
if (current) {
|
|
51
|
+
await (await current).close().catch(() => { });
|
|
52
|
+
}
|
|
53
|
+
}, 300);
|
|
54
|
+
};
|
|
55
|
+
// Wait for a single animation frame to let canvas paints settle.
|
|
56
|
+
const waitForNextFrame = async (page) => {
|
|
57
|
+
await page.evaluate(() => new Promise((resolve) => {
|
|
58
|
+
requestAnimationFrame(() => resolve());
|
|
59
|
+
}));
|
|
60
|
+
};
|
|
4
61
|
export const renderHTMLToImage = async (html, outputPath, width, height, isMermaid = false, omitBackground = false) => {
|
|
5
|
-
//
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
62
|
+
// Charts are rendered in a dedicated browser to avoid shared-page timing issues.
|
|
63
|
+
const useSharedBrowser = reuseBrowser && !html.includes("data-chart-ready");
|
|
64
|
+
const browser = useSharedBrowser ? await acquireBrowser() : await puppeteer.launch({ args: browserLaunchArgs });
|
|
65
|
+
let page = null;
|
|
66
|
+
try {
|
|
67
|
+
page = await browser.newPage();
|
|
68
|
+
// Adjust page settings if needed (like width, height, etc.)
|
|
69
|
+
await page.setViewport({ width, height });
|
|
70
|
+
// Set the page content to the HTML generated from the Markdown
|
|
71
|
+
await page.setContent(html, { waitUntil: "domcontentloaded" });
|
|
72
|
+
await page.addStyleTag({ content: "html,body{margin:0;padding:0;overflow:hidden}" });
|
|
73
|
+
if (isMermaid) {
|
|
74
|
+
await page.waitForFunction(() => {
|
|
75
|
+
const element = document.querySelector(".mermaid");
|
|
76
|
+
return element && element.dataset.ready === "true";
|
|
77
|
+
}, { timeout: 20000 });
|
|
78
|
+
}
|
|
79
|
+
if (html.includes("data-chart-ready")) {
|
|
80
|
+
await page.waitForFunction(() => {
|
|
81
|
+
const canvas = document.querySelector("canvas[data-chart-ready='true']");
|
|
82
|
+
return !!canvas;
|
|
83
|
+
}, { timeout: 20000 });
|
|
84
|
+
// Give the browser a couple of frames to paint the canvas.
|
|
85
|
+
await waitForNextFrame(page);
|
|
86
|
+
await waitForNextFrame(page);
|
|
87
|
+
}
|
|
88
|
+
// Measure the size of the page and scale the page to the width and height
|
|
89
|
+
await page.evaluate(({ vw, vh }) => {
|
|
90
|
+
const documentElement = document.documentElement;
|
|
91
|
+
const scrollWidth = Math.max(documentElement.scrollWidth, document.body.scrollWidth || 0);
|
|
92
|
+
const scrollHeight = Math.max(documentElement.scrollHeight, document.body.scrollHeight || 0);
|
|
93
|
+
const scale = Math.min(vw / (scrollWidth || vw), vh / (scrollHeight || vh), 1); // <=1 で縮小のみ
|
|
94
|
+
documentElement.style.overflow = "hidden";
|
|
95
|
+
document.body.style.zoom = String(scale);
|
|
96
|
+
}, { vw: width, vh: height });
|
|
97
|
+
// Step 3: Capture screenshot of the page (which contains the Markdown-rendered HTML)
|
|
98
|
+
await page.screenshot({ path: outputPath, omitBackground });
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (page) {
|
|
102
|
+
await page.close().catch(() => { });
|
|
103
|
+
}
|
|
104
|
+
if (useSharedBrowser) {
|
|
105
|
+
await releaseBrowser(browser);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
await browser.close().catch(() => { });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
33
111
|
};
|
|
34
112
|
export const renderMarkdownToImage = async (markdown, style, outputPath, width, height) => {
|
|
35
113
|
const header = `<head><style>${style}</style></head>`;
|