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 +204 -166
- package/dist/index.d.ts +82 -37
- 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,30 @@ 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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- [
|
|
28
|
-
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
48
|
+
npm i react-shiki
|
|
50
49
|
```
|
|
51
50
|
|
|
52
51
|
## Usage
|
|
53
52
|
|
|
54
|
-
You can use the `ShikiHighlighter` component
|
|
55
|
-
to highlight code.
|
|
53
|
+
You can use either the `ShikiHighlighter` component or the `useShikiHighlighter` hook to highlight code.
|
|
56
54
|
|
|
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.
|
|
55
|
+
**Using the Component:**
|
|
61
56
|
|
|
62
57
|
```tsx
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
-
###
|
|
81
|
+
### Common Configuration Options
|
|
123
82
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
className?: string | undefined;
|
|
130
|
-
children?: ReactNode | undefined;
|
|
131
|
-
node?: Element | undefined;
|
|
132
|
-
}
|
|
94
|
+
### Component-specific Props
|
|
133
95
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
{
|
|
145
|
-
|
|
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
|
|
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
|
-
###
|
|
146
|
+
### Handling Inline Code
|
|
165
147
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
264
|
+
import tokyoNight from "../styles/tokyo-night.json";
|
|
235
265
|
|
|
236
|
-
// component
|
|
266
|
+
// Using the component
|
|
237
267
|
<ShikiHighlighter language="tsx" theme={tokyoNight}>
|
|
238
|
-
{
|
|
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
|
|
275
|
+
### Custom Languages
|
|
246
276
|
|
|
247
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
308
|
+
// With the hook
|
|
309
|
+
const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
|
|
310
|
+
customLanguages: [mcfunction, bosque],
|
|
311
|
+
});
|
|
275
312
|
```
|
|
276
313
|
|
|
277
|
-
### Custom
|
|
314
|
+
### Custom Transformers
|
|
278
315
|
|
|
279
316
|
```tsx
|
|
280
|
-
import { customTransformer } from
|
|
317
|
+
import { customTransformer } from "../utils/shikiTransformers";
|
|
281
318
|
|
|
282
|
-
// component
|
|
283
|
-
<ShikiHighlighter
|
|
284
|
-
|
|
285
|
-
|
|
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",
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
### Throttling real-time highlighting
|
|
332
|
+
### Throttling Real-time Highlighting
|
|
299
333
|
|
|
300
|
-
|
|
301
|
-
`delay` option.
|
|
334
|
+
For improved performance when highlighting frequently changing code:
|
|
302
335
|
|
|
303
336
|
```tsx
|
|
304
|
-
|
|
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
|
|
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(
|
|
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=
|
|
433
|
+
theme="github-dark"
|
|
394
434
|
delay={150}
|
|
395
435
|
{...props}
|
|
396
436
|
>
|
|
397
|
-
{
|
|
437
|
+
{code}
|
|
398
438
|
</ShikiHighlighter>
|
|
399
439
|
) : (
|
|
400
|
-
<code className={className}
|
|
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,
|
|
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,52 @@ 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
52
|
/**
|
|
53
|
-
*
|
|
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
|
|
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
|
+
/**
|
|
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
|
|
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}
|
|
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
|
|
175
|
+
* Supports single theme and multi-theme highlighting, custom themes
|
|
176
|
+
* and languages, custom transformers, and optional throttling.
|
|
158
177
|
*
|
|
159
|
-
*
|
|
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: [
|
|
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,
|
|
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:
|
|
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
|
|
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),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"]}
|