@dr-ishaan/rehype-perfect-code-blocks 1.3.0 → 1.3.3

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +0 -0
  3. package/README.md +225 -13
  4. package/dist/astro.d.ts +0 -0
  5. package/dist/astro.d.ts.map +1 -1
  6. package/dist/astro.js +9 -2
  7. package/dist/astro.js.map +1 -1
  8. package/dist/color-utils.d.ts +0 -0
  9. package/dist/color-utils.d.ts.map +0 -0
  10. package/dist/color-utils.js +0 -0
  11. package/dist/color-utils.js.map +0 -0
  12. package/dist/copy-script.d.ts +1 -1
  13. package/dist/copy-script.d.ts.map +1 -1
  14. package/dist/copy-script.js +47 -14
  15. package/dist/copy-script.js.map +1 -1
  16. package/dist/index.d.ts +0 -0
  17. package/dist/index.d.ts.map +0 -0
  18. package/dist/index.js +0 -0
  19. package/dist/index.js.map +0 -0
  20. package/dist/meta.d.ts +0 -0
  21. package/dist/meta.d.ts.map +0 -0
  22. package/dist/meta.js +0 -0
  23. package/dist/meta.js.map +0 -0
  24. package/dist/remark.d.ts +0 -0
  25. package/dist/remark.d.ts.map +0 -0
  26. package/dist/remark.js +0 -0
  27. package/dist/remark.js.map +0 -0
  28. package/dist/shiki.d.ts +0 -0
  29. package/dist/shiki.d.ts.map +0 -0
  30. package/dist/shiki.js +0 -0
  31. package/dist/shiki.js.map +0 -0
  32. package/dist/styles.css +65 -0
  33. package/dist/transformer.d.ts +0 -0
  34. package/dist/transformer.d.ts.map +0 -0
  35. package/dist/transformer.js +0 -0
  36. package/dist/transformer.js.map +0 -0
  37. package/dist/types.d.ts +0 -0
  38. package/dist/types.d.ts.map +0 -0
  39. package/dist/types.js +0 -0
  40. package/dist/types.js.map +0 -0
  41. package/dist/word-diff.d.ts +0 -0
  42. package/dist/word-diff.d.ts.map +0 -0
  43. package/dist/word-diff.js +0 -0
  44. package/dist/word-diff.js.map +0 -0
  45. package/package.json +2 -2
  46. package/src/astro.ts +9 -2
  47. package/src/color-utils.ts +0 -0
  48. package/src/copy-script.ts +47 -14
  49. package/src/index.ts +0 -0
  50. package/src/meta.ts +0 -0
  51. package/src/remark.ts +0 -0
  52. package/src/shiki.ts +0 -0
  53. package/src/styles.css +65 -0
  54. package/src/transformer.ts +0 -0
  55. package/src/types.ts +0 -0
  56. package/src/vite-raw.d.ts +0 -0
  57. package/src/word-diff.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,69 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.3] — 2026-06-20
9
+
10
+ ### Summary
11
+
12
+ Patch release fixing CSS conflicts between the plugin and Tailwind CSS Preflight (and similar framework base resets). The plugin's `:where()` zero-specificity design — normally a feature — meant Tailwind's bare-element resets (`pre { overflow-x: auto }`, `code { font-family: ui-monospace }`, `button { background: transparent }`, `* { border-width: 0 }`) overrode the plugin's critical rules. This caused double scrollbars, wrong mono font, stripped copy-button styling, and long lines wrapping instead of scrolling. The fix adds a small block of "framework-reset overrides" with real specificity that beat framework base resets without `!important`.
13
+
14
+ ### Bug fixes
15
+
16
+ - **Tailwind Preflight / daisyUI CSS conflict** — Added a block of "framework-reset overrides" in `src/styles.css` with REAL specificity (`.pcb pre` = (0,1,1), `.pcb__copy` = (0,1,0), `.pcb__bar` = (0,1,0), `.pcb__code` = (0,1,0)) that beat framework base resets targeting bare `pre`/`code`/`button`/`*` elements (specificity (0,0,1) or (0,0,0)). The overrides cover:
17
+ - `.pcb pre { overflow: visible }` — prevents Tailwind's `pre { overflow-x: auto }` from creating a double scrollbar (one on `<pre>`, one on `.pcb__body`).
18
+ - `.pcb pre, .pcb code { font-family: var(--pcb-font-mono) }` — prevents Tailwind's `pre, code { font-family: ui-monospace }` from overriding the plugin's `--pcb-font-mono`.
19
+ - `.pcb__copy { appearance: none; background: transparent; border: 1px solid transparent; cursor: pointer }` — prevents Tailwind's `button { background: transparent; background-image: none }` from stripping the copy button's base styling.
20
+ - `.pcb__bar { border-bottom: 1px solid var(--pcb-border) }` — prevents Tailwind's `* { border-width: 0 }` from nuking the header bar's bottom border.
21
+ - `.pcb__code { white-space: pre }` — prevents Tailwind utilities like `break-words` or global `pre { white-space: pre-wrap }` from wrapping long lines instead of scrolling.
22
+
23
+ The `:where()` zero-specificity rules are preserved for all other styling — user CSS still wins without `!important` arms races. The new overrides only set the properties that frameworks clobber; everything else stays in `:where()`. No `!important` is used in the override block — the specificity is high enough to beat framework resets on its own.
24
+
25
+ ### Verification
26
+
27
+ - All 1114 pre-existing tests pass (no regressions).
28
+ - New `test-tailwind-compat.mjs` adds 20 regression tests covering: `:where()` rules preserved, framework-reset overrides exist with real specificity, overrides come after `:where()` rules (so they win on tie), no `!important` in override declarations, specificity verification (`.pcb pre` beats `pre`, `.pcb__copy` beats `button`, etc.), and documentation comments.
29
+ - Total: 1134/1134 tests passing.
30
+
31
+ ## [1.3.2] — 2026-06-20
32
+
33
+ ### Summary
34
+
35
+ Patch release fixing a bug where the copy button was unclickable in Astro build output. The root cause was a script injection order race condition: the `.no-js` class (which hides the copy button via CSS when JS is disabled) was added AFTER the copy script ran, so the copy script's `swapNoJs()` was a no-op and the MutationObserver didn't catch the attribute change. Result: `.no-js` stayed on `<html>` permanently, `html.no-js .pcb__copy { display: none !important; }` hid the button, and clicks never reached it.
36
+
37
+ ### Bug fixes
38
+
39
+ - **Copy button unclickable in Astro build output** — Three complementary fixes:
40
+ 1. **Reversed script injection order** in `src/astro.ts`: the `.no-js` add script is now injected BEFORE the copy script, so the copy script's `swapNoJs()` correctly detects and removes the `.no-js` class at load time.
41
+ 2. **MutationObserver now watches attribute changes**: the observer on `documentElement` was previously configured with `{ childList: true, subtree: true }` only — it caught new DOM nodes but NOT class attribute changes on `<html>`. Now configured with `{ childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }` so it catches `.no-js` being added by any later script.
42
+ 3. **Defensive `DOMContentLoaded` + `window.load` re-check**: the copy script now re-runs `swapNoJs()` on both events as belt-and-suspenders. If a framework adds `.no-js` in a way the MutationObserver doesn't catch (e.g., before the observer is set up, or in a different document context), these event handlers will catch it.
43
+
44
+ ### Verification
45
+
46
+ - All 1092 pre-existing tests pass (no regressions).
47
+ - New `test-copy-button-fix.mjs` adds 22 regression tests covering: injection order in built output, MutationObserver configuration (attributes + class filter), defensive event handlers, `swapNoJs()` function behavior, functional simulation of both the fixed order and the old buggy order + defensive fix, and the CSS rule.
48
+ - Total: 1114/1114 tests passing.
49
+
50
+ ## [1.3.1] — 2026-06-19
51
+
52
+ ### Summary
53
+
54
+ Documentation-only release. Updates `README.md` to cover all v1.3.0 features (5 architectural patterns), the new advanced APIs (`runHighlighterTask`, `disposeHighlighter`, `wordDiff`, `hasChanges`, `DiffToken`), the new `wordDiff` option, theme-aware color defaults, the updated architecture diagram, the expanded test suite (1092 tests across 14 suites), the updated comparison table, and the corrected file structure. No code changes; no behavior changes.
55
+
56
+ ### Documentation
57
+
58
+ - **README.md** — comprehensive update for v1.3.0:
59
+ - New "What's new in v1.3.0" section with a table of the 5 adopted patterns (source, new export/option).
60
+ - New "Advanced APIs (v1.3.0+)" section documenting `runHighlighterTask`, `disposeHighlighter`, `wordDiff`, `hasChanges`, and the `DiffToken` type, with usage examples.
61
+ - New "Theme-aware defaults (v1.3.0+)" subsection in Theming, documenting the 7 auto-derived `--pcb-*` variables and the cascade order.
62
+ - New `wordDiff` row in the Modes options table, with a full usage example showing the input markdown and resulting HTML.
63
+ - Updated Architecture diagram and design-decisions list (10 items, up from 6) to mention the task queue, theme-aware defaults, dispose lifecycle, and SPA-robust copy button.
64
+ - Updated Testing section: 1092 tests across 14 suites (was "110 tests across three suites").
65
+ - Updated Comparison table: 4 new rows for word-level diff, theme-aware defaults, highlighter task queue, dispose lifecycle, and SPA-robust copy button.
66
+ - Updated File structure: new `color-utils.ts`, `word-diff.ts` source files; new `test-issue-11.mjs`, `test-issue-12.mjs`, `test-architecture-patterns.mjs` test files; updated descriptions for `shiki.ts`, `transformer.ts`, `copy-script.ts`, `index.ts`.
67
+ - New "Changelog" section in README with highlights for each version.
68
+ - Updated "Why this exists" bullet list to mention the new v1.3.0 features.
69
+ - Updated test count from "110 tests pass" to "1092 tests pass".
70
+
8
71
  ## [1.3.0] — 2026-06-19
