react-shiki 0.4.0 → 0.5.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/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  # 🎨 [react-shiki](https://npmjs.com/react-shiki)
2
2
 
3
3
  > [!NOTE]
4
- > This library is still in development, more features will
5
- > continue to be implemented, and API may change.
4
+ > This library is still in development. More features will be implemented, and the API may change.
6
5
  > Contributions are welcome!
7
6
 
8
- Performant client side syntax highlighting component + hook
9
- for react built with [Shiki](https://shiki.matsu.io/)
7
+ A performant client-side syntax highlighting component and hook for React, built with [Shiki](https://shiki.matsu.io/).
10
8
 
11
9
  [See the demo page with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/)
12
10
 
@@ -16,29 +14,29 @@ for react built with [Shiki](https://shiki.matsu.io/)
16
14
  - [Features](#features)
17
15
  - [Installation](#installation)
18
16
  - [Usage](#usage)
19
- - [`react-markdown`](#react-markdown)
20
- - [Check if code is inline](#check-if-code-is-inline)
21
- - [Custom themes](#custom-themes)
22
- - [Custom languages](#custom-languages)
23
- - [Preloading custom languages](#preloading-custom-languages)
24
- - [Custom transformers](#custom-transformers)
17
+ - [Basic Usage](#basic-usage)
18
+ - [Integration with react-markdown](#integration-with-react-markdown)
19
+ - [Handling Inline Code](#handling-inline-code)
20
+ - [Multi-theme Support](#multi-theme-support)
21
+ - [Custom Themes](#custom-themes)
22
+ - [Custom Languages](#custom-languages)
23
+ - [Preloading Custom Languages](#preloading-custom-languages)
24
+ - [Custom Transformers](#custom-transformers)
25
25
  - [Performance](#performance)
26
- - [Throttling real-time highlighting](#throttling-real-time-highlighting)
27
- - [Streaming and LLM chat UI](#streaming-and-llm-chat-ui)
28
- <!--toc:end-->
26
+ - [Throttling Real-time Highlighting](#throttling-real-time-highlighting)
27
+ - [Streaming and LLM Chat UI](#streaming-and-llm-chat-ui)
28
+ <!--toc:end-->
29
29
 
30
30
  ## Features
31
31
 
32
- - 🖼️ Provides a `ShikiHighlighter` component for highlighting code as children,
33
- as well as a `useShikiHighlighter` hook for more flexibility
34
- - 🔐 No `dangerouslySetInnerHTML`, output from Shiki is parsed using `html-react-parser`
32
+ - 🖼️ Provides both a `ShikiHighlighter` component and a `useShikiHighlighter` hook for more flexibility
33
+ - 🔐 No `dangerouslySetInnerHTML` - output from Shiki is parsed using `html-react-parser`
35
34
  - 📦 Supports all built-in Shiki languages and themes
36
35
  - 🖌️ Full support for custom TextMate themes and languages
37
36
  - 🔧 Supports passing custom Shiki transformers to the highlighter
38
37
  - 🚰 Performant highlighting of streamed code, with optional throttling
39
38
  - 📚 Includes minimal default styles for code blocks
40
- - 🚀 Shiki dynamically imports only the languages and themes used on a page,
41
- optimizing for performance
39
+ - 🚀 Shiki dynamically imports only the languages and themes used on a page for optimal performance
42
40
  - 🖥️ `ShikiHighlighter` component displays a language label for each code block
43
41
  when `showLanguage` is set to `true` (default)
44
42
  - 🎨 Customizable styling of generated code blocks and language labels
@@ -46,30 +44,20 @@ for react built with [Shiki](https://shiki.matsu.io/)
46
44
  ## Installation
47
45
 
48
46
  ```bash
49
- pnpm install react-shiki
47
+ npm install react-shiki
50
48
  ```
51
49
 
52
50
  ## Usage
53
51
 
54
- You can use the `ShikiHighlighter` component, or the `useShikiHighlighter` hook
55
- to highlight code.
52
+ ### Basic Usage
56
53
 
57
- `useShikiHighlighter` is a hook that takes in the code to be highlighted, the
58
- language, and the theme, and returns the highlighted code as React elements.
59
- It's useful for users who want full control over the rendering of highlighted
60
- code.
54
+ You can use either the `ShikiHighlighter` component or the `useShikiHighlighter` hook to highlight code.
61
55
 
62
- ```tsx
63
- const highlightedCode = useShikiHighlighter(code, language, theme, options);
64
- ```
65
-
66
- The `ShikiHighlighter` component is imported in your project, with the code to
67
- be highlighted passed as it's children.
68
-
69
- Shiki automatically handles dynamically importing only the languages and themes
70
- used on the page.
56
+ **Using the Component:**
71
57
 
72
58
  ```tsx
59
+ import { ShikiHighlighter } from 'react-shiki';
60
+
73
61
  function CodeBlock() {
74
62
  return (
75
63
  <ShikiHighlighter language="jsx" theme="ayu-dark">
@@ -79,111 +67,124 @@ function CodeBlock() {
79
67
  }
80
68
  ```
81
69
 
82
- The `ShikiHighlighter` component will follow a similar API to `react-syntax-highlighter`,
83
- but uses Shiki and is optimized for performant sequential highlighting. As of now,
84
- not all of `react-syntax-highlighter` functionality is supported, but the goal of
85
- this component is to eventually act as a drop in replacement for `react-syntax-highlighter`.
86
-
87
- The component accepts several props in addition to language and theme:
88
-
89
- - `showLanguage: boolean` - Shows the language name in the top right corner of
90
- the code block
91
- - `addDefaultStyles`: boolean - Adds default styles (padding, overflow handling,
92
- and border-radius) to the code block
93
- - `as: string` - The component to be rendered. Defaults to 'pre'
94
- - `delay: number` - Delay between highlights in milliseconds, useful for throttling
95
- rapid highlighting on the client
96
- - `className: string` - Class names to be passed to the component
97
- - `style: object` - Inline style object to be passed to the component
98
- - `langStyle: object` - Inline style object to be passed to the language label
99
- - `langClassName: string` - Class names to be passed to the language label
100
- - `customLanguages: LanguageRegistration[]` - Custom languages to be preloaded for highlighting
70
+ The `ShikiHighlighter` component accepts the following props:
71
+
72
+ | Prop | Type | Default | Description |
73
+ |------|------|---------|-------------|
74
+ | `language` | `string` | - | Language of the code to highlight |
75
+ | `theme` | `string \| object` | 'github-dark' | Shiki theme to use |
76
+ | `showLanguage` | `boolean` | true | Shows the language name in the top right corner |
77
+ | `addDefaultStyles` | `boolean` | true | Adds default styling to the code block |
78
+ | `as` | `string` | 'pre' | Root element to render |
79
+ | `delay` | `number` | 0 | Delay between highlights in milliseconds |
80
+ | `customLanguages` | `array` | - | Custom languages to preload |
81
+ | `transformers` | `array` | - | Custom Shiki transformers |
82
+ | `className` | `string` | - | Custom class names for the component |
83
+ | `langClassName` | `string` | - | Class names for the language label |
84
+ | `style` | `object` | - | Inline style object for the component |
85
+ | `langStyle` | `object` | - | Inline style object for the language label |
86
+
87
+
88
+ **Using the Hook:**
101
89
 
102
90
  ```tsx
103
- function Houston() {
104
- return (
105
- <ShikiHighlighter
106
- language="jsx"
107
- className="code-block"
108
- theme="houston"
109
- showLanguage={false}
110
- addDefaultStyles={true}
111
- as="div"
112
- style={{
113
- textAlign: "left",
114
- }}
115
- >
116
- {code.trim()}
117
- </ShikiHighlighter>
118
- );
91
+ import { useShikiHighlighter } from 'react-shiki';
92
+
93
+ function CustomCodeBlock({ code, language }) {
94
+ const highlightedCode = useShikiHighlighter(code, language, 'github-dark');
95
+
96
+ return <div className="custom-code-block">{highlightedCode}</div>;
119
97
  }
120
98
  ```
121
99
 
122
- ### `react-markdown`
100
+ **The hook accepts the following parameters:**
123
101
 
124
- ```tsx
125
- import type { ReactNode } from "react";
126
- import ShikiHighlighter, { type Element } from "react-shiki";
102
+ | Param | Type | Description |
103
+ |------|------|---------|
104
+ | `code` | `string` | The code to be highlighted |
105
+ | `language` | `string \| object` | The language for highlighting |
106
+ | `themeInput` | `string \| object` | The theme or themes to be used for highlighting |
107
+ | `options` | `object` | Optional configuration options |
127
108
 
128
- interface CodeHighlightProps {
129
- className?: string | undefined;
130
- children?: ReactNode | undefined;
131
- node?: Element | undefined;
132
- }
109
+ **`options`:**
110
+ | Param | Type | Default | Description |
111
+ |------|------|---------|-------------|
112
+ | `delay` | `number` | `0` (disabled) | The delay between highlights in milliseconds |
113
+ | `transformers` | `array` | `[]` | Transformers for the Shiki pipeline |
114
+ | `customLanguages` | `array` | `[]` | Custom languages to preload |
115
+ | `cssVariablePrefix` | `string` | `'--shiki'` | Prefix of CSS variables used to store theme colors |
116
+ | `defaultColor` | `string` | `'light'` | The default theme mode when using multiple themes. Can be set to `false` to disable the default theme |
133
117
 
134
- export const CodeHighlight = ({
135
- className,
136
- children,
137
- node,
138
- ...props
139
- }: CodeHighlightProps): JSX.Element => {
118
+ ### Integration with react-markdown
119
+
120
+ Create a component to handle syntax highlighting:
121
+ ```tsx
122
+ import ReactMarkdown from "react-markdown";
123
+ import { ShikiHighlighter, isInlineCode } from "react-shiki";
124
+
125
+ const CodeHighlight = ({ className, children, node, ...props }) => {
126
+ const code = String(children).trim();
140
127
  const match = className?.match(/language-(\w+)/);
141
128
  const language = match ? match[1] : undefined;
129
+ const isInline = node ? isInlineCode(node) : undefined;
142
130
 
143
- <ShikiHighlighter language={language} theme={"houston"} {...props}>
144
- {String(children).trim()}
145
- </ShikiHighlighter>;
131
+ return !isInline ? (
132
+ <ShikiHighlighter language={language} theme="github-dark" {...props}>
133
+ {code}
134
+ </ShikiHighlighter>
135
+ ) : (
136
+ <code className={className} {...props}>
137
+ {code}
138
+ </code>
139
+ );
146
140
  };
147
141
  ```
148
142
 
149
- Pass `CodeHighlight` to `react-markdown` as a code component:
150
-
143
+ Pass the component to react-markdown as a code component:
151
144
  ```tsx
152
- import ReactMarkdown from "react-markdown";
153
- import { CodeHighlight } from "./CodeHighlight";
154
-
155
145
  <ReactMarkdown
156
146
  components={{
157
147
  code: CodeHighlight,
158
148
  }}
159
149
  >
160
150
  {markdown}
161
- </ReactMarkdown>;
151
+ </ReactMarkdown>
162
152
  ```
163
153
 
164
- ### Check if code is inline
154
+ ### Handling Inline Code
155
+
156
+ **Method 1: Using the `isInlineCode` helper:**
165
157
 
166
- There are two built-in ways to check if a code block is inline, both provide the same result:
167
- `react-shiki` exports `isInlineCode` which parses the `node`
168
- prop to determine if the code is inline based on the presence of line breaks:
158
+ There are two ways to check if a code block is inline, both provide the same result:
159
+ `react-shiki` exports `isInlineCode` which parses the `node`
160
+ prop (from `react-markdown`) to determine if the code is inline:
169
161
 
170
162
  ```tsx
171
- const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
172
-
173
- return !isInline ? (
174
- <ShikiHighlighter language={language} theme={"houston"} {...props}>
175
- {String(children).trim()}
176
- </ShikiHighlighter>
177
- ) : (
178
- <code className={className} {...props}>
179
- {children}
180
- </code>
181
- );
163
+ import { isInlineCode, ShikiHighlighter } from "react-shiki";
164
+
165
+ const CodeHighlight = ({ className, children, node, ...props }) => {
166
+ const match = className?.match(/language-(\w+)/);
167
+ const language = match ? match[1] : undefined;
168
+ const isInline = node ? isInlineCode(node) : undefined;
169
+
170
+ return !isInline ? (
171
+ <ShikiHighlighter language={language} theme="github-dark" {...props}>
172
+ {String(children).trim()}
173
+ </ShikiHighlighter>
174
+ ) : (
175
+ <code className={className} {...props}>
176
+ {children}
177
+ </code>
178
+ );
179
+ };
182
180
  ```
183
181
 
182
+ **Method 2: Using the rehype plugin:**
183
+
184
184
  `react-shiki` also exports `rehypeInlineCodeProperty`, a rehype plugin that adds
185
- an `inline` property to `react-markdown` to determine if code is inline based on
185
+ an `inline` property to `react-markdown` to determine if code is inline based on
186
186
  the presence of a `<pre>` tag as a parent of `<code>`.
187
+
187
188
  It's passed as a rehype plugin to `react-markdown`:
188
189
 
189
190
  ```tsx
@@ -214,99 +215,145 @@ const CodeHighlight = ({
214
215
  const language = match ? match[1] : undefined;
215
216
  const code = String(children).trim();
216
217
 
218
+ return !inline ? (
219
+ <ShikiHighlighter language={language} theme="github-dark" {...props}>
220
+ {code}
221
+ </ShikiHighlighter>
222
+ ) : (
223
+ <code className={className} {...props}>
224
+ {code}
225
+ </code>
226
+ );
227
+ };
228
+ ```
229
+
230
+ ### Multi-theme Support
231
+
232
+ To use multiple theme modes, pass an object with your multi-theme configuration to the `theme` prop in the `ShikiHighlighter` component:
233
+
234
+ ```tsx
235
+ <ShikiHighlighter
236
+ language="tsx"
237
+ theme={{
238
+ light: "github-light",
239
+ dark: "github-dark",
240
+ dim: "github-dark-dimmed",
241
+ }}
242
+ defaultColor="dark"
243
+ >
244
+ {code.trim()}
245
+ </ShikiHighlighter>
246
+ ```
217
247
 
218
- return !inline ? (
219
- <ShikiHighlighter language={language} theme={"houston"} {...props}>
220
- {code}
221
- </ShikiHighlighter>
222
- ) : (
223
- <code className={className} {...props}>
224
- {children}
225
- </code>
248
+ Or, when using the hook, pass it to the `theme` parameter:
249
+
250
+ ```tsx
251
+ const highlightedCode = useShikiHighlighter(
252
+ code,
253
+ "tsx",
254
+ {
255
+ light: "github-light",
256
+ dark: "github-dark",
257
+ dim: "github-dark-dimmed"
258
+ },
259
+ {
260
+ defaultColor: "dark",
261
+ }
226
262
  );
227
263
  ```
228
264
 
229
- ### Custom themes
265
+ See [shiki's documentation](https://shiki.matsu.io/docs/themes) for more information on dual and multi theme support, and for the CSS needed to make the themes reactive to your site's theme.
230
266
 
231
- Pass custom TextMate themes as a JSON object:
267
+ ### Custom Themes
268
+
269
+ Custom themes can be passed as a TextMate theme in JavaScript object. For example, [it should look like this](https://github.com/antfu/textmate-grammars-themes/blob/main/packages/tm-themes/themes/dark-plus.json).
232
270
 
233
271
  ```tsx
234
- import tokyoNight from '../styles/tokyo-night.json';
272
+ import tokyoNight from "../styles/tokyo-night.json";
235
273
 
236
- // component
274
+ // Using the component
237
275
  <ShikiHighlighter language="tsx" theme={tokyoNight}>
238
- {String(code).trim()}
239
- </ShikiHighlighter>;
276
+ {code.trim()}
277
+ </ShikiHighlighter>
240
278
 
241
- // hook
279
+ // Using the hook
242
280
  const highlightedCode = useShikiHighlighter(code, "tsx", tokyoNight);
243
281
  ```
244
282
 
245
- ### Custom languages
283
+ ### Custom Languages
246
284
 
247
- Pass custom TextMate languages as a JSON object:
285
+ Custom languages should be passed as a TextMate grammar in JavaScript object. For example, [it should look like this](https://github.com/shikijs/textmate-grammars-themes/blob/main/packages/tm-grammars/grammars/typescript.json)
248
286
 
249
287
  ```tsx
250
- import mcfunction from "../langs/mcfunction.tmLanguage.json"
288
+ import mcfunction from "../langs/mcfunction.tmLanguage.json";
251
289
 
252
- // component
253
- <ShikiHighlighter language={mcfunction} theme="github-dark" >
254
- {String(code).trim()}
255
- </ShikiHighlighter>;
290
+ // Using the component
291
+ <ShikiHighlighter language={mcfunction} theme="github-dark">
292
+ {code.trim()}
293
+ </ShikiHighlighter>
256
294
 
257
- // hook
295
+ // Using the hook
258
296
  const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark");
259
297
  ```
260
- #### Preloading custom languages
261
298
 
262
- For dynamic highlighting scenarios (like LLM chat apps) where language selection happens at runtime, preload custom languages to make them available when needed:
299
+ #### Preloading Custom Languages
300
+
301
+ For dynamic highlighting scenarios where language selection happens at runtime:
263
302
 
264
303
  ```tsx
265
- import mcfunction from "../langs/mcfunction.tmLanguage.json"
266
- import bosque from "../langs/bosque.tmLanguage.json"
304
+ import mcfunction from "../langs/mcfunction.tmLanguage.json";
305
+ import bosque from "../langs/bosque.tmLanguage.json";
267
306
 
268
- // component
269
- <ShikiHighlighter language={mcfunction} theme="github-dark" customLanguages={[mcfunction, bosque]} >
270
- {String(code).trim()}
271
- </ShikiHighlighter>;
307
+ // With the component
308
+ <ShikiHighlighter
309
+ language="typescript"
310
+ theme="github-dark"
311
+ customLanguages={[mcfunction, bosque]}
312
+ >
313
+ {code.trim()}
314
+ </ShikiHighlighter>
272
315
 
273
- // hook
274
- const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark", { customLanguages: [mcfunction, bosque] });
316
+ // With the hook
317
+ const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
318
+ customLanguages: [mcfunction, bosque],
319
+ });
275
320
  ```
276
321
 
277
- ### Custom transformers
322
+ ### Custom Transformers
278
323
 
279
324
  ```tsx
280
- import { customTransformer } from '../utils/shikiTransformers';
325
+ import { customTransformer } from "../utils/shikiTransformers";
281
326
 
282
- // component
283
- <ShikiHighlighter
284
- language="tsx"
285
- transformers={[customTransformer]}
286
- >
287
- {String(code).trim()}
288
- </ShikiHighlighter>;
327
+ // Using the component
328
+ <ShikiHighlighter language="tsx" transformers={[customTransformer]}>
329
+ {code.trim()}
330
+ </ShikiHighlighter>
289
331
 
290
- // hook
291
- const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", [customTransformer]);
332
+ // Using the hook
333
+ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
334
+ transformers: [customTransformer],
335
+ });
292
336
  ```
293
337
 
294
338
  ## Performance
295
339
 
296
- `react-shiki` supports performance-optimized highlighting on the client.
297
-
298
- ### Throttling real-time highlighting
340
+ ### Throttling Real-time Highlighting
299
341
 
300
- Throttling real-time highlighting on the client is possible with the
301
- `delay` option.
342
+ For improved performance when highlighting frequently changing code:
302
343
 
303
344
  ```tsx
304
- const highlightedCode = useShikiHighlighter(code, language, theme, {
345
+ // With the component
346
+ <ShikiHighlighter language="tsx" theme="github-dark" delay={150}>
347
+ {code.trim()}
348
+ </ShikiHighlighter>
349
+
350
+ // With the hook
351
+ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
305
352
  delay: 150,
306
353
  });
307
354
  ```
308
355
 
309
- ### Streaming and LLM chat UI
356
+ ### Streaming and LLM Chat UI
310
357
 
311
358
  `react-shiki` can be used to highlight streamed code from LLM responses in real-time.
312
359
 
@@ -337,7 +384,7 @@ export const CodeHighlight = ({
337
384
 
338
385
  const isInline = node ? isInlineCode(node) : false;
339
386
 
340
- const highlightedCode = useShikiHighlighter(language, code, tokyoNight, {
387
+ const highlightedCode = useShikiHighlighter(code, language, tokyoNight, {
341
388
  delay: 150,
342
389
  });
343
390
 
@@ -384,22 +431,21 @@ export const CodeHighlight = ({
384
431
  }: CodeHighlightProps): JSX.Element => {
385
432
  const match = className?.match(/language-(\w+)/);
386
433
  const language = match ? match[1] : undefined;
434
+ const code = String(children).trim();
387
435
 
388
436
  const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
389
437
 
390
438
  return !isInline ? (
391
439
  <ShikiHighlighter
392
440
  language={language}
393
- theme={"houston"}
441
+ theme="github-dark"
394
442
  delay={150}
395
443
  {...props}
396
444
  >
397
- {String(children).trim()}
445
+ {code}
398
446
  </ShikiHighlighter>
399
447
  ) : (
400
- <code className={className} {...props}>
401
- {children}
402
- </code>
448
+ <code className={className}>{code}</code>
403
449
  );
404
450
  };
405
451
  ```
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { LanguageRegistration as LanguageRegistration$1, ShikiTransformer, BundledLanguage, SpecialLanguage, ThemeRegistration, BundledTheme } from 'shiki';
3
- import { Element, Root } from 'hast';
2
+ import { LanguageRegistration as LanguageRegistration$1, CodeOptionsMultipleThemes, BundledTheme, CodeToHastOptionsCommon, BundledLanguage, SpecialLanguage, ThemeRegistrationAny, StringLiteralUnion } from 'shiki';
3
+ import { Element } from 'hast';
4
4
  export { Element } from 'hast';
5
5
 
6
6
  /**
@@ -40,33 +40,49 @@ interface LanguageRegistration extends Omit<LanguageRegistration$1, 'repository'
40
40
  }
41
41
 
42
42
  /**
43
- * Languages for syntax highlighting.
43
+ * A Shiki BundledLanguage or a custom textmate grammar object
44
44
  * @see https://shiki.style/languages
45
45
  */
46
46
  type Language = BundledLanguage | LanguageRegistration | SpecialLanguage | (string & {}) | undefined;
47
47
  /**
48
- * A textmate theme object or a Shiki BundledTheme
48
+ * A Shiki BundledTheme or a custom textmate theme object
49
49
  * @see https://shiki.style/themes
50
50
  */
51
- type Theme = ThemeRegistration | BundledTheme;
51
+ type Theme = ThemeRegistrationAny | StringLiteralUnion<BundledTheme>;
52
+ /**
53
+ * A map of color names to themes.
54
+ * This allows you to specify multiple themes for the generated code.
55
+ * Supports custom textmate theme objects in addition to Shiki's bundled themes
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * useShikiHighlighter(code, language, {
60
+ * light: 'github-light',
61
+ * dark: 'github-dark',
62
+ * dim: 'github-dark-dimmed'
63
+ * })
64
+ * ```
65
+ *
66
+ * @see https://shiki.style/guide/dual-themes
67
+ */
68
+ type Themes = {
69
+ [key: string]: ThemeRegistrationAny | StringLiteralUnion<BundledTheme>;
70
+ };
52
71
  /**
53
72
  * Configuration options for the syntax highlighter
54
73
  */
55
- type HighlighterOptions = {
74
+ type ReactShikiOptions = {
56
75
  /**
57
76
  * Minimum time (in milliseconds) between highlight operations.
58
77
  * @default undefined (no throttling)
59
78
  */
60
79
  delay?: number;
61
80
  /**
62
- * Custom Shiki transformers to apply to the highlighted code.
63
- */
64
- transformers?: ShikiTransformer[];
65
- /**
66
- * Custom textmate grammar to be preloaded for highlighting.
81
+ * Custom textmate grammars to be preloaded for highlighting.
67
82
  */
68
83
  customLanguages?: LanguageRegistration | LanguageRegistration[];
69
84
  };
85
+ type HighlighterOptions = ReactShikiOptions & Pick<CodeOptionsMultipleThemes<BundledTheme>, 'defaultColor' | 'cssVariablePrefix'> & Pick<CodeToHastOptionsCommon, 'transformers'>;
70
86
 
71
87
  /**
72
88
  * Props for the ShikiHighlighter component
@@ -74,6 +90,7 @@ type HighlighterOptions = {
74
90
  interface ShikiHighlighterProps extends HighlighterOptions {
75
91
  /**
76
92
  * The programming language for syntax highlighting
93
+ * Supports custom textmate grammar objects in addition to Shiki's bundled languages
77
94
  * @see https://shiki.style/languages
78
95
  */
79
96
  language: Language;
@@ -82,21 +99,17 @@ interface ShikiHighlighterProps extends HighlighterOptions {
82
99
  */
83
100
  children: string;
84
101
  /**
85
- * The color theme for syntax highlighting
86
- * Supports Shiki and custom textmate themes
102
+ * The color theme or themes for syntax highlighting
103
+ * Supports single, dual, or multiple themes
104
+ * Supports custom textmate theme objects in addition to Shiki's bundled themes
105
+ *
106
+ * @example
107
+ * theme='github-dark' // single theme
108
+ * theme={{ light: 'github-light', dark: 'github-dark' }} // multi-theme
109
+ *
87
110
  * @see https://shiki.style/themes
88
111
  */
89
- theme: Theme;
90
- /**
91
- * The delay in milliseconds between streamed updates
92
- * Use this when highlighting code being streamed on the client
93
- * @default undefined (no delay)
94
- */
95
- delay?: number;
96
- /**
97
- * Pass custom Shiki transformers to the highlighter
98
- */
99
- transformers?: ShikiTransformer[];
112
+ theme: Theme | Themes;
100
113
  /**
101
114
  * Controls the application of default styles to the generated code blocks
102
115
  *
@@ -130,10 +143,6 @@ interface ShikiHighlighterProps extends HighlighterOptions {
130
143
  * @default 'pre'
131
144
  */
132
145
  as?: React.ElementType;
133
- /**
134
- * Custom languages to be preloaded for highlighting.
135
- */
136
- customLanguages?: LanguageRegistration[];
137
146
  }
138
147
  /**
139
148
  * ShikiHighlighter is a React component that provides syntax highlighting for code snippets.
@@ -144,26 +153,59 @@ interface ShikiHighlighterProps extends HighlighterOptions {
144
153
  * <ShikiHighlighter
145
154
  * language="typescript"
146
155
  * theme="github-dark"
147
- * delay={100} // Optional throttling for streamed updates
156
+ * delay={100}
157
+ * transformers={[customTransformer]}
158
+ * addDefaultStyles={false}
159
+ * className="code-block"
160
+ * langClassName="lang-label"
161
+ * langStyle={{ color: 'blue' }},
162
+ * style={{ fontSize: '1.2rem' }}
148
163
  * >
149
164
  * {code}
150
165
  * </ShikiHighlighter>
151
166
  * ```
152
167
  */
153
- declare const ShikiHighlighter: ({ language, theme, delay, transformers, addDefaultStyles, style, langStyle, className, langClassName, showLanguage, children: code, as: Element, customLanguages, }: ShikiHighlighterProps) => React.ReactElement;
168
+ declare const ShikiHighlighter: ({ language, theme, delay, transformers, defaultColor, cssVariablePrefix, addDefaultStyles, style, langStyle, className, langClassName, showLanguage, children: code, as: Element, customLanguages, }: ShikiHighlighterProps) => React.ReactElement;
154
169
 
155
170
  /**
156
171
  * A React hook that provides syntax highlighting using Shiki.
157
- * Supports optional throttled highlights and custom themes.
172
+ * Supports single theme and multi-theme highlighting, custom themes
173
+ * and languages, custom transformers, and optional throttling.
158
174
  *
159
- * @example
175
+ * ```ts
176
+ * // Basic Usage
177
+ * const highlightedCode = useShikiHighlighter( code, 'typescript', 'github-dark');
178
+ * ```
179
+ *
180
+ * ```ts
181
+ * // Custom Languages, Transformers, and Delay
160
182
  * const highlightedCode = useShikiHighlighter(code, language, theme, {
161
- * transformers: [removeTabIndexFromPre],
183
+ * transformers: [customTransformer],
162
184
  * delay: 150
163
185
  * customLanguages: ['bosque', 'mcfunction']
164
186
  * });
187
+ * ```
188
+ *
189
+ * ```ts
190
+ * // Multiple Themes, Custom Languages, Delay, and Custom Transformers
191
+ * const highlightedCode = useShikiHighlighter(
192
+ * code,
193
+ * language,
194
+ * {
195
+ * light: 'github-light',
196
+ * dark: 'github-dark',
197
+ * dim: 'github-dark-dimmed'
198
+ * },
199
+ * {
200
+ * delay: 150,
201
+ * defaultColor: 'dim',
202
+ * transformers: [customTransformer],
203
+ * customLanguages: ['bosque', 'mcfunction']
204
+ * }
205
+ * );
206
+ * ```
165
207
  */
166
- declare const useShikiHighlighter: (code: string, lang: Language, theme: Theme, options?: HighlighterOptions) => ReactNode;
208
+ declare const useShikiHighlighter: (code: string, lang: Language, themeInput: Theme | Themes, options?: HighlighterOptions) => ReactNode;
167
209
 
168
210
  /**
169
211
  * Rehype plugin to add an 'inline' property to <code> elements
@@ -175,7 +217,7 @@ declare const useShikiHighlighter: (code: string, lang: Language, theme: Theme,
175
217
  * @example
176
218
  * <ReactMarkdown rehypePlugins={[rehypeInlineCodeProperty]} />
177
219
  */
178
- declare function rehypeInlineCodeProperty(): (tree: Root) => undefined;
220
+ declare function rehypeInlineCodeProperty(): (tree: any) => undefined;
179
221
  /**
180
222
  * Function to determine if code is inline based on the presence of line breaks
181
223
  *
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- function L(e,{insertAt:r}={}){if(!e||typeof document>"u")return;let i=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style");t.type="text/css",r==="top"&&i.firstChild?i.insertBefore(t,i.firstChild):i.appendChild(t),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document.createTextNode(e))}L(`.relative{position:relative}.defaultStyles pre{overflow:auto;border-radius:.5rem;padding:1.25rem 1.5rem}.languageLabel{position:absolute;right:.75rem;top:.5rem;font-family:monospace;font-size:.75rem;letter-spacing:-.05em;color:#6b7280d9}
2
- `);import R from"react";import{clsx as b}from"clsx";import{useEffect as O,useRef as j,useState as M}from"react";import z from"html-react-parser";import{createHighlighter as H,createSingletonShorthands as $}from"shiki";import{visit as w}from"unist-util-visit";import{bundledLanguages as E,isSpecialLang as N}from"shiki";function P(){return function(e){w(e,"element",function(r,i,t){r.tagName==="code"&&t.tagName!=="pre"&&(r.properties.inline=!0)})}}var A=e=>!(e.children||[]).filter(i=>i.type==="text").map(i=>i.value).join("").includes(`
3
- `),S={pre(e){return"properties"in e&&(e.properties.tabindex="-1"),e}},T=(e,r,i)=>{let t=Date.now();clearTimeout(r.current.timeoutId);let o=Math.max(0,r.current.nextAllowedTime-t);r.current.timeoutId=setTimeout(()=>{e().catch(console.error),r.current.nextAllowedTime=t+i},o)},u=(e,r=[])=>{if(e&&typeof e=="object")return{isCustom:!0,languageId:e.name,displayLanguageId:e.name,resolvedLanguage:e};if(typeof e=="string"){let i=e;if(e in E||N(e))return{isCustom:!1,languageId:e,displayLanguageId:i};let t=r.find(o=>o.fileTypes?.includes(e)||o.scopeName?.split(".")[1]===e||o.name?.toLowerCase()===e.toLowerCase());return t?{isCustom:!0,languageId:t.name,displayLanguageId:i,resolvedLanguage:t}:{isCustom:!1,languageId:"plaintext",displayLanguageId:i}}return{isCustom:!1,languageId:"plaintext",displayLanguageId:"plaintext"}};var B=$(H),I=new Map,x=(e,r,i,t={})=>{let[o,p]=M(null),d=typeof i=="string"?i:i.name,h=t.customLanguages?Array.isArray(t.customLanguages)?t.customLanguages:[t.customLanguages]:[],{isCustom:c,languageId:f,resolvedLanguage:g}=u(r,h),m=j({nextAllowedTime:0,timeoutId:void 0}),n=async(s,l)=>{let a=I.get(s);return a||(a=H({langs:[l],themes:[i]}),I.set(s,a)),a};return O(()=>{let s=!0,l=[S,...t.transformers||[]],a=async()=>{let y=await(c&&g?await n(`${g.name}--${d}`,g):B).codeToHtml(e,{lang:f,theme:i,transformers:l});s&&p(z(y))};return t.delay?T(a,m,t.delay):a().catch(console.error),()=>{s=!1,clearTimeout(m.current.timeoutId)}},[e,r]),o};var D=({language:e,theme:r,delay:i,transformers:t,addDefaultStyles:o=!0,style:p,langStyle:d,className:h,langClassName:c,showLanguage:f=!0,children:g,as:m="pre",customLanguages:n})=>{let s={delay:i,transformers:t,customLanguages:n},l=n?Array.isArray(n)?n:[n]:[],{isCustom:a,languageId:C,displayLanguageId:y,resolvedLanguage:v}=u(e,l),k=x(g,e,r,s);return R.createElement(m,{"data-testid":"shiki-container",className:b("relative","not-prose",o&&"defaultStyles",h),style:p,id:"shiki-container"},f&&e?R.createElement("span",{className:b("languageLabel",c),style:d,id:"language-label"},a?`${v?.scopeName.split(".")[1]}`:y||C):null,k)};export{D as default,A as isInlineCode,P as rehypeInlineCodeProperty,x as useShikiHighlighter};
1
+ function v(e,{insertAt:o}={}){if(!e||typeof document>"u")return;let i=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style");t.type="text/css",o==="top"&&i.firstChild?i.insertBefore(t,i.firstChild):i.appendChild(t),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document.createTextNode(e))}v(`.relative{position:relative}.defaultStyles pre{overflow:auto;border-radius:.5rem;padding:1.25rem 1.5rem}.languageLabel{position:absolute;right:.75rem;top:.5rem;font-family:monospace;font-size:.75rem;letter-spacing:-.05em;color:#6b7280d9}
2
+ `);import A from"react";import{clsx as N}from"clsx";import{useEffect as V,useMemo as O,useRef as _,useState as U}from"react";import q from"html-react-parser";import{createHighlighter as w,createSingletonShorthands as G}from"shiki";import{visit as $}from"unist-util-visit";import{bundledLanguages as z,isSpecialLang as D}from"shiki";function F(){return e=>{$(e,"element",(o,i,t)=>{o.tagName==="code"&&t.tagName!=="pre"&&(o.properties.inline=!0)})}}var B=e=>!(e.children||[]).filter(i=>i.type==="text").map(i=>i.value).join("").includes(`
3
+ `),k={pre(e){return"properties"in e&&(e.properties.tabindex="-1"),e}},E=(e,o,i)=>{let t=Date.now();clearTimeout(o.current.timeoutId);let r=Math.max(0,o.current.nextAllowedTime-t);o.current.timeoutId=setTimeout(()=>{e().catch(console.error),o.current.nextAllowedTime=t+i},r)},f=(e,o=[])=>{if(e&&typeof e=="object")return{isCustom:!0,languageId:e.name,displayLanguageId:e.name,resolvedLanguage:e};if(typeof e=="string"){let i=e;if(e in z||D(e))return{isCustom:!1,languageId:e,displayLanguageId:i};let t=o.find(r=>r.fileTypes?.includes(e)||r.scopeName?.split(".")[1]===e||r.name?.toLowerCase()===e.toLowerCase());return t?{isCustom:!0,languageId:t.name,displayLanguageId:i,resolvedLanguage:t}:{isCustom:!1,languageId:"plaintext",displayLanguageId:i}}return{isCustom:!1,languageId:"plaintext",displayLanguageId:"plaintext"}};function I(e){let o=typeof e=="object"&&"tokenColors"in e&&Array.isArray(e.tokenColors),i=typeof e=="object"&&e!==null&&!o,t=typeof e=="object"&&e!==null&&!o&&Object.entries(e).some(([r,s])=>r&&s&&r.trim()!==""&&s!==""&&(typeof s=="string"||o));return i?{isMultiTheme:!0,themeId:t?`multi-${Object.values(e).map(s=>(typeof s=="string"?s:s?.name)||"custom").sort().join("-")}`:"multi-default",multiTheme:t?e:null,themesToLoad:t?Object.values(e):[]}:{isMultiTheme:!1,themeId:typeof e=="string"?e:e?.name||"custom",singleTheme:e,themesToLoad:[e]}}var M={light:"github-light",dark:"github-dark"},J=G(w),j=new Map,K=async(e,o,i)=>{let t=j.get(e);return t||(t=w({langs:[o],themes:i}),j.set(e,t)),t},R=(e,o,i,t={})=>{let[r,s]=U(null),g=t.customLanguages?Array.isArray(t.customLanguages)?t.customLanguages:[t.customLanguages]:[],u=g.map(n=>n.name||"").sort().join("-"),h=O(()=>[k,...t.transformers||[]],[t.transformers]),{isMultiTheme:y,themeId:d,multiTheme:T,singleTheme:L,themesToLoad:C}=O(()=>I(i),[i]),{isCustom:a,languageId:m,resolvedLanguage:c}=O(()=>f(o,g),[o,u]),x=`${m}-${d}`,p=_({nextAllowedTime:0,timeoutId:void 0}),S=()=>{let{defaultColor:n,cssVariablePrefix:l}=t,b={lang:m,transformers:h},H=y?{themes:T||M,defaultColor:n,cssVariablePrefix:l}:{theme:L||M.dark};return{...b,...H}};return V(()=>{let n=!0,l=async()=>{let b=a&&c?await K(x,c,C):J,H=S(),P=await b.codeToHtml(e,H);n&&s(q(P))};return t.delay?E(l,p,t.delay):l().catch(console.error),()=>{n=!1,clearTimeout(p.current.timeoutId)}},[e,m,d,u,h,t.delay,t.defaultColor,t.cssVariablePrefix]),r};var Q=({language:e,theme:o,delay:i,transformers:t,defaultColor:r,cssVariablePrefix:s,addDefaultStyles:g=!0,style:u,langStyle:h,className:y,langClassName:d,showLanguage:T=!0,children:L,as:C="pre",customLanguages:a})=>{let m={delay:i,transformers:t,customLanguages:a,defaultColor:r,cssVariablePrefix:s},c=a?Array.isArray(a)?a:[a]:[],{isCustom:x,languageId:p,displayLanguageId:S,resolvedLanguage:n}=f(e,c),l=R(L,e,o,m);return A.createElement(C,{"data-testid":"shiki-container",className:N("relative","not-prose",g&&"defaultStyles",y),style:u,id:"shiki-container"},T&&e?A.createElement("span",{className:N("languageLabel",d),style:h,id:"language-label"},x?`${n?.scopeName.split(".")[1]}`:S||p):null,l)};export{Q as default,B as isInlineCode,F as rehypeInlineCodeProperty,R as useShikiHighlighter};
4
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["#style-inject:#style-inject","../src/styles.css","../src/ShikiHighlighter.tsx","../src/useShiki.ts","../src/utils.ts"],"sourcesContent":["\n export default function styleInject(css, { insertAt } = {}) {\n if (!css || typeof document === 'undefined') return\n \n const head = document.head || document.getElementsByTagName('head')[0]\n const style = document.createElement('style')\n style.type = 'text/css'\n \n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n \n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n }\n ","import styleInject from '#style-inject';styleInject(\".relative{position:relative}.defaultStyles pre{overflow:auto;border-radius:.5rem;padding:1.25rem 1.5rem}.languageLabel{position:absolute;right:.75rem;top:.5rem;font-family:monospace;font-size:.75rem;letter-spacing:-.05em;color:#6b7280d9}\\n\")","import './styles.css';\nimport React from 'react';\nimport { clsx } from 'clsx';\nimport { useShikiHighlighter } from './useShiki';\nimport type { Language, Theme, HighlighterOptions } from './types';\nimport type { ShikiTransformer } from 'shiki';\nimport { resolveLanguage } from './utils';\nimport type { LanguageRegistration } from './customTypes';\n\n/**\n * Props for the ShikiHighlighter component\n */\nexport interface ShikiHighlighterProps extends HighlighterOptions {\n /**\n * The programming language for syntax highlighting\n * @see https://shiki.style/languages\n */\n language: Language;\n\n /**\n * The code to be highlighted\n */\n children: string;\n\n /**\n * The color theme for syntax highlighting\n * Supports Shiki and custom textmate themes\n * @see https://shiki.style/themes\n */\n theme: Theme;\n\n /**\n * The delay in milliseconds between streamed updates\n * Use this when highlighting code being streamed on the client\n * @default undefined (no delay)\n */\n delay?: number;\n\n /**\n * Pass custom Shiki transformers to the highlighter\n */\n transformers?: ShikiTransformer[];\n\n /**\n * Controls the application of default styles to the generated code blocks\n *\n * Default styles include padding, overflow handling, border radius, language label styling, and font settings\n * @default true\n */\n addDefaultStyles?: boolean;\n\n /**\n * Add custom inline styles to the generated code block\n */\n style?: React.CSSProperties;\n\n /**\n * Add custom inline styles to the language label\n */\n langStyle?: React.CSSProperties;\n\n /**\n * Add custom CSS class names to the generated code block\n */\n className?: string;\n\n /**\n * Add custom CSS class names to the language label\n */\n langClassName?: string;\n\n /**\n * Whether to show the language label\n * @default true\n */\n showLanguage?: boolean;\n\n /**\n * The HTML element that wraps the generated code block.\n * @default 'pre'\n */\n as?: React.ElementType;\n\n /**\n * Custom languages to be preloaded for highlighting.\n */\n customLanguages?: LanguageRegistration[];\n}\n\n/**\n * ShikiHighlighter is a React component that provides syntax highlighting for code snippets.\n * It uses Shiki to render beautiful, theme-based syntax highlighting with optimized performance.\n *\n * @example\n * ```tsx\n * <ShikiHighlighter\n * language=\"typescript\"\n * theme=\"github-dark\"\n * delay={100} // Optional throttling for streamed updates\n * >\n * {code}\n * </ShikiHighlighter>\n * ```\n */\nexport const ShikiHighlighter = ({\n language,\n theme,\n delay,\n transformers,\n addDefaultStyles = true,\n style,\n langStyle,\n className,\n langClassName,\n showLanguage = true,\n children: code,\n as: Element = 'pre',\n customLanguages,\n}: ShikiHighlighterProps): React.ReactElement => {\n const options: HighlighterOptions = {\n delay,\n transformers,\n customLanguages,\n };\n\n const normalizedCustomLanguages = customLanguages\n ? Array.isArray(customLanguages)\n ? customLanguages\n : [customLanguages]\n : [];\n\n const { isCustom, languageId, displayLanguageId, resolvedLanguage } =\n resolveLanguage(language, normalizedCustomLanguages);\n\n const highlightedCode = useShikiHighlighter(\n code,\n language,\n theme,\n options\n );\n\n return (\n <Element\n data-testid=\"shiki-container\"\n className={clsx(\n 'relative',\n 'not-prose',\n addDefaultStyles && 'defaultStyles',\n className\n )}\n style={style}\n id=\"shiki-container\"\n >\n {showLanguage && language ? (\n <span\n className={clsx('languageLabel', langClassName)}\n style={langStyle}\n id=\"language-label\"\n >\n {isCustom\n ? `${resolvedLanguage?.scopeName.split('.')[1]}`\n : displayLanguageId || languageId}\n </span>\n ) : null}\n {highlightedCode}\n </Element>\n );\n};\n","import { useEffect, useRef, useState, type ReactNode } from 'react';\n\nimport parse from 'html-react-parser';\n\nimport {\n createHighlighter,\n createSingletonShorthands,\n type Highlighter,\n} from 'shiki';\n\nimport type {\n LanguageRegistration,\n ShikiLanguageRegistration,\n} from './customTypes';\n\nimport type {\n Language,\n Theme,\n HighlighterOptions,\n TimeoutState,\n} from './types';\n\nimport {\n removeTabIndexFromPre,\n throttleHighlighting,\n resolveLanguage,\n} from './utils';\n\n// Use Shiki managed singleton for bundled languages, create a fresh instance for custom languages\nconst highlighter = createSingletonShorthands(createHighlighter);\nconst customHighlighterCache = new Map<string, Promise<Highlighter>>();\n\n/**\n * A React hook that provides syntax highlighting using Shiki.\n * Supports optional throttled highlights and custom themes.\n *\n * @example\n * const highlightedCode = useShikiHighlighter(code, language, theme, {\n * transformers: [removeTabIndexFromPre],\n * delay: 150\n * customLanguages: ['bosque', 'mcfunction']\n * });\n */\nexport const useShikiHighlighter = (\n code: string,\n lang: Language,\n theme: Theme,\n options: HighlighterOptions = {}\n) => {\n const [highlightedCode, setHighlightedCode] =\n useState<ReactNode | null>(null);\n\n // Setup themeKey for highlighter caching, checks if theme is string (builtin) or object (custom)\n const themeKey = typeof theme === 'string' ? theme : theme.name;\n\n const normalizedCustomLanguages: LanguageRegistration[] =\n options.customLanguages\n ? Array.isArray(options.customLanguages)\n ? options.customLanguages\n : [options.customLanguages]\n : [];\n\n const { isCustom, languageId, resolvedLanguage } = resolveLanguage(\n lang,\n normalizedCustomLanguages\n );\n\n const timeoutControl = useRef<TimeoutState>({\n nextAllowedTime: 0,\n timeoutId: undefined,\n });\n\n const getCachedCustomHighlighter = async (\n cacheKey: string,\n customLang: LanguageRegistration | typeof lang\n ) => {\n let instance = customHighlighterCache.get(cacheKey);\n if (!instance) {\n instance = createHighlighter({\n langs: [customLang as ShikiLanguageRegistration],\n themes: [theme],\n });\n customHighlighterCache.set(cacheKey, instance);\n }\n return instance;\n };\n\n useEffect(() => {\n let isMounted = true;\n\n const transformers = [\n removeTabIndexFromPre,\n ...(options.transformers || []),\n ];\n\n const highlightCode = async () => {\n const codeHighlighter =\n isCustom && resolvedLanguage\n ? await getCachedCustomHighlighter(\n `${resolvedLanguage.name}--${themeKey}`,\n resolvedLanguage\n )\n : highlighter;\n\n const html = await codeHighlighter.codeToHtml(code, {\n lang: languageId,\n theme,\n transformers,\n });\n\n if (isMounted) {\n setHighlightedCode(parse(html));\n }\n };\n\n if (options.delay) {\n throttleHighlighting(highlightCode, timeoutControl, options.delay);\n } else {\n highlightCode().catch(console.error);\n }\n\n return () => {\n isMounted = false;\n clearTimeout(timeoutControl.current.timeoutId);\n };\n }, [code, lang]);\n\n return highlightedCode;\n};\n","import { visit } from 'unist-util-visit';\nimport { bundledLanguages, isSpecialLang } from 'shiki';\nimport type { ShikiTransformer } from 'shiki';\nimport type { Element, Root } from 'hast';\nimport type { Language, TimeoutState } from './types';\nimport type { LanguageRegistration } from './customTypes';\n\n/**\n * Type for the HAST node, use to type `node` from react-markdown\n */\nexport type { Element };\n\n/**\n * Rehype plugin to add an 'inline' property to <code> elements\n * Sets 'inline' property to true if the <code> is not within a <pre> tag\n *\n * Pass this plugin to the `rehypePlugins` prop of react-markdown\n * You can then access `inline` as a prop in components passed to react-markdown\n *\n * @example\n * <ReactMarkdown rehypePlugins={[rehypeInlineCodeProperty]} />\n */\nexport function rehypeInlineCodeProperty() {\n return function (tree: Root): undefined {\n visit(\n tree as any,\n 'element',\n function (node: Element, _index, parent: Element) {\n if (node.tagName === 'code' && parent.tagName !== 'pre') {\n node.properties.inline = true;\n }\n }\n );\n };\n}\n\n/**\n * Function to determine if code is inline based on the presence of line breaks\n *\n * @example\n * const isInline = node && isInlineCode(node: Element)\n */\nexport const isInlineCode = (node: Element): boolean => {\n const textContent = (node.children || [])\n .filter((child) => child.type === 'text')\n .map((child) => child.value)\n .join('');\n\n return !textContent.includes('\\n');\n};\n\n/**\n * Shiki transformer to remove tabindex from <pre> elements\n *\n * Consider retaining tabindex for WCAG 3.1 compliance, scrollable code blocks should be focusable\n * https://github.com/shikijs/shiki/issues/428\n * https://www.w3.org/WAI/standards-guidelines/act/rules/0ssw9k/proposed/\n */\nexport const removeTabIndexFromPre: ShikiTransformer = {\n pre(node) {\n if ('properties' in node) {\n node.properties.tabindex = '-1';\n }\n return node;\n },\n};\n\n/**\n * Optionally throttles rapid sequential highlighting operations\n * Exported for testing in __tests__/throttleHighlighting.test.ts\n *\n * @example\n * const timeoutControl = useRef<TimeoutState>({\n * nextAllowedTime: 0,\n * timeoutId: undefined\n * });\n *\n * throttleHighlighting(highlightCode, timeoutControl, 1000);\n */\nexport const throttleHighlighting = (\n performHighlight: () => Promise<void>,\n timeoutControl: React.MutableRefObject<TimeoutState>,\n throttleMs: number\n) => {\n const now = Date.now();\n clearTimeout(timeoutControl.current.timeoutId);\n\n const delay = Math.max(0, timeoutControl.current.nextAllowedTime - now);\n timeoutControl.current.timeoutId = setTimeout(() => {\n performHighlight().catch(console.error);\n timeoutControl.current.nextAllowedTime = now + throttleMs;\n }, delay);\n};\n\ntype ResolvedLanguage = {\n isCustom: boolean;\n languageId: string;\n displayLanguageId?: string;\n resolvedLanguage?: LanguageRegistration;\n};\n\n/**\n * Determines whether a language is custom or built-in, normalizes\n * the language identifier and returns an object with metadata\n *\n * @param lang - The language identifier, either as a string name or language object\n * @param customLanguages - Optional array of custom language definitions\n * @returns Object containing:\n * - isCustom: Whether the language requires custom highlighting\n * - languageId: The normalized language identifier\n * - displayLanguageId: The display name for language label\n * - resolvedLanguage: The full language definition if custom\n */\nexport const resolveLanguage = (\n lang: Language,\n customLanguages: LanguageRegistration[] = []\n): ResolvedLanguage => {\n // Language is custom but not preloaded\n if (lang && typeof lang === 'object') {\n return {\n isCustom: true,\n languageId: lang.name,\n displayLanguageId: lang.name,\n resolvedLanguage: lang,\n };\n }\n\n // Language is a string\n if (typeof lang === 'string') {\n const displayLanguageId = lang;\n\n // Check if its built-in\n if (lang in bundledLanguages || isSpecialLang(lang)) {\n return {\n isCustom: false,\n languageId: lang,\n displayLanguageId,\n };\n }\n\n // Check if it matches a preloaded custom language\n const customMatch = customLanguages.find(\n (cl) =>\n cl.fileTypes?.includes(lang) ||\n cl.scopeName?.split('.')[1] === lang ||\n cl.name?.toLowerCase() === lang.toLowerCase()\n );\n\n if (customMatch) {\n return {\n isCustom: true,\n languageId: customMatch.name,\n displayLanguageId,\n resolvedLanguage: customMatch,\n };\n }\n\n // If unknown, highlight in plaintext but display unknown language\n return {\n isCustom: false,\n languageId: 'plaintext',\n displayLanguageId,\n };\n }\n\n // Fallback\n return {\n isCustom: false,\n languageId: 'plaintext',\n displayLanguageId: 'plaintext',\n };\n};\n"],"mappings":"AACyB,SAARA,EAA6BC,EAAK,CAAE,SAAAC,CAAS,EAAI,CAAC,EAAG,CAC1D,GAAI,CAACD,GAAO,OAAO,SAAa,IAAa,OAE7C,IAAME,EAAO,SAAS,MAAQ,SAAS,qBAAqB,MAAM,EAAE,CAAC,EAC/DC,EAAQ,SAAS,cAAc,OAAO,EAC5CA,EAAM,KAAO,WAETF,IAAa,OACXC,EAAK,WACPA,EAAK,aAAaC,EAAOD,EAAK,UAAU,EAK1CA,EAAK,YAAYC,CAAK,EAGpBA,EAAM,WACRA,EAAM,WAAW,QAAUH,EAE3BG,EAAM,YAAY,SAAS,eAAeH,CAAG,CAAC,CAElD,CCvB8BI,EAAY;AAAA,CAAiP,ECCrS,OAAOC,MAAW,QAClB,OAAS,QAAAC,MAAY,OCFrB,OAAS,aAAAC,EAAW,UAAAC,EAAQ,YAAAC,MAAgC,QAE5D,OAAOC,MAAW,oBAElB,OACE,qBAAAC,EACA,6BAAAC,MAEK,QCRP,OAAS,SAAAC,MAAa,mBACtB,OAAS,oBAAAC,EAAkB,iBAAAC,MAAqB,QAqBzC,SAASC,GAA2B,CACzC,OAAO,SAAUC,EAAuB,CACtCJ,EACEI,EACA,UACA,SAAUC,EAAeC,EAAQC,EAAiB,CAC5CF,EAAK,UAAY,QAAUE,EAAO,UAAY,QAChDF,EAAK,WAAW,OAAS,GAE7B,CACF,CACF,CACF,CAQO,IAAMG,EAAgBH,GAMpB,EALcA,EAAK,UAAY,CAAC,GACpC,OAAQI,GAAUA,EAAM,OAAS,MAAM,EACvC,IAAKA,GAAUA,EAAM,KAAK,EAC1B,KAAK,EAAE,EAEU,SAAS;AAAA,CAAI,EAUtBC,EAA0C,CACrD,IAAIL,EAAM,CACR,MAAI,eAAgBA,IAClBA,EAAK,WAAW,SAAW,MAEtBA,CACT,CACF,EAcaM,EAAuB,CAClCC,EACAC,EACAC,IACG,CACH,IAAMC,EAAM,KAAK,IAAI,EACrB,aAAaF,EAAe,QAAQ,SAAS,EAE7C,IAAMG,EAAQ,KAAK,IAAI,EAAGH,EAAe,QAAQ,gBAAkBE,CAAG,EACtEF,EAAe,QAAQ,UAAY,WAAW,IAAM,CAClDD,EAAiB,EAAE,MAAM,QAAQ,KAAK,EACtCC,EAAe,QAAQ,gBAAkBE,EAAMD,CACjD,EAAGE,CAAK,CACV,EAqBaC,EAAkB,CAC7BC,EACAC,EAA0C,CAAC,IACtB,CAErB,GAAID,GAAQ,OAAOA,GAAS,SAC1B,MAAO,CACL,SAAU,GACV,WAAYA,EAAK,KACjB,kBAAmBA,EAAK,KACxB,iBAAkBA,CACpB,EAIF,GAAI,OAAOA,GAAS,SAAU,CAC5B,IAAME,EAAoBF,EAG1B,GAAIA,KAAQjB,GAAoBC,EAAcgB,CAAI,EAChD,MAAO,CACL,SAAU,GACV,WAAYA,EACZ,kBAAAE,CACF,EAIF,IAAMC,EAAcF,EAAgB,KACjCG,GACCA,EAAG,WAAW,SAASJ,CAAI,GAC3BI,EAAG,WAAW,MAAM,GAAG,EAAE,CAAC,IAAMJ,GAChCI,EAAG,MAAM,YAAY,IAAMJ,EAAK,YAAY,CAChD,EAEA,OAAIG,EACK,CACL,SAAU,GACV,WAAYA,EAAY,KACxB,kBAAAD,EACA,iBAAkBC,CACpB,EAIK,CACL,SAAU,GACV,WAAY,YACZ,kBAAAD,CACF,CACF,CAGA,MAAO,CACL,SAAU,GACV,WAAY,YACZ,kBAAmB,WACrB,CACF,ED9IA,IAAMG,EAAcC,EAA0BC,CAAiB,EACzDC,EAAyB,IAAI,IAatBC,EAAsB,CACjCC,EACAC,EACAC,EACAC,EAA8B,CAAC,IAC5B,CACH,GAAM,CAACC,EAAiBC,CAAkB,EACxCC,EAA2B,IAAI,EAG3BC,EAAW,OAAOL,GAAU,SAAWA,EAAQA,EAAM,KAErDM,EACJL,EAAQ,gBACJ,MAAM,QAAQA,EAAQ,eAAe,EACnCA,EAAQ,gBACR,CAACA,EAAQ,eAAe,EAC1B,CAAC,EAED,CAAE,SAAAM,EAAU,WAAAC,EAAY,iBAAAC,CAAiB,EAAIC,EACjDX,EACAO,CACF,EAEMK,EAAiBC,EAAqB,CAC1C,gBAAiB,EACjB,UAAW,MACb,CAAC,EAEKC,EAA6B,MACjCC,EACAC,IACG,CACH,IAAIC,EAAWpB,EAAuB,IAAIkB,CAAQ,EAClD,OAAKE,IACHA,EAAWrB,EAAkB,CAC3B,MAAO,CAACoB,CAAuC,EAC/C,OAAQ,CAACf,CAAK,CAChB,CAAC,EACDJ,EAAuB,IAAIkB,EAAUE,CAAQ,GAExCA,CACT,EAEA,OAAAC,EAAU,IAAM,CACd,IAAIC,EAAY,GAEVC,EAAe,CACnBC,EACA,GAAInB,EAAQ,cAAgB,CAAC,CAC/B,EAEMoB,EAAgB,SAAY,CAShC,IAAMC,EAAO,MAPXf,GAAYE,EACR,MAAMI,EACJ,GAAGJ,EAAiB,IAAI,KAAKJ,CAAQ,GACrCI,CACF,EACAhB,GAE6B,WAAWK,EAAM,CAClD,KAAMU,EACN,MAAAR,EACA,aAAAmB,CACF,CAAC,EAEGD,GACFf,EAAmBoB,EAAMD,CAAI,CAAC,CAElC,EAEA,OAAIrB,EAAQ,MACVuB,EAAqBH,EAAeV,EAAgBV,EAAQ,KAAK,EAEjEoB,EAAc,EAAE,MAAM,QAAQ,KAAK,EAG9B,IAAM,CACXH,EAAY,GACZ,aAAaP,EAAe,QAAQ,SAAS,CAC/C,CACF,EAAG,CAACb,EAAMC,CAAI,CAAC,EAERG,CACT,EDxBO,IAAMuB,EAAmB,CAAC,CAC/B,SAAAC,EACA,MAAAC,EACA,MAAAC,EACA,aAAAC,EACA,iBAAAC,EAAmB,GACnB,MAAAC,EACA,UAAAC,EACA,UAAAC,EACA,cAAAC,EACA,aAAAC,EAAe,GACf,SAAUC,EACV,GAAIC,EAAU,MACd,gBAAAC,CACF,IAAiD,CAC/C,IAAMC,EAA8B,CAClC,MAAAX,EACA,aAAAC,EACA,gBAAAS,CACF,EAEME,EAA4BF,EAC9B,MAAM,QAAQA,CAAe,EAC3BA,EACA,CAACA,CAAe,EAClB,CAAC,EAEC,CAAE,SAAAG,EAAU,WAAAC,EAAY,kBAAAC,EAAmB,iBAAAC,CAAiB,EAChEC,EAAgBnB,EAAUc,CAAyB,EAE/CM,EAAkBC,EACtBX,EACAV,EACAC,EACAY,CACF,EAEA,OACES,EAAA,cAACX,EAAA,CACC,cAAY,kBACZ,UAAWY,EACT,WACA,YACAnB,GAAoB,gBACpBG,CACF,EACA,MAAOF,EACP,GAAG,mBAEFI,GAAgBT,EACfsB,EAAA,cAAC,QACC,UAAWC,EAAK,gBAAiBf,CAAa,EAC9C,MAAOF,EACP,GAAG,kBAEFS,EACG,GAAGG,GAAkB,UAAU,MAAM,GAAG,EAAE,CAAC,CAAC,GAC5CD,GAAqBD,CAC3B,EACE,KACHI,CACH,CAEJ","names":["styleInject","css","insertAt","head","style","styleInject","React","clsx","useEffect","useRef","useState","parse","createHighlighter","createSingletonShorthands","visit","bundledLanguages","isSpecialLang","rehypeInlineCodeProperty","tree","node","_index","parent","isInlineCode","child","removeTabIndexFromPre","throttleHighlighting","performHighlight","timeoutControl","throttleMs","now","delay","resolveLanguage","lang","customLanguages","displayLanguageId","customMatch","cl","highlighter","createSingletonShorthands","createHighlighter","customHighlighterCache","useShikiHighlighter","code","lang","theme","options","highlightedCode","setHighlightedCode","useState","themeKey","normalizedCustomLanguages","isCustom","languageId","resolvedLanguage","resolveLanguage","timeoutControl","useRef","getCachedCustomHighlighter","cacheKey","customLang","instance","useEffect","isMounted","transformers","removeTabIndexFromPre","highlightCode","html","parse","throttleHighlighting","ShikiHighlighter","language","theme","delay","transformers","addDefaultStyles","style","langStyle","className","langClassName","showLanguage","code","Element","customLanguages","options","normalizedCustomLanguages","isCustom","languageId","displayLanguageId","resolvedLanguage","resolveLanguage","highlightedCode","useShikiHighlighter","React","clsx"]}
1
+ {"version":3,"sources":["#style-inject:#style-inject","../src/styles.css","../src/ShikiHighlighter.tsx","../src/useShiki.ts","../src/utils.ts"],"sourcesContent":["\n export default function styleInject(css, { insertAt } = {}) {\n if (!css || typeof document === 'undefined') return\n \n const head = document.head || document.getElementsByTagName('head')[0]\n const style = document.createElement('style')\n style.type = 'text/css'\n \n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n \n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n }\n ","import styleInject from '#style-inject';styleInject(\".relative{position:relative}.defaultStyles pre{overflow:auto;border-radius:.5rem;padding:1.25rem 1.5rem}.languageLabel{position:absolute;right:.75rem;top:.5rem;font-family:monospace;font-size:.75rem;letter-spacing:-.05em;color:#6b7280d9}\\n\")","import './styles.css';\n// biome-ignore lint/style/useImportType: React import is needed\nimport React from 'react';\nimport { clsx } from 'clsx';\nimport { useShikiHighlighter } from './useShiki';\nimport type {\n Language,\n Theme,\n HighlighterOptions,\n Themes,\n} from './types';\nimport { resolveLanguage } from './utils';\n\n/**\n * Props for the ShikiHighlighter component\n */\nexport interface ShikiHighlighterProps extends HighlighterOptions {\n /**\n * The programming language for syntax highlighting\n * Supports custom textmate grammar objects in addition to Shiki's bundled languages\n * @see https://shiki.style/languages\n */\n language: Language;\n\n /**\n * The code to be highlighted\n */\n children: string;\n\n /**\n * The color theme or themes for syntax highlighting\n * Supports single, dual, or multiple themes\n * Supports custom textmate theme objects in addition to Shiki's bundled themes\n *\n * @example\n * theme='github-dark' // single theme\n * theme={{ light: 'github-light', dark: 'github-dark' }} // multi-theme\n *\n * @see https://shiki.style/themes\n */\n theme: Theme | Themes;\n\n /**\n * Controls the application of default styles to the generated code blocks\n *\n * Default styles include padding, overflow handling, border radius, language label styling, and font settings\n * @default true\n */\n addDefaultStyles?: boolean;\n\n /**\n * Add custom inline styles to the generated code block\n */\n style?: React.CSSProperties;\n\n /**\n * Add custom inline styles to the language label\n */\n langStyle?: React.CSSProperties;\n\n /**\n * Add custom CSS class names to the generated code block\n */\n className?: string;\n\n /**\n * Add custom CSS class names to the language label\n */\n langClassName?: string;\n\n /**\n * Whether to show the language label\n * @default true\n */\n showLanguage?: boolean;\n\n /**\n * The HTML element that wraps the generated code block.\n * @default 'pre'\n */\n as?: React.ElementType;\n}\n/**\n * ShikiHighlighter is a React component that provides syntax highlighting for code snippets.\n * It uses Shiki to render beautiful, theme-based syntax highlighting with optimized performance.\n *\n * @example\n * ```tsx\n * <ShikiHighlighter\n * language=\"typescript\"\n * theme=\"github-dark\"\n * delay={100}\n * transformers={[customTransformer]}\n * addDefaultStyles={false}\n * className=\"code-block\"\n * langClassName=\"lang-label\"\n * langStyle={{ color: 'blue' }},\n * style={{ fontSize: '1.2rem' }}\n * >\n * {code}\n * </ShikiHighlighter>\n * ```\n */\nexport const ShikiHighlighter = ({\n language,\n theme,\n delay,\n transformers,\n defaultColor,\n cssVariablePrefix,\n addDefaultStyles = true,\n style,\n langStyle,\n className,\n langClassName,\n showLanguage = true,\n children: code,\n as: Element = 'pre',\n customLanguages,\n}: ShikiHighlighterProps): React.ReactElement => {\n const options: HighlighterOptions = {\n delay,\n transformers,\n customLanguages,\n defaultColor,\n cssVariablePrefix,\n };\n\n const normalizedCustomLanguages = customLanguages\n ? Array.isArray(customLanguages)\n ? customLanguages\n : [customLanguages]\n : [];\n\n const { isCustom, languageId, displayLanguageId, resolvedLanguage } =\n resolveLanguage(language, normalizedCustomLanguages);\n\n const highlightedCode = useShikiHighlighter(\n code,\n language,\n theme,\n options\n );\n\n return (\n <Element\n data-testid=\"shiki-container\"\n className={clsx(\n 'relative',\n 'not-prose',\n addDefaultStyles && 'defaultStyles',\n className\n )}\n style={style}\n id=\"shiki-container\"\n >\n {showLanguage && language ? (\n <span\n className={clsx('languageLabel', langClassName)}\n style={langStyle}\n id=\"language-label\"\n >\n {isCustom\n ? `${resolvedLanguage?.scopeName.split('.')[1]}`\n : displayLanguageId || languageId}\n </span>\n ) : null}\n {highlightedCode}\n </Element>\n );\n};\n","import {\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport parse from 'html-react-parser';\n\nimport {\n createHighlighter,\n createSingletonShorthands,\n type Highlighter,\n type CodeToHastOptions,\n type CodeOptionsSingleTheme,\n type CodeOptionsMultipleThemes,\n} from 'shiki';\n\nimport type {\n LanguageRegistration,\n ShikiLanguageRegistration,\n} from './customTypes';\n\nimport type {\n Language,\n Theme,\n HighlighterOptions,\n TimeoutState,\n Themes,\n} from './types';\n\nimport {\n removeTabIndexFromPre,\n throttleHighlighting,\n resolveLanguage,\n resolveTheme,\n} from './utils';\n\nconst DEFAULT_THEMES = {\n light: 'github-light',\n dark: 'github-dark',\n};\n\n// Use Shiki managed singleton for bundled languages, create and cache a fresh instance for custom languages\nconst bundledHighlighter = createSingletonShorthands(createHighlighter);\nconst customLangHighlighter = new Map<string, Promise<Highlighter>>();\n\nconst getCachedCustomHighlighter = async (\n cacheKey: string,\n customLang: LanguageRegistration,\n themesToLoad: Theme[]\n) => {\n let instance = customLangHighlighter.get(cacheKey);\n\n if (!instance) {\n instance = createHighlighter({\n langs: [customLang as ShikiLanguageRegistration],\n themes: themesToLoad,\n });\n customLangHighlighter.set(cacheKey, instance);\n }\n return instance;\n};\n\n/**\n * A React hook that provides syntax highlighting using Shiki.\n * Supports single theme and multi-theme highlighting, custom themes\n * and languages, custom transformers, and optional throttling.\n *\n * ```ts\n * // Basic Usage\n * const highlightedCode = useShikiHighlighter( code, 'typescript', 'github-dark');\n * ```\n *\n * ```ts\n * // Custom Languages, Transformers, and Delay\n * const highlightedCode = useShikiHighlighter(code, language, theme, {\n * transformers: [customTransformer],\n * delay: 150\n * customLanguages: ['bosque', 'mcfunction']\n * });\n * ```\n *\n * ```ts\n * // Multiple Themes, Custom Languages, Delay, and Custom Transformers\n * const highlightedCode = useShikiHighlighter(\n * code,\n * language,\n * {\n * light: 'github-light',\n * dark: 'github-dark',\n * dim: 'github-dark-dimmed'\n * },\n * {\n * delay: 150,\n * defaultColor: 'dim',\n * transformers: [customTransformer],\n * customLanguages: ['bosque', 'mcfunction']\n * }\n * );\n * ```\n */\nexport const useShikiHighlighter = (\n code: string,\n lang: Language,\n themeInput: Theme | Themes,\n options: HighlighterOptions = {}\n) => {\n const [highlightedCode, setHighlightedCode] =\n useState<ReactNode | null>(null);\n\n const normalizedCustomLanguages: LanguageRegistration[] =\n options.customLanguages\n ? Array.isArray(options.customLanguages)\n ? options.customLanguages\n : [options.customLanguages]\n : [];\n\n const customLangId = normalizedCustomLanguages\n .map((lang) => lang.name || '')\n .sort()\n .join('-');\n\n const transformers = useMemo(() => {\n return [removeTabIndexFromPre, ...(options.transformers || [])];\n }, [options.transformers]);\n\n const { isMultiTheme, themeId, multiTheme, singleTheme, themesToLoad } =\n useMemo(() => resolveTheme(themeInput), [themeInput]);\n\n const { isCustom, languageId, resolvedLanguage } = useMemo(\n () => resolveLanguage(lang, normalizedCustomLanguages),\n [lang, customLangId]\n );\n\n const cacheKey = `${languageId}-${themeId}`;\n\n const timeoutControl = useRef<TimeoutState>({\n nextAllowedTime: 0,\n timeoutId: undefined,\n });\n\n const buildShikiOptions = (): CodeToHastOptions => {\n const { defaultColor, cssVariablePrefix } = options;\n const commonOptions = { lang: languageId, transformers };\n\n const themeOptions = isMultiTheme\n ? ({\n themes: multiTheme || DEFAULT_THEMES,\n defaultColor,\n cssVariablePrefix,\n } as CodeOptionsMultipleThemes)\n : ({\n theme: singleTheme || DEFAULT_THEMES.dark,\n } as CodeOptionsSingleTheme);\n\n return { ...commonOptions, ...themeOptions };\n };\n\n useEffect(() => {\n let isMounted = true;\n\n const highlightCode = async () => {\n const codeHighlighter =\n isCustom && resolvedLanguage\n ? await getCachedCustomHighlighter(\n cacheKey,\n resolvedLanguage,\n themesToLoad\n )\n : bundledHighlighter;\n\n const highlighterOptions: CodeToHastOptions = buildShikiOptions();\n\n const html = await codeHighlighter.codeToHtml(\n code,\n highlighterOptions\n );\n if (isMounted) {\n setHighlightedCode(parse(html));\n }\n };\n\n if (options.delay) {\n throttleHighlighting(highlightCode, timeoutControl, options.delay);\n } else {\n highlightCode().catch(console.error);\n }\n\n return () => {\n isMounted = false;\n clearTimeout(timeoutControl.current.timeoutId);\n };\n }, [\n code,\n languageId,\n themeId,\n customLangId,\n transformers,\n options.delay,\n options.defaultColor,\n options.cssVariablePrefix,\n ]);\n\n return highlightedCode;\n};\n","import { visit } from 'unist-util-visit';\nimport { bundledLanguages, isSpecialLang } from 'shiki';\nimport type { ShikiTransformer, ThemeRegistrationAny } from 'shiki';\nimport type { Element } from 'hast';\nimport type { Language, Theme, Themes, TimeoutState } from './types';\nimport type { LanguageRegistration } from './customTypes';\n\n/**\n * Type for the HAST node, use to type `node` from react-markdown\n */\nexport type { Element };\n\n/**\n * Rehype plugin to add an 'inline' property to <code> elements\n * Sets 'inline' property to true if the <code> is not within a <pre> tag\n *\n * Pass this plugin to the `rehypePlugins` prop of react-markdown\n * You can then access `inline` as a prop in components passed to react-markdown\n *\n * @example\n * <ReactMarkdown rehypePlugins={[rehypeInlineCodeProperty]} />\n */\nexport function rehypeInlineCodeProperty() {\n return (tree: any): undefined => {\n visit(tree, 'element', (node: Element, _index, parent: Element) => {\n if (node.tagName === 'code' && parent.tagName !== 'pre') {\n node.properties.inline = true;\n }\n });\n };\n}\n\n/**\n * Function to determine if code is inline based on the presence of line breaks\n *\n * @example\n * const isInline = node && isInlineCode(node: Element)\n */\nexport const isInlineCode = (node: Element): boolean => {\n const textContent = (node.children || [])\n .filter((child) => child.type === 'text')\n .map((child) => child.value)\n .join('');\n\n return !textContent.includes('\\n');\n};\n\n/**\n * Shiki transformer to remove tabindex from <pre> elements\n *\n * Consider retaining tabindex for WCAG 3.1 compliance, scrollable code blocks should be focusable\n * https://github.com/shikijs/shiki/issues/428\n * https://www.w3.org/WAI/standards-guidelines/act/rules/0ssw9k/proposed/\n */\nexport const removeTabIndexFromPre: ShikiTransformer = {\n pre(node) {\n if ('properties' in node) {\n node.properties.tabindex = '-1';\n }\n return node;\n },\n};\n\n/**\n * Optionally throttles rapid sequential highlighting operations\n * Exported for testing in __tests__/throttleHighlighting.test.ts\n *\n * @example\n * const timeoutControl = useRef<TimeoutState>({\n * nextAllowedTime: 0,\n * timeoutId: undefined\n * });\n *\n * throttleHighlighting(highlightCode, timeoutControl, 1000);\n */\nexport const throttleHighlighting = (\n performHighlight: () => Promise<void>,\n timeoutControl: React.MutableRefObject<TimeoutState>,\n throttleMs: number\n) => {\n const now = Date.now();\n clearTimeout(timeoutControl.current.timeoutId);\n\n const delay = Math.max(0, timeoutControl.current.nextAllowedTime - now);\n timeoutControl.current.timeoutId = setTimeout(() => {\n performHighlight().catch(console.error);\n timeoutControl.current.nextAllowedTime = now + throttleMs;\n }, delay);\n};\n\ntype ResolvedLanguage = {\n isCustom: boolean;\n languageId: string;\n displayLanguageId?: string;\n resolvedLanguage?: LanguageRegistration;\n};\n\n/**\n * Determines whether a language is custom or built-in, normalizes\n * the language identifier and returns the resolved language with metadata\n *\n * @param lang - The language identifier, either as a string name or language object\n * @param customLanguages - Optional array of custom language definitions\n * @returns Object containing:\n * - isCustom: Whether the language requires custom highlighting\n * - languageId: The normalized language identifier\n * - displayLanguageId: The display name for language label\n * - resolvedLanguage: The full language definition if custom\n */\nexport const resolveLanguage = (\n lang: Language,\n customLanguages: LanguageRegistration[] = []\n): ResolvedLanguage => {\n // Language is custom but not preloaded\n if (lang && typeof lang === 'object') {\n return {\n isCustom: true,\n languageId: lang.name,\n displayLanguageId: lang.name,\n resolvedLanguage: lang,\n };\n }\n\n // Language is a string\n if (typeof lang === 'string') {\n const displayLanguageId = lang;\n\n // Check if its built-in\n if (lang in bundledLanguages || isSpecialLang(lang)) {\n return {\n isCustom: false,\n languageId: lang,\n displayLanguageId,\n };\n }\n\n // Check if it matches a preloaded custom language\n const customMatch = customLanguages.find(\n (cl) =>\n cl.fileTypes?.includes(lang) ||\n cl.scopeName?.split('.')[1] === lang ||\n cl.name?.toLowerCase() === lang.toLowerCase()\n );\n\n if (customMatch) {\n return {\n isCustom: true,\n languageId: customMatch.name,\n displayLanguageId,\n resolvedLanguage: customMatch,\n };\n }\n\n // If unknown, highlight in plaintext but display unknown language\n return {\n isCustom: false,\n languageId: 'plaintext',\n displayLanguageId,\n };\n }\n\n // Fallback\n return {\n isCustom: false,\n languageId: 'plaintext',\n displayLanguageId: 'plaintext',\n };\n};\n\n/**\n * Determines theme configuration and returns the resolved theme with metadata\n * @param themeInput - The theme input, either as a string name or theme object\n * @returns Object containing:\n * - isMultiTheme: If theme input is a multi-theme configuration\n * - themeId: Theme reference identifier\n * - multiTheme: The multi-theme config if it exists\n * - singleTheme: The single theme if it exists\n * - themesToLoad: The themes to load when creating the highlighter\n */\nexport function resolveTheme(themeInput: Theme | Themes): {\n isMultiTheme: boolean;\n themeId: Theme;\n multiTheme?: Themes | ThemeRegistrationAny | null;\n singleTheme?: Theme | undefined;\n themesToLoad: Theme[];\n} {\n const isTextmateTheme =\n typeof themeInput === 'object' &&\n 'tokenColors' in themeInput &&\n Array.isArray(themeInput.tokenColors);\n\n // Assume non textmate objects are multi theme configs\n const isMultiThemeConfig =\n typeof themeInput === 'object' &&\n themeInput !== null &&\n !isTextmateTheme;\n\n const validMultiThemeObj =\n typeof themeInput === 'object' &&\n themeInput !== null &&\n !isTextmateTheme &&\n Object.entries(themeInput).some(\n ([key, value]) =>\n key &&\n value &&\n key.trim() !== '' &&\n value !== '' &&\n (typeof value === 'string' || isTextmateTheme)\n );\n\n if (isMultiThemeConfig) {\n const themeId = validMultiThemeObj\n ? `multi-${Object.values(themeInput)\n .map(\n (theme) =>\n (typeof theme === 'string' ? theme : theme?.name) ||\n 'custom'\n )\n .sort()\n .join('-')}`\n : 'multi-default';\n\n // If config is invalid, return null to handle fallback in `buildShikiOptions()`\n return {\n isMultiTheme: true,\n themeId,\n multiTheme: validMultiThemeObj ? themeInput : null,\n themesToLoad: validMultiThemeObj ? Object.values(themeInput) : [],\n };\n }\n\n return {\n isMultiTheme: false,\n themeId:\n typeof themeInput === 'string'\n ? themeInput\n : themeInput?.name || 'custom',\n singleTheme: themeInput,\n themesToLoad: [themeInput],\n };\n}\n"],"mappings":"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","names":["styleInject","css","insertAt","head","style","styleInject","React","clsx","useEffect","useMemo","useRef","useState","parse","createHighlighter","createSingletonShorthands","visit","bundledLanguages","isSpecialLang","rehypeInlineCodeProperty","tree","node","_index","parent","isInlineCode","child","removeTabIndexFromPre","throttleHighlighting","performHighlight","timeoutControl","throttleMs","now","delay","resolveLanguage","lang","customLanguages","displayLanguageId","customMatch","cl","resolveTheme","themeInput","isTextmateTheme","isMultiThemeConfig","validMultiThemeObj","key","value","theme","DEFAULT_THEMES","bundledHighlighter","createSingletonShorthands","createHighlighter","customLangHighlighter","getCachedCustomHighlighter","cacheKey","customLang","themesToLoad","instance","useShikiHighlighter","code","lang","themeInput","options","highlightedCode","setHighlightedCode","useState","normalizedCustomLanguages","customLangId","transformers","useMemo","removeTabIndexFromPre","isMultiTheme","themeId","multiTheme","singleTheme","resolveTheme","isCustom","languageId","resolvedLanguage","resolveLanguage","timeoutControl","useRef","buildShikiOptions","defaultColor","cssVariablePrefix","commonOptions","themeOptions","useEffect","isMounted","highlightCode","codeHighlighter","highlighterOptions","html","parse","throttleHighlighting","ShikiHighlighter","language","theme","delay","transformers","defaultColor","cssVariablePrefix","addDefaultStyles","style","langStyle","className","langClassName","showLanguage","code","Element","customLanguages","options","normalizedCustomLanguages","isCustom","languageId","displayLanguageId","resolvedLanguage","resolveLanguage","highlightedCode","useShikiHighlighter","React","clsx"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-shiki",
3
3
  "description": "Syntax highlighter component for react using shiki",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Bassim Shahidy",