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 CHANGED
@@ -1,11 +1,12 @@
1
1
  # 🎨 [react-shiki](https://npmjs.com/react-shiki)
2
2
 
3
3
  > [!NOTE]
4
- > This package is still a work in progress, fully functional but not
5
- > extensively tested.
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 using [Shiki](https://shiki.matsu.io/)
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 in addition to
32
- - 🖌️ Full support for custom TextMate themes in a JavaScript object format
33
- - 🚰 Performant highlighting of streamed code on the client, with optional throttling
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
- - 🎨 Users can customize the styling of the generated code blocks by passing
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
- [pnpm|bun|yarn|npm] install react-shiki
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`, good but marks multiline inline
163
- code tags as code blocks.
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 more accurate way
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
- And can be accessed as a prop:
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
- {String(children)}
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
- ```tsx:title=CodeHighlight.tsx
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
- LLM chatbot UI, it renders markdown and highlights code in memoized chat messages.
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 title=CodeHighlight.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 title=CodeHighlight.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 title=ChatMessages.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 }}>
@@ -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 c(e,{insertAt:t}={}){if(!e||typeof document>"u")return;let i=document.head||document.getElementsByTagName("head")[0],r=document.createElement("style");r.type="text/css",t==="top"&&i.firstChild?i.insertBefore(r,i.firstChild):i.appendChild(r),r.styleSheet?r.styleSheet.cssText=e:r.appendChild(document.createTextNode(e))}c(`.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 y from"react";import{clsx as x}from"clsx";import{useEffect as b,useRef as w,useState as k}from"react";import E from"html-react-parser";import{createHighlighter as L,ShikiError as f}from"shiki";import{visit as T}from"unist-util-visit";function S(){return function(e){T(e,"element",function(t,i,r){r&&r.tagName==="pre"?t.properties.inline=!1:t.properties.inline=!0})}}var H=e=>e.position?.start.line===e.position?.end.line,u={pre(e){return"properties"in e&&(e.properties.tabindex="-1"),e}};var d=null,P=async e=>{d||(d=L({themes:[],langs:[]}));let t=await d;return await R(t,e),t},R=async(e,t)=>{try{await e.loadTheme(t)}catch(i){throw i instanceof f&&console.warn("Error loading theme:",i.message),i}},C=async(e,t)=>{let i=t??"plaintext";try{return await e.loadLanguage(i),i}catch(r){return r instanceof f&&r.message.includes("not included in this bundle")&&console.warn(`Language '${i}' not supported, falling back to plaintext`),"plaintext"}},I=(e,t,i)=>{let r=Date.now();clearTimeout(t.current.timeoutId);let n=Math.max(0,t.current.nextAllowedTime-r);t.current.timeoutId=setTimeout(()=>{e().catch(console.error),t.current.nextAllowedTime=r+i},n)},p=(e,t,i,r={})=>{let[n,h]=k(null),o=w({nextAllowedTime:0,timeoutId:void 0});return b(()=>{let a=!0,l=async()=>{let s=await P(i),m=await C(s,t),g=s.codeToHtml(e,{lang:m,theme:i,transformers:[u]});a&&h(E(g))};return r.delay?I(l,o,r.delay):l().catch(console.error),()=>{a=!1,clearTimeout(o.current.timeoutId)}},[e,t]),n};var v=({language:e,theme:t,delay:i,addDefaultStyles:r=!0,style:n,langStyle:h,className:o,langClassName:a,showLanguage:l=!0,children:s,as:m="pre"})=>{let g=p(s,e,t,{delay:i});return y.createElement(m,{className:x("relative","not-prose",r&&"defaultStyles",o),style:n,id:"shiki-container"},l&&e?y.createElement("span",{className:x("languageLabel",a),style:h,id:"language-label"},e):null,g)};export{v as default,H as isInlineCode,S as rehypeInlineCodeProperty,p as useShikiHighlighter};
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":"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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"]}
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.2.4",
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": "^1.12.1",
46
+ "shiki": "^3.0.0",
47
47
  "unist-util-visit": "^5.0.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@testing-library/jest-dom": "^6.0.0",
51
- "@testing-library/react": "^14.0.0",
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.10.10",
54
- "@types/react": "^18.3.3",
55
- "@vitejs/plugin-react": "^4.0.0",
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.2.4",
58
- "vitest": "^0.34.0"
57
+ "tsup": "^8.3.6",
58
+ "vitest": "^0.34.6"
59
59
  },
60
60
  "scripts": {
61
61
  "dev": "tsup --watch",