9
72
 
10
73
  ### Summary
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -10,9 +10,33 @@ Beautiful, configurable code blocks for Astro, MDX, and any rehype pipeline. Bui
10
10
  - **rehype-pretty-code meta syntax** — `title="..."`, `{1,3-5}`, `/word/`, `/word/3-5#id`
11
11
  - **Auto terminal frame** for `sh`/`bash`/`zsh` etc., editor frame for everything else
12
12
  - **Dual themes** via Shiki's `themes: { light, dark }` — emits `--shiki-light` / `--shiki-dark` CSS vars
13
+ - **Theme-aware color defaults** — `--pcb-*` variables auto-derived from the loaded Shiki theme with WCAG contrast enforcement (v1.3.0+)
14
+ - **Word-level diff** — opt-in `wordDiff: true` wraps changed words in `<mark class="pcb__word-diff--{add,del}">` within `+`/`-` diff lines (v1.3.0+)
15
+ - **SPA-robust copy button** — event delegation + MutationObserver + `astro:page-load` for React/Vue/Astro view transitions (v1.3.0+)
16
+ - **Highlighter lifecycle** — `disposeHighlighter()` for long-running dev servers (v1.3.0+)
13
17
  - **CSS variables everywhere** — every visual property is a `--pcb-*` var, scoped with `:where()` for zero-specificity
14
18
  - **Configurable copy button** — hover mode, custom icons, custom duration, custom labels
