react-shiki 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -36
- package/dist/index.d.ts +49 -21
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
> [!
|
|
2
|
-
> This package is still a work in progress,
|
|
3
|
-
|
|
1
|
+
> [!NOTE]
|
|
2
|
+
> This package is still a work in progress, fully functional but not extensively tested.
|
|
4
3
|
|
|
5
4
|
# 🎨 [react-shiki](https://react-shiki.vercel.app/)
|
|
6
5
|
|
|
7
6
|
Syntax highlighting component and hook for react using [Shiki](https://shiki.matsu.io/)
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
[See the demo page for a version of this README with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/)
|
|
8
|
+
[See the demo page with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/)
|
|
11
9
|
|
|
12
10
|
## Features
|
|
11
|
+
|
|
13
12
|
- 🖼️ Provides a `ShikiHighlighter` component for highlighting code as children, as well as a `useShikiHighlighter` hook for more flexibility
|
|
14
|
-
- 🔐 No `dangerouslySetInnerHTML`, output from Shiki is parsed using `html-react-parser`
|
|
13
|
+
- 🔐 No `dangerouslySetInnerHTML`, output from Shiki is parsed using `html-react-parser`
|
|
15
14
|
- 📦 Supports all Shiki languages and themes
|
|
15
|
+
- 🚰 Supports performant highlighting of streamed code on the client, with optional throttling
|
|
16
16
|
- 📚 Includes minimal default styles for code blocks
|
|
17
17
|
- 🚀 Shiki dynamically imports only the languages and themes used on a page, optimizing for performance
|
|
18
18
|
- 🖥️ `ShikiHighlighter` component displays a language label for each code block when `showLanguage` is set to `true` (default)
|
|
19
19
|
- 🎨 Users can customize the styling of the generated code blocks by passing a `style` object or a `className`
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
## Installation
|
|
23
22
|
|
|
24
23
|
```bash
|
|
25
|
-
[pnpm|bun|yarn|npm]
|
|
24
|
+
[pnpm|bun|yarn|npm] install react-shiki
|
|
26
25
|
```
|
|
27
26
|
|
|
28
27
|
## Usage
|
|
@@ -30,6 +29,7 @@ Syntax highlighting component and hook for react using [Shiki](https://shiki.mat
|
|
|
30
29
|
You can use the `ShikiHighlighter` component, or the `useShikiHighlighter` hook to highlight code.
|
|
31
30
|
|
|
32
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.
|
|
32
|
+
|
|
33
33
|
```tsx
|
|
34
34
|
const highlightedCode = useShikiHighlighter(code, language, theme);
|
|
35
35
|
```
|
|
@@ -48,7 +48,6 @@ function CodeBlock() {
|
|
|
48
48
|
}
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
52
51
|
The component accepts several props in addition to language and theme:
|
|
53
52
|
|
|
54
53
|
- `showLanguage: boolean` - Shows the language name in the top right corner of the code block.
|
|
@@ -67,7 +66,7 @@ function Houston() {
|
|
|
67
66
|
addDefaultStyles={true}
|
|
68
67
|
as="div"
|
|
69
68
|
style={{
|
|
70
|
-
textAlign:
|
|
69
|
+
textAlign: "left",
|
|
71
70
|
}}
|
|
72
71
|
>
|
|
73
72
|
{code.trim()}
|
|
@@ -76,11 +75,27 @@ function Houston() {
|
|
|
76
75
|
}
|
|
77
76
|
```
|
|
78
77
|
|
|
78
|
+
react-shiki exports `isInlineCode` to check if a code block is inline.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
|
|
82
|
+
|
|
83
|
+
return !isInline ? (
|
|
84
|
+
<ShikiHighlighter language={language} theme={"houston"} {...props}>
|
|
85
|
+
{String(children)}
|
|
86
|
+
</ShikiHighlighter>
|
|
87
|
+
) : (
|
|
88
|
+
<code className={className} {...props}>
|
|
89
|
+
{children}
|
|
90
|
+
</code>
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
79
94
|
It can also be used with `react-markdown`:
|
|
95
|
+
|
|
80
96
|
```tsx
|
|
81
|
-
import type { ReactNode } from
|
|
82
|
-
import
|
|
83
|
-
import ShikiHighlighter, { isInlineCode, type Element } from 'react-shiki';
|
|
97
|
+
import type { ReactNode } from "react";
|
|
98
|
+
import ShikiHighlighter, { type Element } from "react-shiki";
|
|
84
99
|
|
|
85
100
|
interface CodeHighlightProps {
|
|
86
101
|
className?: string | undefined;
|
|
@@ -95,30 +110,19 @@ export const CodeHighlight = ({
|
|
|
95
110
|
...props
|
|
96
111
|
}: CodeHighlightProps): JSX.Element => {
|
|
97
112
|
const match = className?.match(/language-(\w+)/);
|
|
98
|
-
|
|
99
|
-
const language = match ? (match[1] as BundledLanguage) : undefined;
|
|
113
|
+
const language = match ? match[1] : undefined;
|
|
100
114
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<ShikiHighlighter
|
|
105
|
-
language={language as BundledLanguage}
|
|
106
|
-
theme={'houston'}
|
|
107
|
-
{...props}>
|
|
108
|
-
{String(children)}
|
|
109
|
-
</ShikiHighlighter>
|
|
110
|
-
) : (
|
|
111
|
-
<code className={className} {...props}>
|
|
112
|
-
{children}
|
|
113
|
-
</code>
|
|
114
|
-
);
|
|
115
|
+
<ShikiHighlighter language={language} theme={"houston"} {...props}>
|
|
116
|
+
{String(children)}
|
|
117
|
+
</ShikiHighlighter>;
|
|
115
118
|
};
|
|
116
119
|
```
|
|
117
120
|
|
|
118
121
|
Pass CodeHighlight to `react-markdown` as a code component:
|
|
122
|
+
|
|
119
123
|
```tsx
|
|
120
|
-
import ReactMarkdown from
|
|
121
|
-
import { CodeHighlight } from
|
|
124
|
+
import ReactMarkdown from "react-markdown";
|
|
125
|
+
import { CodeHighlight } from "./CodeHighlight";
|
|
122
126
|
|
|
123
127
|
<ReactMarkdown
|
|
124
128
|
components={{
|
|
@@ -126,11 +130,106 @@ import { CodeHighlight } from './CodeHighlight';
|
|
|
126
130
|
}}
|
|
127
131
|
>
|
|
128
132
|
{markdown}
|
|
129
|
-
</ReactMarkdown
|
|
133
|
+
</ReactMarkdown>;
|
|
130
134
|
```
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
### Custom themes
|
|
137
|
+
|
|
138
|
+
```tsx:title=CodeHighlight.tsx
|
|
139
|
+
import tokyoNight from '@styles/tokyo-night.mjs';
|
|
140
|
+
|
|
141
|
+
<ShikiHighlighter language="tsx" theme={tokyoNight}>
|
|
142
|
+
{String(code)}
|
|
143
|
+
</ShikiHighlighter>;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Client-side highlighting
|
|
147
|
+
|
|
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.
|
|
149
|
+
|
|
150
|
+
Using `useShikiHighlighter` hook:
|
|
151
|
+
|
|
152
|
+
```tsx title=CodeHighlight.tsx
|
|
153
|
+
import type { ReactNode } from "react";
|
|
154
|
+
import { isInlineCode, useShikiHighlighter, type Element } from "react-shiki";
|
|
155
|
+
import tokyoNight from "@styles/tokyo-night.mjs";
|
|
156
|
+
|
|
157
|
+
interface CodeHighlightProps {
|
|
158
|
+
className?: string | undefined;
|
|
159
|
+
children?: ReactNode | undefined;
|
|
160
|
+
node?: Element | undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const CodeHighlight = ({
|
|
164
|
+
className,
|
|
165
|
+
children,
|
|
166
|
+
node,
|
|
167
|
+
...props
|
|
168
|
+
}: CodeHighlightProps) => {
|
|
169
|
+
const code = String(children);
|
|
170
|
+
const language = className?.match(/language-(\w+)/)?.[1];
|
|
171
|
+
|
|
172
|
+
const isInline = node ? isInlineCode(node) : false;
|
|
173
|
+
|
|
174
|
+
const highlightedCode = useShikiHighlighter(language, code, tokyoNight, {
|
|
175
|
+
delay: 150,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return !isInline ? (
|
|
179
|
+
<div className="shiki not-prose relative [&_pre]:overflow-auto [&_pre]:rounded-lg [&_pre]:px-6 [&_pre]:py-5">
|
|
180
|
+
{language ? (
|
|
181
|
+
<span className="absolute right-3 top-2 text-xs tracking-tighter text-muted-foreground/85">
|
|
182
|
+
{language}
|
|
183
|
+
</span>
|
|
184
|
+
) : null}
|
|
185
|
+
{highlightedCode}
|
|
186
|
+
</div>
|
|
187
|
+
) : (
|
|
188
|
+
<code className={className} {...props}>
|
|
189
|
+
{children}
|
|
190
|
+
</code>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Or using the `ShikiHighlighter` component:
|
|
196
|
+
|
|
197
|
+
```tsx title=CodeHighlight.tsx
|
|
198
|
+
import type { ReactNode } from "react";
|
|
199
|
+
import ShikiHighlighter, { isInlineCode, type Element } from "react-shiki";
|
|
200
|
+
|
|
201
|
+
interface CodeHighlightProps {
|
|
202
|
+
className?: string | undefined;
|
|
203
|
+
children?: ReactNode | undefined;
|
|
204
|
+
node?: Element | undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const CodeHighlight = ({
|
|
208
|
+
className,
|
|
209
|
+
children,
|
|
210
|
+
node,
|
|
211
|
+
...props
|
|
212
|
+
}: CodeHighlightProps): JSX.Element => {
|
|
213
|
+
const match = className?.match(/language-(\w+)/);
|
|
214
|
+
const language = match ? match[1] : undefined;
|
|
215
|
+
|
|
216
|
+
const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
|
|
217
|
+
|
|
218
|
+
return !isInline ? (
|
|
219
|
+
<ShikiHighlighter language={language} theme={"houston"} {...props}>
|
|
220
|
+
{String(children)}
|
|
221
|
+
</ShikiHighlighter>
|
|
222
|
+
) : (
|
|
223
|
+
<code className={className} {...props}>
|
|
224
|
+
{children}
|
|
225
|
+
</code>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Passed to `react-markdown` as a code component in memoized chat messages:
|
|
231
|
+
|
|
232
|
+
```tsx title=ChatMessages.tsx
|
|
134
233
|
const RenderedMessage = React.memo(({ message }: { message: Message }) => (
|
|
135
234
|
<div className={cn(messageStyles[message.role])}>
|
|
136
235
|
<ReactMarkdown components={{ code: CodeHighlight }}>
|
|
@@ -141,11 +240,11 @@ const RenderedMessage = React.memo(({ message }: { message: Message }) => (
|
|
|
141
240
|
|
|
142
241
|
export const ChatMessages = ({ messages }: { messages: Message[] }) => {
|
|
143
242
|
return (
|
|
144
|
-
<div className=
|
|
243
|
+
<div className="space-y-4">
|
|
145
244
|
{messages.map((message) => (
|
|
146
245
|
<RenderedMessage key={message.id} message={message} />
|
|
147
246
|
))}
|
|
148
247
|
</div>
|
|
149
248
|
);
|
|
150
249
|
};
|
|
151
|
-
```
|
|
250
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,23 +1,54 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
|
-
import { BundledLanguage, BundledTheme } from 'shiki';
|
|
2
|
+
import { BundledLanguage, ThemeRegistration, BundledTheme } from 'shiki';
|
|
3
3
|
import { Element } from 'hast';
|
|
4
4
|
export { Element } from 'hast';
|
|
5
5
|
|
|
6
|
-
|
|
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 {
|
|
7
31
|
/**
|
|
8
32
|
* The programming language for syntax highlighting
|
|
9
33
|
* @see https://shiki.style/languages
|
|
10
34
|
*/
|
|
11
|
-
language:
|
|
35
|
+
language: Language;
|
|
12
36
|
/**
|
|
13
37
|
* The code to be highlighted
|
|
14
38
|
*/
|
|
15
39
|
children: string;
|
|
16
40
|
/**
|
|
17
|
-
* The color theme for syntax highlighting
|
|
41
|
+
* The color theme for syntax highlighting
|
|
42
|
+
* Supports Shiki and custom textmate themes
|
|
18
43
|
* @see https://shiki.style/themes
|
|
19
44
|
*/
|
|
20
|
-
theme:
|
|
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;
|
|
21
52
|
/**
|
|
22
53
|
* Controls the application of default styles to the generated code blocks
|
|
23
54
|
*
|
|
@@ -46,29 +77,26 @@ interface ShikiHighlighterProps {
|
|
|
46
77
|
}
|
|
47
78
|
/**
|
|
48
79
|
* ShikiHighlighter is a React component that provides syntax highlighting for code snippets.
|
|
49
|
-
*
|
|
50
|
-
* It uses the Shiki library to render beautiful, theme-based syntax highlighting.
|
|
80
|
+
* It uses Shiki to render beautiful, theme-based syntax highlighting with optimized performance.
|
|
51
81
|
*
|
|
52
82
|
* @example
|
|
53
83
|
* ```tsx
|
|
54
|
-
* <ShikiHighlighter
|
|
55
|
-
*
|
|
84
|
+
* <ShikiHighlighter
|
|
85
|
+
* language="typescript"
|
|
86
|
+
* theme="github-dark"
|
|
87
|
+
* delay={100} // Optional throttling for streamed updates
|
|
88
|
+
* >
|
|
89
|
+
* {code}
|
|
56
90
|
* </ShikiHighlighter>
|
|
57
91
|
* ```
|
|
58
92
|
*/
|
|
59
|
-
declare const ShikiHighlighter: ({ language, theme, addDefaultStyles, style, className, showLanguage, children: code, as: Element, }: ShikiHighlighterProps) => React.ReactElement;
|
|
93
|
+
declare const ShikiHighlighter: ({ language, theme, delay, addDefaultStyles, style, className, showLanguage, children: code, as: Element, }: ShikiHighlighterProps) => React.ReactElement;
|
|
60
94
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* ```tsx
|
|
67
|
-
* const highlightedCode = useShikiHighlighter(code, language, theme);
|
|
68
|
-
* ```
|
|
69
|
-
*
|
|
70
|
-
*/
|
|
71
|
-
declare const useShikiHighlighter: (code: string, lang: BundledLanguage | undefined, theme: BundledTheme) => ReactNode | null;
|
|
95
|
+
/************************************************************************
|
|
96
|
+
** A React hook that provides syntax highlighting using Shiki. Uses a **
|
|
97
|
+
** singleton highlighter instance and supports throttled updates. **
|
|
98
|
+
*************************************************************************/
|
|
99
|
+
declare const useShikiHighlighter: (code: string, lang: Language, theme: Theme, options?: HighlighterOptions) => ReactNode | null;
|
|
72
100
|
|
|
73
101
|
declare const isInlineCode: (node: Element) => boolean;
|
|
74
102
|
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
function
|
|
2
|
-
`);import
|
|
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};
|
|
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';\n// biome-ignore lint/style/useImportType: Package needs React to be imported\nimport React from 'react';\nimport type { BundledLanguage, BundledTheme } from 'shiki';\nimport { useShikiHighlighter } from '@/useShiki';\nimport { clsx } from 'clsx';\n\nexport interface ShikiHighlighterProps {\n /** \n * The programming language for syntax highlighting\n * @see https://shiki.style/languages\n */\n language: BundledLanguage;\n\n /** \n * The code to be highlighted \n */\n children: string;\n\n /** \n * The color theme for syntax highlighting, only Shiki themes are supported at this time\n * @see https://shiki.style/themes\n */\n theme: BundledTheme;\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 *\n * It uses the Shiki library to render beautiful, theme-based syntax highlighting.\n *\n * @example\n * ```tsx\n * <ShikiHighlighter language=\"tsx\" theme=\"vitesse-black\">\n * {code.trim()}\n * </ShikiHighlighter>\n * ```\n */\nexport const ShikiHighlighter = ({\n language,\n theme,\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);\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 parse from 'html-react-parser';\nimport { type ReactNode, useEffect, useState } from 'react';\nimport { type BundledLanguage, type BundledTheme, codeToHtml } from 'shiki';\nimport { removeTabIndexFromPre } from '@/utils';\n\n/**\n * \n * `useShikiHighlighter` is a custom hook that takes in the code to be highlighted, the language, and the theme, and returns the highlighted code parsed as a ReactNode.\n *\n * @example\n * ```tsx\n * const highlightedCode = useShikiHighlighter(code, language, theme);\n * ```\n * \n */\nexport const useShikiHighlighter = (\n code: string,\n lang: BundledLanguage | undefined,\n theme: BundledTheme\n): ReactNode | null => {\n const [highlightedCode, setHighlightedCode] = useState<ReactNode | null>(\n null\n );\n\n useEffect(() => {\n const highlightCode = async () => {\n try {\n const html = await codeToHtml(code, {\n lang: lang as BundledLanguage,\n theme,\n transformers: [removeTabIndexFromPre],\n });\n setHighlightedCode(parse(html));\n } catch (error) {\n const fallbackHtml = await codeToHtml(code, {\n lang: 'plaintext',\n theme,\n transformers: [removeTabIndexFromPre],\n });\n setHighlightedCode(parse(fallbackHtml));\n }\n };\n\n highlightCode();\n }, [code, theme, 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":"AACyB,SAARA,EAA6BC,EAAK,CAAE,SAAAC,CAAS,EAAI,CAAC,EAAG,CAC1D,GAAI,CAACD,GAAO,OAAO,SAAa,IAAa,OAE7C,IAAME,EAAO,SAAS,MAAQ,SAAS,qBAAqB,MAAM,EAAE,CAAC,EAC/DC,EAAQ,SAAS,cAAc,OAAO,EAC5CA,EAAM,KAAO,WAETF,IAAa,OACXC,EAAK,WACPA,EAAK,aAAaC,EAAOD,EAAK,UAAU,EAK1CA,EAAK,YAAYC,CAAK,EAGpBA,EAAM,WACRA,EAAM,WAAW,QAAUH,EAE3BG,EAAM,YAAY,SAAS,eAAeH,CAAG,CAAC,CAElD,CCvB8BI,EAAY;AAAA,CAA6O,ECEjS,OAAOC,MAAW,QCFlB,OAAOC,MAAW,oBAClB,OAAyB,aAAAC,EAAW,YAAAC,MAAgB,QACpD,OAAkD,cAAAC,MAAkB,QCA7D,IAAMC,EAAgBC,GACpBA,EAAK,UAAU,MAAM,OAASA,EAAK,UAAU,IAAI,KAI7CC,EAAwB,CACnC,IAAID,EAAe,CACjBA,EAAK,WAAW,SAAW,IAC7B,CACF,EDIO,IAAME,EAAsB,CACjCC,EACAC,EACAC,IACqB,CACrB,GAAM,CAACC,EAAiBC,CAAkB,EAAIC,EAC5C,IACF,EAEA,OAAAC,EAAU,IAAM,EACQ,SAAY,CAChC,GAAI,CACF,IAAMC,EAAO,MAAMC,EAAWR,EAAM,CAClC,KAAMC,EACN,MAAAC,EACA,aAAc,CAACO,CAAqB,CACtC,CAAC,EACDL,EAAmBM,EAAMH,CAAI,CAAC,CAChC,MAAgB,CACd,IAAMI,EAAe,MAAMH,EAAWR,EAAM,CAC1C,KAAM,YACN,MAAAE,EACA,aAAc,CAACO,CAAqB,CACtC,CAAC,EACDL,EAAmBM,EAAMC,CAAY,CAAC,CACxC,CACF,GAEc,CAChB,EAAG,CAACX,EAAME,EAAOD,CAAI,CAAC,EAEfE,CACT,ED1CA,OAAS,QAAAS,MAAY,OA+Dd,IAAMC,EAAmB,CAAC,CAC/B,SAAAC,EACA,MAAAC,EACA,iBAAAC,EAAmB,GACnB,MAAAC,EACA,UAAAC,EACA,aAAAC,EAAe,GACf,SAAUC,EACV,GAAIC,EAAU,KAChB,IAAiD,CAC/C,IAAMC,EAAkBC,EAAoBH,EAAMN,EAAUC,CAAK,EAEjE,OACES,EAAA,cAACH,EAAA,CACC,UAAWT,EACT,WACAI,GAAoB,YACpBE,CACF,EACA,MAAOD,GAENE,GAAgBL,EACfU,EAAA,cAAC,QAAK,UAAU,gBAAgB,MAAOP,GACpCH,CACH,EACE,KACHQ,CACH,CAEJ","names":["styleInject","css","insertAt","head","style","styleInject","React","parse","useEffect","useState","codeToHtml","isInlineCode","node","removeTabIndexFromPre","useShikiHighlighter","code","lang","theme","highlightedCode","setHighlightedCode","useState","useEffect","html","codeToHtml","removeTabIndexFromPre","parse","fallbackHtml","clsx","ShikiHighlighter","language","theme","addDefaultStyles","style","className","showLanguage","code","Element","highlightedCode","useShikiHighlighter","React"]}
|
|
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"]}
|
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.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bassim Shahidy",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/hast": "^3.0.4",
|
|
49
|
+
"@types/node": "22.10.10",
|
|
49
50
|
"@types/react": "^18.3.3",
|
|
50
51
|
"tsup": "^8.2.4"
|
|
51
52
|
},
|