@dr-ishaan/rehype-perfect-code-blocks 1.1.6 → 1.2.0
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 +112 -0
- package/dist/astro.d.ts.map +1 -1
- package/dist/astro.js +34 -7
- package/dist/astro.js.map +1 -1
- package/dist/copy-script.d.ts +2 -1
- package/dist/copy-script.d.ts.map +1 -1
- package/dist/copy-script.js +16 -2
- package/dist/copy-script.js.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/meta.d.ts.map +1 -1
- package/dist/meta.js +51 -7
- package/dist/meta.js.map +1 -1
- package/dist/shiki.d.ts.map +1 -1
- package/dist/shiki.js +254 -25
- package/dist/shiki.js.map +1 -1
- package/dist/transformer.d.ts +15 -0
- package/dist/transformer.d.ts.map +1 -1
- package/dist/transformer.js +273 -26
- package/dist/transformer.js.map +1 -1
- package/dist/types.d.ts +109 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +23 -4
- package/src/astro.ts +36 -9
- package/src/copy-script.ts +16 -2
- package/src/index.ts +15 -1
- package/src/meta.ts +51 -7
- package/src/shiki.ts +258 -26
- package/src/transformer.ts +286 -24
- package/src/types.ts +105 -5
package/dist/types.d.ts
CHANGED
|
@@ -47,6 +47,16 @@ export interface PerfectCodeOptions {
|
|
|
47
47
|
wrap?: boolean;
|
|
48
48
|
/** Auto-collapse blocks longer than N lines. null = never. Default: null */
|
|
49
49
|
collapseAfter?: number | null;
|
|
50
|
+
/**
|
|
51
|
+
* Per-line collapsible sections.
|
|
52
|
+
* Pass a meta string like `collapse="5-12,20-30"` to wrap matching line ranges
|
|
53
|
+
* in `<details><summary>N collapsed lines</summary>...</details>`.
|
|
54
|
+
* Style options: 'github' (default), 'collapsible-start', 'collapsible-end', 'collapsible-auto'.
|
|
55
|
+
* Default: null (disabled).
|
|
56
|
+
*/
|
|
57
|
+
collapseRanges?: string | null;
|
|
58
|
+
/** Style for collapsible sections. Default: 'github'. */
|
|
59
|
+
collapseStyle?: 'github' | 'collapsible-start' | 'collapsible-end' | 'collapsible-auto';
|
|
50
60
|
/** Show visible whitespace (tabs/spaces). Default: false */
|
|
51
61
|
showWhitespace?: false | 'all' | 'boundary' | 'trailing' | 'leading';
|
|
52
62
|
/** Render vertical indent guides. false | true (default 2) | number (indent width). Default: false */
|
|
@@ -62,11 +72,15 @@ export interface PerfectCodeOptions {
|
|
|
62
72
|
engine?: 'auto' | 'shiki' | 'passthrough';
|
|
63
73
|
/** Shiki options passed through when the plugin calls Shiki itself. */
|
|
64
74
|
shiki?: {
|
|
65
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Theme — string for single theme, { light, dark } for dual-theme via CSS vars,
|
|
77
|
+
* or a Record<string, string> for multi-theme (3+ themes) support.
|
|
78
|
+
* Multi-theme example: `{ light: 'github-light', dark: 'github-dark', dim: 'github-dark-dimmed' }`.
|
|
79
|
+
*/
|
|
66
80
|
theme?: string | {
|
|
67
81
|
light: string;
|
|
68
82
|
dark: string;
|
|
69
|
-
}
|
|
83
|
+
} | Record<string, string>;
|
|
70
84
|
/** Pre-loaded languages. Defaults to a sensible set; missing langs are lazy-loaded. */
|
|
71
85
|
langs?: string[];
|
|
72
86
|
/**
|
|
@@ -77,6 +91,12 @@ export interface PerfectCodeOptions {
|
|
|
77
91
|
regexEngine?: 'oniguruma' | 'javascript';
|
|
78
92
|
/** Additional Shiki transformers to apply (see @shikijs/transformers). */
|
|
79
93
|
transformers?: ShikiTransformer[];
|
|
94
|
+
/**
|
|
95
|
+
* Controls whether user-provided transformers run 'before' or 'after' the
|
|
96
|
+
* auto-registered ones (default: 'after'). Use 'before' to give user
|
|
97
|
+
* transformers first access to the code text.
|
|
98
|
+
*/
|
|
99
|
+
transformerOrder?: 'before' | 'after';
|
|
80
100
|
/** Override the highlighter factory (e.g. for custom TextMate grammars). */
|
|
81
101
|
getHighlighter?: (opts: {
|
|
82
102
|
themes: string[];
|
|
@@ -100,6 +120,38 @@ export interface PerfectCodeOptions {
|
|
|
100
120
|
* round-trip. Faster + safer. Default: true.
|
|
101
121
|
*/
|
|
102
122
|
useHastApi?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Disable auto-registration of @shikijs/transformers. When true, ONLY the
|
|
125
|
+
* transformers in `shiki.transformers` are applied. Default: false.
|
|
126
|
+
* Useful for advanced users who want full manual control.
|
|
127
|
+
*/
|
|
128
|
+
disableAutoTransformers?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Strip all comments from the rendered code (// ..., # ..., /* ... *\/, <!-- ... -->).
|
|
131
|
+
* Powered by @shikijs/transformers `transformerRemoveComments`. Default: false.
|
|
132
|
+
*/
|
|
133
|
+
removeComments?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Remove line breaks from the rendered code (joins all lines into one).
|
|
136
|
+
* Powered by @shikijs/transformers `transformerRemoveLineBreaks`. Default: false.
|
|
137
|
+
* Useful for compact inline-style code blocks.
|
|
138
|
+
*/
|
|
139
|
+
removeLineBreaks?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* When `true`, treat {1,3-5} meta ranges as zero-indexed (line 0 is the first
|
|
142
|
+
* line). When `false` (default), line numbers start at 1.
|
|
143
|
+
*/
|
|
144
|
+
zeroIndexed?: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Programmatic per-line class assignment (Shiki's `transformerCompactLineOptions`).
|
|
147
|
+
* Example: `[{ line: 1, classes: ['highlight'] }, { line: 3, classes: ['add'] }]`.
|
|
148
|
+
* Default: [] (disabled).
|
|
149
|
+
*/
|
|
150
|
+
lineOptions?: {
|
|
151
|
+
line: number;
|
|
152
|
+
classes?: string[];
|
|
153
|
+
attrs?: Record<string, string>;
|
|
154
|
+
}[];
|
|
103
155
|
/**
|
|
104
156
|
* Custom // [!code xxx] notations mapped to CSS classes. Default: {}.
|
|
105
157
|
* Example: `{ 'my-marker': 'pcb__line--custom' }` lets users write
|
|
@@ -127,7 +179,7 @@ export interface PerfectCodeOptions {
|
|
|
127
179
|
tokensMap?: Record<string, string>;
|
|
128
180
|
/**
|
|
129
181
|
* Auto-switch to terminal preset for these languages. Default:
|
|
130
|
-
* ['sh', 'bash', 'zsh', 'shell', 'console', 'powershell', 'bat', 'cmd', 'fish']
|
|
182
|
+
* ['sh', 'bash', 'zsh', 'shell', 'console', 'powershell', 'bat', 'cmd', 'fish', 'ansi']
|
|
131
183
|
*/
|
|
132
184
|
terminalLangs?: string[];
|
|
133
185
|
/**
|
|
@@ -159,7 +211,19 @@ export interface PerfectCodeOptions {
|
|
|
159
211
|
*/
|
|
160
212
|
defaultInlineLang?: string;
|
|
161
213
|
/**
|
|
162
|
-
*
|
|
214
|
+
* Replace tabs with N spaces before tokenization. 0 disables (default).
|
|
215
|
+
* Useful for languages where Shiki's tab rendering doesn't match the
|
|
216
|
+
* surrounding code style.
|
|
217
|
+
*/
|
|
218
|
+
tabWidth?: number;
|
|
219
|
+
/**
|
|
220
|
+
* Strip leading `#` comment lines from terminal code when copying to clipboard.
|
|
221
|
+
* Default: true (only effective when preset === 'terminal').
|
|
222
|
+
*/
|
|
223
|
+
copyStripComments?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Add `role="region"`, `aria-label`, and `tabindex="0"` to scrollable code
|
|
226
|
+
* blocks (WCAG 2.1.1 keyboard accessible, 4.1.2 name-role-value).
|
|
163
227
|
* Default: true.
|
|
164
228
|
*/
|
|
165
229
|
accessibleScroll?: boolean;
|
|
@@ -174,6 +238,12 @@ export interface PerfectCodeOptions {
|
|
|
174
238
|
* Default: true.
|
|
175
239
|
*/
|
|
176
240
|
hideCopyWithoutJs?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Add a screen-reader-only `<span class="pcb__sr-only">Terminal window</span>`
|
|
243
|
+
* to terminal-preset blocks that have no title. Improves screen reader context.
|
|
244
|
+
* Default: true.
|
|
245
|
+
*/
|
|
246
|
+
terminalSrOnlyTitle?: boolean;
|
|
177
247
|
/**
|
|
178
248
|
* Additional rehype plugins to run BEFORE rehype-perfect-code-blocks.
|
|
179
249
|
* Pass `rehypeRaw` here if your markdown contains raw HTML (`<details>`,
|
|
@@ -205,6 +275,37 @@ export interface PerfectCodeOptions {
|
|
|
205
275
|
onVisitTitle?: (element: unknown) => void;
|
|
206
276
|
/** Called for the caption element (if present). */
|
|
207
277
|
onVisitCaption?: (element: unknown) => void;
|
|
278
|
+
/**
|
|
279
|
+
* Localized UI strings. Defaults are English. Override per-locale by
|
|
280
|
+
* passing a different `texts` object based on the current language.
|
|
281
|
+
*/
|
|
282
|
+
texts?: {
|
|
283
|
+
/** Copy button label (default: 'copy'). */
|
|
284
|
+
copyLabel?: string;
|
|
285
|
+
/** Label shown after successful copy (default: 'copied!'). */
|
|
286
|
+
doneLabel?: string;
|
|
287
|
+
/** Aria-label for the copy button (default: 'Copy code'). */
|
|
288
|
+
copyAriaLabel?: string;
|
|
289
|
+
/** Screen-reader-only title for terminal-preset blocks (default: 'Terminal window'). */
|
|
290
|
+
terminalSrOnlyTitle?: string;
|
|
291
|
+
/** aria-label prefix for scrollable body (default: 'Code block'). */
|
|
292
|
+
codeBlockAriaPrefix?: string;
|
|
293
|
+
/** Summary text for collapsed sections, with `{n}` placeholder (default: '{n} collapsed lines'). */
|
|
294
|
+
collapsedLinesLabel?: (n: number) => string;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Custom logger. Defaults to `console`. Useful for silencing warnings in
|
|
298
|
+
* production or routing them to a structured logger.
|
|
299
|
+
*/
|
|
300
|
+
logger?: {
|
|
301
|
+
warn: (msg: string) => void;
|
|
302
|
+
error: (msg: string) => void;
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Nonce to add to injected `<script>` and `<style>` tags. Enables strict CSP
|
|
306
|
+
* (`script-src 'self' 'nonce-...'`). Default: undefined (no nonce).
|
|
307
|
+
*/
|
|
308
|
+
cspNonce?: string;
|
|
208
309
|
/** Visual preset. Default: 'default' */
|
|
209
310
|
preset?: 'default' | 'terminal' | 'minimal';
|
|
210
311
|
/** Inject the bundled CSS automatically. Set false to ship your own. Default: true */
|
|
@@ -241,6 +342,10 @@ export interface ParsedMeta {
|
|
|
241
342
|
id?: string;
|
|
242
343
|
}[];
|
|
243
344
|
lineNumbersStart: number | null;
|
|
345
|
+
collapseRanges: {
|
|
346
|
+
from: number;
|
|
347
|
+
to: number;
|
|
348
|
+
}[];
|
|
244
349
|
flags: {
|
|
245
350
|
wrap: boolean | null;
|
|
246
351
|
lineNumbers: boolean | null;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAEvC,MAAM,WAAW,kBAAkB;IAEjC,sEAAsE;IACtE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;OAOG;IACH,UAAU,CAAC,EACP,OAAO,GACP;QACE,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;QAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACN,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iEAAiE;IACjE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAG7B,+EAA+E;IAC/E,WAAW,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IAC1C,wFAAwF;IACxF,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IACvC,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,0FAA0F;IAC1F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2FAA2F;IAC3F,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,sDAAsD;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wCAAwC;IACxC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,4DAA4D;IAC5D,cAAc,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;IACrE,sGAAsG;IACtG,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAChC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,aAAa,CAAC;IAC1C,uEAAuE;IACvE,KAAK,CAAC,EAAE;QACN
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dr-ishaan/rehype-perfect-code-blocks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -19,12 +19,33 @@
|
|
|
19
19
|
"types": "./dist/remark.d.ts",
|
|
20
20
|
"import": "./dist/remark.js"
|
|
21
21
|
},
|
|
22
|
+
"./meta": {
|
|
23
|
+
"types": "./dist/meta.d.ts",
|
|
24
|
+
"import": "./dist/meta.js"
|
|
25
|
+
},
|
|
26
|
+
"./transformer": {
|
|
27
|
+
"types": "./dist/transformer.d.ts",
|
|
28
|
+
"import": "./dist/transformer.js"
|
|
29
|
+
},
|
|
30
|
+
"./shiki": {
|
|
31
|
+
"types": "./dist/shiki.d.ts",
|
|
32
|
+
"import": "./dist/shiki.js"
|
|
33
|
+
},
|
|
34
|
+
"./copy-script": {
|
|
35
|
+
"types": "./dist/copy-script.d.ts",
|
|
36
|
+
"import": "./dist/copy-script.js"
|
|
37
|
+
},
|
|
38
|
+
"./types": {
|
|
39
|
+
"types": "./dist/types.d.ts",
|
|
40
|
+
"import": "./dist/types.js"
|
|
41
|
+
},
|
|
22
42
|
"./styles.css": "./dist/styles.css"
|
|
23
43
|
},
|
|
24
44
|
"files": [
|
|
25
45
|
"dist",
|
|
26
46
|
"src",
|
|
27
47
|
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
28
49
|
"LICENSE"
|
|
29
50
|
],
|
|
30
51
|
"scripts": {
|
|
@@ -65,7 +86,7 @@
|
|
|
65
86
|
"node": ">=18.0.0"
|
|
66
87
|
},
|
|
67
88
|
"peerDependencies": {
|
|
68
|
-
"astro": "^4.0.0 || ^5.0.0"
|
|
89
|
+
"astro": "^4.0.0 || ^5.0.0 || ^6.0.0"
|
|
69
90
|
},
|
|
70
91
|
"peerDependenciesMeta": {
|
|
71
92
|
"astro": {
|
|
@@ -79,8 +100,6 @@
|
|
|
79
100
|
"@shikijs/transformers": "^4.2.0",
|
|
80
101
|
"hast": "^1.0.0",
|
|
81
102
|
"hast-util-from-html": "^2.0.0",
|
|
82
|
-
"rehype-raw": "^7.0.0",
|
|
83
|
-
"remark-gfm": "^4.0.1",
|
|
84
103
|
"unist-util-visit": "^5.0.0"
|
|
85
104
|
},
|
|
86
105
|
"devDependencies": {
|
package/src/astro.ts
CHANGED
|
@@ -29,6 +29,17 @@ import { dirname, join } from 'node:path';
|
|
|
29
29
|
|
|
30
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Load the bundled CSS at runtime via readFileSync rather than via Vite's
|
|
34
|
+
* `?raw` query. The `?raw` approach only works when Vite is bundling the
|
|
35
|
+
* module (i.e. inside an Astro/Vite project that resolves the package
|
|
36
|
+
* through Vite's pipeline). When the package is consumed by a plain Node
|
|
37
|
+
* script, a non-Vite bundler, or — importantly — when it is *symlinked*
|
|
38
|
+
* into a project (so Vite treats it as a linked dep), the `?raw` query
|
|
39
|
+
* fails with `Unknown file extension ".css"`.
|
|
40
|
+
*
|
|
41
|
+
* Using readFileSync at runtime is more portable.
|
|
42
|
+
*/
|
|
32
43
|
function loadCss(): string {
|
|
33
44
|
try {
|
|
34
45
|
return readFileSync(join(__dirname, 'styles.css'), 'utf8');
|
|
@@ -41,6 +52,13 @@ function loadCss(): string {
|
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
/** Escape a string for use as an HTML attribute value (defense in depth). */
|
|
56
|
+
function escapeAttr(s: string): string {
|
|
57
|
+
return s.replace(/[<>"'&]/g, (c) => ({
|
|
58
|
+
'<': '<', '>': '>', '"': '"', "'": ''', '&': '&',
|
|
59
|
+
}[c] ?? c));
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
/** Recursively walk a directory and return all .html file paths. */
|
|
45
63
|
function findHtmlFiles(dir: string): string[] {
|
|
46
64
|
const results: string[] = [];
|
|
@@ -68,7 +86,7 @@ export default function perfectCode(
|
|
|
68
86
|
hooks: {
|
|
69
87
|
'astro:config:setup': ({ updateConfig }) => {
|
|
70
88
|
// 1. Register the remark + rehype plugins with the Markdown pipeline.
|
|
71
|
-
const userRehypePlugins = options.rehypePlugins ?? [];
|
|
89
|
+
const userRehypePlugins = (options.rehypePlugins ?? []) as unknown[];
|
|
72
90
|
updateConfig({
|
|
73
91
|
markdown: {
|
|
74
92
|
syntaxHighlight: 'shiki',
|
|
@@ -80,8 +98,10 @@ export default function perfectCode(
|
|
|
80
98
|
: undefined,
|
|
81
99
|
remarkPlugins: [remarkPreserveCodeMeta],
|
|
82
100
|
rehypePlugins: [
|
|
83
|
-
...
|
|
101
|
+
...userRehypePlugins,
|
|
84
102
|
[rehypePerfectCodeBlocks, options],
|
|
103
|
+
// Cast: Astro's rehypePlugins type is a complex union; our
|
|
104
|
+
// array shape ([plugin, options]) is one of the supported forms.
|
|
85
105
|
] as never,
|
|
86
106
|
},
|
|
87
107
|
});
|
|
@@ -90,36 +110,43 @@ export default function perfectCode(
|
|
|
90
110
|
'astro:build:done': ({ dir }) => {
|
|
91
111
|
// 2. After Astro finishes building all pages, inject CSS + scripts
|
|
92
112
|
// into every generated .html file. This is the most reliable
|
|
93
|
-
// method — it works with Astro v4, v5, static and SSR modes,
|
|
113
|
+
// method — it works with Astro v4, v5, v6, static and SSR modes,
|
|
94
114
|
// and doesn't require the user to import anything in their layout.
|
|
115
|
+
//
|
|
116
|
+
// (The previous `injectScript('page', '<style>...')` approach
|
|
117
|
+
// breaks on Astro 6 where injectScript expects JS, not HTML.)
|
|
95
118
|
const injections: string[] = [];
|
|
119
|
+
// CSP nonce: if set, add `nonce="..."` to all <script> and <style> tags
|
|
120
|
+
// so they pass a strict Content-Security-Policy.
|
|
121
|
+
const nonceAttr = options.cspNonce ? ` nonce="${escapeAttr(options.cspNonce)}"` : '';
|
|
96
122
|
|
|
97
123
|
// CSS
|
|
98
124
|
if (options.injectStyles !== false) {
|
|
99
125
|
const css = loadCss();
|
|
100
126
|
if (css) {
|
|
101
|
-
injections.push(`<style data-pcb>${css}</style>`);
|
|
127
|
+
injections.push(`<style data-pcb${nonceAttr}>${css}</style>`);
|
|
102
128
|
}
|
|
103
129
|
}
|
|
104
130
|
|
|
105
131
|
// Copy-button script
|
|
106
132
|
if (options.copyButton !== false) {
|
|
107
|
-
injections.push(`<script>${COPY_SCRIPT}</script>`);
|
|
108
|
-
|
|
133
|
+
injections.push(`<script${nonceAttr}>${COPY_SCRIPT}</script>`);
|
|
109
134
|
// Graceful degradation: .no-js class
|
|
110
135
|
if (options.hideCopyWithoutJs !== false) {
|
|
111
136
|
injections.push(
|
|
112
|
-
`<script>document.documentElement.classList.add('no-js');</script>`
|
|
137
|
+
`<script${nonceAttr}>document.documentElement.classList.add('no-js');</script>`
|
|
113
138
|
);
|
|
114
139
|
}
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
// Manual theme override
|
|
118
143
|
if (options.theme && options.theme !== 'auto') {
|
|
119
|
-
const safeTheme = ['dark', 'light'].includes(options.theme)
|
|
144
|
+
const safeTheme = ['dark', 'light'].includes(options.theme)
|
|
145
|
+
? options.theme
|
|
146
|
+
: 'auto';
|
|
120
147
|
if (safeTheme !== 'auto') {
|
|
121
148
|
injections.push(
|
|
122
|
-
`<script>document.documentElement.setAttribute('data-theme','${safeTheme}');</script>`
|
|
149
|
+
`<script${nonceAttr}>document.documentElement.setAttribute('data-theme','${safeTheme}');</script>`
|
|
123
150
|
);
|
|
124
151
|
}
|
|
125
152
|
}
|
package/src/copy-script.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - Falls back to execCommand for non-secure contexts
|
|
11
11
|
* - Respects `prefers-reduced-motion` (the CSS handles the animation)
|
|
12
12
|
* - Reads `data-done-label`, `data-success-icon`, `data-feedback-duration` from the button
|
|
13
|
+
* - Strips leading `#` comment lines when `data-strip-comments` is set (terminal preset)
|
|
13
14
|
* - Announces "Copied" to screen readers via an aria-live region (WCAG 4.1.2)
|
|
14
15
|
* - Hides copy button when JS is disabled (via .no-js class on <html>)
|
|
15
16
|
*/
|
|
@@ -50,6 +51,15 @@ export const COPY_SCRIPT = `
|
|
|
50
51
|
return btn.querySelector('svg');
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// Strip leading comment lines (e.g. shell prompts like "# comment") from
|
|
55
|
+
// the text before copying. Used for terminal-preset blocks where the
|
|
56
|
+
// displayed code may include comments the user doesn't want on the clipboard.
|
|
57
|
+
function stripComments(text) {
|
|
58
|
+
// Strip lines that start with optional whitespace followed by # (shell),
|
|
59
|
+
// // (C-style), or REM (Windows batch). Keep everything else.
|
|
60
|
+
return text.replace(/^[ \\t]*(?:#|\\/\\/|REM\\b).*$/gm, '').replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
document.addEventListener('click', function (e) {
|
|
54
64
|
var btn = e.target && e.target.closest && e.target.closest('.pcb__copy');
|
|
55
65
|
if (!btn) return;
|
|
@@ -60,12 +70,16 @@ export const COPY_SCRIPT = `
|
|
|
60
70
|
var done = btn.getAttribute('data-done-label') || 'copied!';
|
|
61
71
|
var duration = parseInt(btn.getAttribute('data-feedback-duration') || '1600', 10);
|
|
62
72
|
var successIconHtml = btn.getAttribute('data-success-icon');
|
|
73
|
+
var stripCommentsFlag = btn.hasAttribute('data-strip-comments');
|
|
63
74
|
|
|
64
75
|
var label = findLabel(btn);
|
|
65
76
|
var icon = findIcon(btn);
|
|
66
77
|
var originalLabel = label ? label.textContent : null;
|
|
67
78
|
var originalIconHtml = icon ? icon.outerHTML : null;
|
|
68
79
|
|
|
80
|
+
var rawText = code.innerText;
|
|
81
|
+
var textToCopy = stripCommentsFlag ? stripComments(rawText) : rawText;
|
|
82
|
+
|
|
69
83
|
var finish = function () {
|
|
70
84
|
btn.classList.add('pcb__copy--done');
|
|
71
85
|
if (label) label.textContent = done;
|
|
@@ -93,14 +107,14 @@ export const COPY_SCRIPT = `
|
|
|
93
107
|
};
|
|
94
108
|
|
|
95
109
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
96
|
-
navigator.clipboard.writeText(
|
|
110
|
+
navigator.clipboard.writeText(textToCopy).then(finish).catch(fallback);
|
|
97
111
|
} else {
|
|
98
112
|
fallback();
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
function fallback() {
|
|
102
116
|
var ta = document.createElement('textarea');
|
|
103
|
-
ta.value =
|
|
117
|
+
ta.value = textToCopy;
|
|
104
118
|
ta.style.position = 'fixed';
|
|
105
119
|
ta.style.opacity = '0';
|
|
106
120
|
document.body.appendChild(ta);
|
package/src/index.ts
CHANGED
|
@@ -94,6 +94,8 @@ function resolveDefaults(opts: PerfectCodeOptions): Required<PerfectCodeOptions>
|
|
|
94
94
|
errorLevels: opts.errorLevels ?? true,
|
|
95
95
|
wrap: opts.wrap ?? false,
|
|
96
96
|
collapseAfter: opts.collapseAfter ?? null,
|
|
97
|
+
collapseRanges: opts.collapseRanges ?? null,
|
|
98
|
+
collapseStyle: opts.collapseStyle ?? 'github',
|
|
97
99
|
showWhitespace: opts.showWhitespace ?? false,
|
|
98
100
|
indentGuides: opts.indentGuides ?? false,
|
|
99
101
|
caption: opts.caption ?? true,
|
|
@@ -102,11 +104,17 @@ function resolveDefaults(opts: PerfectCodeOptions): Required<PerfectCodeOptions>
|
|
|
102
104
|
theme: { light: 'github-light', dark: 'github-dark' },
|
|
103
105
|
langs: [],
|
|
104
106
|
transformers: [],
|
|
107
|
+
transformerOrder: 'after',
|
|
105
108
|
...userShiki,
|
|
106
109
|
},
|
|
107
110
|
keepBackground: opts.keepBackground ?? false,
|
|
108
111
|
styleToClass: opts.styleToClass ?? false,
|
|
109
112
|
useHastApi: opts.useHastApi ?? true,
|
|
113
|
+
disableAutoTransformers: opts.disableAutoTransformers ?? false,
|
|
114
|
+
removeComments: opts.removeComments ?? false,
|
|
115
|
+
removeLineBreaks: opts.removeLineBreaks ?? false,
|
|
116
|
+
zeroIndexed: opts.zeroIndexed ?? false,
|
|
117
|
+
lineOptions: opts.lineOptions ?? [],
|
|
110
118
|
customNotations: opts.customNotations ?? {},
|
|
111
119
|
magicComments: opts.magicComments ?? [
|
|
112
120
|
{
|
|
@@ -119,14 +127,17 @@ function resolveDefaults(opts: PerfectCodeOptions): Required<PerfectCodeOptions>
|
|
|
119
127
|
inlineDefaultLang: opts.inlineDefaultLang ?? opts.defaultInlineLang ?? '',
|
|
120
128
|
defaultInlineLang: opts.defaultInlineLang ?? opts.inlineDefaultLang ?? '',
|
|
121
129
|
tokensMap: opts.tokensMap ?? {},
|
|
122
|
-
terminalLangs: opts.terminalLangs ?? ['sh', 'bash', 'zsh', 'shell', 'console', 'powershell', 'bat', 'cmd', 'fish'],
|
|
130
|
+
terminalLangs: opts.terminalLangs ?? ['sh', 'bash', 'zsh', 'shell', 'console', 'powershell', 'bat', 'cmd', 'fish', 'ansi'],
|
|
123
131
|
extractFileNameFromCode: opts.extractFileNameFromCode ?? false,
|
|
124
132
|
languageLabels: opts.languageLabels ?? {},
|
|
125
133
|
languageAliases: opts.languageAliases ?? {},
|
|
126
134
|
defaultBlockLang: opts.defaultBlockLang ?? '',
|
|
135
|
+
tabWidth: opts.tabWidth ?? 0,
|
|
136
|
+
copyStripComments: opts.copyStripComments ?? true,
|
|
127
137
|
accessibleScroll: opts.accessibleScroll ?? true,
|
|
128
138
|
announceCopy: opts.announceCopy ?? true,
|
|
129
139
|
hideCopyWithoutJs: opts.hideCopyWithoutJs ?? true,
|
|
140
|
+
terminalSrOnlyTitle: opts.terminalSrOnlyTitle ?? true,
|
|
130
141
|
rehypePlugins: opts.rehypePlugins ?? [],
|
|
131
142
|
filterMetaString: opts.filterMetaString ?? ((s) => s),
|
|
132
143
|
onVisitLine: opts.onVisitLine ?? (() => {}),
|
|
@@ -134,6 +145,9 @@ function resolveDefaults(opts: PerfectCodeOptions): Required<PerfectCodeOptions>
|
|
|
134
145
|
onVisitHighlightedChars: opts.onVisitHighlightedChars ?? (() => {}),
|
|
135
146
|
onVisitTitle: opts.onVisitTitle ?? (() => {}),
|
|
136
147
|
onVisitCaption: opts.onVisitCaption ?? (() => {}),
|
|
148
|
+
texts: opts.texts ?? {},
|
|
149
|
+
logger: opts.logger ?? console,
|
|
150
|
+
cspNonce: opts.cspNonce ?? '',
|
|
137
151
|
preset: opts.preset ?? 'default',
|
|
138
152
|
injectStyles: opts.injectStyles ?? true,
|
|
139
153
|
theme: opts.theme ?? 'auto',
|
package/src/meta.ts
CHANGED
|
@@ -56,6 +56,7 @@ export function parseMeta(meta: string | undefined): ParsedMeta {
|
|
|
56
56
|
highlightGroups: [],
|
|
57
57
|
wordHighlights: [],
|
|
58
58
|
lineNumbersStart: null,
|
|
59
|
+
collapseRanges: [],
|
|
59
60
|
flags: {
|
|
60
61
|
wrap: null,
|
|
61
62
|
lineNumbers: null,
|
|
@@ -84,6 +85,16 @@ export function parseMeta(meta: string | undefined): ParsedMeta {
|
|
|
84
85
|
continue;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// collapse="5-12,20-30" — per-line collapsible sections
|
|
89
|
+
if (tok.startsWith('collapse=')) {
|
|
90
|
+
const val = unquote(tok.slice('collapse='.length));
|
|
91
|
+
result.collapseRanges = parseCollapseRanges(val);
|
|
92
|
+
if (result.collapseRanges.length > 0) {
|
|
93
|
+
result.flags.collapse = true;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
// {1,3-5} or {1,3-5}#id — line highlight (optionally grouped)
|
|
88
99
|
if (tok.startsWith('{') && tok.includes('}')) {
|
|
89
100
|
const closeIdx = tok.indexOf('}');
|
|
@@ -136,8 +147,9 @@ export function parseMeta(meta: string | undefined): ParsedMeta {
|
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
149
|
|
|
139
|
-
// ln{N} or showLineNumbers{N} — start line numbers at N
|
|
140
|
-
|
|
150
|
+
// ln{N} or showLineNumbers{N} or lineNumbers{N} — start line numbers at N
|
|
151
|
+
// (issue #5: also accept `linenumbers` as a third alternative, case-insensitive)
|
|
152
|
+
const startMatch = tok.match(/^(?:ln|showlinenumbers|linenumbers)\{(\d+)\}$/i);
|
|
141
153
|
if (startMatch) {
|
|
142
154
|
result.lineNumbersStart = parseInt(startMatch[1], 10);
|
|
143
155
|
result.flags.lineNumbers = true;
|
|
@@ -164,8 +176,8 @@ function tokenize(input: string): string[] {
|
|
|
164
176
|
while (i < input.length && /\s/.test(input[i])) i++;
|
|
165
177
|
if (i >= input.length) break;
|
|
166
178
|
|
|
167
|
-
// Quoted key="value" or key='value' (title=, caption=)
|
|
168
|
-
if (/^(?:title|caption)=$/.test(input.slice(i).match(/^[a-z]+=/i)?.[0] ?? '')) {
|
|
179
|
+
// Quoted key="value" or key='value' (title=, caption=, collapse=)
|
|
180
|
+
if (/^(?:title|caption|collapse)=$/.test(input.slice(i).match(/^[a-z]+=/i)?.[0] ?? '')) {
|
|
169
181
|
const eq = input.indexOf('=', i);
|
|
170
182
|
let j = eq + 1;
|
|
171
183
|
const quote = input[j];
|
|
@@ -225,8 +237,9 @@ function tokenize(input: string): string[] {
|
|
|
225
237
|
}
|
|
226
238
|
}
|
|
227
239
|
|
|
228
|
-
// ln{N} or showLineNumbers{N}
|
|
229
|
-
|
|
240
|
+
// ln{N} or showLineNumbers{N} or lineNumbers{N}
|
|
241
|
+
// (issue #5: also accept `linenumbers` as a prefix)
|
|
242
|
+
if (/^(?:ln|showlinenumbers|linenumbers)\{/i.test(input.slice(i))) {
|
|
230
243
|
const closeIdx = input.indexOf('}', i);
|
|
231
244
|
const stop = closeIdx === -1 ? input.length : closeIdx + 1;
|
|
232
245
|
tokens.push(input.slice(i, stop));
|
|
@@ -265,7 +278,16 @@ function findMatchingQuote(s: string, start: number): number {
|
|
|
265
278
|
|
|
266
279
|
function parseRanges(spec: string): number[] {
|
|
267
280
|
const out = new Set<number>();
|
|
268
|
-
|
|
281
|
+
// Issue #4: previously `3 - 5` was split into ['3', '-', '5'] by the
|
|
282
|
+
// `[\s,]+` separator and the regex `/^(\d+)(?:-(\d+))?$/` rejected the
|
|
283
|
+
// bare '-'. Result: `{3 - 5}` parsed as [3, 5] instead of [3, 4, 5].
|
|
284
|
+
//
|
|
285
|
+
// Fix: pre-normalize whitespace around `-` inside the spec so that
|
|
286
|
+
// `3 - 5` becomes `3-5` before splitting. The split still happens on
|
|
287
|
+
// `,` and remaining whitespace (between range tokens), so `{1, 3 - 5, 7}`
|
|
288
|
+
// → `1, 3-5, 7` → splits to ['1', '3-5', '7'] → [1, 3, 4, 5, 7]. ✓
|
|
289
|
+
const normalized = spec.replace(/\s*-\s*/g, '-');
|
|
290
|
+
for (const part of normalized.split(/[\s,]+/)) {
|
|
269
291
|
if (!part) continue;
|
|
270
292
|
const m = part.match(/^(\d+)(?:-(\d+))?$/);
|
|
271
293
|
if (!m) continue;
|
|
@@ -288,6 +310,28 @@ function unquote(s: string): string {
|
|
|
288
310
|
return s;
|
|
289
311
|
}
|
|
290
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Parse a collapse-range spec like "5-12,20-30" into structured ranges.
|
|
315
|
+
* Each part is either `N` (single line) or `N-M` (range, inclusive).
|
|
316
|
+
* Whitespace around commas and `-` is allowed.
|
|
317
|
+
*
|
|
318
|
+
* Example: "5-12, 20-30" → [{from:5,to:12}, {from:20,to:30}]
|
|
319
|
+
*/
|
|
320
|
+
function parseCollapseRanges(spec: string): { from: number; to: number }[] {
|
|
321
|
+
const out: { from: number; to: number }[] = [];
|
|
322
|
+
// Normalize whitespace around `-` so `5 - 12` parses correctly (mirrors parseRanges).
|
|
323
|
+
const normalized = spec.replace(/\s*-\s*/g, '-');
|
|
324
|
+
for (const part of normalized.split(/[\s,]+/)) {
|
|
325
|
+
if (!part) continue;
|
|
326
|
+
const m = part.match(/^(\d+)(?:-(\d+))?$/);
|
|
327
|
+
if (!m) continue;
|
|
328
|
+
const start = parseInt(m[1], 10);
|
|
329
|
+
const end = m[2] ? parseInt(m[2], 10) : start;
|
|
330
|
+
out.push({ from: Math.min(start, end), to: Math.max(start, end) });
|
|
331
|
+
}
|
|
332
|
+
return out.sort((a, b) => a.from - b.from);
|
|
333
|
+
}
|
|
334
|
+
|
|
291
335
|
function unescapeRegex(s: string): string {
|
|
292
336
|
return s.replace(/\\\//g, '/').replace(/\\\\/g, '\\');
|
|
293
337
|
}
|