15
- - **110 tests pass** — edge cases, stress tests, and feature parity tests
19
+ - **1092 tests pass** — edge cases, stress tests, regression suites, and architecture-pattern tests
20
+
21
+ ## What's new in v1.3.0
22
+
23
+ v1.3.0 adopts **5 architectural patterns** identified through a systematic source-code comparison of 6 community packages (rehype-pretty-code, expressive-code, @shikijs/transformers, VitePress, Docusaurus, astro-expressive-code):
24
+
25
+ | # | Pattern | Source | New export / option |
26
+ |---|---|---|---|
27
+ | 1 | **Highlighter task queue** — serializes all highlighter operations globally, prevents race conditions in parallel builds | expressive-code | `runHighlighterTask<T>(taskFn)` |
28
+ | 2 | **Color-contrast-aware theme defaults** — `--pcb-*` variables auto-derived from the loaded Shiki theme with WCAG contrast enforcement | expressive-code | (internal; `src/color-utils.ts`) |
29
+ | 3 | **`disposeHighlighter()` lifecycle** — releases cached Shiki highlighters (WASM engine + grammars) for long-running dev servers | VitePress | `disposeHighlighter()` |
30
+ | 4 | **Event-delegation copy button + MutationObserver** — SPA-robust for React/Vue/Astro view transitions | VitePress + expressive-code | (internal; copy-script.ts) |
31
+ | 5 | **Word-level diff** — opt-in `wordDiff: true` wraps changed words in `<mark>` elements within diff lines | expressive-code | `wordDiff` option + `wordDiff()` / `hasChanges()` utilities |
32
+
33
+ No breaking API changes. All new behavior is opt-in or backward-compatible. See [CHANGELOG.md](./CHANGELOG.md) for full details.
34
+
35
+ ### Recent bug fixes (v1.2.1, v1.2.2)
36
+
37
+ - **v1.2.2** — Fixed DoS bug where `{1-1000000}` line-highlight range caused `RangeError: Maximum call stack size exceeded` (issue #11).
38
+ - **v1.2.1** — Fixed case-sensitive language loader that rejected `JS`/`TypeScript`/`Python` (issue #12).
39
+ - **v1.2.0** — Adopted 23 features from community competitors (transformers, terminal frames, i18n, CSP nonces, etc.).
16
40
 
17
41
  ## Install
18
42
 
@@ -248,6 +272,7 @@ All options are optional. Defaults match the demo.
248
272
  | --- | --- | --- | --- |
249
273
  | `highlight` | `boolean` | `true` | Enable `{1,3-5}` meta + `// [!code highlight]` |
250
274
  | `diff` | `boolean` | `true` | Enable `+`/`-` prefix + `// [!code ++]` / `[!code --]` |
275
+ | `wordDiff` | `boolean` | `false` | **(v1.3.0)** When `diff` is also true, wrap changed words in `<mark class="pcb__word-diff--{add,del}">` within adjacent `+`/`-` diff line pairs. Uses LCS-based word diff. |
251
276
  | `focus` | `boolean` | `true` | Enable `// [!code focus]` |
252
277
  | `errorLevels` | `boolean` | `true` | Enable `// [!code error]` / `[!code warning]` |
253
278
  | `wrap` | `boolean` | `false` | Default wrap mode |
@@ -256,6 +281,42 @@ All options are optional. Defaults match the demo.
256
281
  | `indentGuides` | `boolean \| number` | `false` | Render indent guides |
257
282
  | `caption` | `boolean` | `true` | Render `caption="..."` meta as `<figcaption>` |
258
283
 
284
+ #### Word-level diff example (v1.3.0+)
285
+
286
+ ```ts
287
+ perfectCode({
288
+ diff: true,
289
+ wordDiff: true, // opt-in
290
+ })
291
+ ```
292
+
293
+ With this markdown:
294
+
295
+ ````md
296
+ ```js
297
+ - const x = computeValue(1)
298
+ + const y = computeValue(2)
299
+ ```
300
+ ````
301
+
302
+ The output wraps `x`→`y` and `1`→`2` in `<mark>` elements so readers can see exactly what changed within each diff line, not just which lines changed:
303
+
304
+ ```html
305
+ <span class="pcb__line pcb__line--del">
306
+ <span class="pcb__code">
307
+ <mark class="pcb__word-diff pcb__word-diff--del">x</mark>
308
+ <!-- unchanged words render as plain text -->
309
+ <mark class="pcb__word-diff pcb__word-diff--del">1</mark>
310
+ </span>
311
+ </span>
312
+ <span class="pcb__line pcb__line--add">
313
+ <span class="pcb__code">
314
+ <mark class="pcb__word-diff pcb__word-diff--add">y</mark>
315
+ <mark class="pcb__word-diff pcb__word-diff--add">2</mark>
316
+ </span>
317
+ </span>
318
+ ```
319
+
259
320
  ### Engine
260
321
 
261
322
  | Option | Type | Default |
@@ -292,6 +353,89 @@ perfectCode({
292
353
  })
293
354
  ```
294
355
 
356
+ ## Advanced APIs (v1.3.0+)
357
+
358
+ These exported functions are for advanced use cases — long-running dev servers, parallel build pipelines, custom diff tooling. Most users don't need them.
359
+
360
+ ### `runHighlighterTask<T>(taskFn: () => Promise<T>): Promise<T>`
361
+
362
+ **Source:** Pattern 1, adopted from [expressive-code](https://github.com/expressive-code/expressive-code).
363
+
364
+ A mutually exclusive FIFO queue that serializes all highlighter operations (createHighlighter, loadLanguage, codeToHast) globally. The plugin uses this internally to prevent race conditions in parallel static-site builds where multiple unified pipelines share the same module-level highlighter cache.
365
+
366
+ You can use it directly if you're calling Shiki outside the plugin and want to share the same serialization guarantee:
367
+
368
+ ```ts
369
+ import { runHighlighterTask } from '@dr-ishaan/rehype-perfect-code-blocks';
370
+
371
+ // Ensure this runs in the same queue as plugin-internal highlighter calls
372
+ const result = await runHighlighterTask(async () => {
373
+ return highlighter.codeToHtml(code, { lang: 'ts' });
374
+ });
375
+ ```
376
+
377
+ ### `disposeHighlighter(): void`
378
+
379
+ **Source:** Pattern 3, adopted from [VitePress](https://vitepress.dev).
380
+
381
+ Releases all cached Shiki highlighters (WASM engine + loaded grammars + theme cache) and clears the cache. Intended for long-running dev servers / watch mode where themes change over time, or during cleanup of a build pipeline.
382
+
383
+ After calling, the next render creates a fresh highlighter.
384
+
385
+ ```ts
386
+ import { disposeHighlighter } from '@dr-ishaan/rehype-perfect-code-blocks';
387
+
388
+ // In a Vite dev server shutdown hook:
389
+ server.http2.close(() => disposeHighlighter());
390
+
391
+ // Or when the user changes their theme in a config-reload hook:
392
+ configReloadEmitter.on('reload', () => {
393
+ disposeHighlighter();
394
+ // next render will create a fresh highlighter with the new theme
395
+ });
396
+ ```
397
+
398
+ ### `wordDiff(oldStr: string, newStr: string): DiffToken[]`
399
+
400
+ **Source:** Pattern 5, selective adoption from [expressive-code](https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-text-markers/src/index.ts).
401
+
402
+ A self-contained LCS-based word diff algorithm (~80 lines, no external deps). Computes a per-word diff between two strings and returns an array of `{ text, type }` tokens where `type` is `'add'`, `'del'`, or `'equal'`.
403
+
404
+ You can use it standalone for custom diff UIs outside the plugin:
405
+
406
+ ```ts
407
+ import { wordDiff, hasChanges } from '@dr-ishaan/rehype-perfect-code-blocks';
408
+
409
+ const tokens = wordDiff('const x = 1', 'const y = 2');
410
+ // → [
411
+ // { text: 'const ', type: 'equal' },
412
+ // { text: 'x', type: 'del' },
413
+ // { text: 'y', type: 'add' },
414
+ // { text: ' = ', type: 'equal' },
415
+ // { text: '1', type: 'del' },
416
+ // { text: '2', type: 'add' },
417
+ // ]
418
+
419
+ if (hasChanges(tokens)) {
420
+ // render the diff in your own UI
421
+ }
422
+ ```
423
+
424
+ The plugin uses this internally when the `wordDiff: true` option is set — see the [Modes](#modes) table above.
425
+
426
+ ### `hasChanges(tokens: DiffToken[]): boolean`
427
+
428
+ Returns `true` if the diff result contains at least one `add` or `del` token. Useful for skipping the rendering of unchanged diff pairs.
429
+
430
+ ### `DiffToken` type
431
+
432
+ ```ts
433
+ interface DiffToken {
434
+ text: string;
435
+ type: 'add' | 'del' | 'equal';
436
+ }
437
+ ```
438
+
295
439
  ### Styling
296
440
 
297
441
  | Option | Type | Default |
@@ -302,6 +446,26 @@ perfectCode({
302
446
 
303
447
  ## Theming
304
448
 
449
+ ### Theme-aware defaults (v1.3.0+)
450
+
451
+ The `<pre>` element receives inline `--pcb-*` CSS variable defaults **derived from the loaded Shiki theme** — automatically, with no configuration. This means code blocks look good with ANY Shiki theme out of the box, without you having to manually tune line-number colors, diff backgrounds, or focus highlights.
452
+
453
+ The defaults computed per theme:
454
+
455
+ | Variable | How it's derived |
456
+ | --- | --- |
457
+ | `--pcb-bg` | Theme background color |
458
+ | `--pcb-fg` | Theme foreground color |
459
+ | `--pcb-ln-fg` | Line-number color, contrast-adjusted against `--pcb-bg` to meet WCAG AA (ratio ≥ 3.0) |
460
+ | `--pcb-line-highlight-bg` | Subtle highlight tint: 12% mix of `--pcb-fg` over `--pcb-bg` |
461
+ | `--pcb-line-add-bg` | Diff add background: 18% mix of green (`#22863a`) over `--pcb-bg` |
462
+ | `--pcb-line-del-bg` | Diff del background: 18% mix of red (`#cb2431`) over `--pcb-bg` |
463
+ | `--pcb-line-focus-bg` | Focus dim: 4% mix of `--pcb-fg` over `--pcb-bg` |
464
+
465
+ The static `dist/styles.css` continues to ship its own generic defaults; the runtime overrides them with theme-aware values via inline styles on `<pre>`. You can still override any `--pcb-*` variable in your own CSS — the cascade order is: `dist/styles.css` < inline `<pre style>` < your CSS.
466
+
467
+ ### Manual overrides
468
+
305
469
  Every visual property is a `--pcb-*` CSS variable on `.pcb`. Override any subset:
306
470
 
307
471
  ```css
@@ -353,6 +517,7 @@ Markdown fence
353
517
  ┌──────────────────────────────┐
354
518
  │ Shiki (via Astro or direct) │ ← tokenizes to <pre><code>...tokens...</code></pre>
355
519
  │ + @shikijs/transformers │ ← applies diff/focus/highlight/error/word
520
+ │ + runHighlighterTask queue │ ← (v1.3.0) serializes all Shiki calls
356
521
  └──────────────────────────────┘
357
522
 
358
523
 
@@ -361,13 +526,15 @@ Markdown fence
361
526
  │ - reads data-meta │ - maps Shiki classes → pcb__line--* namespace
362
527
  │ - builds header bar │ - adds gutter, copy button, caption
363
528
  │ - applies keepBackground │ - calls visitor hooks
529
+ │ - applies theme-aware │ - (v1.3.0) applies wordDiff post-processing
530
+ │ --pcb-* defaults (v1.3.0) │
364
531
  └──────────────────────────────┘
365
532
 
366
533
 
367
- Final HTML
534
+ Final HTML (with inline --pcb-* theme-aware defaults on <pre>)
368
535
  ```
369
536
 
370
- Key design decisions (learned from rehype-pretty-code):
537
+ Key design decisions (learned from rehype-pretty-code + expressive-code + VitePress):
371
538
 
372
539
  1. **Let Shiki do the work** — we delegate line splitting, diff detection, and word highlighting to Shiki's official transformers; we just remap their classes (`diff add` → `pcb__line--add`, etc.)
373
540
  2. **Pass `meta: { __raw }` to Shiki** — this is the contract that lets all `@shikijs/transformers` work
@@ -375,20 +542,35 @@ Key design decisions (learned from rehype-pretty-code):
375
542
  4. **Lazy-load languages** — any Shiki-bundled language just works, no preconfiguration needed
376
543
  5. **Graceful unknown-language fallback** — filter out unknowns before `createHighlighter` (which throws synchronously) and fall back to `plaintext`
377
544
  6. **`:where()` zero-specificity** — every default selector uses `:where(.pcb ...)` so user CSS always wins without `!important` arms races
545
+ 7. **(v1.3.0) Mutually exclusive task queue** — all highlighter operations run inside `runHighlighterTask()`, preventing race conditions in parallel builds (from expressive-code)
546
+ 8. **(v1.3.0) Theme-aware CSS variable defaults** — `--pcb-*` defaults are derived from the loaded Shiki theme with WCAG contrast enforcement, applied as inline styles on `<pre>` (from expressive-code)
547
+ 9. **(v1.3.0) Disposable highlighter** — `disposeHighlighter()` releases the WASM engine + grammars for long-running dev servers (from VitePress)
548
+ 10. **(v1.3.0) SPA-robust copy button** — event delegation + MutationObserver + `astro:page-load` for React/Vue/Astro view transitions (from VitePress + expressive-code)
378
549
 
379
550
  ## Testing
380
551
 
381
- The package ships with 110 tests across three suites:
552
+ The package ships with **1092 tests** across seven suites:
382
553
 
383
554
  ```bash
384
555
  npm test
385
556
  ```
386
557
 
387
558
  | Suite | Tests | What it covers |
388
- | --- | --- | --- |
389
- | `test-edge-cases.mjs` | 50 | Basic blocks, all meta flags, language detection, highlighting ranges, diff, presets, escape handling, multiple blocks |
559
+ | --- | ---: | --- |
560
+ | `test-meta-parser.mjs` | 161 | Fence-meta parser: title, `{1,3-5}`, `/word/`, `ln{N}`, caption, flags, edge cases |
561
+ | `test-dom-structure.mjs` | 113 | Output HTML structure: `<figure>`, `<pre>`, `<code>`, header bar, gutter, copy button |
562
+ | `test-options.mjs` | 108 | All plugin options: ornaments, structure, modes, engine, customization, hooks, styling |
563
+ | `test-notations.mjs` | 51 | VitePress-style `// [!code xxx]` inline notations + Docusaurus-style magic comments |
564
+ | `test-security.mjs` | 49 | CSP nonce support, XSS prevention, `aria-*` accessibility attributes |
565
+ | `test-integration.mjs` | 69 | End-to-end integration with remark/rehype/rehype-raw pipelines |
566
+ | `test-regression.mjs` | 91 | Regression tests for historical bugs (issues #1–#10) |
567
+ | `test-css.mjs` | 120 | CSS output: `--pcb-*` variables, `:where()` specificity, dual-theme switching |
568
+ | `test-edge-cases.mjs` | 50 | Basic blocks, all meta flags, language detection, highlighting ranges, diff, presets, escape handling |
390
569
  | `stress-tests.mjs` | 17 | 100-line blocks, CRLF, tabs, unicode, concurrent overrides, all-options-at-once |
391
570
  | `new-feature-tests.mjs` | 43 | VitePress notations, magic comments, word highlights, dual themes, captions, visitor hooks, configurable copy button, terminal auto-detection, filename extraction |
571
+ | `test-issue-12.mjs` | 28 | Regression: case-insensitive language loader (`JS`/`TypeScript`/`Python`) |
572
+ | `test-issue-11.mjs` | 51 | Regression: line-range stack overflow (`{1-1000000}` DoS vector) |
573
+ | `test-architecture-patterns.mjs` | 41 | v1.3.0 architecture patterns: task queue, theme-aware defaults, dispose, SPA copy button, word-diff |
392
574
 
393
575
  ## Comparison with alternatives
394
576
 
@@ -405,8 +587,10 @@ npm test
405
587
  | `// highlight-next-line` | ✅ | ❌ | ❌ | ✅ | ❌ |
406
588
  | Custom magic comments | ✅ | ❌ | ❌ | ✅ | ❌ |
407
589
  | `/word/` meta | ✅ | ✅ | ❌ | ❌ | ✅ |
590
+ | **Word-level diff** (v1.3.0) | ✅ (`wordDiff: true`) | ❌ | ❌ | ❌ | ✅ (`plugin-text-markers`) |
408
591
  | `caption="..."` | ✅ | ✅ | ❌ | ❌ | ❌ |
409
592
  | Dual themes via CSS vars | ✅ | ✅ | ✅ | ⚠️ | ✅ |
593
+ | **Theme-aware color defaults** (v1.3.0) | ✅ (WCAG-enforced) | ❌ | ❌ | ❌ | ✅ |
410
594
  | Auto terminal frame | ✅ | ❌ | ❌ | ❌ | ✅ |
411
595
  | Filename from comment | ✅ | ❌ | ❌ | ❌ | ✅ |
412
596
  | Visible whitespace | ✅ | ❌ | ❌ | ❌ | ❌ |
@@ -415,6 +599,9 @@ npm test
415
599
  | `filterMetaString` | ✅ | ✅ | ❌ | ❌ | ❌ |
416
600
  | `getHighlighter` escape hatch | ✅ | ✅ | ❌ | ❌ | ❌ |
417
601
  | User-supplied Shiki transformers | ✅ | ✅ | ❌ | ❌ | ❌ |
602
+ | **Highlighter task queue** (v1.3.0) | ✅ (`runHighlighterTask`) | ❌ | ❌ | ❌ | ✅ |
603
+ | **`disposeHighlighter()` lifecycle** (v1.3.0) | ✅ | ❌ | ✅ | ❌ | ❌ |
604
+ | **SPA-robust copy button** (v1.3.0) | ✅ (MutationObserver + `astro:page-load`) | ❌ (inline `onclick`) | ✅ (event delegation) | ✅ (React) | ✅ (MutationObserver) |
418
605
  | Zero-specificity CSS vars | ✅ | ❌ | ❌ | ❌ | ⚠️ |
419
606
  | Astro integration | ✅ | ⚠️ | ❌ | ❌ | ✅ |
420
607
  | Standalone rehype | ✅ | ✅ | ❌ | ❌ | ❌ |
@@ -426,6 +613,7 @@ rehype-perfect-code-blocks/
426
613
  ├── package.json
427
614
  ├── tsconfig.json
428
615
  ├── README.md
616
+ ├── CHANGELOG.md
429
617
  ├── LICENSE
430
618
  ├── .gitignore
431
619
  ├── .npmignore
@@ -435,19 +623,43 @@ rehype-perfect-code-blocks/
435
623
  │ ├── types.ts ← full options + ParsedMeta + ResolvedBlock
436
624
  │ ├── meta.ts ← fence-meta parser (title, {1,3-5}, /word/, ln{N}, caption, flags)
437
625
  │ ├── remark.ts ← remarkPreserveCodeMeta (carries meta to hast)
438
- │ ├── shiki.ts ← Shiki caller: transformers, dual themes, lazy lang loading
439
- │ ├── transformer.ts ← hast walker: <pre> → <figure class="pcb">
440
- │ ├── copy-script.ts ← ~500-byte inline copy-button client script
626
+ │ ├── shiki.ts ← Shiki caller: transformers, dual themes, lazy lang loading, task queue (v1.3.0)
627
+ │ ├── transformer.ts ← hast walker: <pre> → <figure class="pcb">, word-diff post-processing (v1.3.0)
628
+ │ ├── copy-script.ts ← ~1.2KB inline copy-button client script (event delegation + MutationObserver, v1.3.0)
629
+ │ ├── color-utils.ts ← (v1.3.0) color manipulation + WCAG contrast + theme-aware default computation
630
+ │ ├── word-diff.ts ← (v1.3.0) LCS-based word diff algorithm
441
631
  │ ├── styles.css ← full stylesheet with --pcb-* variables
442
632
  │ ├── astro.ts ← Astro integration (one-liner)
443
- │ ├── index.ts ← standalone rehype plugin entry
633
+ │ ├── index.ts ← standalone rehype plugin entry (exports runHighlighterTask, disposeHighlighter, wordDiff, hasChanges)
444
634
  │ └── vite-raw.d.ts ← type shim for ?raw imports
445
635
  ├── dist/ ← built ESM + .d.ts + styles.css
446
- ├── test-edge-cases.mjs 50 tests
447
- ├── stress-tests.mjs 17 tests
448
- └── new-feature-tests.mjs 43 tests
636
+ ├── test-meta-parser.mjs 161 tests
637
+ ├── test-dom-structure.mjs 113 tests
638
+ ├── test-options.mjs 108 tests
639
+ ├── test-notations.mjs ← 51 tests
640
+ ├── test-security.mjs ← 49 tests
641
+ ├── test-integration.mjs ← 69 tests
642
+ ├── test-regression.mjs ← 91 tests
643
+ ├── test-css.mjs ← 120 tests
644
+ ├── test-edge-cases.mjs ← 50 tests
645
+ ├── stress-tests.mjs ← 17 tests
646
+ ├── new-feature-tests.mjs ← 43 tests
647
+ ├── test-issue-12.mjs ← 28 tests (case-insensitive lang loader)
648
+ ├── test-issue-11.mjs ← 51 tests (line-range stack overflow)
649
+ └── test-architecture-patterns.mjs ← 41 tests (v1.3.0 patterns)
449
650
  ```
450
651
 
652
+ ## Changelog
653
+
654
+ See [CHANGELOG.md](./CHANGELOG.md) for version history. Highlights:
655
+
656
+ - **v1.3.0** — Adopted 5 architectural patterns from community packages (highlighter task queue, theme-aware color defaults, `disposeHighlighter()` lifecycle, SPA-robust copy button, word-level diff).
657
+ - **v1.2.2** — Fixed `{1-1000000}` line-range stack overflow DoS (issue #11).
658
+ - **v1.2.1** — Fixed case-sensitive language loader rejecting `JS`/`TypeScript`/`Python` (issue #12).
659
+ - **v1.2.0** — Adopted 23 features from community competitors (transformers, terminal frames, i18n, CSP nonces, etc.).
660
+ - **v1.1.x** — Accessibility, performance, and security improvements.
661
+ - **v1.0.0** — Initial release.
662
+
451
663
  ## License
452
664
 
453
665
  MIT
package/dist/astro.d.ts CHANGED
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"astro.d.ts","sourceRoot":"","sources":["../src/astro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAI9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAyDrD,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,OAAO,GAAE,kBAAuB,GAC/B,gBAAgB,CAiGlB"}
1
+ {"version":3,"file":"astro.d.ts","sourceRoot":"","sources":["../src/astro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAI9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAyDrD,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,OAAO,GAAE,kBAAuB,GAC/B,gBAAgB,CAwGlB"}
package/dist/astro.js CHANGED
@@ -120,11 +120,18 @@ export default function perfectCode(options = {}) {
120
120
  }
121
121
  // Copy-button script
122
122
  if (options.copyButton !== false) {
123
- injections.push(`<script${nonceAttr}>${COPY_SCRIPT}</script>`);
124
- // Graceful degradation: .no-js class
123
+ // Graceful degradation: .no-js class MUST be added BEFORE the copy
124
+ // script runs, so the copy script's swapNoJs() can detect and remove
125
+ // it. If we add .no-js AFTER the copy script, swapNoJs() is a no-op
126
+ // (the class isn't there yet), and the MutationObserver (which only
127
+ // watches childList by default) won't catch the attribute change —
128
+ // leaving .no-js on <html> permanently and hiding the copy button
129
+ // via the `html.no-js .pcb__copy { display: none !important; }` rule.
130
+ // See issue: copy button not working in Astro build output.
125
131
  if (options.hideCopyWithoutJs !== false) {
126
132
  injections.push(`<script${nonceAttr}>document.documentElement.classList.add('no-js');</script>`);
127
133
  }
134
+ injections.push(`<script${nonceAttr}>${COPY_SCRIPT}</script>`);
128
135
  }
129
136
  // Manual theme override
130
137
  if (options.theme && options.theme !== 'auto') {
package/dist/astro.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"astro.js","sourceRoot":"","sources":["../src/astro.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"astro.js","sourceRoot":"","sources":["../src/astro.ts"],"names":[],"mappings":"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"}
File without changes
File without changes
File without changes
File without changes
@@ -21,5 +21,5 @@
21
21
  * class swap when new code blocks are added to the DOM (SPA support).
22
22
  * - `astro:page-load` event listener for Astro view transitions.
23
23
  */
24
- export declare const COPY_SCRIPT = "\n(function () {\n if (window.__pcbCopyReady) return;\n window.__pcbCopyReady = true;\n\n // Remove the .no-js class so the copy buttons become visible (graceful degradation).\n function swapNoJs() {\n if (document.documentElement.classList.contains('no-js')) {\n document.documentElement.classList.remove('no-js');\n document.documentElement.classList.add('js');\n }\n }\n swapNoJs();\n\n // Reuse a single aria-live region for all copy announcements.\n var liveRegion = null;\n function ensureLiveRegion() {\n if (liveRegion && document.body.contains(liveRegion)) return liveRegion;\n liveRegion = document.querySelector('.pcb__sr-live');\n if (!liveRegion) {\n liveRegion = document.createElement('span');\n liveRegion.className = 'pcb__sr-live';\n liveRegion.setAttribute('aria-live', 'polite');\n liveRegion.setAttribute('aria-atomic', 'true');\n liveRegion.setAttribute('role', 'status');\n document.body.appendChild(liveRegion);\n }\n return liveRegion;\n }\n ensureLiveRegion();\n\n function announce(msg) {\n var lr = ensureLiveRegion();\n if (!lr) return;\n lr.textContent = '';\n // Force re-announcement by clearing then setting on next tick.\n setTimeout(function () { lr.textContent = msg; }, 50);\n }\n\n function findLabel(btn) {\n return btn.querySelector('.pcb__copy-label');\n }\n\n function findIcon(btn) {\n return btn.querySelector('svg');\n }\n\n // Strip leading comment lines (e.g. shell prompts like \"# comment\") from\n // the text before copying. Used for terminal-preset blocks where the\n // displayed code may include comments the user doesn't want on the clipboard.\n function stripComments(text) {\n // Strip lines that start with optional whitespace followed by # (shell),\n // // (C-style), or REM (Windows batch). Keep everything else.\n return text.replace(/^[ \\t]*(?:#|\\/\\/|REM\\b).*$/gm, '').replace(/\\n{3,}/g, '\\n\\n').trim();\n }\n\n // Event-delegated click handler. Works for buttons added after initial\n // render (e.g. via React/Vue re-render or Astro view transitions) because\n // the listener is on document, not on each button.\n document.addEventListener('click', function (e) {\n var btn = e.target && e.target.closest && e.target.closest('.pcb__copy');\n if (!btn) return;\n var figure = btn.closest('.pcb');\n var code = figure && figure.querySelector('pre code');\n if (!code) return;\n\n var done = btn.getAttribute('data-done-label') || 'copied!';\n var duration = parseInt(btn.getAttribute('data-feedback-duration') || '1600', 10);\n var successIconHtml = btn.getAttribute('data-success-icon');\n var stripCommentsFlag = btn.hasAttribute('data-strip-comments');\n\n var label = findLabel(btn);\n var icon = findIcon(btn);\n var originalLabel = label ? label.textContent : null;\n var originalIconHtml = icon ? icon.outerHTML : null;\n\n var rawText = code.innerText;\n var textToCopy = stripCommentsFlag ? stripComments(rawText) : rawText;\n\n var finish = function () {\n btn.classList.add('pcb__copy--done');\n if (label) label.textContent = done;\n if (successIconHtml && icon) {\n var tmp = document.createElement('span');\n tmp.innerHTML = successIconHtml;\n var newIcon = tmp.firstChild;\n if (newIcon) {\n icon.replaceWith(newIcon);\n icon = newIcon;\n }\n }\n announce(done);\n setTimeout(function () {\n btn.classList.remove('pcb__copy--done');\n if (label && originalLabel != null) label.textContent = originalLabel;\n if (originalIconHtml && icon) {\n var tmp2 = document.createElement('span');\n tmp2.innerHTML = originalIconHtml;\n var oldIcon = icon;\n icon = tmp2.firstChild;\n if (icon) oldIcon.replaceWith(icon);\n }\n }, duration);\n };\n\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(textToCopy).then(finish).catch(fallback);\n } else {\n fallback();\n }\n\n function fallback() {\n var ta = document.createElement('textarea');\n ta.value = textToCopy;\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); finish(); } catch (_) {}\n document.body.removeChild(ta);\n }\n });\n\n // Pattern 4: MutationObserver for SPA support.\n // When new code blocks are inserted into the DOM (e.g. by React/Vue\n // re-render, Astro view transitions, or Turbolinks navigation), the\n // .no-js \u2192 .js class swap may need to be re-applied so newly-added\n // copy buttons become visible. The observer watches for added .pcb nodes.\n if (typeof MutationObserver !== 'undefined') {\n var pendingObserve = false;\n var observer = new MutationObserver(function (mutations) {\n // Batch checks with microtask to avoid layout thrash.\n if (pendingObserve) return;\n pendingObserve = true;\n Promise.resolve().then(function () {\n pendingObserve = false;\n // If any new .pcb nodes were added, ensure the .js class is set\n // (in case the page was rendered server-side with .no-js and the\n // client took over after initial load).\n for (var i = 0; i < mutations.length; i++) {\n if (mutations[i].addedNodes && mutations[i].addedNodes.length) {\n swapNoJs();\n ensureLiveRegion();\n break;\n }\n }\n });\n });\n // Observe the whole document subtree for added nodes.\n observer.observe(document.documentElement, { childList: true, subtree: true });\n }\n\n // Pattern 4: astro:page-load event listener for Astro view transitions.\n // Astro emits this event after a view transition completes; the new page's\n // DOM may have replaced the old, so re-apply the .no-js \u2192 .js swap.\n if (typeof document.addEventListener === 'function') {\n document.addEventListener('astro:page-load', function () {\n swapNoJs();\n ensureLiveRegion();\n });\n }\n})();\n";
24
+ export declare const COPY_SCRIPT = "\n(function () {\n if (window.__pcbCopyReady) return;\n window.__pcbCopyReady = true;\n\n // Remove the .no-js class so the copy buttons become visible (graceful degradation).\n function swapNoJs() {\n if (document.documentElement.classList.contains('no-js')) {\n document.documentElement.classList.remove('no-js');\n document.documentElement.classList.add('js');\n }\n }\n swapNoJs();\n\n // Reuse a single aria-live region for all copy announcements.\n var liveRegion = null;\n function ensureLiveRegion() {\n if (liveRegion && document.body.contains(liveRegion)) return liveRegion;\n liveRegion = document.querySelector('.pcb__sr-live');\n if (!liveRegion) {\n liveRegion = document.createElement('span');\n liveRegion.className = 'pcb__sr-live';\n liveRegion.setAttribute('aria-live', 'polite');\n liveRegion.setAttribute('aria-atomic', 'true');\n liveRegion.setAttribute('role', 'status');\n document.body.appendChild(liveRegion);\n }\n return liveRegion;\n }\n ensureLiveRegion();\n\n function announce(msg) {\n var lr = ensureLiveRegion();\n if (!lr) return;\n lr.textContent = '';\n // Force re-announcement by clearing then setting on next tick.\n setTimeout(function () { lr.textContent = msg; }, 50);\n }\n\n function findLabel(btn) {\n return btn.querySelector('.pcb__copy-label');\n }\n\n function findIcon(btn) {\n return btn.querySelector('svg');\n }\n\n // Strip leading comment lines (e.g. shell prompts like \"# comment\") from\n // the text before copying. Used for terminal-preset blocks where the\n // displayed code may include comments the user doesn't want on the clipboard.\n function stripComments(text) {\n // Strip lines that start with optional whitespace followed by # (shell),\n // // (C-style), or REM (Windows batch). Keep everything else.\n return text.replace(/^[ \\t]*(?:#|\\/\\/|REM\\b).*$/gm, '').replace(/\\n{3,}/g, '\\n\\n').trim();\n }\n\n // Event-delegated click handler. Works for buttons added after initial\n // render (e.g. via React/Vue re-render or Astro view transitions) because\n // the listener is on document, not on each button.\n document.addEventListener('click', function (e) {\n var btn = e.target && e.target.closest && e.target.closest('.pcb__copy');\n if (!btn) return;\n var figure = btn.closest('.pcb');\n var code = figure && figure.querySelector('pre code');\n if (!code) return;\n\n var done = btn.getAttribute('data-done-label') || 'copied!';\n var duration = parseInt(btn.getAttribute('data-feedback-duration') || '1600', 10);\n var successIconHtml = btn.getAttribute('data-success-icon');\n var stripCommentsFlag = btn.hasAttribute('data-strip-comments');\n\n var label = findLabel(btn);\n var icon = findIcon(btn);\n var originalLabel = label ? label.textContent : null;\n var originalIconHtml = icon ? icon.outerHTML : null;\n\n var rawText = code.innerText;\n var textToCopy = stripCommentsFlag ? stripComments(rawText) : rawText;\n\n var finish = function () {\n btn.classList.add('pcb__copy--done');\n if (label) label.textContent = done;\n if (successIconHtml && icon) {\n var tmp = document.createElement('span');\n tmp.innerHTML = successIconHtml;\n var newIcon = tmp.firstChild;\n if (newIcon) {\n icon.replaceWith(newIcon);\n icon = newIcon;\n }\n }\n announce(done);\n setTimeout(function () {\n btn.classList.remove('pcb__copy--done');\n if (label && originalLabel != null) label.textContent = originalLabel;\n if (originalIconHtml && icon) {\n var tmp2 = document.createElement('span');\n tmp2.innerHTML = originalIconHtml;\n var oldIcon = icon;\n icon = tmp2.firstChild;\n if (icon) oldIcon.replaceWith(icon);\n }\n }, duration);\n };\n\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(textToCopy).then(finish).catch(fallback);\n } else {\n fallback();\n }\n\n function fallback() {\n var ta = document.createElement('textarea');\n ta.value = textToCopy;\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); finish(); } catch (_) {}\n document.body.removeChild(ta);\n }\n });\n\n // Pattern 4: MutationObserver for SPA support + .no-js race fix.\n // Watches for:\n // - New code blocks added to the DOM (React/Vue re-render, Astro view\n // transitions, Turbolinks) \u2192 re-apply .no-js \u2192 .js swap + ensure\n // aria-live region.\n // - Attribute changes on <html> (specifically the class attribute) \u2192\n // catches the case where a later script adds .no-js AFTER this script\n // ran (a race condition that previously left copy buttons hidden).\n if (typeof MutationObserver !== 'undefined') {\n var pendingObserve = false;\n var observer = new MutationObserver(function (mutations) {\n // Batch checks with microtask to avoid layout thrash.\n if (pendingObserve) return;\n pendingObserve = true;\n Promise.resolve().then(function () {\n pendingObserve = false;\n var needsSwap = false;\n for (var i = 0; i < mutations.length; i++) {\n var m = mutations[i];\n // childList: new nodes added (SPA navigation, view transitions)\n if (m.type === 'childList' && m.addedNodes && m.addedNodes.length) {\n needsSwap = true;\n }\n // attributes: class changed on <html> (the .no-js race fix)\n if (m.type === 'attributes' && m.attributeName === 'class') {\n needsSwap = true;\n }\n }\n if (needsSwap) {\n swapNoJs();\n ensureLiveRegion();\n }\n });\n });\n // Observe documentElement for BOTH childList (new nodes) AND attribute\n // changes (class attribute \u2014 catches .no-js being added by a later script).\n observer.observe(document.documentElement, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['class'],\n });\n }\n\n // Defensive: re-run swapNoJs() on DOMContentLoaded and window.load.\n // Belt + suspenders for the .no-js race \u2014 if a framework (Astro, Next.js,\n // etc.) adds .no-js in a way the MutationObserver doesn't catch (e.g.,\n // before the observer is set up, or in a different document context),\n // these event handlers will catch it.\n if (typeof document.addEventListener === 'function') {\n document.addEventListener('DOMContentLoaded', function () {\n swapNoJs();\n ensureLiveRegion();\n });\n }\n if (typeof window.addEventListener === 'function') {\n window.addEventListener('load', function () {\n swapNoJs();\n ensureLiveRegion();\n });\n }\n\n // Pattern 4: astro:page-load event listener for Astro view transitions.\n // Astro emits this event after a view transition completes; the new page's\n // DOM may have replaced the old, so re-apply the .no-js \u2192 .js swap.\n if (typeof document.addEventListener === 'function') {\n document.addEventListener('astro:page-load', function () {\n swapNoJs();\n ensureLiveRegion();\n });\n }\n})();\n";
25
25
  //# sourceMappingURL=copy-script.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"copy-script.d.ts","sourceRoot":"","sources":["../src/copy-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,WAAW,8kMAkKvB,CAAC"}
1
+ {"version":3,"file":"copy-script.d.ts","sourceRoot":"","sources":["../src/copy-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,WAAW,kxOAmMvB,CAAC"}
@@ -144,11 +144,14 @@ export const COPY_SCRIPT = `
144
144
  }
145
145
  });
