@ampless/plugin-mermaid 0.1.0-beta.0 → 0.1.0-beta.2

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.ja.md CHANGED
@@ -35,16 +35,36 @@ export default defineConfig({
35
35
  ```ts
36
36
  mermaidPlugin({
37
37
  version: '11.15.0', // 既定値(固定 x.y.z)
38
- theme: 'default', // default | dark | forest | neutral | base
38
+ theme: 'auto', // auto | default | dark | forest | neutral | base
39
39
  securityLevel: 'strict', // strict | loose | antiscript | sandbox
40
40
  })
41
41
  ```
42
42
 
43
- | オプション | デフォルト | 備考 |
44
- | --------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
45
- | `version` | `'11.15.0'` | jsDelivr から読み込む mermaid のバージョン。`x` / `x.y` / `x.y.z` に一致する必要あり。不正値は `console.warn` してデフォルトにフォールバック。 |
46
- | `theme` | `'default'` | `default` / `dark` / `forest` / `neutral` / `base` のいずれか。それ以外は `default` にフォールバック。 |
47
- | `securityLevel` | `'strict'` | `strict` / `loose` / `antiscript` / `sandbox` のいずれか。それ以外は `strict` にフォールバック。[セキュリティ](#セキュリティ--cdn-に関する注意)参照。 |
43
+ | オプション | デフォルト | 備考 |
44
+ | --------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | `version` | `'11.15.0'` | jsDelivr から読み込む mermaid のバージョン。`x` / `x.y` / `x.y.z` に一致する必要あり。不正値は `console.warn` してデフォルトにフォールバック。 |
46
+ | `theme` | `'auto'` | `auto` / `default` / `dark` / `forest` / `neutral` / `base` のいずれか。`'auto'`(既定)はサイトのカラースキームに追従します([カラースキーム](#カラースキーム)参照)。それ以外は `'auto'` にフォールバック。 |
47
+ | `securityLevel` | `'strict'` | `strict` / `loose` / `antiscript` / `sandbox` のいずれか。それ以外は `strict` にフォールバック。[セキュリティ](#セキュリティ--cdn-に関する注意)参照。 |
48
+
49
+ ## カラースキーム
50
+
51
+ 既定の `theme: 'auto'` では、図のテーマがサイトのライト/ダークのカラースキームに追従するため、ダーク背景でも図のテキストが読みやすく保たれます(mermaid はテーマ色を描画後 SVG に焼き込むため、ダークページに明テーマだと低コントラストになります):
52
+
53
+ | サイトのスキーム | 使用する mermaid テーマ |
54
+ | ---------------- | ----------------------- |
55
+ | light | `default` |
56
+ | dark | `dark` |
57
+
58
+ スキームは実行時に次の順で判定します:
59
+
60
+ 1. `<html data-color-scheme>` 属性 — サイトがスキームを固定する場合(サイト内トグル含む)に ampless が `'light'` / `'dark'` を設定します。
61
+ 2. 属性が無い場合(サイト設定 `auto`)は OS の `prefers-color-scheme` を使用します(ガード付き。`matchMedia` 未定義環境では light 扱い)。
62
+
63
+ **ライブ切替。** スキームが変わると図はその場で再描画されます — サイト内トグルが `data-color-scheme` を切り替えたとき、および(`auto` モードで属性がスキームを固定していないとき)OS の設定が変わったときの両方に追従します。mermaid は既存 SVG を再着色できないため、描画済みの各図は元ソースを `data-mermaid-src` 属性に保持しておき、そこから再描画します。図の無いページではスキーム変更時もライブラリをダウンロードしません。
64
+
65
+ **固定。** 明示テーマ(例: `theme: 'dark'`)を渡すと、サイトのスキームに関わらずそのテーマに固定され、ライブ再描画も無効になります。従来の既定 `'default'` でのライトページ出力は不変です。
66
+
67
+ > **カスタムダークテーマの注意。** `'auto'` は `data-color-scheme` / `prefers-color-scheme` のシグナルで判定し、テーマの見た目の暗さは見ません。テーマがダークなデザインでも `<html>` に `data-color-scheme="dark"` を設定していない場合、`'auto'` は light と判定して mermaid が明テーマ(暗い文字)になり、ダーク背景と衝突します。その場合は `theme: 'dark'` を固定してください(またはテーマ側で `data-color-scheme="dark"` を設定)。
48
68
 
49
69
  ## コードブロックの検出方法
50
70
 
@@ -77,7 +97,8 @@ graph TD
77
97
 
78
98
  - **冪等な再スキャン** — 処理済みブロックは `data-ampless-done` でマークするため、二重描画しません。
79
99
  - **SPA / App Router 遷移** — head スクリプトは一度だけ実行されますが、`document.body` に張ったデバウンス付き `MutationObserver` が、クライアント遷移で後から挿入された投稿コンテンツを再スキャンします。
80
- - **失敗時の復旧**動的 import が失敗した場合はキャッシュした import Promise を破棄し、ブロックのマークも外すため次回スキャンで再試行されます。失敗は握り潰さず `console.warn` で報告します。個別の図の描画失敗時は元のコードブロックを残します。
100
+ - **ライブなスキーム切替**`<html>`(`data-color-scheme`)への `MutationObserver` と、`auto` モード時の `matchMedia('(prefers-color-scheme: dark)')` リスナが、スキーム変更時に図を再描画します。短時間に連続して切り替えても最終スキームへ収束します(古いスキームに対して解決した描画結果は破棄して再キックします)。
101
+ - **失敗時の復旧** — 動的 import が失敗した場合はキャッシュした import Promise を破棄し、ブロックのマークも外すため次回スキャンで再試行されます。失敗は握り潰さず `console.warn` で報告します。個別の図の描画失敗時は元のコードブロックを残します(再描画の失敗時は直前の SVG を残し、次回のスキーム変更で再試行します)。
81
102
 
82
103
  ## セキュリティ / CDN に関する注意
83
104
 
package/README.md CHANGED
@@ -35,16 +35,36 @@ export default defineConfig({
35
35
  ```ts
36
36
  mermaidPlugin({
37
37
  version: '11.15.0', // pinned default
38
- theme: 'default', // default | dark | forest | neutral | base
38
+ theme: 'auto', // auto | default | dark | forest | neutral | base
39
39
  securityLevel: 'strict', // strict | loose | antiscript | sandbox
40
40
  })
41
41
  ```
42
42
 
43
- | Option | Default | Notes |
44
- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
45
- | `version` | `'11.15.0'` | mermaid version loaded from jsDelivr. Must match `x` / `x.y` / `x.y.z`. Invalid values fall back to the default with a `console.warn`. |
46
- | `theme` | `'default'` | One of `default` / `dark` / `forest` / `neutral` / `base`. Anything else falls back to `default`. |
47
- | `securityLevel` | `'strict'` | One of `strict` / `loose` / `antiscript` / `sandbox`. Anything else falls back to `strict`. See [Security](#security--cdn-notes). |
43
+ | Option | Default | Notes |
44
+ | --------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | `version` | `'11.15.0'` | mermaid version loaded from jsDelivr. Must match `x` / `x.y` / `x.y.z`. Invalid values fall back to the default with a `console.warn`. |
46
+ | `theme` | `'auto'` | One of `auto` / `default` / `dark` / `forest` / `neutral` / `base`. `'auto'` (the default) follows the site color scheme — see [Color scheme](#color-scheme). Anything else falls back to `'auto'`. |
47
+ | `securityLevel` | `'strict'` | One of `strict` / `loose` / `antiscript` / `sandbox`. Anything else falls back to `strict`. See [Security](#security--cdn-notes). |
48
+
49
+ ## Color scheme
50
+
51
+ With the default `theme: 'auto'`, the diagram theme adapts to the site's light/dark color scheme so the diagram text stays readable on a dark background (mermaid bakes its theme colors into the rendered SVG, so a light theme on a dark page is otherwise low-contrast):
52
+
53
+ | Site scheme | mermaid theme used |
54
+ | ----------- | ------------------ |
55
+ | light | `default` |
56
+ | dark | `dark` |
57
+
58
+ The scheme is detected at runtime, in this order:
59
+
60
+ 1. The `<html data-color-scheme>` attribute — ampless sets `'light'` / `'dark'` here when the site pins a scheme (including an in-site toggle).
61
+ 2. When the attribute is absent (site setting `auto`), the OS `prefers-color-scheme` is used (guarded — a missing `matchMedia` is treated as light).
62
+
63
+ **Live switching.** Diagrams re-render in place when the scheme changes — both when an in-site toggle flips `data-color-scheme`, and (in `auto` mode, when no attribute pins the scheme) when the OS preference changes. Because mermaid cannot re-tint an existing SVG, each rendered diagram keeps its source on a `data-mermaid-src` attribute and is re-rendered from it; pages with no diagram never download the library on a scheme change.
64
+
65
+ **Pinning.** Pass an explicit theme (e.g. `theme: 'dark'`) to pin that theme regardless of the site scheme and disable the live re-render. Light-page output with the previous `'default'` default is unchanged.
66
+
67
+ > **Custom dark themes:** `'auto'` keys off the `data-color-scheme` / `prefers-color-scheme` signal, **not** the theme's visual darkness. If your theme renders a dark design but doesn't set `data-color-scheme="dark"` on `<html>`, `'auto'` resolves to light and mermaid uses its dark-text light theme, which clashes with the dark background. Pin `theme: 'dark'` in that case (or have the theme set `data-color-scheme="dark"`).
48
68
 
49
69
  ## How code blocks are detected
50
70
 
@@ -77,7 +97,8 @@ The two plugins are designed to run together in any order. `@ampless/plugin-high
77
97
 
78
98
  - **Idempotent re-scan** — processed blocks are marked with `data-ampless-done`, so the scan never double-renders.
79
99
  - **SPA / App Router navigation** — the head script runs once, but a debounced `MutationObserver` on `document.body` re-scans when client navigation injects new post content.
80
- - **Failure recovery** — if the dynamic import fails, the cached import promise is cleared and the block marks are removed so a later scan retries; failures are reported via `console.warn` rather than swallowed. A per-diagram render failure leaves the original code block visible.
100
+ - **Live scheme switching** — a `MutationObserver` on `<html>` (`data-color-scheme`) plus, in `auto` mode, a `matchMedia('(prefers-color-scheme: dark)')` listener re-render the diagrams when the scheme changes. A short burst of switches converges on the final scheme: a render that resolves against a now-stale scheme is discarded and re-kicked.
101
+ - **Failure recovery** — if the dynamic import fails, the cached import promise is cleared and the block marks are removed so a later scan retries; failures are reported via `console.warn` rather than swallowed. A per-diagram render failure leaves the original code block visible (and a re-render failure leaves the previous SVG, retried on the next scheme change).
81
102
 
82
103
  ## Security / CDN notes
83
104
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AmplessPlugin } from 'ampless';
2
2
 
3
- declare const THEMES: readonly ["default", "dark", "forest", "neutral", "base"];
3
+ declare const THEMES: readonly ["auto", "default", "dark", "forest", "neutral", "base"];
4
4
  declare const SECURITY_LEVELS: readonly ["strict", "loose", "antiscript", "sandbox"];
5
5
  type MermaidTheme = (typeof THEMES)[number];
6
6
  type MermaidSecurityLevel = (typeof SECURITY_LEVELS)[number];
@@ -13,7 +13,18 @@ interface MermaidPluginOptions {
13
13
  * responsibility.
14
14
  */
15
15
  version?: string;
16
- /** mermaid theme. One of default / dark / forest / neutral / base. */
16
+ /**
17
+ * mermaid theme. One of `auto` / `default` / `dark` / `forest` /
18
+ * `neutral` / `base`. Default `'auto'`.
19
+ *
20
+ * `'auto'` (the default) adapts to the site's color scheme at runtime:
21
+ * it renders with `'dark'` on a dark scheme and `'default'` (mermaid's
22
+ * light theme) otherwise, and live-re-renders when the scheme changes.
23
+ * The scheme is read from the `<html data-color-scheme>` attribute
24
+ * (`'light'` / `'dark'`); when the attribute is absent (site setting
25
+ * `auto`) it follows the OS `prefers-color-scheme`. Any explicit theme
26
+ * pins that theme regardless of the site scheme.
27
+ */
17
28
  theme?: MermaidTheme;
18
29
  /**
19
30
  * mermaid `securityLevel`. Default `'strict'`. `'loose'` enables
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // src/index.ts
2
2
  import { definePlugin } from "ampless";
3
3
  var DEFAULT_VERSION = "11.15.0";
4
- var DEFAULT_THEME = "default";
4
+ var DEFAULT_THEME = "auto";
5
5
  var DEFAULT_SECURITY_LEVEL = "strict";
6
6
  var VERSION_RE = /^[0-9]+(\.[0-9]+){0,2}$/;
7
- var THEMES = ["default", "dark", "forest", "neutral", "base"];
7
+ var THEMES = ["auto", "default", "dark", "forest", "neutral", "base"];
8
8
  var SECURITY_LEVELS = ["strict", "loose", "antiscript", "sandbox"];
9
9
  function pickAllowed(value, allowed, fallback, label) {
10
10
  if (value === void 0) return fallback;
@@ -24,13 +24,47 @@ function pickVersion(value) {
24
24
  }
25
25
  function buildBody(version, theme, securityLevel) {
26
26
  const SEC = JSON.stringify(securityLevel);
27
- const THEME = JSON.stringify(theme);
27
+ const CONFIGURED = JSON.stringify(theme);
28
28
  const SRC = JSON.stringify(
29
29
  `https://cdn.jsdelivr.net/npm/mermaid@${version}/dist/mermaid.esm.min.mjs`
30
30
  );
31
31
  return `(function () {
32
+ var configured = ${CONFIGURED};
32
33
  var modPromise;
33
34
  var counter = 0;
35
+ // Which theme the (singleton) mermaid library was last initialized with \u2014
36
+ // not the per-SVG render state. Each rendered wrap remembers its own theme
37
+ // via the data-mermaid-theme attribute.
38
+ var initedTheme;
39
+ // Resolve the active color scheme: explicit data-color-scheme wins;
40
+ // otherwise follow the OS preference, guarded so a missing matchMedia
41
+ // (older / non-browser environments) just means "light".
42
+ function isDark() {
43
+ var attr = document.documentElement.getAttribute('data-color-scheme');
44
+ if (attr === 'dark') return true;
45
+ if (attr === 'light') return false;
46
+ return typeof window.matchMedia === 'function'
47
+ ? window.matchMedia('(prefers-color-scheme: dark)').matches
48
+ : false;
49
+ }
50
+ // Mirror of chooseMermaidTheme (src/theme.ts): explicit theme pins;
51
+ // 'auto' maps dark->'dark' / light->'default'.
52
+ function effectiveTheme() {
53
+ return configured === 'auto' ? (isDark() ? 'dark' : 'default') : configured;
54
+ }
55
+ function ensureMermaidTheme(mermaid, t) {
56
+ if (initedTheme !== t) {
57
+ mermaid.initialize({ startOnLoad: false, securityLevel: ${SEC}, theme: t });
58
+ initedTheme = t;
59
+ }
60
+ }
61
+ function freshId() {
62
+ var uuid = globalThis.crypto && globalThis.crypto.randomUUID
63
+ ? globalThis.crypto.randomUUID() : undefined;
64
+ return uuid ? 'm' + uuid : 'ampless-mmd-' + (counter++);
65
+ }
66
+ // Initial pass: turn each <pre><code class="language-mermaid"> into a
67
+ // rendered <div class="ampless-mermaid"> carrying its source + theme.
34
68
  function scan() {
35
69
  var blocks = Array.prototype.slice.call(
36
70
  document.querySelectorAll('pre > code.language-mermaid:not([data-ampless-done])')
@@ -42,18 +76,29 @@ function buildBody(version, theme, securityLevel) {
42
76
  if (!modPromise) modPromise = import(${SRC});
43
77
  modPromise.then(function (mod) {
44
78
  var mermaid = mod.default;
45
- mermaid.initialize({ startOnLoad: false, securityLevel: ${SEC}, theme: ${THEME} });
79
+ ensureMermaidTheme(mermaid, effectiveTheme());
46
80
  blocks.forEach(function (code) {
47
81
  var pre = code.closest('pre');
48
- var uuid = globalThis.crypto && globalThis.crypto.randomUUID
49
- ? globalThis.crypto.randomUUID() : undefined;
50
- var id = uuid ? 'm' + uuid : 'ampless-mmd-' + (counter++);
82
+ var src = code.textContent || '';
83
+ var t = effectiveTheme();
84
+ var id = freshId();
51
85
  Promise.resolve()
52
- .then(function () { return mermaid.render(id, code.textContent || ''); })
86
+ .then(function () { return mermaid.render(id, src); })
53
87
  .then(function (res) {
88
+ if (effectiveTheme() !== t) {
89
+ // Stale: the scheme changed mid-render. Discard this SVG and
90
+ // re-run scan() \u2014 the block is still a <pre><code>, so
91
+ // rerenderAll() (which only sees div.ampless-mermaid) cannot
92
+ // pick it up. Unmark so scan() processes it again.
93
+ code.removeAttribute('data-ampless-done');
94
+ scan();
95
+ return;
96
+ }
54
97
  var wrap = document.createElement('div');
55
98
  wrap.className = 'ampless-mermaid';
56
99
  wrap.innerHTML = res.svg;
100
+ wrap.setAttribute('data-mermaid-src', src);
101
+ wrap.setAttribute('data-mermaid-theme', t);
57
102
  (pre || code).replaceWith(wrap);
58
103
  })
59
104
  .catch(function (e) {
@@ -70,6 +115,49 @@ function buildBody(version, theme, securityLevel) {
70
115
  console.warn('[ampless-mermaid] load failed', e);
71
116
  });
72
117
  }
118
+ // Re-render already-rendered diagrams whose baked-in theme no longer
119
+ // matches the active scheme (mermaid bakes the theme into the SVG, so it
120
+ // cannot be re-tinted \u2014 only re-rendered from the saved source).
121
+ function rerenderAll() {
122
+ var theme = effectiveTheme();
123
+ var wraps = Array.prototype.slice
124
+ .call(document.querySelectorAll('div.ampless-mermaid[data-mermaid-src]'))
125
+ .filter(function (w) { return w.getAttribute('data-mermaid-theme') !== theme; });
126
+ // No stale diagram on this page -> do NOT import mermaid (preserves the
127
+ // "no Mermaid block -> never download the library" property).
128
+ if (!wraps.length) return;
129
+ if (!modPromise) modPromise = import(${SRC});
130
+ modPromise.then(function (mod) {
131
+ var mermaid = mod.default;
132
+ var t = effectiveTheme();
133
+ ensureMermaidTheme(mermaid, t);
134
+ wraps.forEach(function (wrap) {
135
+ var id = freshId();
136
+ var src = wrap.getAttribute('data-mermaid-src') || '';
137
+ Promise.resolve()
138
+ .then(function () { return mermaid.render(id, src); })
139
+ .then(function (res) {
140
+ if (effectiveTheme() !== t) {
141
+ // Stale: leave the old SVG + old data-mermaid-theme in place
142
+ // (so it is still seen as out-of-date) and re-kick rerenderAll
143
+ // to converge on the current theme.
144
+ rerenderAll();
145
+ return;
146
+ }
147
+ wrap.innerHTML = res.svg;
148
+ wrap.setAttribute('data-mermaid-theme', t);
149
+ })
150
+ .catch(function (e) {
151
+ // Leave old SVG + old data-mermaid-theme so the next scheme
152
+ // change re-attempts this wrap (its theme still won't match).
153
+ console.warn('[ampless-mermaid] re-render failed', e);
154
+ });
155
+ });
156
+ }).catch(function (e) {
157
+ modPromise = undefined;
158
+ console.warn('[ampless-mermaid] load failed', e);
159
+ });
160
+ }
73
161
  function init() {
74
162
  scan();
75
163
  // SPA / App Router client navigation: the head script runs once but new
@@ -81,6 +169,28 @@ function buildBody(version, theme, securityLevel) {
81
169
  t = setTimeout(scan, 100);
82
170
  });
83
171
  obs.observe(document.body, { childList: true, subtree: true });
172
+ // In-site theme toggle: watch the <html> data-color-scheme attribute
173
+ // (body childList mutations never reflect this) and re-render.
174
+ var schemeObs = new MutationObserver(function () { rerenderAll(); });
175
+ schemeObs.observe(document.documentElement, {
176
+ attributes: true,
177
+ attributeFilter: ['data-color-scheme'],
178
+ });
179
+ }
180
+ // OS scheme change in 'auto' mode (site setting 'auto' -> no attribute).
181
+ // No-op while data-color-scheme pins the scheme; fires once the attribute
182
+ // is removed (fixed -> auto) so the OS preference takes over again.
183
+ if (configured === 'auto' && typeof window.matchMedia === 'function') {
184
+ var mql = window.matchMedia('(prefers-color-scheme: dark)');
185
+ var onChange = function () {
186
+ if (document.documentElement.getAttribute('data-color-scheme')) return;
187
+ rerenderAll();
188
+ };
189
+ if (typeof mql.addEventListener === 'function') {
190
+ mql.addEventListener('change', onChange);
191
+ } else if (typeof mql.addListener === 'function') {
192
+ mql.addListener(onChange); // Safari < 14
193
+ }
84
194
  }
85
195
  }
86
196
  if (document.readyState === 'loading') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/plugin-mermaid",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Mermaid diagram plugin for ampless — renders `code.language-mermaid` blocks on the public site as diagrams via a lazily CDN-loaded mermaid.js",
5
5
  "license": "MIT",
6
6
  "type": "module",