@dr-ishaan/rehype-perfect-code-blocks 1.3.0 → 1.3.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/CHANGELOG.md +40 -0
- package/LICENSE +0 -0
- package/README.md +225 -13
- package/dist/astro.d.ts +0 -0
- package/dist/astro.d.ts.map +1 -1
- package/dist/astro.js +9 -2
- package/dist/astro.js.map +1 -1
- package/dist/color-utils.d.ts +0 -0
- package/dist/color-utils.d.ts.map +0 -0
- package/dist/color-utils.js +0 -0
- package/dist/color-utils.js.map +0 -0
- package/dist/copy-script.d.ts +1 -1
- package/dist/copy-script.d.ts.map +1 -1
- package/dist/copy-script.js +47 -14
- package/dist/copy-script.js.map +1 -1
- package/dist/index.d.ts +0 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +0 -0
- package/dist/meta.d.ts +0 -0
- package/dist/meta.d.ts.map +0 -0
- package/dist/meta.js +0 -0
- package/dist/meta.js.map +0 -0
- package/dist/remark.d.ts +0 -0
- package/dist/remark.d.ts.map +0 -0
- package/dist/remark.js +0 -0
- package/dist/remark.js.map +0 -0
- package/dist/shiki.d.ts +0 -0
- package/dist/shiki.d.ts.map +0 -0
- package/dist/shiki.js +0 -0
- package/dist/shiki.js.map +0 -0
- package/dist/styles.css +0 -0
- package/dist/transformer.d.ts +0 -0
- package/dist/transformer.d.ts.map +0 -0
- package/dist/transformer.js +0 -0
- package/dist/transformer.js.map +0 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.d.ts.map +0 -0
- package/dist/types.js +0 -0
- package/dist/types.js.map +0 -0
- package/dist/word-diff.d.ts +0 -0
- package/dist/word-diff.d.ts.map +0 -0
- package/dist/word-diff.js +0 -0
- package/dist/word-diff.js.map +0 -0
- package/package.json +2 -2
- package/src/astro.ts +9 -2
- package/src/color-utils.ts +0 -0
- package/src/copy-script.ts +47 -14
- package/src/index.ts +0 -0
- package/src/meta.ts +0 -0
- package/src/remark.ts +0 -0
- package/src/shiki.ts +0 -0
- package/src/styles.css +0 -0
- package/src/transformer.ts +0 -0
- package/src/types.ts +0 -0
- package/src/vite-raw.d.ts +0 -0
- package/src/word-diff.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ 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.2] — 2026-06-20
|
|
9
|
+
|
|
10
|
+
### Summary
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
### Bug fixes
|
|
15
|
+
|
|
16
|
+
- **Copy button unclickable in Astro build output** — Three complementary fixes:
|
|
17
|
+
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.
|
|
18
|
+
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.
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
### Verification
|
|
22
|
+
|
|
23
|
+
- All 1092 pre-existing tests pass (no regressions).
|
|
24
|
+
- 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.
|
|
25
|
+
- Total: 1114/1114 tests passing.
|
|
26
|
+
|
|
27
|
+
## [1.3.1] — 2026-06-19
|
|
28
|
+
|
|
29
|
+
### Summary
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
|
|
35
|
+
- **README.md** — comprehensive update for v1.3.0:
|
|
36
|
+
- New "What's new in v1.3.0" section with a table of the 5 adopted patterns (source, new export/option).
|
|
37
|
+
- New "Advanced APIs (v1.3.0+)" section documenting `runHighlighterTask`, `disposeHighlighter`, `wordDiff`, `hasChanges`, and the `DiffToken` type, with usage examples.
|
|
38
|
+
- New "Theme-aware defaults (v1.3.0+)" subsection in Theming, documenting the 7 auto-derived `--pcb-*` variables and the cascade order.
|
|
39
|
+
- New `wordDiff` row in the Modes options table, with a full usage example showing the input markdown and resulting HTML.
|
|
40
|
+
- 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.
|
|
41
|
+
- Updated Testing section: 1092 tests across 14 suites (was "110 tests across three suites").
|
|
42
|
+
- Updated Comparison table: 4 new rows for word-level diff, theme-aware defaults, highlighter task queue, dispose lifecycle, and SPA-robust copy button.
|
|
43
|
+
- 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`.
|
|
44
|
+
- New "Changelog" section in README with highlights for each version.
|
|
45
|
+
- Updated "Why this exists" bullet list to mention the new v1.3.0 features.
|
|
46
|
+
- Updated test count from "110 tests pass" to "1092 tests pass".
|
|
47
|
+
|
|
8
48
|
## [1.3.0] — 2026-06-19
|
|
9
49
|
|
|
10
50
|
### 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
|
-
- **
|
|
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
|
|
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-
|
|
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 ← ~
|
|
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-
|
|
447
|
-
├──
|
|
448
|
-
|
|
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
|
package/dist/astro.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
124
|
-
//
|
|
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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"}
|
package/dist/color-utils.d.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/dist/color-utils.js
CHANGED
|
File without changes
|
package/dist/color-utils.js.map
CHANGED
|
File without changes
|
package/dist/copy-script.d.ts
CHANGED
|
@@ -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 //
|
|
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,
|
|
1
|
+
{"version":3,"file":"copy-script.d.ts","sourceRoot":"","sources":["../src/copy-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,WAAW,kxOAmMvB,CAAC"}
|
package/dist/copy-script.js
CHANGED
|
@@ -144,11 +144,14 @@ export const COPY_SCRIPT = `
|
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
// Pattern 4: MutationObserver for SPA support.
|
|
148
|
-
//
|
|
149
|
-
// re-render, Astro view
|
|
150
|
-
// .no-js → .js
|
|
151
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
173
|
-
|
|
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/dist/copy-script.js.map
CHANGED
|
@@ -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
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
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
|
package/dist/meta.d.ts.map
CHANGED
|
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
|
package/dist/remark.d.ts.map
CHANGED
|
File without changes
|
package/dist/remark.js
CHANGED
|
File without changes
|
package/dist/remark.js.map
CHANGED
|
File without changes
|
package/dist/shiki.d.ts
CHANGED
|
File without changes
|
package/dist/shiki.d.ts.map
CHANGED
|
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
|
File without changes
|
package/dist/transformer.d.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/dist/transformer.js
CHANGED
|
File without changes
|
package/dist/transformer.js.map
CHANGED
|
File without changes
|
package/dist/types.d.ts
CHANGED
|
File without changes
|
package/dist/types.d.ts.map
CHANGED
|
File without changes
|
package/dist/types.js
CHANGED
|
File without changes
|
package/dist/types.js.map
CHANGED
|
File without changes
|
package/dist/word-diff.d.ts
CHANGED
|
File without changes
|
package/dist/word-diff.d.ts.map
CHANGED
|
File without changes
|
package/dist/word-diff.js
CHANGED
|
File without changes
|
package/dist/word-diff.js.map
CHANGED
|
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.
|
|
3
|
+
"version": "1.3.2",
|
|
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",
|
|
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
|
-
|
|
134
|
-
//
|
|
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
|
package/src/color-utils.ts
CHANGED
|
File without changes
|
package/src/copy-script.ts
CHANGED
|
@@ -144,11 +144,14 @@ export const COPY_SCRIPT = `
|
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
// Pattern 4: MutationObserver for SPA support.
|
|
148
|
-
//
|
|
149
|
-
// re-render, Astro view
|
|
150
|
-
// .no-js → .js
|
|
151
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
173
|
-
|
|
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
|
File without changes
|
package/src/transformer.ts
CHANGED
|
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
|