react-shiki 0.2.3 → 0.2.4

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
@@ -16,6 +16,7 @@ for react using [Shiki](https://shiki.matsu.io/)
16
16
  - [Installation](#installation)
17
17
  - [Usage](#usage)
18
18
  - [`react-markdown`](#react-markdown)
19
+ - [Check if code is inline](#check-if-code-is-inline)
19
20
  - [Custom themes](#custom-themes)
20
21
  - [Performance](#performance)
21
22
  - [Throttling real-time highlighting](#throttling-real-time-highlighting)
@@ -74,7 +75,10 @@ function CodeBlock() {
74
75
  }
75
76
  ```
76
77
 
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
+ The `ShikiHighlighter` component will follow a similar API to `react-syntax-highlighter`,
79
+ but uses Shiki and is optimized for performant sequential highlighting. As of now,
80
+ not all of `react-syntax-highlighter` functionality is supported, but the goal of
81
+ this component is to eventually act as a drop in replacement for `react-syntax-highlighter`.
78
82
 
79
83
  The component accepts several props in addition to language and theme:
80
84
 
@@ -85,9 +89,10 @@ The component accepts several props in addition to language and theme:
85
89
  - `as: string` - The component to be rendered. Defaults to 'pre'
86
90
  - `delay: number` - Delay between highlights in milliseconds, useful for throttling
87
91
  rapid highlighting on the client
88
- - `className: string` - Class name to be passed to the component
92
+ - `className: string` - Class names to be passed to the component
89
93
  - `style: object` - Inline style object to be passed to the component
90
94
  - `langStyle: object` - Inline style object to be passed to the language label
95
+ - `langClassName: string` - Class names to be passed to the language label
91
96
 
92
97
  ```tsx
93
98
  function Houston() {
@@ -109,22 +114,6 @@ function Houston() {
109
114
  }
110
115
  ```
111
116
 
112
- `react-shiki` exports `isInlineCode` to check if a code block is inline:
113
-
114
- ```tsx
115
- const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
116
-
117
- return !isInline ? (
118
- <ShikiHighlighter language={language} theme={"houston"} {...props}>
119
- {String(children)}
120
- </ShikiHighlighter>
121
- ) : (
122
- <code className={className} {...props}>
123
- {children}
124
- </code>
125
- );
126
- ```
127
-
128
117
  ### `react-markdown`
129
118
 
130
119
  ```tsx
@@ -167,6 +156,69 @@ import { CodeHighlight } from "./CodeHighlight";
167
156
  </ReactMarkdown>;
168
157
  ```
169
158
 
159
+ ### Check if code is inline
160
+
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.
164
+
165
+ ```tsx
166
+ const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
167
+
168
+ return !isInline ? (
169
+ <ShikiHighlighter language={language} theme={"houston"} {...props}>
170
+ {String(children)}
171
+ </ShikiHighlighter>
172
+ ) : (
173
+ <code className={className} {...props}>
174
+ {children}
175
+ </code>
176
+ );
177
+ ```
178
+
179
+ `react-shiki` also exports `rehypeInlineCodeProperty`, a more accurate way
180
+ to determine if code is inline.
181
+ It's passed as a rehype plugin to `react-markdown`:
182
+
183
+ ```tsx
184
+ import ReactMarkdown from "react-markdown";
185
+ import { rehypeInlineCodeProperty } from "react-shiki";
186
+
187
+ <ReactMarkdown
188
+ rehypePlugins={[rehypeInlineCodeProperty]}
189
+ components={{
190
+ code: CodeHighlight,
191
+ }}
192
+ >
193
+ {markdown}
194
+ </ReactMarkdown>;
195
+ ```
196
+
197
+ And can be accessed as a prop:
198
+
199
+ ```tsx
200
+ const CodeHighlight = ({
201
+ inline,
202
+ className,
203
+ children,
204
+ node,
205
+ ...props
206
+ }: CodeHighlightProps): JSX.Element => {
207
+ const match = className?.match(/language-(\w+)/);
208
+ const language = match ? match[1] : undefined;
209
+
210
+
211
+ return !inline ? (
212
+ <ShikiHighlighter language={language} theme={"houston"} {...props}>
213
+ {String(children)}
214
+ </ShikiHighlighter>
215
+ ) : (
216
+ <code className={className} {...props}>
217
+ {children}
218
+ </code>
219
+ );
220
+ ```
221
+
170
222
  ### Custom themes
171
223
 
172
224
  ```tsx:title=CodeHighlight.tsx
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
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};
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};
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 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"]}
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"]}
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.3",
4
+ "version": "0.2.4",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Bassim Shahidy",
@@ -40,18 +40,26 @@
40
40
  "react-dom": ">= 15.0.0"
41
41
  },
42
42
  "dependencies": {
43
+ "@types/jest": "^29.5.14",
43
44
  "clsx": "^2.1.1",
44
45
  "html-react-parser": "^5.1.12",
45
- "shiki": "^1.12.1"
46
+ "shiki": "^1.12.1",
47
+ "unist-util-visit": "^5.0.0"
46
48
  },
47
49
  "devDependencies": {
50
+ "@testing-library/jest-dom": "^6.0.0",
51
+ "@testing-library/react": "^14.0.0",
48
52
  "@types/hast": "^3.0.4",
49
53
  "@types/node": "22.10.10",
50
54
  "@types/react": "^18.3.3",
51
- "tsup": "^8.2.4"
55
+ "@vitejs/plugin-react": "^4.0.0",
56
+ "jsdom": "^22.1.0",
57
+ "tsup": "^8.2.4",
58
+ "vitest": "^0.34.0"
52
59
  },
53
60
  "scripts": {
54
61
  "dev": "tsup --watch",
55
- "build": "tsup"
62
+ "build": "tsup",
63
+ "test": "vitest"
56
64
  }
57
65
  }
package/dist/index.d.ts DELETED
@@ -1,107 +0,0 @@
1
- import React, { ReactNode } from 'react';
2
- import { BundledLanguage, ThemeRegistration, BundledTheme } from 'shiki';
3
- import { Element } from 'hast';
4
- export { Element } from 'hast';
5
-
6
- /**
7
- * Languages for syntax highlighting.
8
- * @see https://shiki.style/languages
9
- */
10
- type Language = BundledLanguage | "plaintext" | (string & {}) | undefined;
11
- /**
12
- * A textmate theme object or a Shiki BundledTheme
13
- * @see https://shiki.style/themes
14
- */
15
- type Theme = ThemeRegistration | BundledTheme;
16
- /**
17
- * Configuration options for the syntax highlighter
18
- */
19
- type HighlighterOptions = {
20
- /**
21
- * Minimum time (in milliseconds) between highlight operations.
22
- * Defaults to undefined (no throttling)
23
- */
24
- delay?: number;
25
- };
26
-
27
- /**
28
- * Props for the ShikiHighlighter component
29
- */
30
- interface ShikiHighlighterProps extends HighlighterOptions {
31
- /**
32
- * The programming language for syntax highlighting
33
- * @see https://shiki.style/languages
34
- */
35
- language: Language;
36
- /**
37
- * The code to be highlighted
38
- */
39
- children: string;
40
- /**
41
- * The color theme for syntax highlighting
42
- * Supports Shiki and custom textmate themes
43
- * @see https://shiki.style/themes
44
- */
45
- theme: Theme;
46
- /**
47
- * The delay in milliseconds between streamed updates
48
- * Use this when highlighting code being streamed on the client
49
- * @default undefined (no delay)
50
- */
51
- delay?: number;
52
- /**
53
- * Controls the application of default styles to the generated code blocks
54
- *
55
- * Default styles include padding, overflow handling, border radius, language label styling, and font settings
56
- * @default true
57
- */
58
- addDefaultStyles?: boolean;
59
- /**
60
- * Add custom inline styles to the generated code block
61
- */
62
- style?: React.CSSProperties;
63
- /**
64
- * Add custom inline styles to the language label
65
- */
66
- langStyle?: React.CSSProperties;
67
- /**
68
- * Add custom CSS class names to the generated code block
69
- */
70
- className?: string;
71
- /**
72
- * Whether to show the language label
73
- * @default true
74
- */
75
- showLanguage?: boolean;
76
- /**
77
- * The HTML element that wraps the generated code block.
78
- * @default 'pre'
79
- */
80
- as?: React.ElementType;
81
- }
82
- /**
83
- * ShikiHighlighter is a React component that provides syntax highlighting for code snippets.
84
- * It uses Shiki to render beautiful, theme-based syntax highlighting with optimized performance.
85
- *
86
- * @example
87
- * ```tsx
88
- * <ShikiHighlighter
89
- * language="typescript"
90
- * theme="github-dark"
91
- * delay={100} // Optional throttling for streamed updates
92
- * >
93
- * {code}
94
- * </ShikiHighlighter>
95
- * ```
96
- */
97
- declare const ShikiHighlighter: ({ language, theme, delay, addDefaultStyles, style, langStyle, className, showLanguage, children: code, as: Element, }: ShikiHighlighterProps) => React.ReactElement;
98
-
99
- /**
100
- * A React hook that provides syntax highlighting using Shiki. Uses a
101
- * singleton highlighter instance and supports optional throttled highlights
102
- */
103
- declare const useShikiHighlighter: (code: string, lang: Language, theme: Theme, options?: HighlighterOptions) => ReactNode | null;
104
-
105
- declare const isInlineCode: (node: Element) => boolean;
106
-
107
- export { ShikiHighlighter as default, isInlineCode, useShikiHighlighter };