146
146
 
147
- // Pattern 4: MutationObserver for SPA support.
148
- // When new code blocks are inserted into the DOM (e.g. by React/Vue
149
- // re-render, Astro view transitions, or Turbolinks navigation), the
150
- // .no-js → .js class swap may need to be re-applied so newly-added
151
- // copy buttons become visible. The observer watches for added .pcb nodes.
147
+ // Pattern 4: MutationObserver for SPA support + .no-js race fix.
148
+ // Watches for:
149
+ // - New code blocks added to the DOM (React/Vue re-render, Astro view
150
+ // transitions, Turbolinks) → re-apply .no-js → .js swap + ensure
151
+ // aria-live region.
152
+ // - Attribute changes on <html> (specifically the class attribute) →
153
+ // catches the case where a later script adds .no-js AFTER this script
154
+ // ran (a race condition that previously left copy buttons hidden).
152
155
  if (typeof MutationObserver !== 'undefined') {
153
156
  var pendingObserve = false;
154
157
  var observer = new MutationObserver(function (mutations) {
@@ -157,20 +160,50 @@ export const COPY_SCRIPT = `
157
160
  pendingObserve = true;
158
161
  Promise.resolve().then(function () {
159
162
  pendingObserve = false;
160
- // If any new .pcb nodes were added, ensure the .js class is set
161
- // (in case the page was rendered server-side with .no-js and the
162
- // client took over after initial load).
163
+ var needsSwap = false;
163
164
  for (var i = 0; i < mutations.length; i++) {
164
- if (mutations[i].addedNodes && mutations[i].addedNodes.length) {
165
- swapNoJs();
166
- ensureLiveRegion();
167
- break;
165
+ var m = mutations[i];
166
+ // childList: new nodes added (SPA navigation, view transitions)
167
+ if (m.type === 'childList' && m.addedNodes && m.addedNodes.length) {
168
+ needsSwap = true;
168
169
  }
170
+ // attributes: class changed on <html> (the .no-js race fix)
171
+ if (m.type === 'attributes' && m.attributeName === 'class') {
172
+ needsSwap = true;
173
+ }
174
+ }
175
+ if (needsSwap) {
176
+ swapNoJs();
177
+ ensureLiveRegion();
169
178
  }
170
179
  });
171
180
  });
172
- // Observe the whole document subtree for added nodes.
173
- observer.observe(document.documentElement, { childList: true, subtree: true });
181
+ // Observe documentElement for BOTH childList (new nodes) AND attribute
182
+ // changes (class attribute — catches .no-js being added by a later script).
183
+ observer.observe(document.documentElement, {
184
+ childList: true,
185
+ subtree: true,
186
+ attributes: true,
187
+ attributeFilter: ['class'],
188
+ });
189
+ }
190
+
191
+ // Defensive: re-run swapNoJs() on DOMContentLoaded and window.load.
192
+ // Belt + suspenders for the .no-js race — if a framework (Astro, Next.js,
193
+ // etc.) adds .no-js in a way the MutationObserver doesn't catch (e.g.,
194
+ // before the observer is set up, or in a different document context),
195
+ // these event handlers will catch it.
196
+ if (typeof document.addEventListener === 'function') {
197
+ document.addEventListener('DOMContentLoaded', function () {
198
+ swapNoJs();
199
+ ensureLiveRegion();
200
+ });
201
+ }
202
+ if (typeof window.addEventListener === 'function') {
203
+ window.addEventListener('load', function () {
204
+ swapNoJs();
205
+ ensureLiveRegion();
206
+ });
174
207
  }
175
208
 
176
209
  // Pattern 4: astro:page-load event listener for Astro view transitions.
@@ -1 +1 @@
1
- {"version":3,"file":"copy-script.js","sourceRoot":"","sources":["../src/copy-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkK1B,CAAC"}
1
+ {"version":3,"file":"copy-script.js","sourceRoot":"","sources":["../src/copy-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmM1B,CAAC"}
package/dist/index.d.ts CHANGED
File without changes
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/index.js.map CHANGED
File without changes
package/dist/meta.d.ts CHANGED
File without changes
File without changes
package/dist/meta.js CHANGED
File without changes
package/dist/meta.js.map CHANGED
File without changes
package/dist/remark.d.ts CHANGED
File without changes
File without changes
package/dist/remark.js CHANGED
File without changes
File without changes
package/dist/shiki.d.ts CHANGED
File without changes
File without changes
package/dist/shiki.js CHANGED
File without changes
package/dist/shiki.js.map CHANGED
File without changes
package/dist/styles.css CHANGED
@@ -6,6 +6,8 @@
6
6
  - Solid gutter background hides code scrolling behind it
7
7
  - All defaults use :where() for zero specificity so user CSS wins
8
8
  - Every visual property is a --pcb-* CSS variable
9
+ - Framework-reset overrides (Tailwind/daisyUI compat) use real
10
+ specificity so they beat bare-element resets like `pre { ... }`
9
11
  ============================================================ */
10
12
 
11
13
  :where(.pcb) {
@@ -251,6 +253,69 @@
251
253
  display: block;
252
254
  }
253
255
 
256
+ /* ============================================================
257
+ Framework-reset overrides (Tailwind Preflight / daisyUI compat)
258
+ ------------------------------------------------------------
259
+ Tailwind Preflight targets bare `pre`, `code`, and `button`
260
+ elements with specificity (0,0,1). Our :where() rules above
261
+ have zero specificity (0,0,0), so Tailwind's resets WIN and
262
+ break the plugin's overflow model, font inheritance, and
263
+ copy-button styling.
264
+
265
+ These rules use REAL specificity (.pcb pre = (0,1,1),
266
+ .pcb__copy = (0,1,0)) to beat framework base resets WITHOUT
267
+ !important. They only set the properties that frameworks
268
+ clobber — everything else stays in :where() so user CSS
269
+ still wins.
270
+
271
+ If you're NOT using a CSS framework, these rules are harmless
272
+ (they set the same values as the :where() rules above).
273
+ ============================================================ */
274
+
275
+ /* Tailwind sets `pre { overflow-x: auto }` which creates a double-
276
+ scrollbar (one on <pre>, one on .pcb__body). Force <pre> to
277
+ visible so only .pcb__body scrolls. */
278
+ .pcb pre {
279
+ overflow: visible;
280
+ }
281
+
282
+ /* Tailwind sets `pre, code { font-family: ui-monospace, ... }` which
283
+ overrides the plugin's `font: inherit` and ignores --pcb-font-mono.
284
+ Re-assert the plugin's font. */
285
+ .pcb pre,
286
+ .pcb code {
287
+ font-family: var(--pcb-font-mono);
288
+ }
289
+
290
+ /* Tailwind sets `button { background: transparent; background-image: none }`
291
+ which strips the copy button's hover background. Re-assert the plugin's
292
+ button base. (.pcb__copy already has specificity (0,1,0) which beats
293
+ Tailwind's `button` (0,0,1), but we re-assert here for clarity and to
294
+ catch cases where frameworks add higher-specificity button resets.) */
295
+ .pcb__copy {
296
+ appearance: none;
297
+ background: transparent;
298
+ border: 1px solid transparent;
299
+ cursor: pointer;
300
+ }
301
+
302
+ /* Tailwind sets `*, ::before, ::after { border-width: 0 }` which nukes
303
+ borders on the header bar and copy button. Re-assert. */
304
+ .pcb__bar {
305
+ border-bottom: 1px solid var(--pcb-border);
306
+ }
307
+
308
+ /* Tailwind's box-sizing: border-box is fine (the plugin expects it).
309
+ No override needed. */
310
+
311
+ /* Some Tailwind utilities (e.g., `@apply break-words` or global
312
+ `pre { white-space: pre-wrap }`) can clobber the plugin's
313
+ `white-space: pre` on .pcb__code, causing long lines to wrap
314
+ instead of scroll. Re-assert with real specificity. */
315
+ .pcb__code {
316
+ white-space: pre;
317
+ }
318
+
254
319
  /* ---------- Row-based line grid ----------
255
320
  Each <span class="pcb__line"> uses display:contents so its children
256
321
  (.pcb__ln and .pcb__code) become grid items in the <code> container. */
File without changes
File without changes
File without changes
File without changes
package/dist/types.d.ts CHANGED
File without changes
File without changes
package/dist/types.js CHANGED
File without changes
package/dist/types.js.map CHANGED
File without changes
File without changes
File without changes
package/dist/word-diff.js CHANGED
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dr-ishaan/rehype-perfect-code-blocks",
3
- "version": "1.3.0",
3
+ "version": "1.3.3",
4
4
  "description": "Beautiful, configurable code blocks for Astro / MDX / any rehype pipeline. Built on Shiki, inspired by rehype-pretty-code, VitePress, Docusaurus, and Expressive Code.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -51,7 +51,7 @@
51
51
  "scripts": {
52
52
  "build": "tsc -p tsconfig.json && cp src/styles.css dist/styles.css",
53
53
  "dev": "tsc -p tsconfig.json --watch",
54
- "test": "node test-meta-parser.mjs && node test-dom-structure.mjs && node test-options.mjs && node test-notations.mjs && node test-security.mjs && node test-integration.mjs && node test-regression.mjs && node test-css.mjs && node test-edge-cases.mjs && node stress-tests.mjs && node new-feature-tests.mjs && node test-issue-12.mjs && node test-issue-11.mjs && node test-architecture-patterns.mjs",
54
+ "test": "node test-meta-parser.mjs && node test-dom-structure.mjs && node test-options.mjs && node test-notations.mjs && node test-security.mjs && node test-integration.mjs && node test-regression.mjs && node test-css.mjs && node test-edge-cases.mjs && node stress-tests.mjs && node new-feature-tests.mjs && node test-issue-12.mjs && node test-issue-11.mjs && node test-architecture-patterns.mjs && node test-copy-button-fix.mjs && node test-tailwind-compat.mjs",
55
55
  "prepublishOnly": "npm run build && npm test",
56
56
  "prepack": "npm run build"
57
57
  },
package/src/astro.ts CHANGED
@@ -130,13 +130,20 @@ export default function perfectCode(
130
130
 
131
131
  // Copy-button script
132
132
  if (options.copyButton !== false) {
133
- injections.push(`<script${nonceAttr}>${COPY_SCRIPT}</script>`);
134
- // Graceful degradation: .no-js class
133
+ // Graceful degradation: .no-js class MUST be added BEFORE the copy
134
+ // script runs, so the copy script's swapNoJs() can detect and remove
135
+ // it. If we add .no-js AFTER the copy script, swapNoJs() is a no-op
136
+ // (the class isn't there yet), and the MutationObserver (which only
137
+ // watches childList by default) won't catch the attribute change —
138
+ // leaving .no-js on <html> permanently and hiding the copy button
139
+ // via the `html.no-js .pcb__copy { display: none !important; }` rule.
140
+ // See issue: copy button not working in Astro build output.
135
141
  if (options.hideCopyWithoutJs !== false) {
136
142
  injections.push(
137
143
  `<script${nonceAttr}>document.documentElement.classList.add('no-js');</script>`
138
144
  );
139
145
  }
146
+ injections.push(`<script${nonceAttr}>${COPY_SCRIPT}</script>`);
140
147
  }
141
148
 
142
149
  // Manual theme override
File without changes
@@ -144,11 +144,14 @@ export const COPY_SCRIPT = `
144
144
  }
145
145
  });
