@riebeckite/plugin-mermaid 0.0.21 → 0.0.23

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @riebeckite/plugin-mermaid
2
2
 
3
+ <!-- Generated from docs/docs/plugins/mermaid.md. Edit the canonical documentation in docs/docs/plugins and run `pnpm docs:sync`. -->
4
+
3
5
  Mermaid diagram rendering for ` ```mermaid ` code blocks.
4
6
 
5
7
  [日本語](./README_ja.md)
@@ -8,7 +10,7 @@ Mermaid diagram rendering for ` ```mermaid ` code blocks.
8
10
 
9
11
  `mermaid()` replaces mermaid code blocks with a `<figure class="rr-mermaid">`
10
12
  that renders to SVG. Diagrams are rendered at build time with a headless browser by
11
- default, with an automatic client-side fallback. It runs with `order: -10`.
13
+ default. It runs with `order: -10`.
12
14
 
13
15
  ## Usage
14
16
 
@@ -42,10 +44,11 @@ export default defineConfig({
42
44
  Rendering uses Chromium's layout engine, not JSDOM polyfills or custom
43
45
  `getBBox` / text-width estimation
44
46
  - Mermaid runs with `securityLevel: "strict"`, the selected theme, transparent
45
- background, and a unique SVG id per diagram
47
+ background, deterministic IDs seeded by the diagram ID, and a unique SVG id per
48
+ diagram
46
49
  - Invalid diagrams report `ruleId: "invalid-diagram"`; Chromium renderer
47
- failures report `ruleId: "renderer-error"`. When build SVG is unavailable,
48
- the figure remains `data-mermaid="pending"` for client fallback
50
+ failures report `ruleId: "renderer-error"`. Build rendering failures stop the
51
+ build by default, so a failed render cannot be stored as successful content
49
52
 
50
53
  ### Client (`initMermaidDiagrams`)
51
54
 
@@ -61,16 +64,21 @@ export default defineConfig({
61
64
  | Option | Type | Default | Description |
62
65
  | ------ | ---- | ------- | ----------- |
63
66
  | `render` | `"build" \| "client"` | `"build"` | When diagrams are rendered |
67
+ | `onRenderError` | `"error" \| "client"` | `"error"` | Behavior when build rendering fails |
64
68
  | `theme` | `string \| { light: string; dark: string }` | `{ light: "default", dark: "dark" }` | Mermaid theme |
65
69
  | `caption` | `boolean` | `true` | Show title / `%% caption:` as `figcaption` |
66
70
  | `fallback` | `boolean` | `true` | Show the diagram source in `<details>` |
67
71
 
68
72
  `render` modes:
69
73
 
70
- - `"build"` — render SVG at build time; diagrams that fail fall back to client
71
- rendering
74
+ - `"build"` — render SVG at build time; failures stop the build unless
75
+ `onRenderError` is `"client"`
72
76
  - `"client"` — skip build-time rendering, render in the browser only
73
77
 
78
+ Set `onRenderError: "client"` only when client-side retry is an intentional
79
+ availability trade-off. This makes the generated HTML depend on the renderer's
80
+ availability.
81
+
74
82
  ## Exports
75
83
 
76
84
  - `mermaid(options?)` — plugin factory
@@ -79,4 +87,4 @@ export default defineConfig({
79
87
 
80
88
  ## See also
81
89
 
82
- - [Plugin guide](../../../docs/docs/reference/plugin-api.md)
90
+ - [Plugin guide](https://github.com/Rerurate514/riebeckite/blob/main/docs/docs/reference/plugin-api.md)
package/README_ja.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @riebeckite/plugin-mermaid
2
2
 
3
- `mermaid` コードブロックを SVG の図として表示するプラグインです。既定ではビルド時に描画し、描画できなかった図だけをブラウザ側で再試行します。
3
+ <!-- Generated from docs/docs/plugins/mermaid.ja.md. Edit the canonical documentation in docs/docs/plugins and run `pnpm docs:sync`. -->
4
+
5
+ `mermaid` コードブロックを SVG の図として表示するプラグインです。既定ではビルド時に描画します。
4
6
 
5
7
  [English](./README.md)
6
8
 
@@ -29,7 +31,7 @@ export default defineConfig({
29
31
 
30
32
  `render: "build"` では、Puppeteer が起動するヘッドレス Chromium 上で Mermaid を実行し、静的な SVG を生成します。簡易な DOM 実装や寸法の推測には頼らないため、ブラウザと同じレイアウトエンジンで図を作れます。Mermaid は `securityLevel: "strict"` で実行されます。
31
33
 
32
- 構文エラーは `invalid-diagram`、描画環境の問題は `renderer-error` として区別して診断します。ビルドで SVG を作れなかった図には `data-mermaid="pending"` が付き、クライアント側が描画を引き継ぎます。
34
+ 構文エラーは `invalid-diagram`、描画環境の問題は `renderer-error` として区別して診断します。ビルド時の描画に失敗した場合、既定ではビルドを失敗させます。失敗した図を成功したコンテンツとして保存しません。SVG ID は図の ID をシードにして決定的に生成します。
33
35
 
34
36
  ## クライアント側の再試行
35
37
 
@@ -42,11 +44,12 @@ export default defineConfig({
42
44
  | 項目 | 既定値 | 説明 |
43
45
  | --- | --- | --- |
44
46
  | `render` | `"build"` | `"build"` または `"client"` のいずれで描画するか |
47
+ | `onRenderError` | `"error"` | ビルド時の描画失敗時に `"error"` または `"client"` のどちらにするか |
45
48
  | `theme` | `{ light: "default", dark: "dark" }` | Mermaid のテーマ名、または明暗別のテーマ |
46
49
  | `caption` | `true` | タイトルまたは `%% caption:` をキャプションとして表示する |
47
50
  | `fallback` | `true` | 元の Mermaid 記法を `<details>` に残す |
48
51
 
49
- `"client"` はビルド時の描画を行いません。
52
+ `"client"` はビルド時の描画を行いません。`render: "build"` で `onRenderError: "client"` を指定するときだけ、ビルド時の描画失敗をクライアント側へ明示的に委譲します。この設定では生成HTMLが描画環境の可用性に依存します。
50
53
 
51
54
  ## 主なエクスポート
52
55
 
@@ -56,4 +59,4 @@ export default defineConfig({
56
59
 
57
60
  ## 関連資料
58
61
 
59
- - [プラグインシステム](../../../docs/docs/reference/plugin-api.ja.md)
62
+ - [プラグインシステム](https://github.com/Rerurate514/riebeckite/blob/main/docs/docs/reference/plugin-api.ja.md)
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { MermaidOptions } from "./src/types.js";
2
- export type { MermaidClientOptions, MermaidOptions, MermaidRenderMode, MermaidRenderSession, MermaidTheme, } from "./src/types.js";
2
+ export type { MermaidClientOptions, MermaidOptions, MermaidRenderFailureMode, MermaidRenderMode, MermaidRenderSession, MermaidTheme, } from "./src/types.js";
3
3
  export declare function mermaid(options?: MermaidOptions): import("@riebeckite/core").RiebeckitePlugin<MermaidOptions>;
package/dist/index.js CHANGED
@@ -66,6 +66,7 @@ var init_hast = __esm({
66
66
  // src/rehype.ts
67
67
  var rehype_exports = {};
68
68
  __export(rehype_exports, {
69
+ MermaidBuildRenderError: () => MermaidBuildRenderError,
69
70
  rehypeMermaid: () => rehypeMermaid
70
71
  });
71
72
  function rehypeMermaid(options = {}, session) {
@@ -81,6 +82,7 @@ function rehypeMermaid(options = {}, session) {
81
82
  theme,
82
83
  caption: options.caption !== false,
83
84
  fallback: options.fallback !== false,
85
+ onRenderError: options.onRenderError ?? "error",
84
86
  session
85
87
  })
86
88
  );
@@ -94,6 +96,9 @@ async function replaceMermaidBlock(parent, index, pre, file, options) {
94
96
  const caption = options.caption ? extractCaption(source, pre, code) : null;
95
97
  const id = `rr-mermaid-${hashSource(source)}`;
96
98
  const renderResult = shouldRenderAtBuild(options.render) ? await renderStaticSvg(id, source, options.theme, file, options.session) : null;
99
+ if (renderResult?.ok === false && options.onRenderError === "error") {
100
+ throw new MermaidBuildRenderError(renderResult);
101
+ }
97
102
  const staticSvg = renderResult?.ok ? renderResult.svg : null;
98
103
  parent.children = parent.children ?? [];
99
104
  parent.children[index] = buildFigure({
@@ -102,7 +107,7 @@ async function replaceMermaidBlock(parent, index, pre, file, options) {
102
107
  caption,
103
108
  staticSvg,
104
109
  fallback: options.fallback,
105
- clientFallback: shouldRenderAtClient(options.render) || renderResult?.ok === false
110
+ clientFallback: shouldRenderAtClient(options.render) || renderResult?.ok === false && options.onRenderError === "client"
106
111
  });
107
112
  }
108
113
  function buildFigure(input) {
@@ -154,14 +159,11 @@ async function renderStaticSvg(id, source, theme, file, session) {
154
159
  );
155
160
  if (result.ok === false) {
156
161
  reportMermaidDiagnostic(file, result.message, result.kind);
157
- const label = result.kind === "invalid-diagram" ? "invalid diagram" : "renderer failed";
158
- console.warn(`[mermaid] ${label}: ${result.message}`);
159
162
  }
160
163
  return result;
161
164
  } catch (error) {
162
165
  const message = formatError2(error);
163
166
  reportMermaidDiagnostic(file, message, "renderer-error");
164
- console.warn(`[mermaid] renderer failed: ${message}`);
165
167
  return { ok: false, kind: "renderer-error", message };
166
168
  }
167
169
  }
@@ -212,12 +214,20 @@ function formatError2(error) {
212
214
  if (error instanceof Error) return error.message;
213
215
  return String(error);
214
216
  }
215
- var DEFAULT_THEME, CAPTION_PATTERN;
217
+ var DEFAULT_THEME, CAPTION_PATTERN, MermaidBuildRenderError;
216
218
  var init_rehype = __esm({
217
219
  "src/rehype.ts"() {
218
220
  init_hast();
219
221
  DEFAULT_THEME = { light: "default", dark: "dark" };
220
222
  CAPTION_PATTERN = /^%%\s*caption\s*:\s*(.+)$/im;
223
+ MermaidBuildRenderError = class extends Error {
224
+ kind;
225
+ constructor(result) {
226
+ super(`Mermaid build rendering failed: ${result.message}`);
227
+ this.name = "MermaidBuildRenderError";
228
+ this.kind = result.kind;
229
+ }
230
+ };
221
231
  }
222
232
  });
223
233
 
@@ -230,12 +240,15 @@ import {
230
240
 
231
241
  // src/render-static.ts
232
242
  import { access } from "node:fs/promises";
243
+ import { createRequire } from "node:module";
233
244
  import { join } from "node:path";
234
- import { fileURLToPath } from "node:url";
245
+ import { fileURLToPath, pathToFileURL } from "node:url";
235
246
  var RENDER_TIMEOUT_MS = 3e4;
236
247
  var MERMAID_SCRIPT_SUBPATH = "mermaid/dist/mermaid.min.js";
248
+ var PUPPETEER_PACKAGE = "puppeteer";
237
249
  function createMermaidRenderSession(dependencies = {}) {
238
250
  const launchBrowser = dependencies.launch ?? defaultLaunchBrowser;
251
+ const timeoutMs = dependencies.timeoutMs ?? RENDER_TIMEOUT_MS;
239
252
  let state = null;
240
253
  let queue = Promise.resolve();
241
254
  let exitListenerAttached = false;
@@ -264,7 +277,7 @@ function createMermaidRenderSession(dependencies = {}) {
264
277
  attachExitListener();
265
278
  try {
266
279
  const page = await browser.newPage();
267
- page.setDefaultTimeout(RENDER_TIMEOUT_MS);
280
+ page.setDefaultTimeout(timeoutMs);
268
281
  await page.setContent("<!doctype html><html><body></body></html>");
269
282
  await page.addScriptTag({ path: await resolveMermaidScriptPath() });
270
283
  state = { browser, page };
@@ -288,11 +301,10 @@ function createMermaidRenderSession(dependencies = {}) {
288
301
  if (current) await teardownBrowser(current.browser);
289
302
  }
290
303
  async function evaluateRender(session, id, source, theme) {
291
- const result = await session.page.evaluate(
292
- renderMermaidInPage,
293
- id,
294
- source,
295
- theme
304
+ const result = await withTimeout(
305
+ session.page.evaluate(renderMermaidInPage, id, source, theme),
306
+ timeoutMs,
307
+ "Mermaid rendering timed out."
296
308
  );
297
309
  return normalizeRenderResult(result);
298
310
  }
@@ -347,7 +359,9 @@ async function renderMermaidInPage(id, source, theme) {
347
359
  mermaid2.initialize({
348
360
  startOnLoad: false,
349
361
  securityLevel: "strict",
350
- theme
362
+ theme,
363
+ deterministicIds: true,
364
+ deterministicIDSeed: id
351
365
  });
352
366
  const { svg } = await mermaid2.render(id, source);
353
367
  return { ok: true, svg };
@@ -371,7 +385,21 @@ async function importPuppeteer() {
371
385
  "specifier",
372
386
  "return import(specifier)"
373
387
  );
374
- return importModule("puppeteer");
388
+ try {
389
+ return await importModule(PUPPETEER_PACKAGE);
390
+ } catch (error) {
391
+ const fallback = resolvePuppeteerFromWorkingDirectory();
392
+ if (!fallback) throw error;
393
+ return importModule(fallback);
394
+ }
395
+ }
396
+ function resolvePuppeteerFromWorkingDirectory() {
397
+ try {
398
+ const require2 = createRequire(join(process.cwd(), "package.json"));
399
+ return pathToFileURL(require2.resolve(PUPPETEER_PACKAGE)).href;
400
+ } catch {
401
+ return null;
402
+ }
375
403
  }
376
404
  async function resolveMermaidScriptPath() {
377
405
  const candidates = [
@@ -420,7 +448,7 @@ function normalizeRenderResult(value) {
420
448
  };
421
449
  }
422
450
  const result = value;
423
- if (result.ok === true && typeof result.svg === "string") {
451
+ if (result.ok === true && typeof result.svg === "string" && result.svg.trimStart().startsWith("<svg")) {
424
452
  return { ok: true, svg: result.svg };
425
453
  }
426
454
  if (result.ok === false) {
@@ -436,6 +464,15 @@ function normalizeRenderResult(value) {
436
464
  message: "Mermaid renderer returned an invalid result."
437
465
  };
438
466
  }
467
+ function withTimeout(promise, timeoutMs, message) {
468
+ let timer;
469
+ const timeout = new Promise((_, reject) => {
470
+ timer = setTimeout(() => reject(new Error(message)), timeoutMs);
471
+ });
472
+ return Promise.race([promise, timeout]).finally(() => {
473
+ if (timer !== void 0) clearTimeout(timer);
474
+ });
475
+ }
439
476
  function normalizeErrorKind(value) {
440
477
  return value === "invalid-diagram" || value === "renderer-error" ? value : "renderer-error";
441
478
  }
@@ -451,8 +488,9 @@ function mermaid(options = {}) {
451
488
  name: "mermaid",
452
489
  order: -10,
453
490
  processedContentCache: {
454
- version: "mermaid-v1",
455
- dependencyMode: "none"
491
+ version: "mermaid-v3",
492
+ dependencyMode: "unsafe",
493
+ appliesToContent: ({ source }) => containsMermaidFence(source) ? "applies" : "does-not-apply"
456
494
  },
457
495
  options,
458
496
  validateOptions: validateMermaidOptions,
@@ -465,10 +503,21 @@ function mermaid(options = {}) {
465
503
  dispose: async () => {
466
504
  await session.dispose();
467
505
  },
468
- assets: [createStyleAsset("mermaid")],
469
- clientEntries: [createClientEntry("mermaid", "initMermaidDiagrams")]
506
+ assets: [
507
+ createStyleAsset("mermaid", "@riebeckite/plugin-mermaid/style.css")
508
+ ],
509
+ clientEntries: [
510
+ createClientEntry(
511
+ "mermaid",
512
+ "@riebeckite/plugin-mermaid/client",
513
+ "initMermaidDiagrams"
514
+ )
515
+ ]
470
516
  });
471
517
  }
518
+ function containsMermaidFence(source) {
519
+ return /^\s*(?:`{3,}|~{3,})[^\n`~]*\bmermaid\b[^\n`~]*$/im.test(source);
520
+ }
472
521
  function validateMermaidOptions(options) {
473
522
  if (!options) return [];
474
523
  const issues = [];
@@ -478,6 +527,12 @@ function validateMermaidOptions(options) {
478
527
  message: 'Expected "build" or "client".'
479
528
  });
480
529
  }
530
+ if (options.onRenderError !== void 0 && options.onRenderError !== "error" && options.onRenderError !== "client") {
531
+ issues.push({
532
+ path: "onRenderError",
533
+ message: 'Expected "error" or "client".'
534
+ });
535
+ }
481
536
  if (!isMermaidTheme(options.theme)) {
482
537
  issues.push({
483
538
  path: "theme",
@@ -1,2 +1,8 @@
1
- import type { HastNode, MermaidOptions, MermaidRenderSession } from "./types.js";
1
+ import type { HastNode, MermaidBuildRenderErrorKind, MermaidBuildRenderResult, MermaidOptions, MermaidRenderSession } from "./types.js";
2
+ export declare class MermaidBuildRenderError extends Error {
3
+ readonly kind: MermaidBuildRenderErrorKind;
4
+ constructor(result: Extract<MermaidBuildRenderResult, {
5
+ ok: false;
6
+ }>);
7
+ }
2
8
  export declare function rehypeMermaid(options: MermaidOptions, session: MermaidRenderSession): (tree: HastNode, file: unknown) => Promise<void>;
@@ -6,6 +6,7 @@ export type MermaidSessionDependencies = {
6
6
  * lifecycle can be asserted without launching Chrome.
7
7
  */
8
8
  launch?: () => Promise<Browser>;
9
+ timeoutMs?: number;
9
10
  };
10
11
  /**
11
12
  * Creates a build-scoped Mermaid render session.
@@ -1,10 +1,12 @@
1
1
  export type MermaidRenderMode = "build" | "client";
2
+ export type MermaidRenderFailureMode = "error" | "client";
2
3
  export type MermaidTheme = string | {
3
4
  light: string;
4
5
  dark: string;
5
6
  };
6
7
  export type MermaidOptions = {
7
8
  render?: MermaidRenderMode;
9
+ onRenderError?: MermaidRenderFailureMode;
8
10
  theme?: MermaidTheme;
9
11
  caption?: boolean;
10
12
  fallback?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-mermaid",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "dependencies": {
24
24
  "mermaid": "^11.12.2",
25
25
  "puppeteer": "^25.11.0",
26
- "@riebeckite/core": "0.0.21"
26
+ "@riebeckite/core": "0.0.23"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"