@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/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
- /** Theme — string for single theme, { light, dark } for dual-theme via CSS vars. */
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
- * Add `role="region"` and `aria-label` to scrollable code blocks (WCAG 4.1.2).
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;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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.1.6",
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
+ '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '&': '&amp;',
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
- ...(userRehypePlugins as unknown[]),
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) ? options.theme : 'auto';
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
  }
@@ -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(code.innerText).then(finish).catch(fallback);
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 = code.innerText;
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
- const startMatch = tok.match(/^(?:ln|showlinenumbers)\{(\d+)\}$/i);
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
- if (/^(?:ln|showlinenumbers)\{/i.test(input.slice(i))) {
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
- for (const part of spec.split(/[\s,]+/)) {
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
  }