react-shiki 0.4.1 → 0.5.1

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,30 @@ 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)
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-->
17
+ - [Common Configuration Options](#common-configuration-options)
18
+ - [Component-specific Props](#component-specific-props)
19
+ - [Integration with react-markdown](#integration-with-react-markdown)
20
+ - [Handling Inline Code](#handling-inline-code)
21
+ - [Multi-theme Support](#multi-theme-support)
22
+ - [Custom Themes](#custom-themes)
23
+ - [Custom Languages](#custom-languages)
24
+ - [Preloading Custom Languages](#preloading-custom-languages)
25
+ - [Custom Transformers](#custom-transformers)
26
+ - [Performance](#performance)
27
+ - [Throttling Real-time Highlighting](#throttling-real-time-highlighting)
28
+ - [Streaming and LLM Chat UI](#streaming-and-llm-chat-ui)
29
+ <!--toc:end-->
29
30
 
30
31
  ## Features
31
32
 
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`
33
+ - 🖼️ Provides both a `ShikiHighlighter` component and a `useShikiHighlighter` hook for more flexibility
34
+ - 🔐 No `dangerouslySetInnerHTML` - output from Shiki is parsed using `html-react-parser`
35
35
  - 📦 Supports all built-in Shiki languages and themes
36
36
  - 🖌️ Full support for custom TextMate themes and languages
37
37
  - 🔧 Supports passing custom Shiki transformers to the highlighter
38
38
  - 🚰 Performant highlighting of streamed code, with optional throttling
39
39
  - 📚 Includes minimal default styles for code blocks
40
- - 🚀 Shiki dynamically imports only the languages and themes used on a page,
41
- optimizing for performance
40
+ - 🚀 Shiki dynamically imports only the languages and themes used on a page for optimal performance
42
41
  - 🖥️ `ShikiHighlighter` component displays a language label for each code block
43
42
  when `showLanguage` is set to `true` (default)
44
43
  - 🎨 Customizable styling of generated code blocks and language labels
@@ -46,30 +45,18 @@ for react built with [Shiki](https://shiki.matsu.io/)
46
45
  ## Installation
47
46
 
48
47
  ```bash
49
- pnpm install react-shiki
48
+ npm i react-shiki
50
49
  ```
51
50
 
52
51
  ## Usage
53
52
 
54
- You can use the `ShikiHighlighter` component, or the `useShikiHighlighter` hook
55
- to highlight code.
53
+ You can use either the `ShikiHighlighter` component or the `useShikiHighlighter` hook to highlight code.
56
54
 
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.
55
+ **Using the Component:**
61
56
 
62
57
  ```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.
58
+ import { ShikiHighlighter } from "react-shiki";
71
59
 
72
- ```tsx
73
60
  function CodeBlock() {
74
61
  return (
75
62
  <ShikiHighlighter language="jsx" theme="ayu-dark">
@@ -79,111 +66,117 @@ function CodeBlock() {
79
66
  }
80
67
  ```
81
68
 
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
69
+ **Using the Hook:**
101
70
 
102
71
  ```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
- );
72
+ import { useShikiHighlighter } from "react-shiki";
73
+
74
+ function CodeBlock({ code, language }) {
75
+ const highlightedCode = useShikiHighlighter(code, language, "github-dark");
76
+
77
+ return <div className="code-block">{highlightedCode}</div>;
119
78
  }
120
79
  ```
121
80
 
122
- ### `react-markdown`
81
+ ### Common Configuration Options
123
82
 
124
- ```tsx
125
- import type { ReactNode } from "react";
126
- import ShikiHighlighter, { type Element } from "react-shiki";
83
+ | Option | Type | Default | Description |
84
+ | ------------------- | ------------------ | --------------- | ------------------------------------------------------------------------- |
85
+ | `code` | `string` | - | Code to highlight |
86
+ | `language` | `string \| object` | - | Language to highlight, built-in or custom textmate grammer object |
87
+ | `theme` | `string \| object` | `'github-dark'` | Single or multi-theme configuration, built-in or custom textmate theme object |
88
+ | `delay` | `number` | `0` | Delay between highlights (in milliseconds) |
89
+ | `transformers` | `array` | `[]` | Custom Shiki transformers for modifying the highlighting output |
90
+ | `customLanguages` | `array` | `[]` | Array of custom languages to preload |
91
+ | `cssVariablePrefix` | `string` | `'--shiki'` | Prefix for CSS variables storing theme colors |
92
+ | `defaultColor` | `string \| false` | `'light'` | Default theme mode when using multiple themes, can also disable default theme |
127
93
 
128
- interface CodeHighlightProps {
129
- className?: string | undefined;
130
- children?: ReactNode | undefined;
131
- node?: Element | undefined;
132
- }
94
+ ### Component-specific Props
133
95
 
134
- export const CodeHighlight = ({
135
- className,
136
- children,
137
- node,
138
- ...props
139
- }: CodeHighlightProps): JSX.Element => {
96
+ The `ShikiHighlighter` component offers minimal built-in styling and customization options out-of-the-box:
97
+
98
+ | Prop | Type | Default | Description |
99
+ | ------------------ | --------- | ------- | ---------------------------------------------------------- |
100
+ | `showLanguage` | `boolean` | `true` | Displays language label in top-right corner |
101
+ | `addDefaultStyles` | `boolean` | `true` | Adds minimal default styling to the highlighted code block |
102
+ | `as` | `string` | `'pre'` | Component's Root HTML element |
103
+ | `className` | `string` | - | Custom class name for the code block |
104
+ | `langClassName` | `string` | - | Class name for styling the language label |
105
+ | `style` | `object` | - | Inline style object for the code block |
106
+ | `langStyle` | `object` | - | Inline style object for the language label |
107
+
108
+ ### Integration with react-markdown
109
+
110
+ Create a component to handle syntax highlighting:
111
+
112
+ ```tsx
113
+ import ReactMarkdown from "react-markdown";
114
+ import { ShikiHighlighter, isInlineCode } from "react-shiki";
115
+
116
+ const CodeHighlight = ({ className, children, node, ...props }) => {
117
+ const code = String(children).trim();
140
118
  const match = className?.match(/language-(\w+)/);
141
119
  const language = match ? match[1] : undefined;
120
+ const isInline = node ? isInlineCode(node) : undefined;
142
121
 
143
- <ShikiHighlighter language={language} theme={"houston"} {...props}>
144
- {String(children).trim()}
145
- </ShikiHighlighter>;
122
+ return !isInline ? (
123
+ <ShikiHighlighter language={language} theme="github-dark" {...props}>
124
+ {code}
125
+ </ShikiHighlighter>
126
+ ) : (
127
+ <code className={className} {...props}>
128
+ {code}
129
+ </code>
130
+ );
146
131
  };
147
132
  ```
148
133
 
149
- Pass `CodeHighlight` to `react-markdown` as a code component:
134
+ Pass the component to react-markdown as a code component:
150
135
 
151
136
  ```tsx
152
- import ReactMarkdown from "react-markdown";
153
- import { CodeHighlight } from "./CodeHighlight";
154
-
155
137
  <ReactMarkdown
156
138
  components={{
157
139
  code: CodeHighlight,
158
140
  }}
159
141
  >
160
142
  {markdown}
161
- </ReactMarkdown>;
143
+ </ReactMarkdown>
162
144
  ```
163
145
 
164
- ### Check if code is inline
146
+ ### Handling Inline Code
165
147
 
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:
148
+ **Method 1: Using the `isInlineCode` helper:**
149
+
150
+ There are two ways to check if a code block is inline, both provide the same result:
151
+ `react-shiki` exports `isInlineCode` which parses the `node`
152
+ prop (from `react-markdown`) to determine if the code is inline:
169
153
 
170
154
  ```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
- );
155
+ import { isInlineCode, ShikiHighlighter } from "react-shiki";
156
+
157
+ const CodeHighlight = ({ className, children, node, ...props }) => {
158
+ const match = className?.match(/language-(\w+)/);
159
+ const language = match ? match[1] : undefined;
160
+ const isInline = node ? isInlineCode(node) : undefined;
161
+
162
+ return !isInline ? (
163
+ <ShikiHighlighter language={language} theme="github-dark" {...props}>
164
+ {String(children).trim()}
165
+ </ShikiHighlighter>
166
+ ) : (
167
+ <code className={className} {...props}>
168
+ {children}
169
+ </code>
170
+ );
171
+ };
182
172
  ```
183
173
 
174
+ **Method 2: Using the rehype plugin:**
175
+
184
176
  `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
177
+ an `inline` property to `react-markdown` to determine if code is inline based on
186
178
  the presence of a `<pre>` tag as a parent of `<code>`.
179
+
187
180
  It's passed as a rehype plugin to `react-markdown`:
188
181
 
189
182
  ```tsx
@@ -214,99 +207,145 @@ const CodeHighlight = ({
214
207
  const language = match ? match[1] : undefined;
215
208
  const code = String(children).trim();
216
209
 
210
+ return !inline ? (
211
+ <ShikiHighlighter language={language} theme="github-dark" {...props}>
212
+ {code}
213
+ </ShikiHighlighter>
214
+ ) : (
215
+ <code className={className} {...props}>
216
+ {code}
217
+ </code>
218
+ );
219
+ };
220
+ ```
221
+
222
+ ### Multi-theme Support
223
+
224
+ To use multiple theme modes, pass an object with your multi-theme configuration to the `theme` prop in the `ShikiHighlighter` component:
225
+
226
+ ```tsx
227
+ <ShikiHighlighter
228
+ language="tsx"
229
+ theme={{
230
+ light: "github-light",
231
+ dark: "github-dark",
232
+ dim: "github-dark-dimmed",
233
+ }}
234
+ defaultColor="dark"
235
+ >
236
+ {code.trim()}
237
+ </ShikiHighlighter>
238
+ ```
239
+
240
+ Or, when using the hook, pass it to the `theme` parameter:
217
241
 
218
- return !inline ? (
219
- <ShikiHighlighter language={language} theme={"houston"} {...props}>
220
- {code}
221
- </ShikiHighlighter>
222
- ) : (
223
- <code className={className} {...props}>
224
- {children}
225
- </code>
242
+ ```tsx
243
+ const highlightedCode = useShikiHighlighter(
244
+ code,
245
+ "tsx",
246
+ {
247
+ light: "github-light",
248
+ dark: "github-dark",
249
+ dim: "github-dark-dimmed",
250
+ },
251
+ {
252
+ defaultColor: "dark",
253
+ }
226
254
  );
227
255
  ```
228
256
 
229
- ### Custom themes
257
+ 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.
258
+
259
+ ### Custom Themes
230
260
 
231
- Pass custom TextMate themes as a JSON object:
261
+ 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
262
 
233
263
  ```tsx
234
- import tokyoNight from '../styles/tokyo-night.json';
264
+ import tokyoNight from "../styles/tokyo-night.json";
235
265
 
236
- // component
266
+ // Using the component
237
267
  <ShikiHighlighter language="tsx" theme={tokyoNight}>
238
- {String(code).trim()}
239
- </ShikiHighlighter>;
268
+ {code.trim()}
269
+ </ShikiHighlighter>
240
270
 
241
- // hook
271
+ // Using the hook
242
272
  const highlightedCode = useShikiHighlighter(code, "tsx", tokyoNight);
243
273
  ```
244
274
 
245
- ### Custom languages
275
+ ### Custom Languages
246
276
 
247
- Pass custom TextMate languages as a JSON object:
277
+ 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
278
 
249
279
  ```tsx
250
- import mcfunction from "../langs/mcfunction.tmLanguage.json"
280
+ import mcfunction from "../langs/mcfunction.tmLanguage.json";
251
281
 
252
- // component
253
- <ShikiHighlighter language={mcfunction} theme="github-dark" >
254
- {String(code).trim()}
255
- </ShikiHighlighter>;
282
+ // Using the component
283
+ <ShikiHighlighter language={mcfunction} theme="github-dark">
284
+ {code.trim()}
285
+ </ShikiHighlighter>
256
286
 
257
- // hook
287
+ // Using the hook
258
288
  const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark");
259
289
  ```
260
- #### Preloading custom languages
261
290
 
262
- For dynamic highlighting scenarios (like LLM chat apps) where language selection happens at runtime, preload custom languages to make them available when needed:
291
+ #### Preloading Custom Languages
292
+
293
+ For dynamic highlighting scenarios where language selection happens at runtime:
263
294
 
264
295
  ```tsx
265
- import mcfunction from "../langs/mcfunction.tmLanguage.json"
266
- import bosque from "../langs/bosque.tmLanguage.json"
296
+ import mcfunction from "../langs/mcfunction.tmLanguage.json";
297
+ import bosque from "../langs/bosque.tmLanguage.json";
267
298
 
268
- // component
269
- <ShikiHighlighter language={mcfunction} theme="github-dark" customLanguages={[mcfunction, bosque]} >
270
- {String(code).trim()}
271
- </ShikiHighlighter>;
299
+ // With the component
300
+ <ShikiHighlighter
301
+ language="typescript"
302
+ theme="github-dark"
303
+ customLanguages={[mcfunction, bosque]}
304
+ >
305
+ {code.trim()}
306
+ </ShikiHighlighter>
272
307
 
273
- // hook
274
- const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark", { customLanguages: [mcfunction, bosque] });
308
+ // With the hook
309
+ const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
310
+ customLanguages: [mcfunction, bosque],
311
+ });
275
312
  ```
276
313
 
277
- ### Custom transformers
314
+ ### Custom Transformers
278
315
 
279
316
  ```tsx
280
- import { customTransformer } from '../utils/shikiTransformers';
317
+ import { customTransformer } from "../utils/shikiTransformers";
281
318
 
282
- // component
283
- <ShikiHighlighter
284
- language="tsx"
285
- transformers={[customTransformer]}
286
- >
287
- {String(code).trim()}
288
- </ShikiHighlighter>;
319
+ // Using the component
320
+ <ShikiHighlighter language="tsx" transformers={[customTransformer]}>
321
+ {code.trim()}
322
+ </ShikiHighlighter>
289
323
 
290
- // hook
291
- const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", [customTransformer]);
324
+ // Using the hook
325
+ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
326
+ transformers: [customTransformer],
327
+ });
292
328
  ```
293
329
 
294
330
  ## Performance
295
331
 
296
- `react-shiki` supports performance-optimized highlighting on the client.
297
-
298
- ### Throttling real-time highlighting
332
+ ### Throttling Real-time Highlighting
299
333
 
300
- Throttling real-time highlighting on the client is possible with the
301
- `delay` option.
334
+ For improved performance when highlighting frequently changing code:
302
335
 
303
336
  ```tsx
304
- const highlightedCode = useShikiHighlighter(code, language, theme, {
337
+ // With the component
338
+ <ShikiHighlighter language="tsx" theme="github-dark" delay={150}>
339
+ {code.trim()}
340
+ </ShikiHighlighter>
341
+
342
+ // With the hook
343
+ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
305
344
  delay: 150,
306
345
  });
307
346
  ```
308
347
 
309
- ### Streaming and LLM chat UI
348
+ ### Streaming and LLM Chat UI
310
349
 
311
350
  `react-shiki` can be used to highlight streamed code from LLM responses in real-time.
312
351
 
@@ -337,7 +376,7 @@ export const CodeHighlight = ({
337
376
 
338
377
  const isInline = node ? isInlineCode(node) : false;
339
378
 
340
- const highlightedCode = useShikiHighlighter(language, code, tokyoNight, {
379
+ const highlightedCode = useShikiHighlighter(code, language, tokyoNight, {
341
380
  delay: 150,
342
381
  });
343
382
 
@@ -384,22 +423,21 @@ export const CodeHighlight = ({
384
423
  }: CodeHighlightProps): JSX.Element => {
385
424
  const match = className?.match(/language-(\w+)/);
386
425
  const language = match ? match[1] : undefined;
426
+ const code = String(children).trim();
387
427
 
388
428
  const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
389
429
 
390
430
  return !isInline ? (
391
431
  <ShikiHighlighter
392
432
  language={language}
393
- theme={"houston"}
433
+ theme="github-dark"
394
434
  delay={150}
395
435
  {...props}
396
436
  >
397
- {String(children).trim()}
437
+ {code}
398
438
  </ShikiHighlighter>
399
439
  ) : (
400
- <code className={className} {...props}>
401
- {children}
402
- </code>
440
+ <code className={className}>{code}</code>
403
441
  );
404
442
  };
405
443
  ```
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,52 @@ 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
52
  /**
53
- * Configuration options for the syntax highlighter
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
+ };
71
+ /**
72
+ * Configuration options specific to react-shiki
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
+ /**
86
+ * Configuration options for the syntax highlighter
87
+ */
88
+ type HighlighterOptions = ReactShikiOptions & Pick<CodeOptionsMultipleThemes<BundledTheme>, 'defaultColor' | 'cssVariablePrefix'> & Pick<CodeToHastOptionsCommon, 'transformers'>;
70
89
 
71
90
  /**
72
91
  * Props for the ShikiHighlighter component
@@ -74,6 +93,7 @@ type HighlighterOptions = {
74
93
  interface ShikiHighlighterProps extends HighlighterOptions {
75
94
  /**
76
95
  * The programming language for syntax highlighting
96
+ * Supports custom textmate grammar objects in addition to Shiki's bundled languages
77
97
  * @see https://shiki.style/languages
78
98
  */
79
99
  language: Language;
@@ -82,21 +102,17 @@ interface ShikiHighlighterProps extends HighlighterOptions {
82
102
  */
83
103
  children: string;
84
104
  /**
85
- * The color theme for syntax highlighting
86
- * Supports Shiki and custom textmate themes
105
+ * The color theme or themes for syntax highlighting
106
+ * Supports single, dual, or multiple themes
107
+ * Supports custom textmate theme objects in addition to Shiki's bundled themes
108
+ *
109
+ * @example
110
+ * theme='github-dark' // single theme
111
+ * theme={{ light: 'github-light', dark: 'github-dark' }} // multi-theme
112
+ *
87
113
  * @see https://shiki.style/themes
88
114
  */
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[];
115
+ theme: Theme | Themes;
100
116
  /**
101
117
  * Controls the application of default styles to the generated code blocks
102
118
  *
@@ -130,10 +146,6 @@ interface ShikiHighlighterProps extends HighlighterOptions {
130
146
  * @default 'pre'
131
147
  */
132
148
  as?: React.ElementType;
133
- /**
134
- * Custom languages to be preloaded for highlighting.
135
- */
136
- customLanguages?: LanguageRegistration[];
137
149
  }
138
150
  /**
139
151
  * ShikiHighlighter is a React component that provides syntax highlighting for code snippets.
@@ -144,26 +156,59 @@ interface ShikiHighlighterProps extends HighlighterOptions {
144
156
  * <ShikiHighlighter
145
157
  * language="typescript"
146
158
  * theme="github-dark"
147
- * delay={100} // Optional throttling for streamed updates
159
+ * delay={100}
160
+ * transformers={[customTransformer]}
161
+ * addDefaultStyles={false}
162
+ * className="code-block"
163
+ * langClassName="lang-label"
164
+ * langStyle={{ color: 'blue' }},
165
+ * style={{ fontSize: '1.2rem' }}
148
166
  * >
149
167
  * {code}
150
168
  * </ShikiHighlighter>
151
169
  * ```
152
170
  */
153
- declare const ShikiHighlighter: ({ language, theme, delay, transformers, addDefaultStyles, style, langStyle, className, langClassName, showLanguage, children: code, as: Element, customLanguages, }: ShikiHighlighterProps) => React.ReactElement;
171
+ declare const ShikiHighlighter: ({ language, theme, delay, transformers, defaultColor, cssVariablePrefix, addDefaultStyles, style, langStyle, className, langClassName, showLanguage, children: code, as: Element, customLanguages, }: ShikiHighlighterProps) => React.ReactElement;
154
172
 
155
173
  /**
156
174
  * A React hook that provides syntax highlighting using Shiki.
157
- * Supports optional throttled highlights and custom themes.
175
+ * Supports single theme and multi-theme highlighting, custom themes
176
+ * and languages, custom transformers, and optional throttling.
158
177
  *
159
- * @example
178
+ * ```ts
179
+ * // Basic Usage
180
+ * const highlightedCode = useShikiHighlighter( code, 'typescript', 'github-dark');
181
+ * ```
182
+ *
183
+ * ```ts
184
+ * // Custom Languages, Transformers, and Delay
160
185
  * const highlightedCode = useShikiHighlighter(code, language, theme, {
161
- * transformers: [removeTabIndexFromPre],
186
+ * transformers: [customTransformer],
162
187
  * delay: 150
163
188
  * customLanguages: ['bosque', 'mcfunction']
164
189
  * });
190
+ * ```
191
+ *
192
+ * ```ts
193
+ * // Multiple Themes, Custom Languages, Delay, and Custom Transformers
194
+ * const highlightedCode = useShikiHighlighter(
195
+ * code,
196
+ * language,
197
+ * {
198
+ * light: 'github-light',
199
+ * dark: 'github-dark',
200
+ * dim: 'github-dark-dimmed'
201
+ * },
202
+ * {
203
+ * delay: 150,
204
+ * defaultColor: 'dim',
205
+ * transformers: [customTransformer],
206
+ * customLanguages: ['bosque', 'mcfunction']
207
+ * }
208
+ * );
209
+ * ```
165
210
  */
166
- declare const useShikiHighlighter: (code: string, lang: Language, theme: Theme, options?: HighlighterOptions) => ReactNode;
211
+ declare const useShikiHighlighter: (code: string, lang: Language, themeInput: Theme | Themes, options?: HighlighterOptions) => ReactNode;
167
212
 
168
213
  /**
169
214
  * Rehype plugin to add an 'inline' property to <code> elements
@@ -175,7 +220,7 @@ declare const useShikiHighlighter: (code: string, lang: Language, theme: Theme,
175
220
  * @example
176
221
  * <ReactMarkdown rehypePlugins={[rehypeInlineCodeProperty]} />
177
222
  */
178
- declare function rehypeInlineCodeProperty(): (tree: Root) => undefined;
223
+ declare function rehypeInlineCodeProperty(): (tree: any) => undefined;
179
224
  /**
180
225
  * Function to determine if code is inline based on the presence of line breaks
181
226
  *
@@ -184,4 +229,4 @@ declare function rehypeInlineCodeProperty(): (tree: Root) => undefined;
184
229
  */
185
230
  declare const isInlineCode: (node: Element) => boolean;
186
231
 
187
- export { ShikiHighlighter as default, isInlineCode, rehypeInlineCodeProperty, useShikiHighlighter };
232
+ export { type HighlighterOptions, type Language, type Theme, type Themes, ShikiHighlighter as default, isInlineCode, rehypeInlineCodeProperty, useShikiHighlighter };
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,i]),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),m=t.customLanguages?Array.isArray(t.customLanguages)?t.customLanguages:[t.customLanguages]:[],u=m.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:g,resolvedLanguage:c}=O(()=>f(o,m),[o,u]),x=`${g}-${d}`,p=_({nextAllowedTime:0,timeoutId:void 0}),S=()=>{let{defaultColor:n,cssVariablePrefix:l}=t,b={lang:g,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,g,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:m=!0,style:u,langStyle:h,className:y,langClassName:d,showLanguage:T=!0,children:L,as:C="pre",customLanguages:a})=>{let g={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,g);return A.createElement(C,{"data-testid":"shiki-container",className:N("relative","not-prose",m&&"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, theme]);\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":"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","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/component.tsx","../src/hook.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 './hook';\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 './extended-types';\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 './extended-types';\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.1",
4
+ "version": "0.5.1",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Bassim Shahidy",