react-shiki 0.2.4 → 0.4.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 +90 -28
- package/dist/index.d.ts +187 -0
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# 🎨 [react-shiki](https://npmjs.com/react-shiki)
|
|
2
2
|
|
|
3
3
|
> [!NOTE]
|
|
4
|
-
> This
|
|
5
|
-
>
|
|
4
|
+
> This library is still in development, more features will
|
|
5
|
+
> continue to be implemented, and API may change.
|
|
6
|
+
> Contributions are welcome!
|
|
6
7
|
|
|
7
8
|
Performant client side syntax highlighting component + hook
|
|
8
|
-
for react
|
|
9
|
+
for react built with [Shiki](https://shiki.matsu.io/)
|
|
9
10
|
|
|
10
11
|
[See the demo page with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/)
|
|
11
12
|
|
|
@@ -18,6 +19,9 @@ for react using [Shiki](https://shiki.matsu.io/)
|
|
|
18
19
|
- [`react-markdown`](#react-markdown)
|
|
19
20
|
- [Check if code is inline](#check-if-code-is-inline)
|
|
20
21
|
- [Custom themes](#custom-themes)
|
|
22
|
+
- [Custom languages](#custom-languages)
|
|
23
|
+
- [Preloading custom languages](#preloading-custom-languages)
|
|
24
|
+
- [Custom transformers](#custom-transformers)
|
|
21
25
|
- [Performance](#performance)
|
|
22
26
|
- [Throttling real-time highlighting](#throttling-real-time-highlighting)
|
|
23
27
|
- [Streaming and LLM chat UI](#streaming-and-llm-chat-ui)
|
|
@@ -28,21 +32,21 @@ for react using [Shiki](https://shiki.matsu.io/)
|
|
|
28
32
|
- 🖼️ Provides a `ShikiHighlighter` component for highlighting code as children,
|
|
29
33
|
as well as a `useShikiHighlighter` hook for more flexibility
|
|
30
34
|
- 🔐 No `dangerouslySetInnerHTML`, output from Shiki is parsed using `html-react-parser`
|
|
31
|
-
- 📦 Supports all Shiki languages and themes
|
|
32
|
-
- 🖌️ Full support for custom TextMate themes
|
|
33
|
-
-
|
|
35
|
+
- 📦 Supports all built-in Shiki languages and themes
|
|
36
|
+
- 🖌️ Full support for custom TextMate themes and languages
|
|
37
|
+
- 🔧 Supports passing custom Shiki transformers to the highlighter
|
|
38
|
+
- 🚰 Performant highlighting of streamed code, with optional throttling
|
|
34
39
|
- 📚 Includes minimal default styles for code blocks
|
|
35
40
|
- 🚀 Shiki dynamically imports only the languages and themes used on a page,
|
|
36
41
|
optimizing for performance
|
|
37
42
|
- 🖥️ `ShikiHighlighter` component displays a language label for each code block
|
|
38
43
|
when `showLanguage` is set to `true` (default)
|
|
39
|
-
- 🎨
|
|
40
|
-
a `style` object or a `className`
|
|
44
|
+
- 🎨 Customizable styling of generated code blocks and language labels
|
|
41
45
|
|
|
42
46
|
## Installation
|
|
43
47
|
|
|
44
48
|
```bash
|
|
45
|
-
|
|
49
|
+
pnpm install react-shiki
|
|
46
50
|
```
|
|
47
51
|
|
|
48
52
|
## Usage
|
|
@@ -93,6 +97,7 @@ The component accepts several props in addition to language and theme:
|
|
|
93
97
|
- `style: object` - Inline style object to be passed to the component
|
|
94
98
|
- `langStyle: object` - Inline style object to be passed to the language label
|
|
95
99
|
- `langClassName: string` - Class names to be passed to the language label
|
|
100
|
+
- `customLanguages: LanguageRegistration[]` - Custom languages to be preloaded for highlighting
|
|
96
101
|
|
|
97
102
|
```tsx
|
|
98
103
|
function Houston() {
|
|
@@ -136,7 +141,7 @@ export const CodeHighlight = ({
|
|
|
136
141
|
const language = match ? match[1] : undefined;
|
|
137
142
|
|
|
138
143
|
<ShikiHighlighter language={language} theme={"houston"} {...props}>
|
|
139
|
-
{String(children)}
|
|
144
|
+
{String(children).trim()}
|
|
140
145
|
</ShikiHighlighter>;
|
|
141
146
|
};
|
|
142
147
|
```
|
|
@@ -158,16 +163,16 @@ import { CodeHighlight } from "./CodeHighlight";
|
|
|
158
163
|
|
|
159
164
|
### Check if code is inline
|
|
160
165
|
|
|
161
|
-
There are two ways to check if a code block is inline:
|
|
162
|
-
`react-shiki` exports `isInlineCode
|
|
163
|
-
|
|
166
|
+
There are two built-in ways to check if a code block is inline, both provide the same result:
|
|
167
|
+
`react-shiki` exports `isInlineCode` which parses the `node`
|
|
168
|
+
prop to determine if the code is inline based on the presence of line breaks:
|
|
164
169
|
|
|
165
170
|
```tsx
|
|
166
171
|
const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
|
|
167
172
|
|
|
168
173
|
return !isInline ? (
|
|
169
174
|
<ShikiHighlighter language={language} theme={"houston"} {...props}>
|
|
170
|
-
{String(children)}
|
|
175
|
+
{String(children).trim()}
|
|
171
176
|
</ShikiHighlighter>
|
|
172
177
|
) : (
|
|
173
178
|
<code className={className} {...props}>
|
|
@@ -176,8 +181,9 @@ return !isInline ? (
|
|
|
176
181
|
);
|
|
177
182
|
```
|
|
178
183
|
|
|
179
|
-
`react-shiki` also exports `rehypeInlineCodeProperty`, a
|
|
180
|
-
to determine if code is inline
|
|
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
|
|
186
|
+
the presence of a `<pre>` tag as a parent of `<code>`.
|
|
181
187
|
It's passed as a rehype plugin to `react-markdown`:
|
|
182
188
|
|
|
183
189
|
```tsx
|
|
@@ -194,7 +200,7 @@ import { rehypeInlineCodeProperty } from "react-shiki";
|
|
|
194
200
|
</ReactMarkdown>;
|
|
195
201
|
```
|
|
196
202
|
|
|
197
|
-
|
|
203
|
+
Now `inline` can be accessed as a prop in the `CodeHighlight` component:
|
|
198
204
|
|
|
199
205
|
```tsx
|
|
200
206
|
const CodeHighlight = ({
|
|
@@ -206,11 +212,12 @@ const CodeHighlight = ({
|
|
|
206
212
|
}: CodeHighlightProps): JSX.Element => {
|
|
207
213
|
const match = className?.match(/language-(\w+)/);
|
|
208
214
|
const language = match ? match[1] : undefined;
|
|
215
|
+
const code = String(children).trim();
|
|
209
216
|
|
|
210
217
|
|
|
211
218
|
return !inline ? (
|
|
212
219
|
<ShikiHighlighter language={language} theme={"houston"} {...props}>
|
|
213
|
-
{
|
|
220
|
+
{code}
|
|
214
221
|
</ShikiHighlighter>
|
|
215
222
|
) : (
|
|
216
223
|
<code className={className} {...props}>
|
|
@@ -221,12 +228,67 @@ return !inline ? (
|
|
|
221
228
|
|
|
222
229
|
### Custom themes
|
|
223
230
|
|
|
224
|
-
|
|
225
|
-
import tokyoNight from '@styles/tokyo-night.mjs';
|
|
231
|
+
Pass custom TextMate themes as a JSON object:
|
|
226
232
|
|
|
233
|
+
```tsx
|
|
234
|
+
import tokyoNight from '../styles/tokyo-night.json';
|
|
235
|
+
|
|
236
|
+
// component
|
|
227
237
|
<ShikiHighlighter language="tsx" theme={tokyoNight}>
|
|
228
|
-
{String(code)}
|
|
238
|
+
{String(code).trim()}
|
|
229
239
|
</ShikiHighlighter>;
|
|
240
|
+
|
|
241
|
+
// hook
|
|
242
|
+
const highlightedCode = useShikiHighlighter(code, "tsx", tokyoNight);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Custom languages
|
|
246
|
+
|
|
247
|
+
Pass custom TextMate languages as a JSON object:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
import mcfunction from "../langs/mcfunction.tmLanguage.json"
|
|
251
|
+
|
|
252
|
+
// component
|
|
253
|
+
<ShikiHighlighter language={mcfunction} theme="github-dark" >
|
|
254
|
+
{String(code).trim()}
|
|
255
|
+
</ShikiHighlighter>;
|
|
256
|
+
|
|
257
|
+
// hook
|
|
258
|
+
const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark");
|
|
259
|
+
```
|
|
260
|
+
#### Preloading custom languages
|
|
261
|
+
|
|
262
|
+
For dynamic highlighting scenarios (like LLM chat apps) where language selection happens at runtime, preload custom languages to make them available when needed:
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import mcfunction from "../langs/mcfunction.tmLanguage.json"
|
|
266
|
+
import bosque from "../langs/bosque.tmLanguage.json"
|
|
267
|
+
|
|
268
|
+
// component
|
|
269
|
+
<ShikiHighlighter language={mcfunction} theme="github-dark" customLanguages={[mcfunction, bosque]} >
|
|
270
|
+
{String(code).trim()}
|
|
271
|
+
</ShikiHighlighter>;
|
|
272
|
+
|
|
273
|
+
// hook
|
|
274
|
+
const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark", { customLanguages: [mcfunction, bosque] });
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Custom transformers
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
import { customTransformer } from '../utils/shikiTransformers';
|
|
281
|
+
|
|
282
|
+
// component
|
|
283
|
+
<ShikiHighlighter
|
|
284
|
+
language="tsx"
|
|
285
|
+
transformers={[customTransformer]}
|
|
286
|
+
>
|
|
287
|
+
{String(code).trim()}
|
|
288
|
+
</ShikiHighlighter>;
|
|
289
|
+
|
|
290
|
+
// hook
|
|
291
|
+
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", [customTransformer]);
|
|
230
292
|
```
|
|
231
293
|
|
|
232
294
|
## Performance
|
|
@@ -248,12 +310,12 @@ const highlightedCode = useShikiHighlighter(code, language, theme, {
|
|
|
248
310
|
|
|
249
311
|
`react-shiki` can be used to highlight streamed code from LLM responses in real-time.
|
|
250
312
|
|
|
251
|
-
I use it for an
|
|
252
|
-
|
|
313
|
+
I use it for an LLM chatbot UI, it renders markdown and highlights
|
|
314
|
+
code in memoized chat messages.
|
|
253
315
|
|
|
254
316
|
Using `useShikiHighlighter` hook:
|
|
255
317
|
|
|
256
|
-
```tsx
|
|
318
|
+
```tsx
|
|
257
319
|
import type { ReactNode } from "react";
|
|
258
320
|
import { isInlineCode, useShikiHighlighter, type Element } from "react-shiki";
|
|
259
321
|
import tokyoNight from "@styles/tokyo-night.mjs";
|
|
@@ -270,7 +332,7 @@ export const CodeHighlight = ({
|
|
|
270
332
|
node,
|
|
271
333
|
...props
|
|
272
334
|
}: CodeHighlightProps) => {
|
|
273
|
-
const code = String(children);
|
|
335
|
+
const code = String(children).trim();
|
|
274
336
|
const language = className?.match(/language-(\w+)/)?.[1];
|
|
275
337
|
|
|
276
338
|
const isInline = node ? isInlineCode(node) : false;
|
|
@@ -304,7 +366,7 @@ export const CodeHighlight = ({
|
|
|
304
366
|
|
|
305
367
|
Or using the `ShikiHighlighter` component:
|
|
306
368
|
|
|
307
|
-
```tsx
|
|
369
|
+
```tsx
|
|
308
370
|
import type { ReactNode } from "react";
|
|
309
371
|
import ShikiHighlighter, { isInlineCode, type Element } from "react-shiki";
|
|
310
372
|
|
|
@@ -332,7 +394,7 @@ export const CodeHighlight = ({
|
|
|
332
394
|
delay={150}
|
|
333
395
|
{...props}
|
|
334
396
|
>
|
|
335
|
-
{String(children)}
|
|
397
|
+
{String(children).trim()}
|
|
336
398
|
</ShikiHighlighter>
|
|
337
399
|
) : (
|
|
338
400
|
<code className={className} {...props}>
|
|
@@ -344,7 +406,7 @@ export const CodeHighlight = ({
|
|
|
344
406
|
|
|
345
407
|
Passed to `react-markdown` as a `code` component in memo-ized chat messages:
|
|
346
408
|
|
|
347
|
-
```tsx
|
|
409
|
+
```tsx
|
|
348
410
|
const RenderedMessage = React.memo(({ message }: { message: Message }) => (
|
|
349
411
|
<div className={cn(messageStyles[message.role])}>
|
|
350
412
|
<ReactMarkdown components={{ code: CodeHighlight }}>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { LanguageRegistration as LanguageRegistration$1, ShikiTransformer, BundledLanguage, SpecialLanguage, ThemeRegistration, BundledTheme } from 'shiki';
|
|
3
|
+
import { Element, Root } from 'hast';
|
|
4
|
+
export { Element } from 'hast';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attribution:
|
|
8
|
+
* This code was written by github:hippotastic in expressive-code/expressive-code
|
|
9
|
+
* https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-shiki/src/languages.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type IShikiRawRepository = LanguageRegistration$1['repository'];
|
|
13
|
+
type IShikiRawRule = IShikiRawRepository[keyof IShikiRawRepository];
|
|
14
|
+
type ILocation = IShikiRawRepository['$vscodeTextmateLocation'];
|
|
15
|
+
interface ILocatable {
|
|
16
|
+
readonly $vscodeTextmateLocation?: ILocation;
|
|
17
|
+
}
|
|
18
|
+
interface IRawRepositoryMap {
|
|
19
|
+
[name: string]: IRawRule;
|
|
20
|
+
}
|
|
21
|
+
type IRawRepository = IRawRepositoryMap & ILocatable;
|
|
22
|
+
interface IRawCapturesMap {
|
|
23
|
+
[captureId: string]: IRawRule;
|
|
24
|
+
}
|
|
25
|
+
type IRawCaptures = IRawCapturesMap & ILocatable;
|
|
26
|
+
interface IRawRule extends Omit<IShikiRawRule, 'applyEndPatternLast' | 'captures' | 'patterns'> {
|
|
27
|
+
readonly applyEndPatternLast?: boolean | number;
|
|
28
|
+
readonly captures?: IRawCaptures;
|
|
29
|
+
readonly comment?: string;
|
|
30
|
+
readonly patterns?: IRawRule[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A less strict version of Shiki's `LanguageRegistration` interface that aligns better with
|
|
34
|
+
* actual grammars found in the wild. This version attempts to reduce the amount
|
|
35
|
+
* of type errors that would occur when importing and adding external grammars,
|
|
36
|
+
* while still being supported by the language processing code.
|
|
37
|
+
*/
|
|
38
|
+
interface LanguageRegistration extends Omit<LanguageRegistration$1, 'repository'> {
|
|
39
|
+
repository?: IRawRepository;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Languages for syntax highlighting.
|
|
44
|
+
* @see https://shiki.style/languages
|
|
45
|
+
*/
|
|
46
|
+
type Language = BundledLanguage | LanguageRegistration | SpecialLanguage | (string & {}) | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* A textmate theme object or a Shiki BundledTheme
|
|
49
|
+
* @see https://shiki.style/themes
|
|
50
|
+
*/
|
|
51
|
+
type Theme = ThemeRegistration | BundledTheme;
|
|
52
|
+
/**
|
|
53
|
+
* Configuration options for the syntax highlighter
|
|
54
|
+
*/
|
|
55
|
+
type HighlighterOptions = {
|
|
56
|
+
/**
|
|
57
|
+
* Minimum time (in milliseconds) between highlight operations.
|
|
58
|
+
* @default undefined (no throttling)
|
|
59
|
+
*/
|
|
60
|
+
delay?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Custom Shiki transformers to apply to the highlighted code.
|
|
63
|
+
*/
|
|
64
|
+
transformers?: ShikiTransformer[];
|
|
65
|
+
/**
|
|
66
|
+
* Custom textmate grammar to be preloaded for highlighting.
|
|
67
|
+
*/
|
|
68
|
+
customLanguages?: LanguageRegistration | LanguageRegistration[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Props for the ShikiHighlighter component
|
|
73
|
+
*/
|
|
74
|
+
interface ShikiHighlighterProps extends HighlighterOptions {
|
|
75
|
+
/**
|
|
76
|
+
* The programming language for syntax highlighting
|
|
77
|
+
* @see https://shiki.style/languages
|
|
78
|
+
*/
|
|
79
|
+
language: Language;
|
|
80
|
+
/**
|
|
81
|
+
* The code to be highlighted
|
|
82
|
+
*/
|
|
83
|
+
children: string;
|
|
84
|
+
/**
|
|
85
|
+
* The color theme for syntax highlighting
|
|
86
|
+
* Supports Shiki and custom textmate themes
|
|
87
|
+
* @see https://shiki.style/themes
|
|
88
|
+
*/
|
|
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[];
|
|
100
|
+
/**
|
|
101
|
+
* Controls the application of default styles to the generated code blocks
|
|
102
|
+
*
|
|
103
|
+
* Default styles include padding, overflow handling, border radius, language label styling, and font settings
|
|
104
|
+
* @default true
|
|
105
|
+
*/
|
|
106
|
+
addDefaultStyles?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Add custom inline styles to the generated code block
|
|
109
|
+
*/
|
|
110
|
+
style?: React.CSSProperties;
|
|
111
|
+
/**
|
|
112
|
+
* Add custom inline styles to the language label
|
|
113
|
+
*/
|
|
114
|
+
langStyle?: React.CSSProperties;
|
|
115
|
+
/**
|
|
116
|
+
* Add custom CSS class names to the generated code block
|
|
117
|
+
*/
|
|
118
|
+
className?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Add custom CSS class names to the language label
|
|
121
|
+
*/
|
|
122
|
+
langClassName?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Whether to show the language label
|
|
125
|
+
* @default true
|
|
126
|
+
*/
|
|
127
|
+
showLanguage?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* The HTML element that wraps the generated code block.
|
|
130
|
+
* @default 'pre'
|
|
131
|
+
*/
|
|
132
|
+
as?: React.ElementType;
|
|
133
|
+
/**
|
|
134
|
+
* Custom languages to be preloaded for highlighting.
|
|
135
|
+
*/
|
|
136
|
+
customLanguages?: LanguageRegistration[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* ShikiHighlighter is a React component that provides syntax highlighting for code snippets.
|
|
140
|
+
* It uses Shiki to render beautiful, theme-based syntax highlighting with optimized performance.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* <ShikiHighlighter
|
|
145
|
+
* language="typescript"
|
|
146
|
+
* theme="github-dark"
|
|
147
|
+
* delay={100} // Optional throttling for streamed updates
|
|
148
|
+
* >
|
|
149
|
+
* {code}
|
|
150
|
+
* </ShikiHighlighter>
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
declare const ShikiHighlighter: ({ language, theme, delay, transformers, addDefaultStyles, style, langStyle, className, langClassName, showLanguage, children: code, as: Element, customLanguages, }: ShikiHighlighterProps) => React.ReactElement;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A React hook that provides syntax highlighting using Shiki.
|
|
157
|
+
* Supports optional throttled highlights and custom themes.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const highlightedCode = useShikiHighlighter(code, language, theme, {
|
|
161
|
+
* transformers: [removeTabIndexFromPre],
|
|
162
|
+
* delay: 150
|
|
163
|
+
* customLanguages: ['bosque', 'mcfunction']
|
|
164
|
+
* });
|
|
165
|
+
*/
|
|
166
|
+
declare const useShikiHighlighter: (code: string, lang: Language, theme: Theme, options?: HighlighterOptions) => ReactNode;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Rehype plugin to add an 'inline' property to <code> elements
|
|
170
|
+
* Sets 'inline' property to true if the <code> is not within a <pre> tag
|
|
171
|
+
*
|
|
172
|
+
* Pass this plugin to the `rehypePlugins` prop of react-markdown
|
|
173
|
+
* You can then access `inline` as a prop in components passed to react-markdown
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* <ReactMarkdown rehypePlugins={[rehypeInlineCodeProperty]} />
|
|
177
|
+
*/
|
|
178
|
+
declare function rehypeInlineCodeProperty(): (tree: Root) => undefined;
|
|
179
|
+
/**
|
|
180
|
+
* Function to determine if code is inline based on the presence of line breaks
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* const isInline = node && isInlineCode(node: Element)
|
|
184
|
+
*/
|
|
185
|
+
declare const isInlineCode: (node: Element) => boolean;
|
|
186
|
+
|
|
187
|
+
export { ShikiHighlighter as default, isInlineCode, rehypeInlineCodeProperty, useShikiHighlighter };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
function
|
|
2
|
-
`);import
|
|
1
|
+
function L(e,{insertAt:r}={}){if(!e||typeof document>"u")return;let i=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style");t.type="text/css",r==="top"&&i.firstChild?i.insertBefore(t,i.firstChild):i.appendChild(t),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document.createTextNode(e))}L(`.relative{position:relative}.defaultStyles pre{overflow:auto;border-radius:.5rem;padding:1.25rem 1.5rem}.languageLabel{position:absolute;right:.75rem;top:.5rem;font-family:monospace;font-size:.75rem;letter-spacing:-.05em;color:#6b7280d9}
|
|
2
|
+
`);import R from"react";import{clsx as b}from"clsx";import{useEffect as O,useRef as j,useState as M}from"react";import z from"html-react-parser";import{createHighlighter as H,createSingletonShorthands as $}from"shiki";import{visit as w}from"unist-util-visit";import{bundledLanguages as E,isSpecialLang as N}from"shiki";function P(){return function(e){w(e,"element",function(r,i,t){r.tagName==="code"&&t.tagName!=="pre"&&(r.properties.inline=!0)})}}var A=e=>!(e.children||[]).filter(i=>i.type==="text").map(i=>i.value).join("").includes(`
|
|
3
|
+
`),S={pre(e){return"properties"in e&&(e.properties.tabindex="-1"),e}},T=(e,r,i)=>{let t=Date.now();clearTimeout(r.current.timeoutId);let o=Math.max(0,r.current.nextAllowedTime-t);r.current.timeoutId=setTimeout(()=>{e().catch(console.error),r.current.nextAllowedTime=t+i},o)},u=(e,r=[])=>{if(e&&typeof e=="object")return{isCustom:!0,languageId:e.name,displayLanguageId:e.name,resolvedLanguage:e};if(typeof e=="string"){let i=e;if(e in E||N(e))return{isCustom:!1,languageId:e,displayLanguageId:i};let t=r.find(o=>o.fileTypes?.includes(e)||o.scopeName?.split(".")[1]===e||o.name?.toLowerCase()===e.toLowerCase());return t?{isCustom:!0,languageId:t.name,displayLanguageId:i,resolvedLanguage:t}:{isCustom:!1,languageId:"plaintext",displayLanguageId:i}}return{isCustom:!1,languageId:"plaintext",displayLanguageId:"plaintext"}};var B=$(H),I=new Map,x=(e,r,i,t={})=>{let[o,p]=M(null),d=typeof i=="string"?i:i.name,h=t.customLanguages?Array.isArray(t.customLanguages)?t.customLanguages:[t.customLanguages]:[],{isCustom:c,languageId:f,resolvedLanguage:g}=u(r,h),m=j({nextAllowedTime:0,timeoutId:void 0}),n=async(s,l)=>{let a=I.get(s);return a||(a=H({langs:[l],themes:[i]}),I.set(s,a)),a};return O(()=>{let s=!0,l=[S,...t.transformers||[]],a=async()=>{let y=await(c&&g?await n(`${g.name}--${d}`,g):B).codeToHtml(e,{lang:f,theme:i,transformers:l});s&&p(z(y))};return t.delay?T(a,m,t.delay):a().catch(console.error),()=>{s=!1,clearTimeout(m.current.timeoutId)}},[e,r]),o};var D=({language:e,theme:r,delay:i,transformers:t,addDefaultStyles:o=!0,style:p,langStyle:d,className:h,langClassName:c,showLanguage:f=!0,children:g,as:m="pre",customLanguages:n})=>{let s={delay:i,transformers:t,customLanguages:n},l=n?Array.isArray(n)?n:[n]:[],{isCustom:a,languageId:C,displayLanguageId:y,resolvedLanguage:v}=u(e,l),k=x(g,e,r,s);return R.createElement(m,{"data-testid":"shiki-container",className:b("relative","not-prose",o&&"defaultStyles",h),style:p,id:"shiki-container"},f&&e?R.createElement("span",{className:b("languageLabel",c),style:d,id:"language-label"},a?`${v?.scopeName.split(".")[1]}`:y||C):null,k)};export{D as default,A as isInlineCode,P as rehypeInlineCodeProperty,x as useShikiHighlighter};
|
|
3
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';\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 * 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/**\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 addDefaultStyles = true,\n style,\n langStyle,\n className,\n langClassName,\n showLanguage = true,\n children: code,\n as: Element = 'pre',\n}: ShikiHighlighterProps): React.ReactElement => {\n const highlightedCode = useShikiHighlighter(code, language, theme, { delay });\n\n return (\n <Element\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 {language}\n </span>\n ) : null}\n {highlightedCode}\n </Element>\n );\n};\n","import { useEffect, useRef, useState, type ReactNode } from 'react';\nimport parse from 'html-react-parser';\n\nimport {\n createHighlighter,\n ShikiError,\n type BundledLanguage,\n type Highlighter\n} from 'shiki';\n\nimport type {\n Language,\n Theme,\n HighlighterOptions,\n TimeoutState\n} from './types';\n\nimport { removeTabIndexFromPre } from './utils';\n\n\n/**\n* Singleton highlighter instance\n*/\nlet highlighterPromise: Promise<Highlighter> | null = null;\n\n\n/**\n* Creates or returns the singleton highlighter instance\n*/\nconst makeHighlighter = async (theme: Theme): Promise<Highlighter> => {\n if (!highlighterPromise) {\n highlighterPromise = createHighlighter({\n themes: [],\n langs: []\n });\n }\n\n const highlighter = await highlighterPromise;\n await loadTheme(highlighter, theme);\n return highlighter;\n};\n\n\n/**\n* Loads a theme dynamically if it hasn't been loaded yet\n*/\nconst loadTheme = async (\n highlighter: Highlighter,\n theme: Theme\n): Promise<void> => {\n try {\n await highlighter.loadTheme(theme);\n } catch (error) {\n if (error instanceof ShikiError) {\n console.warn('Error loading theme:', error.message);\n }\n throw error;\n }\n};\n\n\n/**\n* Loads a language dynamically, falling back to plaintext if not available\n*/\nconst loadLanguage = async (\n highlighter: Highlighter,\n lang: Language\n): Promise<string> => {\n const resolvedLanguage = lang ?? 'plaintext';\n\n try {\n await highlighter.loadLanguage(resolvedLanguage as BundledLanguage);\n return resolvedLanguage;\n } catch (error) {\n if (error instanceof ShikiError && error.message.includes('not included in this bundle')) {\n console.warn(`Language '${resolvedLanguage}' not supported, falling back to plaintext`);\n }\n return 'plaintext';\n }\n};\n\n\n/**\n* Optionally throttles rapid sequential highlighting operations to conserve resources\n* Exported for testing in __tests__/throttleHighlighting.test.ts\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\n/**\n* A React hook that provides syntax highlighting using Shiki. Uses a\n* singleton highlighter instance and supports optional throttled highlights\n*/\nexport const useShikiHighlighter = (\n code: string,\n lang: Language,\n theme: Theme,\n options: HighlighterOptions = {}\n): ReactNode | null => {\n const [highlightedCode, setHighlightedCode] = useState<ReactNode | null>(null);\n const delayRef = useRef<TimeoutState>({\n nextAllowedTime: 0,\n timeoutId: undefined\n });\n\n useEffect(() => {\n let isMounted = true;\n\n const highlightCode = async () => {\n const highlighter = await makeHighlighter(theme);\n const language = await loadLanguage(highlighter, lang);\n\n const html = highlighter.codeToHtml(code, {\n lang: language,\n theme,\n transformers: [removeTabIndexFromPre],\n });\n\n if (isMounted) {\n setHighlightedCode(parse(html));\n }\n };\n\n if (options.delay) {\n throttleHighlighting(highlightCode, delayRef, options.delay);\n } else {\n highlightCode().catch(console.error);\n }\n\n return () => {\n isMounted = false;\n clearTimeout(delayRef.current.timeoutId);\n };\n }, [code, lang]);\n\n return highlightedCode;\n};\n","import type { ShikiTransformer } from 'shiki';\nimport type { Element, Root } from 'hast';\nimport { visit } from 'unist-util-visit';\n\n/**\n * This augmentation ensures that hast.RootData includes the same\n * index signature as unist.Data, preventing type incompatibilities\n * when we pass a hast.Root to unist-util-visit (which expects Node<Data>).\n */\ndeclare module 'hast' {\n interface RootData {\n [key: string]: unknown;\n }\n}\n\n\n/**\n * Rehype plugin to add an 'inline' property to <code> elements.\n * Sets an 'inline' property to true if the <code> is not within a <pre> tag.\n *\n * Pass this plugin to the `rehypePlugins` prop of ReactMarkdown.\n * You can then access `inline` as a prop from ReactMarkdown.\n *\n * @example\n * import ReactMarkdown from 'react-markdown';\n * import { rehypeInlineCodeProperty } from 'react-shiki';\n * <ReactMarkdown rehypePlugins={[rehypeInlineCodeProperty]} />\n */\nexport function rehypeInlineCodeProperty() {\n return function (tree: Root): undefined {\n visit(tree as any, 'element', function (node: Element, _index, parent: Element) {\n if (parent && parent.tagName === 'pre') {\n node.properties.inline = false;\n } else {\n node.properties.inline = true;\n }\n });\n }\n}\n\n\n/**\n * Less accurate way to determine if code is inline.\n * Reports `inline = false` for multiline inline code blocks.\n */\nexport const isInlineCode = (node: Element): boolean => {\n return node.position?.start.line === node.position?.end.line;\n};\n\n\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\nexport type { Element };\n"],"mappings":"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","names":["styleInject","css","insertAt","head","style","styleInject","React","clsx","useEffect","useRef","useState","parse","createHighlighter","ShikiError","visit","rehypeInlineCodeProperty","tree","node","_index","parent","isInlineCode","removeTabIndexFromPre","highlighterPromise","makeHighlighter","theme","createHighlighter","highlighter","loadTheme","error","ShikiError","loadLanguage","lang","resolvedLanguage","throttleHighlighting","performHighlight","timeoutControl","throttleMs","now","delay","useShikiHighlighter","code","options","highlightedCode","setHighlightedCode","useState","delayRef","useRef","useEffect","isMounted","highlightCode","language","html","removeTabIndexFromPre","parse","ShikiHighlighter","language","theme","delay","addDefaultStyles","style","langStyle","className","langClassName","showLanguage","code","Element","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';\nimport React from 'react';\nimport { clsx } from 'clsx';\nimport { useShikiHighlighter } from './useShiki';\nimport type { Language, Theme, HighlighterOptions } from './types';\nimport type { ShikiTransformer } from 'shiki';\nimport { resolveLanguage } from './utils';\nimport type { LanguageRegistration } from './customTypes';\n\n/**\n * Props for the ShikiHighlighter component\n */\nexport interface ShikiHighlighterProps extends HighlighterOptions {\n /**\n * The programming language for syntax highlighting\n * @see https://shiki.style/languages\n */\n language: Language;\n\n /**\n * The code to be highlighted\n */\n children: string;\n\n /**\n * The color theme for syntax highlighting\n * Supports Shiki and custom textmate themes\n * @see https://shiki.style/themes\n */\n theme: Theme;\n\n /**\n * The delay in milliseconds between streamed updates\n * Use this when highlighting code being streamed on the client\n * @default undefined (no delay)\n */\n delay?: number;\n\n /**\n * Pass custom Shiki transformers to the highlighter\n */\n transformers?: ShikiTransformer[];\n\n /**\n * Controls the application of default styles to the generated code blocks\n *\n * Default styles include padding, overflow handling, border radius, language label styling, and font settings\n * @default true\n */\n addDefaultStyles?: boolean;\n\n /**\n * Add custom inline styles to the generated code block\n */\n style?: React.CSSProperties;\n\n /**\n * Add custom inline styles to the language label\n */\n langStyle?: React.CSSProperties;\n\n /**\n * Add custom CSS class names to the generated code block\n */\n className?: string;\n\n /**\n * Add custom CSS class names to the language label\n */\n langClassName?: string;\n\n /**\n * Whether to show the language label\n * @default true\n */\n showLanguage?: boolean;\n\n /**\n * The HTML element that wraps the generated code block.\n * @default 'pre'\n */\n as?: React.ElementType;\n\n /**\n * Custom languages to be preloaded for highlighting.\n */\n customLanguages?: LanguageRegistration[];\n}\n\n/**\n * ShikiHighlighter is a React component that provides syntax highlighting for code snippets.\n * It uses Shiki to render beautiful, theme-based syntax highlighting with optimized performance.\n *\n * @example\n * ```tsx\n * <ShikiHighlighter\n * language=\"typescript\"\n * theme=\"github-dark\"\n * delay={100} // Optional throttling for streamed updates\n * >\n * {code}\n * </ShikiHighlighter>\n * ```\n */\nexport const ShikiHighlighter = ({\n language,\n theme,\n delay,\n transformers,\n addDefaultStyles = true,\n style,\n langStyle,\n className,\n langClassName,\n showLanguage = true,\n children: code,\n as: Element = 'pre',\n customLanguages,\n}: ShikiHighlighterProps): React.ReactElement => {\n const options: HighlighterOptions = {\n delay,\n transformers,\n customLanguages,\n };\n\n const normalizedCustomLanguages = customLanguages\n ? Array.isArray(customLanguages)\n ? customLanguages\n : [customLanguages]\n : [];\n\n const { isCustom, languageId, displayLanguageId, resolvedLanguage } =\n resolveLanguage(language, normalizedCustomLanguages);\n\n const highlightedCode = useShikiHighlighter(\n code,\n language,\n theme,\n options\n );\n\n return (\n <Element\n data-testid=\"shiki-container\"\n className={clsx(\n 'relative',\n 'not-prose',\n addDefaultStyles && 'defaultStyles',\n className\n )}\n style={style}\n id=\"shiki-container\"\n >\n {showLanguage && language ? (\n <span\n className={clsx('languageLabel', langClassName)}\n style={langStyle}\n id=\"language-label\"\n >\n {isCustom\n ? `${resolvedLanguage?.scopeName.split('.')[1]}`\n : displayLanguageId || languageId}\n </span>\n ) : null}\n {highlightedCode}\n </Element>\n );\n};\n","import { useEffect, useRef, useState, type ReactNode } from 'react';\n\nimport parse from 'html-react-parser';\n\nimport {\n createHighlighter,\n createSingletonShorthands,\n type Highlighter,\n} from 'shiki';\n\nimport type {\n LanguageRegistration,\n ShikiLanguageRegistration,\n} from './customTypes';\n\nimport type {\n Language,\n Theme,\n HighlighterOptions,\n TimeoutState,\n} from './types';\n\nimport {\n removeTabIndexFromPre,\n throttleHighlighting,\n resolveLanguage,\n} from './utils';\n\n// Use Shiki managed singleton for bundled languages, create a fresh instance for custom languages\nconst highlighter = createSingletonShorthands(createHighlighter);\nconst customHighlighterCache = new Map<string, Promise<Highlighter>>();\n\n/**\n * A React hook that provides syntax highlighting using Shiki.\n * Supports optional throttled highlights and custom themes.\n *\n * @example\n * const highlightedCode = useShikiHighlighter(code, language, theme, {\n * transformers: [removeTabIndexFromPre],\n * delay: 150\n * customLanguages: ['bosque', 'mcfunction']\n * });\n */\nexport const useShikiHighlighter = (\n code: string,\n lang: Language,\n theme: Theme,\n options: HighlighterOptions = {}\n) => {\n const [highlightedCode, setHighlightedCode] =\n useState<ReactNode | null>(null);\n\n // Setup themeKey for highlighter caching, checks if theme is string (builtin) or object (custom)\n const themeKey = typeof theme === 'string' ? theme : theme.name;\n\n const normalizedCustomLanguages: LanguageRegistration[] =\n options.customLanguages\n ? Array.isArray(options.customLanguages)\n ? options.customLanguages\n : [options.customLanguages]\n : [];\n\n const { isCustom, languageId, resolvedLanguage } = resolveLanguage(\n lang,\n normalizedCustomLanguages\n );\n\n const timeoutControl = useRef<TimeoutState>({\n nextAllowedTime: 0,\n timeoutId: undefined,\n });\n\n const getCachedCustomHighlighter = async (\n cacheKey: string,\n customLang: LanguageRegistration | typeof lang\n ) => {\n let instance = customHighlighterCache.get(cacheKey);\n if (!instance) {\n instance = createHighlighter({\n langs: [customLang as ShikiLanguageRegistration],\n themes: [theme],\n });\n customHighlighterCache.set(cacheKey, instance);\n }\n return instance;\n };\n\n useEffect(() => {\n let isMounted = true;\n\n const transformers = [\n removeTabIndexFromPre,\n ...(options.transformers || []),\n ];\n\n const highlightCode = async () => {\n const codeHighlighter =\n isCustom && resolvedLanguage\n ? await getCachedCustomHighlighter(\n `${resolvedLanguage.name}--${themeKey}`,\n resolvedLanguage\n )\n : highlighter;\n\n const html = await codeHighlighter.codeToHtml(code, {\n lang: languageId,\n theme,\n transformers,\n });\n\n if (isMounted) {\n setHighlightedCode(parse(html));\n }\n };\n\n if (options.delay) {\n throttleHighlighting(highlightCode, timeoutControl, options.delay);\n } else {\n highlightCode().catch(console.error);\n }\n\n return () => {\n isMounted = false;\n clearTimeout(timeoutControl.current.timeoutId);\n };\n }, [code, lang]);\n\n return highlightedCode;\n};\n","import { visit } from 'unist-util-visit';\nimport { bundledLanguages, isSpecialLang } from 'shiki';\nimport type { ShikiTransformer } from 'shiki';\nimport type { Element, Root } from 'hast';\nimport type { Language, TimeoutState } from './types';\nimport type { LanguageRegistration } from './customTypes';\n\n/**\n * Type for the HAST node, use to type `node` from react-markdown\n */\nexport type { Element };\n\n/**\n * Rehype plugin to add an 'inline' property to <code> elements\n * Sets 'inline' property to true if the <code> is not within a <pre> tag\n *\n * Pass this plugin to the `rehypePlugins` prop of react-markdown\n * You can then access `inline` as a prop in components passed to react-markdown\n *\n * @example\n * <ReactMarkdown rehypePlugins={[rehypeInlineCodeProperty]} />\n */\nexport function rehypeInlineCodeProperty() {\n return function (tree: Root): undefined {\n visit(\n tree as any,\n 'element',\n function (node: Element, _index, parent: Element) {\n if (node.tagName === 'code' && parent.tagName !== 'pre') {\n node.properties.inline = true;\n }\n }\n );\n };\n}\n\n/**\n * Function to determine if code is inline based on the presence of line breaks\n *\n * @example\n * const isInline = node && isInlineCode(node: Element)\n */\nexport const isInlineCode = (node: Element): boolean => {\n const textContent = (node.children || [])\n .filter((child) => child.type === 'text')\n .map((child) => child.value)\n .join('');\n\n return !textContent.includes('\\n');\n};\n\n/**\n * Shiki transformer to remove tabindex from <pre> elements\n *\n * Consider retaining tabindex for WCAG 3.1 compliance, scrollable code blocks should be focusable\n * https://github.com/shikijs/shiki/issues/428\n * https://www.w3.org/WAI/standards-guidelines/act/rules/0ssw9k/proposed/\n */\nexport const removeTabIndexFromPre: ShikiTransformer = {\n pre(node) {\n if ('properties' in node) {\n node.properties.tabindex = '-1';\n }\n return node;\n },\n};\n\n/**\n * Optionally throttles rapid sequential highlighting operations\n * Exported for testing in __tests__/throttleHighlighting.test.ts\n *\n * @example\n * const timeoutControl = useRef<TimeoutState>({\n * nextAllowedTime: 0,\n * timeoutId: undefined\n * });\n *\n * throttleHighlighting(highlightCode, timeoutControl, 1000);\n */\nexport const throttleHighlighting = (\n performHighlight: () => Promise<void>,\n timeoutControl: React.MutableRefObject<TimeoutState>,\n throttleMs: number\n) => {\n const now = Date.now();\n clearTimeout(timeoutControl.current.timeoutId);\n\n const delay = Math.max(0, timeoutControl.current.nextAllowedTime - now);\n timeoutControl.current.timeoutId = setTimeout(() => {\n performHighlight().catch(console.error);\n timeoutControl.current.nextAllowedTime = now + throttleMs;\n }, delay);\n};\n\ntype ResolvedLanguage = {\n isCustom: boolean;\n languageId: string;\n displayLanguageId?: string;\n resolvedLanguage?: LanguageRegistration;\n};\n\n/**\n * Determines whether a language is custom or built-in, normalizes\n * the language identifier and returns an object with metadata\n *\n * @param lang - The language identifier, either as a string name or language object\n * @param customLanguages - Optional array of custom language definitions\n * @returns Object containing:\n * - isCustom: Whether the language requires custom highlighting\n * - languageId: The normalized language identifier\n * - displayLanguageId: The display name for language label\n * - resolvedLanguage: The full language definition if custom\n */\nexport const resolveLanguage = (\n lang: Language,\n customLanguages: LanguageRegistration[] = []\n): ResolvedLanguage => {\n // Language is custom but not preloaded\n if (lang && typeof lang === 'object') {\n return {\n isCustom: true,\n languageId: lang.name,\n displayLanguageId: lang.name,\n resolvedLanguage: lang,\n };\n }\n\n // Language is a string\n if (typeof lang === 'string') {\n const displayLanguageId = lang;\n\n // Check if its built-in\n if (lang in bundledLanguages || isSpecialLang(lang)) {\n return {\n isCustom: false,\n languageId: lang,\n displayLanguageId,\n };\n }\n\n // Check if it matches a preloaded custom language\n const customMatch = customLanguages.find(\n (cl) =>\n cl.fileTypes?.includes(lang) ||\n cl.scopeName?.split('.')[1] === lang ||\n cl.name?.toLowerCase() === lang.toLowerCase()\n );\n\n if (customMatch) {\n return {\n isCustom: true,\n languageId: customMatch.name,\n displayLanguageId,\n resolvedLanguage: customMatch,\n };\n }\n\n // If unknown, highlight in plaintext but display unknown language\n return {\n isCustom: false,\n languageId: 'plaintext',\n displayLanguageId,\n };\n }\n\n // Fallback\n return {\n isCustom: false,\n languageId: 'plaintext',\n displayLanguageId: 'plaintext',\n };\n};\n"],"mappings":"AACyB,SAARA,EAA6BC,EAAK,CAAE,SAAAC,CAAS,EAAI,CAAC,EAAG,CAC1D,GAAI,CAACD,GAAO,OAAO,SAAa,IAAa,OAE7C,IAAME,EAAO,SAAS,MAAQ,SAAS,qBAAqB,MAAM,EAAE,CAAC,EAC/DC,EAAQ,SAAS,cAAc,OAAO,EAC5CA,EAAM,KAAO,WAETF,IAAa,OACXC,EAAK,WACPA,EAAK,aAAaC,EAAOD,EAAK,UAAU,EAK1CA,EAAK,YAAYC,CAAK,EAGpBA,EAAM,WACRA,EAAM,WAAW,QAAUH,EAE3BG,EAAM,YAAY,SAAS,eAAeH,CAAG,CAAC,CAElD,CCvB8BI,EAAY;AAAA,CAAiP,ECCrS,OAAOC,MAAW,QAClB,OAAS,QAAAC,MAAY,OCFrB,OAAS,aAAAC,EAAW,UAAAC,EAAQ,YAAAC,MAAgC,QAE5D,OAAOC,MAAW,oBAElB,OACE,qBAAAC,EACA,6BAAAC,MAEK,QCRP,OAAS,SAAAC,MAAa,mBACtB,OAAS,oBAAAC,EAAkB,iBAAAC,MAAqB,QAqBzC,SAASC,GAA2B,CACzC,OAAO,SAAUC,EAAuB,CACtCJ,EACEI,EACA,UACA,SAAUC,EAAeC,EAAQC,EAAiB,CAC5CF,EAAK,UAAY,QAAUE,EAAO,UAAY,QAChDF,EAAK,WAAW,OAAS,GAE7B,CACF,CACF,CACF,CAQO,IAAMG,EAAgBH,GAMpB,EALcA,EAAK,UAAY,CAAC,GACpC,OAAQI,GAAUA,EAAM,OAAS,MAAM,EACvC,IAAKA,GAAUA,EAAM,KAAK,EAC1B,KAAK,EAAE,EAEU,SAAS;AAAA,CAAI,EAUtBC,EAA0C,CACrD,IAAIL,EAAM,CACR,MAAI,eAAgBA,IAClBA,EAAK,WAAW,SAAW,MAEtBA,CACT,CACF,EAcaM,EAAuB,CAClCC,EACAC,EACAC,IACG,CACH,IAAMC,EAAM,KAAK,IAAI,EACrB,aAAaF,EAAe,QAAQ,SAAS,EAE7C,IAAMG,EAAQ,KAAK,IAAI,EAAGH,EAAe,QAAQ,gBAAkBE,CAAG,EACtEF,EAAe,QAAQ,UAAY,WAAW,IAAM,CAClDD,EAAiB,EAAE,MAAM,QAAQ,KAAK,EACtCC,EAAe,QAAQ,gBAAkBE,EAAMD,CACjD,EAAGE,CAAK,CACV,EAqBaC,EAAkB,CAC7BC,EACAC,EAA0C,CAAC,IACtB,CAErB,GAAID,GAAQ,OAAOA,GAAS,SAC1B,MAAO,CACL,SAAU,GACV,WAAYA,EAAK,KACjB,kBAAmBA,EAAK,KACxB,iBAAkBA,CACpB,EAIF,GAAI,OAAOA,GAAS,SAAU,CAC5B,IAAME,EAAoBF,EAG1B,GAAIA,KAAQjB,GAAoBC,EAAcgB,CAAI,EAChD,MAAO,CACL,SAAU,GACV,WAAYA,EACZ,kBAAAE,CACF,EAIF,IAAMC,EAAcF,EAAgB,KACjCG,GACCA,EAAG,WAAW,SAASJ,CAAI,GAC3BI,EAAG,WAAW,MAAM,GAAG,EAAE,CAAC,IAAMJ,GAChCI,EAAG,MAAM,YAAY,IAAMJ,EAAK,YAAY,CAChD,EAEA,OAAIG,EACK,CACL,SAAU,GACV,WAAYA,EAAY,KACxB,kBAAAD,EACA,iBAAkBC,CACpB,EAIK,CACL,SAAU,GACV,WAAY,YACZ,kBAAAD,CACF,CACF,CAGA,MAAO,CACL,SAAU,GACV,WAAY,YACZ,kBAAmB,WACrB,CACF,ED9IA,IAAMG,EAAcC,EAA0BC,CAAiB,EACzDC,EAAyB,IAAI,IAatBC,EAAsB,CACjCC,EACAC,EACAC,EACAC,EAA8B,CAAC,IAC5B,CACH,GAAM,CAACC,EAAiBC,CAAkB,EACxCC,EAA2B,IAAI,EAG3BC,EAAW,OAAOL,GAAU,SAAWA,EAAQA,EAAM,KAErDM,EACJL,EAAQ,gBACJ,MAAM,QAAQA,EAAQ,eAAe,EACnCA,EAAQ,gBACR,CAACA,EAAQ,eAAe,EAC1B,CAAC,EAED,CAAE,SAAAM,EAAU,WAAAC,EAAY,iBAAAC,CAAiB,EAAIC,EACjDX,EACAO,CACF,EAEMK,EAAiBC,EAAqB,CAC1C,gBAAiB,EACjB,UAAW,MACb,CAAC,EAEKC,EAA6B,MACjCC,EACAC,IACG,CACH,IAAIC,EAAWpB,EAAuB,IAAIkB,CAAQ,EAClD,OAAKE,IACHA,EAAWrB,EAAkB,CAC3B,MAAO,CAACoB,CAAuC,EAC/C,OAAQ,CAACf,CAAK,CAChB,CAAC,EACDJ,EAAuB,IAAIkB,EAAUE,CAAQ,GAExCA,CACT,EAEA,OAAAC,EAAU,IAAM,CACd,IAAIC,EAAY,GAEVC,EAAe,CACnBC,EACA,GAAInB,EAAQ,cAAgB,CAAC,CAC/B,EAEMoB,EAAgB,SAAY,CAShC,IAAMC,EAAO,MAPXf,GAAYE,EACR,MAAMI,EACJ,GAAGJ,EAAiB,IAAI,KAAKJ,CAAQ,GACrCI,CACF,EACAhB,GAE6B,WAAWK,EAAM,CAClD,KAAMU,EACN,MAAAR,EACA,aAAAmB,CACF,CAAC,EAEGD,GACFf,EAAmBoB,EAAMD,CAAI,CAAC,CAElC,EAEA,OAAIrB,EAAQ,MACVuB,EAAqBH,EAAeV,EAAgBV,EAAQ,KAAK,EAEjEoB,EAAc,EAAE,MAAM,QAAQ,KAAK,EAG9B,IAAM,CACXH,EAAY,GACZ,aAAaP,EAAe,QAAQ,SAAS,CAC/C,CACF,EAAG,CAACb,EAAMC,CAAI,CAAC,EAERG,CACT,EDxBO,IAAMuB,EAAmB,CAAC,CAC/B,SAAAC,EACA,MAAAC,EACA,MAAAC,EACA,aAAAC,EACA,iBAAAC,EAAmB,GACnB,MAAAC,EACA,UAAAC,EACA,UAAAC,EACA,cAAAC,EACA,aAAAC,EAAe,GACf,SAAUC,EACV,GAAIC,EAAU,MACd,gBAAAC,CACF,IAAiD,CAC/C,IAAMC,EAA8B,CAClC,MAAAX,EACA,aAAAC,EACA,gBAAAS,CACF,EAEME,EAA4BF,EAC9B,MAAM,QAAQA,CAAe,EAC3BA,EACA,CAACA,CAAe,EAClB,CAAC,EAEC,CAAE,SAAAG,EAAU,WAAAC,EAAY,kBAAAC,EAAmB,iBAAAC,CAAiB,EAChEC,EAAgBnB,EAAUc,CAAyB,EAE/CM,EAAkBC,EACtBX,EACAV,EACAC,EACAY,CACF,EAEA,OACES,EAAA,cAACX,EAAA,CACC,cAAY,kBACZ,UAAWY,EACT,WACA,YACAnB,GAAoB,gBACpBG,CACF,EACA,MAAOF,EACP,GAAG,mBAEFI,GAAgBT,EACfsB,EAAA,cAAC,QACC,UAAWC,EAAK,gBAAiBf,CAAa,EAC9C,MAAOF,EACP,GAAG,kBAEFS,EACG,GAAGG,GAAkB,UAAU,MAAM,GAAG,EAAE,CAAC,CAAC,GAC5CD,GAAqBD,CAC3B,EACE,KACHI,CACH,CAEJ","names":["styleInject","css","insertAt","head","style","styleInject","React","clsx","useEffect","useRef","useState","parse","createHighlighter","createSingletonShorthands","visit","bundledLanguages","isSpecialLang","rehypeInlineCodeProperty","tree","node","_index","parent","isInlineCode","child","removeTabIndexFromPre","throttleHighlighting","performHighlight","timeoutControl","throttleMs","now","delay","resolveLanguage","lang","customLanguages","displayLanguageId","customMatch","cl","highlighter","createSingletonShorthands","createHighlighter","customHighlighterCache","useShikiHighlighter","code","lang","theme","options","highlightedCode","setHighlightedCode","useState","themeKey","normalizedCustomLanguages","isCustom","languageId","resolvedLanguage","resolveLanguage","timeoutControl","useRef","getCachedCustomHighlighter","cacheKey","customLang","instance","useEffect","isMounted","transformers","removeTabIndexFromPre","highlightCode","html","parse","throttleHighlighting","ShikiHighlighter","language","theme","delay","transformers","addDefaultStyles","style","langStyle","className","langClassName","showLanguage","code","Element","customLanguages","options","normalizedCustomLanguages","isCustom","languageId","displayLanguageId","resolvedLanguage","resolveLanguage","highlightedCode","useShikiHighlighter","React","clsx"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-shiki",
|
|
3
3
|
"description": "Syntax highlighter component for react using shiki",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bassim Shahidy",
|
|
@@ -43,19 +43,19 @@
|
|
|
43
43
|
"@types/jest": "^29.5.14",
|
|
44
44
|
"clsx": "^2.1.1",
|
|
45
45
|
"html-react-parser": "^5.1.12",
|
|
46
|
-
"shiki": "^
|
|
46
|
+
"shiki": "^3.0.0",
|
|
47
47
|
"unist-util-visit": "^5.0.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@testing-library/jest-dom": "^6.
|
|
51
|
-
"@testing-library/react": "^14.
|
|
50
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
51
|
+
"@testing-library/react": "^14.3.1",
|
|
52
52
|
"@types/hast": "^3.0.4",
|
|
53
|
-
"@types/node": "22.
|
|
54
|
-
"@types/react": "^18.3.
|
|
55
|
-
"@vitejs/plugin-react": "^4.
|
|
53
|
+
"@types/node": "22.13.4",
|
|
54
|
+
"@types/react": "^18.3.18",
|
|
55
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
56
56
|
"jsdom": "^22.1.0",
|
|
57
|
-
"tsup": "^8.
|
|
58
|
-
"vitest": "^0.34.
|
|
57
|
+
"tsup": "^8.3.6",
|
|
58
|
+
"vitest": "^0.34.6"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"dev": "tsup --watch",
|