mulmocast 2.1.26 → 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/assets/html/chart.html +20 -8
- package/lib/cli/bin.js +0 -0
- package/lib/mcp/server.js +0 -0
- package/lib/utils/markdown.js +106 -28
- package/package.json +1 -1
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
|
package/lib/mcp/server.js
CHANGED
|
File without changes
|
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>`;
|