react-shiki 0.2.1 → 0.2.3

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,22 +1,42 @@
1
- > [!NOTE]
2
- > This package is still a work in progress, fully functional but not extensively tested.
1
+ # 🎨 [react-shiki](https://npmjs.com/react-shiki)
3
2
 
4
- # 🎨 [react-shiki](https://react-shiki.vercel.app/)
3
+ > [!NOTE]
4
+ > This package is still a work in progress, fully functional but not
5
+ > extensively tested.
5
6
 
6
- Syntax highlighting component and hook for react using [Shiki](https://shiki.matsu.io/)
7
+ Performant client side syntax highlighting component + hook
8
+ for react using [Shiki](https://shiki.matsu.io/)
7
9
 
8
10
  [See the demo page with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/)
9
11
 
12
+ <!--toc:start-->
13
+
14
+ - 🎨 [react-shiki](https://npmjs.com/react-shiki)
15
+ - [Features](#features)
16
+ - [Installation](#installation)
17
+ - [Usage](#usage)
18
+ - [`react-markdown`](#react-markdown)
19
+ - [Custom themes](#custom-themes)
20
+ - [Performance](#performance)
21
+ - [Throttling real-time highlighting](#throttling-real-time-highlighting)
22
+ - [Streaming and LLM chat UI](#streaming-and-llm-chat-ui)
23
+ <!--toc:end-->
24
+
10
25
  ## Features
11
26
 
12
- - 🖼️ Provides a `ShikiHighlighter` component for highlighting code as children, as well as a `useShikiHighlighter` hook for more flexibility
27
+ - 🖼️ Provides a `ShikiHighlighter` component for highlighting code as children,
28
+ as well as a `useShikiHighlighter` hook for more flexibility
13
29
  - 🔐 No `dangerouslySetInnerHTML`, output from Shiki is parsed using `html-react-parser`
14
- - 📦 Supports all Shiki languages and themes
15
- - 🚰 Supports performant highlighting of streamed code on the client, with optional throttling
30
+ - 📦 Supports all Shiki languages and themes in addition to
31
+ - 🖌️ Full support for custom TextMate themes in a JavaScript object format
32
+ - 🚰 Performant highlighting of streamed code on the client, with optional throttling
16
33
  - 📚 Includes minimal default styles for code blocks
17
- - 🚀 Shiki dynamically imports only the languages and themes used on a page, optimizing for performance
18
- - 🖥️ `ShikiHighlighter` component displays a language label for each code block when `showLanguage` is set to `true` (default)
19
- - 🎨 Users can customize the styling of the generated code blocks by passing a `style` object or a `className`
34
+ - 🚀 Shiki dynamically imports only the languages and themes used on a page,
35
+ optimizing for performance
36
+ - 🖥️ `ShikiHighlighter` component displays a language label for each code block
37
+ when `showLanguage` is set to `true` (default)
38
+ - 🎨 Users can customize the styling of the generated code blocks by passing
39
+ a `style` object or a `className`
20
40
 
21
41
  ## Installation
22
42
 
@@ -26,17 +46,23 @@ Syntax highlighting component and hook for react using [Shiki](https://shiki.mat
26
46
 
27
47
  ## Usage
28
48
 
29
- You can use the `ShikiHighlighter` component, or the `useShikiHighlighter` hook to highlight code.
49
+ You can use the `ShikiHighlighter` component, or the `useShikiHighlighter` hook
50
+ to highlight code.
30
51
 
31
- `useShikiHighlighter` is a hook that takes in the code to be highlighted, the language, and the theme, and returns the highlighted code as React elements. It's useful for users who want full control over the rendering of highlighted code.
52
+ `useShikiHighlighter` is a hook that takes in the code to be highlighted, the
53
+ language, and the theme, and returns the highlighted code as React elements.
54
+ It's useful for users who want full control over the rendering of highlighted
55
+ code.
32
56
 
33
57
  ```tsx
34
- const highlightedCode = useShikiHighlighter(code, language, theme);
58
+ const highlightedCode = useShikiHighlighter(code, language, theme, options);
35
59
  ```
36
60
 
37
- The `ShikiHighlighter` component is imported in your project, with the code to be highlighted passed as it's children.
61
+ The `ShikiHighlighter` component is imported in your project, with the code to
62
+ be highlighted passed as it's children.
38
63
 
39
- Shiki automatically handles dynamically importing only the languages and themes used on the page.
64
+ Shiki automatically handles dynamically importing only the languages and themes
65
+ used on the page.
40
66
 
41
67
  ```tsx
42
68
  function CodeBlock() {
@@ -48,19 +74,27 @@ function CodeBlock() {
48
74
  }
49
75
  ```
50
76
 
77
+ The `ShikiHighlighter` component will follow a similar API to `react-syntax-highlighter`, but uses Shiki and is optimized for performant sequential highlighting. As of now, not all of `react-syntax-highlighter` functionality is supported, but the goal of this component is to eventually act as a drop in replacement for `react-syntax-highlighter`.
78
+
51
79
  The component accepts several props in addition to language and theme:
52
80
 
53
- - `showLanguage: boolean` - Shows the language name in the top right corner of the code block.
54
- - `addDefaultStyles`: boolean - Adds default styles (padding, overflow handling, and border-radius) to the code block.
55
- - `as: string` - The component to be rendered. Defaults to 'pre'.
56
- - `className: string` - Class name to be passed to the component.
57
- - `style: object` - Style object to be passed to the component.
81
+ - `showLanguage: boolean` - Shows the language name in the top right corner of
82
+ the code block
83
+ - `addDefaultStyles`: boolean - Adds default styles (padding, overflow handling,
84
+ and border-radius) to the code block
85
+ - `as: string` - The component to be rendered. Defaults to 'pre'
86
+ - `delay: number` - Delay between highlights in milliseconds, useful for throttling
87
+ rapid highlighting on the client
88
+ - `className: string` - Class name to be passed to the component
89
+ - `style: object` - Inline style object to be passed to the component
90
+ - `langStyle: object` - Inline style object to be passed to the language label
58
91
 
59
92
  ```tsx
60
93
  function Houston() {
61
94
  return (
62
95
  <ShikiHighlighter
63
96
  language="jsx"
97
+ className="code-block"
64
98
  theme="houston"
65
99
  showLanguage={false}
66
100
  addDefaultStyles={true}
@@ -75,7 +109,7 @@ function Houston() {
75
109
  }
76
110
  ```
77
111
 
78
- react-shiki exports `isInlineCode` to check if a code block is inline.
112
+ `react-shiki` exports `isInlineCode` to check if a code block is inline:
79
113
 
80
114
  ```tsx
81
115
  const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
@@ -91,7 +125,7 @@ return !isInline ? (
91
125
  );
92
126
  ```
93
127
 
94
- It can also be used with `react-markdown`:
128
+ ### `react-markdown`
95
129
 
96
130
  ```tsx
97
131
  import type { ReactNode } from "react";
@@ -118,7 +152,7 @@ export const CodeHighlight = ({
118
152
  };
119
153
  ```
120
154
 
121
- Pass CodeHighlight to `react-markdown` as a code component:
155
+ Pass `CodeHighlight` to `react-markdown` as a code component:
122
156
 
123
157
  ```tsx
124
158
  import ReactMarkdown from "react-markdown";
@@ -143,9 +177,27 @@ import tokyoNight from '@styles/tokyo-night.mjs';
143
177
  </ShikiHighlighter>;
144
178
  ```
145
179
 
146
- ## Client-side highlighting
180
+ ## Performance
181
+
182
+ `react-shiki` supports performance-optimized highlighting on the client.
183
+
184
+ ### Throttling real-time highlighting
185
+
186
+ Throttling real-time highlighting on the client is possible with the
187
+ `delay` option.
188
+
189
+ ```tsx
190
+ const highlightedCode = useShikiHighlighter(code, language, theme, {
191
+ delay: 150,
192
+ });
193
+ ```
194
+
195
+ ### Streaming and LLM chat UI
147
196
 
148
- This works great for highlighting in real-time on the client, I use it for an LLM chatbot UI, it renders markdown and highlights code in memoized chat messages. It also Supports an optional delay between highlights for throttling.
197
+ `react-shiki` can be used to highlight streamed code from LLM responses in real-time.
198
+
199
+ I use it for an
200
+ LLM chatbot UI, it renders markdown and highlights code in memoized chat messages.
149
201
 
150
202
  Using `useShikiHighlighter` hook:
151
203
 
@@ -176,9 +228,15 @@ export const CodeHighlight = ({
176
228
  });
177
229
 
178
230
  return !isInline ? (
179
- <div className="shiki not-prose relative [&_pre]:overflow-auto [&_pre]:rounded-lg [&_pre]:px-6 [&_pre]:py-5">
231
+ <div
232
+ className="shiki not-prose relative [&_pre]:overflow-auto
233
+ [&_pre]:rounded-lg [&_pre]:px-6 [&_pre]:py-5"
234
+ >
180
235
  {language ? (
181
- <span className="absolute right-3 top-2 text-xs tracking-tighter text-muted-foreground/85">
236
+ <span
237
+ className="absolute right-3 top-2 text-xs tracking-tighter
238
+ text-muted-foreground/85"
239
+ >
182
240
  {language}
183
241
  </span>
184
242
  ) : null}
@@ -216,7 +274,12 @@ export const CodeHighlight = ({
216
274
  const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
217
275
 
218
276
  return !isInline ? (
219
- <ShikiHighlighter language={language} theme={"houston"} {...props}>
277
+ <ShikiHighlighter
278
+ language={language}
279
+ theme={"houston"}
280
+ delay={150}
281
+ {...props}
282
+ >
220
283
  {String(children)}
221
284
  </ShikiHighlighter>
222
285
  ) : (
@@ -227,7 +290,7 @@ export const CodeHighlight = ({
227
290
  };
228
291
  ```
229
292
 
230
- Passed to `react-markdown` as a code component in memoized chat messages:
293
+ Passed to `react-markdown` as a `code` component in memo-ized chat messages:
231
294
 
232
295
  ```tsx title=ChatMessages.tsx
233
296
  const RenderedMessage = React.memo(({ message }: { message: Message }) => (
package/dist/index.d.ts CHANGED
@@ -60,6 +60,10 @@ interface ShikiHighlighterProps extends HighlighterOptions {
60
60
  * Add custom inline styles to the generated code block
61
61
  */
62
62
  style?: React.CSSProperties;
63
+ /**
64
+ * Add custom inline styles to the language label
65
+ */
66
+ langStyle?: React.CSSProperties;
63
67
  /**
64
68
  * Add custom CSS class names to the generated code block
65
69
  */
@@ -90,12 +94,12 @@ interface ShikiHighlighterProps extends HighlighterOptions {
90
94
  * </ShikiHighlighter>
91
95
  * ```
92
96
  */
93
- declare const ShikiHighlighter: ({ language, theme, delay, addDefaultStyles, style, className, showLanguage, children: code, as: Element, }: ShikiHighlighterProps) => React.ReactElement;
97
+ declare const ShikiHighlighter: ({ language, theme, delay, addDefaultStyles, style, langStyle, className, showLanguage, children: code, as: Element, }: ShikiHighlighterProps) => React.ReactElement;
94
98
 
95
- /************************************************************************
96
- ** A React hook that provides syntax highlighting using Shiki. Uses a **
97
- ** singleton highlighter instance and supports throttled updates. **
98
- *************************************************************************/
99
+ /**
100
+ * A React hook that provides syntax highlighting using Shiki. Uses a
101
+ * singleton highlighter instance and supports optional throttled highlights
102
+ */
99
103
  declare const useShikiHighlighter: (code: string, lang: Language, theme: Theme, options?: HighlighterOptions) => ReactNode | null;
100
104
 
101
105
  declare const isInlineCode: (node: Element) => boolean;
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- function g(e,{insertAt:i}={}){if(!e||typeof document>"u")return;let t=document.head||document.getElementsByTagName("head")[0],r=document.createElement("style");r.type="text/css",i==="top"&&t.firstChild?t.insertBefore(r,t.firstChild):t.appendChild(r),r.styleSheet?r.styleSheet.cssText=e:r.appendChild(document.createTextNode(e))}g(`.relative{position:relative}.container 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 p from"react";import{clsx as I}from"clsx";import{useEffect as T,useRef as H,useState as b}from"react";import S from"html-react-parser";import{createHighlighter as w,ShikiError as u}from"shiki";var x=e=>e.position?.start.line===e.position?.end.line,d={pre(e){e.properties.tabindex="-1"}};var m=null,L=async e=>{m||(m=w({themes:[],langs:[]}));let i=await m;return await E(i,e),i},E=async(e,i)=>{try{await e.loadTheme(i)}catch(t){throw t instanceof u&&console.warn("Error loading theme:",t.message),t}},P=async(e,i)=>{let t=i??"plaintext";try{return await e.loadLanguage(t),t}catch(r){return r instanceof u&&r.message.includes("not included in this bundle")&&console.warn(`Language '${t}' not supported, falling back to plaintext`),"plaintext"}},k=(e,i,t)=>{let r=Date.now();clearTimeout(i.current.timeoutId);let o=Math.max(0,i.current.nextAllowedTime-r);i.current.timeoutId=setTimeout(()=>{e().catch(console.error),i.current.nextAllowedTime=r+t},o)},c=(e,i,t,r={})=>{let[o,h]=b(null),n=H({nextAllowedTime:0,timeoutId:void 0});return T(()=>{let a=!0,l=async()=>{let s=await L(t),f=await P(s,i),y=s.codeToHtml(e,{lang:f,theme:t,transformers:[d]});a&&h(S(y))};return r.delay?k(l,n,r.delay):l().catch(console.error),()=>{a=!1,clearTimeout(n.current.timeoutId)}},[e,i]),o};var R=({language:e,theme:i,delay:t,addDefaultStyles:r=!0,style:o,className:h,showLanguage:n=!0,children:a,as:l="pre"})=>{let s=c(a,e,i,{delay:t});return p.createElement(l,{className:I("relative",r&&"container",h),style:o},n&&e?p.createElement("span",{className:"languageLabel",style:o},e):null,s)};export{R as default,x as isInlineCode,c as useShikiHighlighter};
1
+ function m(e,{insertAt:i}={}){if(!e||typeof document>"u")return;let t=document.head||document.getElementsByTagName("head")[0],r=document.createElement("style");r.type="text/css",i==="top"&&t.firstChild?t.insertBefore(r,t.firstChild):t.appendChild(r),r.styleSheet?r.styleSheet.cssText=e:r.appendChild(document.createTextNode(e))}m(`.relative{position:relative}.container 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 f from"react";import{clsx as k}from"clsx";import{useEffect as T,useRef as H,useState as S}from"react";import b from"html-react-parser";import{createHighlighter as w,ShikiError as p}from"shiki";var x=e=>e.position?.start.line===e.position?.end.line,u={pre(e){e.properties.tabindex="-1"}};var c=null,L=async e=>{c||(c=w({themes:[],langs:[]}));let i=await c;return await E(i,e),i},E=async(e,i)=>{try{await e.loadTheme(i)}catch(t){throw t instanceof p&&console.warn("Error loading theme:",t.message),t}},P=async(e,i)=>{let t=i??"plaintext";try{return await e.loadLanguage(t),t}catch(r){return r instanceof p&&r.message.includes("not included in this bundle")&&console.warn(`Language '${t}' not supported, falling back to plaintext`),"plaintext"}},R=(e,i,t)=>{let r=Date.now();clearTimeout(i.current.timeoutId);let o=Math.max(0,i.current.nextAllowedTime-r);i.current.timeoutId=setTimeout(()=>{e().catch(console.error),i.current.nextAllowedTime=r+t},o)},d=(e,i,t,r={})=>{let[o,h]=S(null),n=H({nextAllowedTime:0,timeoutId:void 0});return T(()=>{let a=!0,l=async()=>{let s=await L(t),g=await P(s,i),y=s.codeToHtml(e,{lang:g,theme:t,transformers:[u]});a&&h(b(y))};return r.delay?R(l,n,r.delay):l().catch(console.error),()=>{a=!1,clearTimeout(n.current.timeoutId)}},[e,i]),o};var C=({language:e,theme:i,delay:t,addDefaultStyles:r=!0,style:o,langStyle:h,className:n,showLanguage:a=!0,children:l,as:s="pre"})=>{let g=d(l,e,i,{delay:t});return f.createElement(s,{className:k("relative",r&&"container",n),style:o},a&&e?f.createElement("span",{className:"languageLabel",style:h},e):null,g)};export{C as default,x as isInlineCode,d as useShikiHighlighter};
3
3
  //# 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}.container 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 CSS class names to the generated code block \n */\n className?: 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 className,\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 addDefaultStyles && 'container',\n className\n )}\n style={style}\n >\n {showLanguage && language ? (\n <span className=\"languageLabel\" style={style}>\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 ** Throttles highlighting operations to prevent overwhelming the highlighter ** **\n *******************************************************************************/\nconst 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/************************************************************************\n ** A React hook that provides syntax highlighting using Shiki. Uses a **\n ** singleton highlighter instance and supports throttled updates. **\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 { Element } from 'hast';\n\nexport const isInlineCode = (node: Element): boolean => {\n return node.position?.start.line === node.position?.end.line;\n};\n\n\nexport const removeTabIndexFromPre = {\n pre(node: Element) {\n node.properties.tabindex = '-1';\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","isInlineCode","node","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","className","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}.container 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 * 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 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 addDefaultStyles && 'container',\n className\n )}\n style={style}\n >\n {showLanguage && language ? (\n <span className=\"languageLabel\" style={langStyle}>\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*/\nconst 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/**\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 { Element } from 'hast';\n\nexport const isInlineCode = (node: Element): boolean => {\n return node.position?.start.line === node.position?.end.line;\n};\n\n\nexport const removeTabIndexFromPre = {\n pre(node: Element) {\n node.properties.tabindex = '-1';\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","isInlineCode","node","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","showLanguage","code","Element","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.1",
4
+ "version": "0.2.3",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Bassim Shahidy",