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.
@@ -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 fully loaded
26
- document.addEventListener('DOMContentLoaded', function() {
27
- // Get the canvas element
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
@@ -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
- // Use Puppeteer to render HTML to an image
6
- const browser = await puppeteer.launch({
7
- args: isCI ? ["--no-sandbox"] : [],
8
- });
9
- const page = await browser.newPage();
10
- // Set the page content to the HTML generated from the Markdown
11
- await page.setContent(html);
12
- // Adjust page settings if needed (like width, height, etc.)
13
- await page.setViewport({ width, height });
14
- await page.addStyleTag({ content: "html,body{margin:0;padding:0;overflow:hidden}" });
15
- if (isMermaid) {
16
- await page.waitForFunction(() => {
17
- const el = document.querySelector(".mermaid");
18
- return el && el.dataset.ready === "true";
19
- }, { timeout: 20000 });
20
- }
21
- // Measure the size of the page and scale the page to the width and height
22
- await page.evaluate(({ vw, vh }) => {
23
- const de = document.documentElement;
24
- const sw = Math.max(de.scrollWidth, document.body.scrollWidth || 0);
25
- const sh = Math.max(de.scrollHeight, document.body.scrollHeight || 0);
26
- const scale = Math.min(vw / (sw || vw), vh / (sh || vh), 1); // <=1 で縮小のみ
27
- de.style.overflow = "hidden";
28
- document.body.style.zoom = String(scale);
29
- }, { vw: width, vh: height });
30
- // Step 3: Capture screenshot of the page (which contains the Markdown-rendered HTML)
31
- await page.screenshot({ path: outputPath, omitBackground });
32
- await browser.close();
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>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.1.26",
3
+ "version": "2.1.27",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",