@ampless/plugin-highlight 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,14 +35,34 @@ export default defineConfig({
35
35
  ```ts
36
36
  highlightPlugin({
37
37
  version: '11.11.1', // 既定値(固定 x.y.z)
38
- theme: 'github', // highlight.js のスタイルシート名
38
+ theme: 'auto', // 'auto' または highlight.js のスタイルシート名
39
39
  })
40
40
  ```
41
41
 
42
- | オプション | デフォルト | 備考 |
43
- | ---------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `version` | `'11.11.1'` | jsDelivr から読み込む highlight.js のバージョン。`x` / `x.y` / `x.y.z` に一致する必要あり。不正値は `console.warn` してデフォルトにフォールバック。 |
45
- | `theme` | `'github'` | highlight.js のスタイルシート名(例: `github` / `github-dark` / `atom-one-dark` / `monokai`)。`/^[a-z0-9][a-z0-9-]{0,40}$/` に一致する必要あり。それ以外は `github` にフォールバック。対応する `styles/<theme>.min.css` を CDN から読み込みます。[highlight.js のスタイル一覧](https://github.com/highlightjs/highlight.js/tree/main/src/styles)参照。 |
42
+ | オプション | デフォルト | 備考 |
43
+ | ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `version` | `'11.11.1'` | jsDelivr から読み込む highlight.js のバージョン。`x` / `x.y` / `x.y.z` に一致する必要あり。不正値は `console.warn` してデフォルトにフォールバック。 |
45
+ | `theme` | `'auto'` | `'auto'`(既定)はサイトのカラースキームに追従します([カラースキーム](#カラースキーム)参照)。または highlight.js のスタイルシート名(例: `github` / `github-dark` / `atom-one-dark` / `monokai`)。明示名は `/^[a-z0-9][a-z0-9-]{0,40}$/` に一致する必要あり。それ以外は `'auto'` にフォールバック。対応する `styles/<theme>.min.css` を CDN から読み込みます。[highlight.js のスタイル一覧](https://github.com/highlightjs/highlight.js/tree/main/src/styles)参照。 |
46
+
47
+ ## カラースキーム
48
+
49
+ 既定の `theme: 'auto'` では、スタイルシートがサイトのライト/ダークのカラースキームに追従するため、ダーク背景でもハイライト済みコードが読みやすく保たれます:
50
+
51
+ | サイトのスキーム | 使用する highlight.js スタイルシート |
52
+ | ---------------- | ------------------------------------ |
53
+ | light | `github` |
54
+ | dark | `github-dark` |
55
+
56
+ スキームは実行時に次の順で判定します:
57
+
58
+ 1. `<html data-color-scheme>` 属性 — サイトがスキームを固定する場合(サイト内トグル含む)に ampless が `'light'` / `'dark'` を設定します。
59
+ 2. 属性が無い場合(サイト設定 `auto`)は OS の `prefers-color-scheme` を使用します(ガード付き。`matchMedia` 未定義環境では light 扱い)。
60
+
61
+ **ライブ切替。** スキームが変わるとテーマ用スタイルシートがその場で差し替わります — サイト内トグルが `data-color-scheme` を切り替えたとき、および(`auto` モードで属性がスキームを固定していないとき)OS の設定が変わったときの両方に追従します。highlight.js はブロックに `hljs` クラスを残すため、差し替えは `<link>` だけで済みます(再ハイライト不要)。差し替えはちらつきなし(新スタイルシートをロードしてから旧 link を削除)で、連続切替時も最終スキームへ収束します。コードブロックの無いページではスキーム変更時もスタイルシートを読み込みません。
62
+
63
+ **固定。** 明示テーマ(例: `theme: 'github-dark'`)を渡すと、サイトのスキームに関わらずそのスタイルシートに固定され、ライブ切替も無効になります。
64
+
65
+ > **カスタムダークテーマの注意。** `'auto'` は `data-color-scheme` / `prefers-color-scheme` のシグナルで判定し、テーマの見た目の暗さは見ません。テーマがダークなデザインでも `<html>` に `data-color-scheme="dark"` を設定していない場合、`'auto'` は light と判定して明テーマの `github` を読み込み、ダーク背景で低コントラストになります。その場合は `theme: 'github-dark'` を固定してください(またはテーマ側で `data-color-scheme="dark"` を設定)。
46
66
 
47
67
  ## コードブロックの検出方法
48
68
 
@@ -72,7 +92,8 @@ const greet = (name: string) => `Hello, ${name}!`
72
92
 
73
93
  - **冪等な再スキャン** — highlight.js は処理済みブロックに `hljs` クラスを付与し、セレクタは `:not(.hljs)` で守るため、再ハイライトしません。
74
94
  - **SPA / App Router 遷移** — head スクリプトは一度だけ実行されますが、`document.body` に張ったデバウンス付き `MutationObserver` が、クライアント遷移で後から挿入された投稿コンテンツを再スキャンします。
75
- - **失敗時の復旧**動的 import が失敗した場合はキャッシュした import Promise を破棄するため次回スキャンで再試行されます。失敗は握り潰さず `console.warn` で報告します。
95
+ - **ライブなスキーム切替**`<html>`(`data-color-scheme`)への `MutationObserver` と、`auto` モード時の `matchMedia('(prefers-color-scheme: dark)')` リスナが、スキーム変更時にスタイルシートを差し替えます。差し替えは直列化(同時に 1 本の `<link>` のみ)かつちらつきなしで、コードブロックの無いページでは no-op です。
96
+ - **失敗時の復旧** — 動的 import が失敗した場合はキャッシュした import Promise を破棄するため次回スキャンで再試行されます。失敗は握り潰さず `console.warn` で報告します。スタイルシートのロードに失敗した場合は直前のテーマを維持します。
76
97
  - **テーマ用スタイルシート** — ハイライト対象のブロックがある場合のみ、id `ampless-hljs-theme` で一度だけ注入されます。
77
98
 
78
99
  ## セキュリティ / CDN に関する注意
package/README.md CHANGED
@@ -35,14 +35,34 @@ export default defineConfig({
35
35
  ```ts
36
36
  highlightPlugin({
37
37
  version: '11.11.1', // pinned default
38
- theme: 'github', // any highlight.js stylesheet name
38
+ theme: 'auto', // 'auto' or any highlight.js stylesheet name
39
39
  })
40
40
  ```
41
41
 
42
- | Option | Default | Notes |
43
- | --------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `version` | `'11.11.1'` | highlight.js version loaded from jsDelivr. Must match `x` / `x.y` / `x.y.z`. Invalid values fall back to the default with a `console.warn`. |
45
- | `theme` | `'github'` | A highlight.js stylesheet name (e.g. `github`, `github-dark`, `atom-one-dark`, `monokai`). Must match `/^[a-z0-9][a-z0-9-]{0,40}$/`; anything else falls back to `github`. The corresponding `styles/<theme>.min.css` is loaded from the CDN. See the [highlight.js styles list](https://github.com/highlightjs/highlight.js/tree/main/src/styles). |
42
+ | Option | Default | Notes |
43
+ | --------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `version` | `'11.11.1'` | highlight.js version loaded from jsDelivr. Must match `x` / `x.y` / `x.y.z`. Invalid values fall back to the default with a `console.warn`. |
45
+ | `theme` | `'auto'` | `'auto'` (the default) follows the site color scheme — see [Color scheme](#color-scheme) — or a highlight.js stylesheet name (e.g. `github`, `github-dark`, `atom-one-dark`, `monokai`). Explicit names must match `/^[a-z0-9][a-z0-9-]{0,40}$/`; anything else falls back to `'auto'`. The corresponding `styles/<theme>.min.css` is loaded from the CDN. See the [highlight.js styles list](https://github.com/highlightjs/highlight.js/tree/main/src/styles). |
46
+
47
+ ## Color scheme
48
+
49
+ With the default `theme: 'auto'`, the stylesheet adapts to the site's light/dark color scheme so highlighted code stays readable on a dark background:
50
+
51
+ | Site scheme | highlight.js stylesheet used |
52
+ | ----------- | ---------------------------- |
53
+ | light | `github` |
54
+ | dark | `github-dark` |
55
+
56
+ The scheme is detected at runtime, in this order:
57
+
58
+ 1. The `<html data-color-scheme>` attribute — ampless sets `'light'` / `'dark'` here when the site pins a scheme (including an in-site toggle).
59
+ 2. When the attribute is absent (site setting `auto`), the OS `prefers-color-scheme` is used (guarded — a missing `matchMedia` is treated as light).
60
+
61
+ **Live switching.** The theme stylesheet swaps 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. highlight.js leaves its `hljs` classes on the blocks, so only the `<link>` is swapped (no re-highlight). The swap is flash-free (the new stylesheet is loaded before the old one is removed) and converges on the final scheme during a burst of switches; a page with no code block never loads a stylesheet on a scheme change.
62
+
63
+ **Pinning.** Pass an explicit theme (e.g. `theme: 'github-dark'`) to pin that stylesheet regardless of the site scheme and disable the live swap.
64
+
65
+ > **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 loads the light `github` stylesheet, which is low-contrast on the dark background. Pin `theme: 'github-dark'` in that case (or have the theme set `data-color-scheme="dark"`).
46
66
 
47
67
  ## How code blocks are detected
48
68
 
@@ -72,7 +92,8 @@ The two plugins are designed to run together in any order. This plugin's selecto
72
92
 
73
93
  - **Idempotent re-scan** — highlight.js adds the `hljs` class to processed blocks, and the selector guards on `:not(.hljs)`, so the scan never re-highlights.
74
94
  - **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.
75
- - **Failure recovery** — if the dynamic import fails, the cached import promise is cleared so a later scan retries; failures are reported via `console.warn` rather than swallowed.
95
+ - **Live scheme switching** — a `MutationObserver` on `<html>` (`data-color-scheme`) plus, in `auto` mode, a `matchMedia('(prefers-color-scheme: dark)')` listener swap the stylesheet when the scheme changes. The swap is serialized (a single in-flight `<link>`) and flash-free, and is a no-op on pages with no code block.
96
+ - **Failure recovery** — if the dynamic import fails, the cached import promise is cleared so a later scan retries; failures are reported via `console.warn` rather than swallowed. A stylesheet that fails to load keeps the previous theme.
76
97
  - **Theme stylesheet** — injected once with id `ampless-hljs-theme`, only when a highlightable block is present.
77
98
 
78
99
  ## Security / CDN notes
package/dist/index.d.ts CHANGED
@@ -11,10 +11,19 @@ interface HighlightPluginOptions {
11
11
  version?: string;
12
12
  /**
13
13
  * highlight.js stylesheet theme name (e.g. `'github'`,
14
- * `'github-dark'`, `'atom-one-dark'`). Must match
15
- * `/^[a-z0-9][a-z0-9-]{0,40}$/`; invalid values fall back to
16
- * `'github'` with a `console.warn`. The corresponding
17
- * `styles/<theme>.min.css` is loaded from the CDN.
14
+ * `'github-dark'`, `'atom-one-dark'`), or the sentinel `'auto'`.
15
+ * Default `'auto'`.
16
+ *
17
+ * `'auto'` (the default) adapts to the site's color scheme at runtime:
18
+ * it loads `github-dark` on a dark scheme and `github` otherwise, and
19
+ * live-swaps the stylesheet when the scheme changes. The scheme is read
20
+ * from the `<html data-color-scheme>` attribute (`'light'` / `'dark'`);
21
+ * when the attribute is absent (site setting `auto`) it follows the OS
22
+ * `prefers-color-scheme`. Any explicit theme pins that stylesheet
23
+ * regardless of the site scheme. Explicit names must match
24
+ * `/^[a-z0-9][a-z0-9-]{0,40}$/`; invalid values fall back to `'auto'`
25
+ * with a `console.warn`. The corresponding `styles/<theme>.min.css` is
26
+ * loaded from the CDN.
18
27
  */
19
28
  theme?: string;
20
29
  }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import { definePlugin } from "ampless";
3
3
  var DEFAULT_VERSION = "11.11.1";
4
- var DEFAULT_THEME = "github";
4
+ var DEFAULT_THEME = "auto";
5
5
  var VERSION_RE = /^[0-9]+(\.[0-9]+){0,2}$/;
6
6
  var THEME_RE = /^[a-z0-9][a-z0-9-]{0,40}$/;
7
7
  function pickVersion(value) {
@@ -14,6 +14,7 @@ function pickVersion(value) {
14
14
  }
15
15
  function pickTheme(value) {
16
16
  if (value === void 0) return DEFAULT_THEME;
17
+ if (value === "auto") return "auto";
17
18
  if (THEME_RE.test(value)) return value;
18
19
  console.warn(
19
20
  `[ampless-highlight] ignoring invalid theme "${value}"; falling back to "${DEFAULT_THEME}".`
@@ -21,12 +22,34 @@ function pickTheme(value) {
21
22
  return DEFAULT_THEME;
22
23
  }
23
24
  function buildBody(version, theme) {
24
- const CSS = JSON.stringify(
25
- `https://cdn.jsdelivr.net/npm/highlight.js@${version}/styles/${theme}.min.css`
26
- );
25
+ const CONFIGURED = JSON.stringify(theme);
26
+ const CSS_PREFIX = JSON.stringify(`https://cdn.jsdelivr.net/npm/highlight.js@${version}/styles/`);
27
27
  const SRC = JSON.stringify(`https://cdn.jsdelivr.net/npm/highlight.js@${version}/+esm`);
28
28
  return `(function () {
29
+ var configured = ${CONFIGURED};
29
30
  var modPromise;
31
+ // Serializes theme swaps: the <link> we are mid-loading and the href we
32
+ // are loading towards. Prevents duplicate id / wrong-theme races when the
33
+ // scheme flips several times before a stylesheet finishes loading.
34
+ var pendingLink;
35
+ var pendingHref;
36
+ // Resolve the active color scheme: explicit data-color-scheme wins;
37
+ // otherwise follow the OS preference, guarded so a missing matchMedia
38
+ // (older / non-browser environments) just means "light".
39
+ function isDark() {
40
+ var attr = document.documentElement.getAttribute('data-color-scheme');
41
+ if (attr === 'dark') return true;
42
+ if (attr === 'light') return false;
43
+ return typeof window.matchMedia === 'function'
44
+ ? window.matchMedia('(prefers-color-scheme: dark)').matches
45
+ : false;
46
+ }
47
+ // Mirror of chooseHighlightHref (src/theme.ts): explicit theme pins;
48
+ // 'auto' maps dark->github-dark / light->github.
49
+ function themeHref() {
50
+ var name = configured === 'auto' ? (isDark() ? 'github-dark' : 'github') : configured;
51
+ return ${CSS_PREFIX} + name + '.min.css';
52
+ }
30
53
  function scan() {
31
54
  var blocks = Array.prototype.slice.call(
32
55
  document.querySelectorAll('pre > code[class*="language-"]:not(.language-mermaid):not(.hljs)')
@@ -37,7 +60,7 @@ function buildBody(version, theme) {
37
60
  var link = document.createElement('link');
38
61
  link.id = 'ampless-hljs-theme';
39
62
  link.rel = 'stylesheet';
40
- link.href = ${CSS};
63
+ link.href = themeHref();
41
64
  document.head.appendChild(link);
42
65
  }
43
66
  if (!modPromise) modPromise = import(${SRC});
@@ -54,6 +77,56 @@ function buildBody(version, theme) {
54
77
  console.warn('[ampless-highlight] load failed', e);
55
78
  });
56
79
  }
80
+ // Swap the active theme stylesheet to match the current scheme. The hljs
81
+ // classes stay on the blocks, so swapping the <link> re-colors them with
82
+ // no re-highlight. FOUC-safe (add new <link>, swap on load) and race-safe
83
+ // (serialized via pendingLink/pendingHref).
84
+ function swapTheme() {
85
+ var active = document.getElementById('ampless-hljs-theme');
86
+ // No code-block page: nothing to swap. A later scan() injects fresh.
87
+ if (!active) return;
88
+ var desired = themeHref();
89
+ // Already on the right theme, or already loading towards it.
90
+ if (active.href === desired || pendingHref === desired) return;
91
+ // A different swap is in flight: drop its stale <link> before starting
92
+ // a new one (avoids two id-less links racing to claim the id).
93
+ if (pendingLink) {
94
+ if (pendingLink.parentNode) pendingLink.parentNode.removeChild(pendingLink);
95
+ pendingLink = undefined;
96
+ pendingHref = undefined;
97
+ }
98
+ // Add the new stylesheet WITHOUT the id (avoids a transient duplicate id)
99
+ // and only promote it once it has loaded, so the old theme stays applied
100
+ // until the new one is ready (no flash of unstyled code).
101
+ var newLink = document.createElement('link');
102
+ newLink.rel = 'stylesheet';
103
+ newLink.href = desired;
104
+ newLink.onload = function () {
105
+ // Re-check: the scheme may have flipped again while loading.
106
+ if (newLink.href === themeHref()) {
107
+ var old = document.getElementById('ampless-hljs-theme');
108
+ if (old && old !== newLink && old.parentNode) old.parentNode.removeChild(old);
109
+ newLink.id = 'ampless-hljs-theme';
110
+ pendingLink = undefined;
111
+ pendingHref = undefined;
112
+ } else {
113
+ // Stale: keep the old (id-bearing) link, drop this one, re-kick.
114
+ if (newLink.parentNode) newLink.parentNode.removeChild(newLink);
115
+ pendingLink = undefined;
116
+ pendingHref = undefined;
117
+ swapTheme();
118
+ }
119
+ };
120
+ newLink.onerror = function (e) {
121
+ if (newLink.parentNode) newLink.parentNode.removeChild(newLink);
122
+ pendingLink = undefined;
123
+ pendingHref = undefined;
124
+ console.warn('[ampless-highlight] theme stylesheet load failed', e);
125
+ };
126
+ pendingLink = newLink;
127
+ pendingHref = desired;
128
+ document.head.appendChild(newLink);
129
+ }
57
130
  function init() {
58
131
  scan();
59
132
  // SPA / App Router client navigation: the head script runs once but new
@@ -65,6 +138,28 @@ function buildBody(version, theme) {
65
138
  t = setTimeout(scan, 100);
66
139
  });
67
140
  obs.observe(document.body, { childList: true, subtree: true });
141
+ // In-site theme toggle: watch the <html> data-color-scheme attribute
142
+ // (body childList mutations never reflect this) and swap the theme.
143
+ var schemeObs = new MutationObserver(function () { swapTheme(); });
144
+ schemeObs.observe(document.documentElement, {
145
+ attributes: true,
146
+ attributeFilter: ['data-color-scheme'],
147
+ });
148
+ }
149
+ // OS scheme change in 'auto' mode (site setting 'auto' -> no attribute).
150
+ // No-op while data-color-scheme pins the scheme; fires once the attribute
151
+ // is removed (fixed -> auto) so the OS preference takes over again.
152
+ if (configured === 'auto' && typeof window.matchMedia === 'function') {
153
+ var mql = window.matchMedia('(prefers-color-scheme: dark)');
154
+ var onChange = function () {
155
+ if (document.documentElement.getAttribute('data-color-scheme')) return;
156
+ swapTheme();
157
+ };
158
+ if (typeof mql.addEventListener === 'function') {
159
+ mql.addEventListener('change', onChange);
160
+ } else if (typeof mql.addListener === 'function') {
161
+ mql.addListener(onChange); // Safari < 14
162
+ }
68
163
  }
69
164
  }
70
165
  if (document.readyState === 'loading') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/plugin-highlight",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Syntax highlighting plugin for ampless — highlights `code.language-*` blocks on the public site via a lazily CDN-loaded highlight.js",
5
5
  "license": "MIT",
6
6
  "type": "module",