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 +212 -166
- package/dist/index.d.ts +77 -35
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
|
|
24
|
-
- [Custom
|
|
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
|
|
27
|
-
- [Streaming and LLM
|
|
28
|
-
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
47
|
+
npm install react-shiki
|
|
50
48
|
```
|
|
51
49
|
|
|
52
50
|
## Usage
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
to highlight code.
|
|
52
|
+
### Basic Usage
|
|
56
53
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
100
|
+
**The hook accepts the following parameters:**
|
|
123
101
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
{
|
|
145
|
-
|
|
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
|
|
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
|
-
###
|
|
154
|
+
### Handling Inline Code
|
|
155
|
+
|
|
156
|
+
**Method 1: Using the `isInlineCode` helper:**
|
|
165
157
|
|
|
166
|
-
There are two
|
|
167
|
-
`react-shiki` exports `isInlineCode` which parses the `node`
|
|
168
|
-
prop to determine if the code is inline
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
272
|
+
import tokyoNight from "../styles/tokyo-night.json";
|
|
235
273
|
|
|
236
|
-
// component
|
|
274
|
+
// Using the component
|
|
237
275
|
<ShikiHighlighter language="tsx" theme={tokyoNight}>
|
|
238
|
-
{
|
|
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
|
|
283
|
+
### Custom Languages
|
|
246
284
|
|
|
247
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
316
|
+
// With the hook
|
|
317
|
+
const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
|
|
318
|
+
customLanguages: [mcfunction, bosque],
|
|
319
|
+
});
|
|
275
320
|
```
|
|
276
321
|
|
|
277
|
-
### Custom
|
|
322
|
+
### Custom Transformers
|
|
278
323
|
|
|
279
324
|
```tsx
|
|
280
|
-
import { customTransformer } from
|
|
325
|
+
import { customTransformer } from "../utils/shikiTransformers";
|
|
281
326
|
|
|
282
|
-
// component
|
|
283
|
-
<ShikiHighlighter
|
|
284
|
-
|
|
285
|
-
|
|
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",
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
### Throttling real-time highlighting
|
|
340
|
+
### Throttling Real-time Highlighting
|
|
299
341
|
|
|
300
|
-
|
|
301
|
-
`delay` option.
|
|
342
|
+
For improved performance when highlighting frequently changing code:
|
|
302
343
|
|
|
303
344
|
```tsx
|
|
304
|
-
|
|
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
|
|
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(
|
|
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=
|
|
441
|
+
theme="github-dark"
|
|
394
442
|
delay={150}
|
|
395
443
|
{...props}
|
|
396
444
|
>
|
|
397
|
-
{
|
|
445
|
+
{code}
|
|
398
446
|
</ShikiHighlighter>
|
|
399
447
|
) : (
|
|
400
|
-
<code className={className}
|
|
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,
|
|
3
|
-
import { Element
|
|
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
|
-
*
|
|
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
|
|
48
|
+
* A Shiki BundledTheme or a custom textmate theme object
|
|
49
49
|
* @see https://shiki.style/themes
|
|
50
50
|
*/
|
|
51
|
-
type Theme =
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
172
|
+
* Supports single theme and multi-theme highlighting, custom themes
|
|
173
|
+
* and languages, custom transformers, and optional throttling.
|
|
158
174
|
*
|
|
159
|
-
*
|
|
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: [
|
|
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,
|
|
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:
|
|
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
|
|
2
|
-
`);import
|
|
3
|
-
`),
|
|
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":"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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/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"]}
|