146
146
 
147
- // Pattern 4: MutationObserver for SPA support.
148
- // When new code blocks are inserted into the DOM (e.g. by React/Vue
149
- // re-render, Astro view transitions, or Turbolinks navigation), the
150
- // .no-js → .js class swap may need to be re-applied so newly-added
151
- // copy buttons become visible. The observer watches for added .pcb nodes.
147
+ // Pattern 4: MutationObserver for SPA support + .no-js race fix.
148
+ // Watches for:
149
+ // - New code blocks added to the DOM (React/Vue re-render, Astro view
150
+ // transitions, Turbolinks) → re-apply .no-js → .js swap + ensure
151
+ // aria-live region.
152
+ // - Attribute changes on <html> (specifically the class attribute) →
153
+ // catches the case where a later script adds .no-js AFTER this script
154
+ // ran (a race condition that previously left copy buttons hidden).
152
155
  if (typeof MutationObserver !== 'undefined') {
153
156
  var pendingObserve = false;
154
157
  var observer = new MutationObserver(function (mutations) {
@@ -157,20 +160,50 @@ export const COPY_SCRIPT = `
157
160
  pendingObserve = true;
158
161
  Promise.resolve().then(function () {
159
162
  pendingObserve = false;
160
- // If any new .pcb nodes were added, ensure the .js class is set
161
- // (in case the page was rendered server-side with .no-js and the
162
- // client took over after initial load).
163
+ var needsSwap = false;
163
164
  for (var i = 0; i < mutations.length; i++) {
164
- if (mutations[i].addedNodes && mutations[i].addedNodes.length) {
165
- swapNoJs();
166
- ensureLiveRegion();
167
- break;
165
+ var m = mutations[i];
166
+ // childList: new nodes added (SPA navigation, view transitions)
167
+ if (m.type === 'childList' && m.addedNodes && m.addedNodes.length) {
168
+ needsSwap = true;
168
169
  }
170
+ // attributes: class changed on <html> (the .no-js race fix)
171
+ if (m.type === 'attributes' && m.attributeName === 'class') {
172
+ needsSwap = true;
173
+ }
174
+ }
175
+ if (needsSwap) {
176
+ swapNoJs();
177
+ ensureLiveRegion();
169
178
  }
170
179
  });
171
180
  });
172
- // Observe the whole document subtree for added nodes.
173
- observer.observe(document.documentElement, { childList: true, subtree: true });
181
+ // Observe documentElement for BOTH childList (new nodes) AND attribute
182
+ // changes (class attribute — catches .no-js being added by a later script).
183
+ observer.observe(document.documentElement, {
184
+ childList: true,
185
+ subtree: true,
186
+ attributes: true,
187
+ attributeFilter: ['class'],
188
+ });
189
+ }
190
+
191
+ // Defensive: re-run swapNoJs() on DOMContentLoaded and window.load.
192
+ // Belt + suspenders for the .no-js race — if a framework (Astro, Next.js,
193
+ // etc.) adds .no-js in a way the MutationObserver doesn't catch (e.g.,
194
+ // before the observer is set up, or in a different document context),
195
+ // these event handlers will catch it.
196
+ if (typeof document.addEventListener === 'function') {
197
+ document.addEventListener('DOMContentLoaded', function () {
198
+ swapNoJs();
199
+ ensureLiveRegion();
200
+ });
201
+ }
202
+ if (typeof window.addEventListener === 'function') {
203
+ window.addEventListener('load', function () {
204
+ swapNoJs();
205
+ ensureLiveRegion();
206
+ });
174
207
  }
175
208
 
176
209
  // Pattern 4: astro:page-load event listener for Astro view transitions.
package/src/index.ts CHANGED
File without changes
package/src/meta.ts CHANGED
File without changes
package/src/remark.ts CHANGED
File without changes
package/src/shiki.ts CHANGED
File without changes
package/src/styles.css CHANGED
@@ -6,6 +6,8 @@
6
6
  - Solid gutter background hides code scrolling behind it
7
7
  - All defaults use :where() for zero specificity so user CSS wins
8
8
  - Every visual property is a --pcb-* CSS variable
9
+ - Framework-reset overrides (Tailwind/daisyUI compat) use real
10
+ specificity so they beat bare-element resets like `pre { ... }`
9
11
  ============================================================ */
10
12
 
11
13
  :where(.pcb) {
@@ -251,6 +253,69 @@
251
253
  display: block;
252
254
  }
253
255
 
256
+ /* ============================================================
257
+ Framework-reset overrides (Tailwind Preflight / daisyUI compat)
258
+ ------------------------------------------------------------
259
+ Tailwind Preflight targets bare `pre`, `code`, and `button`
260
+ elements with specificity (0,0,1). Our :where() rules above
261
+ have zero specificity (0,0,0), so Tailwind's resets WIN and
262
+ break the plugin's overflow model, font inheritance, and
263
+ copy-button styling.
264
+
265
+ These rules use REAL specificity (.pcb pre = (0,1,1),
266
+ .pcb__copy = (0,1,0)) to beat framework base resets WITHOUT
267
+ !important. They only set the properties that frameworks
268
+ clobber — everything else stays in :where() so user CSS
269
+ still wins.
270
+
271
+ If you're NOT using a CSS framework, these rules are harmless
272
+ (they set the same values as the :where() rules above).
273
+ ============================================================ */
274
+
275
+ /* Tailwind sets `pre { overflow-x: auto }` which creates a double-
276
+ scrollbar (one on <pre>, one on .pcb__body). Force <pre> to
277
+ visible so only .pcb__body scrolls. */
278
+ .pcb pre {
279
+ overflow: visible;
280
+ }
281
+
282
+ /* Tailwind sets `pre, code { font-family: ui-monospace, ... }` which
283
+ overrides the plugin's `font: inherit` and ignores --pcb-font-mono.
284
+ Re-assert the plugin's font. */
285
+ .pcb pre,
286
+ .pcb code {
287
+ font-family: var(--pcb-font-mono);
288
+ }
289
+
290
+ /* Tailwind sets `button { background: transparent; background-image: none }`
291
+ which strips the copy button's hover background. Re-assert the plugin's
292
+ button base. (.pcb__copy already has specificity (0,1,0) which beats
293
+ Tailwind's `button` (0,0,1), but we re-assert here for clarity and to
294
+ catch cases where frameworks add higher-specificity button resets.) */
295
+ .pcb__copy {
296
+ appearance: none;
297
+ background: transparent;
298
+ border: 1px solid transparent;
299
+ cursor: pointer;
300
+ }
301
+
302
+ /* Tailwind sets `*, ::before, ::after { border-width: 0 }` which nukes
303
+ borders on the header bar and copy button. Re-assert. */
304
+ .pcb__bar {
305
+ border-bottom: 1px solid var(--pcb-border);
306
+ }
307
+
308
+ /* Tailwind's box-sizing: border-box is fine (the plugin expects it).
309
+ No override needed. */
310
+
311
+ /* Some Tailwind utilities (e.g., `@apply break-words` or global
312
+ `pre { white-space: pre-wrap }`) can clobber the plugin's
313
+ `white-space: pre` on .pcb__code, causing long lines to wrap
314
+ instead of scroll. Re-assert with real specificity. */
315
+ .pcb__code {
316
+ white-space: pre;
317
+ }
318
+
254
319
  /* ---------- Row-based line grid ----------
255
320
  Each <span class="pcb__line"> uses display:contents so its children
256
321
  (.pcb__ln and .pcb__code) become grid items in the <code> container. */
File without changes
package/src/types.ts CHANGED
File without changes
package/src/vite-raw.d.ts CHANGED
File without changes
package/src/word-diff.ts CHANGED
File without changes