react-shiki
Advanced tools
Comparing version
import React, { ReactNode } from 'react'; | ||
import { BundledLanguage, SpecialLanguage, ThemeRegistration, BundledTheme, ShikiTransformer } from 'shiki'; | ||
import { Root, Element } from 'hast'; | ||
import { LanguageRegistration as LanguageRegistration$1, ShikiTransformer, BundledLanguage, SpecialLanguage, ThemeRegistration, BundledTheme } from 'shiki'; | ||
import { Element, Root } from 'hast'; | ||
export { Element } from 'hast'; | ||
/** | ||
* Attribution: | ||
* This code was written by github:hippotastic in expressive-code/expressive-code | ||
* https://github.com/expressive-code/expressive-code/blob/main/packages/%40expressive-code/plugin-shiki/src/languages.ts | ||
*/ | ||
type IShikiRawRepository = LanguageRegistration$1['repository']; | ||
type IShikiRawRule = IShikiRawRepository[keyof IShikiRawRepository]; | ||
type ILocation = IShikiRawRepository['$vscodeTextmateLocation']; | ||
interface ILocatable { | ||
readonly $vscodeTextmateLocation?: ILocation; | ||
} | ||
interface IRawRepositoryMap { | ||
[name: string]: IRawRule; | ||
} | ||
type IRawRepository = IRawRepositoryMap & ILocatable; | ||
interface IRawCapturesMap { | ||
[captureId: string]: IRawRule; | ||
} | ||
type IRawCaptures = IRawCapturesMap & ILocatable; | ||
interface IRawRule extends Omit<IShikiRawRule, 'applyEndPatternLast' | 'captures' | 'patterns'> { | ||
readonly applyEndPatternLast?: boolean | number; | ||
readonly captures?: IRawCaptures; | ||
readonly comment?: string; | ||
readonly patterns?: IRawRule[]; | ||
} | ||
/** | ||
* A less strict version of Shiki's `LanguageRegistration` interface that aligns better with | ||
* actual grammars found in the wild. This version attempts to reduce the amount | ||
* of type errors that would occur when importing and adding external grammars, | ||
* while still being supported by the language processing code. | ||
*/ | ||
interface LanguageRegistration extends Omit<LanguageRegistration$1, 'repository'> { | ||
repository?: IRawRepository; | ||
} | ||
/** | ||
* Languages for syntax highlighting. | ||
* @see https://shiki.style/languages | ||
*/ | ||
type Language = BundledLanguage | SpecialLanguage | (string & {}) | undefined; | ||
type Language = BundledLanguage | LanguageRegistration | SpecialLanguage | (string & {}) | undefined; | ||
/** | ||
@@ -29,2 +65,6 @@ * A textmate theme object or a Shiki BundledTheme | ||
transformers?: ShikiTransformer[]; | ||
/** | ||
* Custom textmate grammar to be preloaded for highlighting. | ||
*/ | ||
customLanguages?: LanguageRegistration | LanguageRegistration[]; | ||
}; | ||
@@ -94,2 +134,6 @@ | ||
as?: React.ElementType; | ||
/** | ||
* Custom languages to be preloaded for highlighting. | ||
*/ | ||
customLanguages?: LanguageRegistration[]; | ||
} | ||
@@ -111,3 +155,3 @@ /** | ||
*/ | ||
declare const ShikiHighlighter: ({ language, theme, delay, transformers, addDefaultStyles, style, langStyle, className, langClassName, showLanguage, children: code, as: Element, }: ShikiHighlighterProps) => React.ReactElement; | ||
declare const ShikiHighlighter: ({ language, theme, delay, transformers, addDefaultStyles, style, langStyle, className, langClassName, showLanguage, children: code, as: Element, customLanguages, }: ShikiHighlighterProps) => React.ReactElement; | ||
@@ -122,2 +166,3 @@ /** | ||
* delay: 150 | ||
* customLanguages: ['bosque', 'mcfunction'] | ||
* }); | ||
@@ -128,7 +173,7 @@ */ | ||
/** | ||
* Rehype plugin to add an 'inline' property to <code> elements. | ||
* Sets an 'inline' property to true if the <code> is not within a <pre> tag. | ||
* Rehype plugin to add an 'inline' property to <code> elements | ||
* Sets 'inline' property to true if the <code> is not within a <pre> tag | ||
* | ||
* Pass this plugin to the `rehypePlugins` prop of ReactMarkdown. | ||
* You can then access `inline` as a prop from ReactMarkdown. | ||
* Pass this plugin to the `rehypePlugins` prop of react-markdown | ||
* You can then access `inline` as a prop in components passed to react-markdown | ||
* | ||
@@ -140,4 +185,6 @@ * @example | ||
/** | ||
* Function to determine if code is inline based on the presence of line breaks. | ||
* Reports `inline = true` for single line fenced code blocks. | ||
* Function to determine if code is inline based on the presence of line breaks | ||
* | ||
* @example | ||
* const isInline = node && isInlineCode(node: Element) | ||
*/ | ||
@@ -144,0 +191,0 @@ declare const isInlineCode: (node: Element) => boolean; |
@@ -1,4 +0,4 @@ | ||
function h(e,{insertAt:i}={}){if(!e||typeof document>"u")return;let r=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style");t.type="text/css",i==="top"&&r.firstChild?r.insertBefore(t,r.firstChild):r.appendChild(t),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document.createTextNode(e))}h(`.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} | ||
`);import x from"react";import{clsx as S}from"clsx";import{useEffect as E,useRef as I,useState as v}from"react";import R from"html-react-parser";import{createHighlighter as N,createSingletonShorthands as P}from"shiki";import{visit as L}from"unist-util-visit";import{bundledLanguages as b,isSpecialLang as H}from"shiki";function C(){return function(e){L(e,"element",function(i,r,t){i.tagName==="code"&&t.tagName!=="pre"&&(i.properties.inline=!0)})}}var k=e=>!(e.children||[]).filter(r=>r.type==="text").map(r=>r.value).join("").includes(` | ||
`),c={pre(e){return"properties"in e&&(e.properties.tabindex="-1"),e}},f=(e,i,r)=>{let t=Date.now();clearTimeout(i.current.timeoutId);let o=Math.max(0,i.current.nextAllowedTime-t);i.current.timeoutId=setTimeout(()=>{e().catch(console.error),i.current.nextAllowedTime=t+r},o)},y=e=>typeof e=="string"&&!(e in b)&&!H(e)?"plaintext":e;var w=P(N),u=(e,i,r,t={})=>{let[o,m]=v(null),p=y(i),n=I({nextAllowedTime:0,timeoutId:void 0});return E(()=>{let a=!0,g=[c,...t.transformers||[]],s=async()=>{let d=await w.codeToHtml(e,{lang:p,theme:r,transformers:g});a&&m(R(d))},{delay:l}=t;return l?f(s,n,l):s().catch(console.error),()=>{a=!1,clearTimeout(n.current.timeoutId)}},[e,i]),o};var B=({language:e,theme:i,delay:r,transformers:t,addDefaultStyles:o=!0,style:m,langStyle:p,className:n,langClassName:a,showLanguage:g=!0,children:s,as:l="pre"})=>{let T=u(s,e,i,{delay:r,transformers:t});return x.createElement(l,{"data-testid":"shiki-container",className:S("relative","not-prose",o&&"defaultStyles",n),style:m,id:"shiki-container"},g&&e?x.createElement("span",{className:S("languageLabel",a),style:p,id:"language-label"},e):null,T)};export{B as default,k as isInlineCode,C as rehypeInlineCodeProperty,u as useShikiHighlighter}; | ||
function L(e,{insertAt:r}={}){if(!e||typeof document>"u")return;let i=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style");t.type="text/css",r==="top"&&i.firstChild?i.insertBefore(t,i.firstChild):i.appendChild(t),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document.createTextNode(e))}L(`.relative{position:relative}.defaultStyles pre{overflow:auto;border-radius:.5rem;padding:1.25rem 1.5rem}.languageLabel{position:absolute;right:.75rem;top:.5rem;font-family:monospace;font-size:.75rem;letter-spacing:-.05em;color:#6b7280d9} | ||
`);import R from"react";import{clsx as b}from"clsx";import{useEffect as O,useRef as j,useState as M}from"react";import z from"html-react-parser";import{createHighlighter as H,createSingletonShorthands as $}from"shiki";import{visit as w}from"unist-util-visit";import{bundledLanguages as E,isSpecialLang as N}from"shiki";function P(){return function(e){w(e,"element",function(r,i,t){r.tagName==="code"&&t.tagName!=="pre"&&(r.properties.inline=!0)})}}var A=e=>!(e.children||[]).filter(i=>i.type==="text").map(i=>i.value).join("").includes(` | ||
`),S={pre(e){return"properties"in e&&(e.properties.tabindex="-1"),e}},T=(e,r,i)=>{let t=Date.now();clearTimeout(r.current.timeoutId);let o=Math.max(0,r.current.nextAllowedTime-t);r.current.timeoutId=setTimeout(()=>{e().catch(console.error),r.current.nextAllowedTime=t+i},o)},u=(e,r=[])=>{if(e&&typeof e=="object")return{isCustom:!0,languageId:e.name,displayLanguageId:e.name,resolvedLanguage:e};if(typeof e=="string"){let i=e;if(e in E||N(e))return{isCustom:!1,languageId:e,displayLanguageId:i};let t=r.find(o=>o.fileTypes?.includes(e)||o.scopeName?.split(".")[1]===e||o.name?.toLowerCase()===e.toLowerCase());return t?{isCustom:!0,languageId:t.name,displayLanguageId:i,resolvedLanguage:t}:{isCustom:!1,languageId:"plaintext",displayLanguageId:i}}return{isCustom:!1,languageId:"plaintext",displayLanguageId:"plaintext"}};var B=$(H),I=new Map,x=(e,r,i,t={})=>{let[o,p]=M(null),d=typeof i=="string"?i:i.name,h=t.customLanguages?Array.isArray(t.customLanguages)?t.customLanguages:[t.customLanguages]:[],{isCustom:c,languageId:f,resolvedLanguage:g}=u(r,h),m=j({nextAllowedTime:0,timeoutId:void 0}),n=async(s,l)=>{let a=I.get(s);return a||(a=H({langs:[l],themes:[i]}),I.set(s,a)),a};return O(()=>{let s=!0,l=[S,...t.transformers||[]],a=async()=>{let y=await(c&&g?await n(`${g.name}--${d}`,g):B).codeToHtml(e,{lang:f,theme:i,transformers:l});s&&p(z(y))};return t.delay?T(a,m,t.delay):a().catch(console.error),()=>{s=!1,clearTimeout(m.current.timeoutId)}},[e,r]),o};var D=({language:e,theme:r,delay:i,transformers:t,addDefaultStyles:o=!0,style:p,langStyle:d,className:h,langClassName:c,showLanguage:f=!0,children:g,as:m="pre",customLanguages:n})=>{let s={delay:i,transformers:t,customLanguages:n},l=n?Array.isArray(n)?n:[n]:[],{isCustom:a,languageId:C,displayLanguageId:y,resolvedLanguage:v}=u(e,l),k=x(g,e,r,s);return R.createElement(m,{"data-testid":"shiki-container",className:b("relative","not-prose",o&&"defaultStyles",h),style:p,id:"shiki-container"},f&&e?R.createElement("span",{className:b("languageLabel",c),style:d,id:"language-label"},a?`${v?.scopeName.split(".")[1]}`:y||C):null,k)};export{D as default,A as isInlineCode,P as rehypeInlineCodeProperty,x as useShikiHighlighter}; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "react-shiki", | ||
"description": "Syntax highlighter component for react using shiki", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"license": "MIT", | ||
@@ -46,15 +46,15 @@ "author": { | ||
"html-react-parser": "^5.1.12", | ||
"shiki": "^1.12.1", | ||
"shiki": "^3.0.0", | ||
"unist-util-visit": "^5.0.0" | ||
}, | ||
"devDependencies": { | ||
"@testing-library/jest-dom": "^6.0.0", | ||
"@testing-library/react": "^14.0.0", | ||
"@testing-library/jest-dom": "^6.6.3", | ||
"@testing-library/react": "^14.3.1", | ||
"@types/hast": "^3.0.4", | ||
"@types/node": "22.10.10", | ||
"@types/react": "^18.3.3", | ||
"@vitejs/plugin-react": "^4.0.0", | ||
"@types/node": "22.13.4", | ||
"@types/react": "^18.3.18", | ||
"@vitejs/plugin-react": "^4.3.4", | ||
"jsdom": "^22.1.0", | ||
"tsup": "^8.2.4", | ||
"vitest": "^0.34.0" | ||
"tsup": "^8.3.6", | ||
"vitest": "^0.34.6" | ||
}, | ||
@@ -61,0 +61,0 @@ "scripts": { |
# 🎨 [react-shiki](https://npmjs.com/react-shiki) | ||
> [!NOTE] | ||
> This package is still a work in progress, fully functional but not | ||
> extensively tested. | ||
> This library is still in development, more features will | ||
> continue to be implemented, and API may change. | ||
> Contributions are welcome! | ||
Performant client side syntax highlighting component + hook | ||
for react using [Shiki](https://shiki.matsu.io/) | ||
for react built with [Shiki](https://shiki.matsu.io/) | ||
@@ -21,2 +22,4 @@ [See the demo page with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/) | ||
- [Custom themes](#custom-themes) | ||
- [Custom languages](#custom-languages) | ||
- [Preloading custom languages](#preloading-custom-languages) | ||
- [Custom transformers](#custom-transformers) | ||
@@ -33,6 +36,6 @@ - [Performance](#performance) | ||
- 🔐 No `dangerouslySetInnerHTML`, output from Shiki is parsed using `html-react-parser` | ||
- 📦 Supports all Shiki languages and themes | ||
- 🖌️ Full support for custom TextMate themes in a JavaScript object format | ||
- 📦 Supports all built-in Shiki languages and themes | ||
- 🖌️ Full support for custom TextMate themes and languages | ||
- 🔧 Supports passing custom Shiki transformers to the highlighter | ||
- 🚰 Performant highlighting of streamed code on the client, with optional throttling | ||
- 🚰 Performant highlighting of streamed code, with optional throttling | ||
- 📚 Includes minimal default styles for code blocks | ||
@@ -43,4 +46,3 @@ - 🚀 Shiki dynamically imports only the languages and themes used on a page, | ||
when `showLanguage` is set to `true` (default) | ||
- 🎨 Users can customize the styling of the generated code blocks by passing | ||
a `style` object or a `className` | ||
- 🎨 Customizable styling of generated code blocks and language labels | ||
@@ -50,3 +52,3 @@ ## Installation | ||
```bash | ||
[pnpm|bun|yarn|npm] install react-shiki | ||
pnpm install react-shiki | ||
``` | ||
@@ -102,2 +104,3 @@ | ||
- `langClassName: string` - Class names to be passed to the language label | ||
- `customLanguages: LanguageRegistration[]` - Custom languages to be preloaded for highlighting | ||
@@ -168,5 +171,5 @@ ```tsx | ||
There are two ways to check if a code block is inline: | ||
`react-shiki` exports `isInlineCode`, good but marks multiline inline | ||
code tags as code blocks. | ||
There are two built-in ways to check if a code block is inline, both provide the same result: | ||
`react-shiki` exports `isInlineCode` which parses the `node` | ||
prop to determine if the code is inline based on the presence of line breaks: | ||
@@ -187,4 +190,5 @@ ```tsx | ||
`react-shiki` also exports `rehypeInlineCodeProperty`, a more accurate way | ||
to determine if code is inline. | ||
`react-shiki` also exports `rehypeInlineCodeProperty`, a rehype plugin that adds | ||
an `inline` property to `react-markdown` to determine if code is inline based on | ||
the presence of a `<pre>` tag as a parent of `<code>`. | ||
It's passed as a rehype plugin to `react-markdown`: | ||
@@ -206,3 +210,3 @@ | ||
And can be accessed as a prop: | ||
Now `inline` can be accessed as a prop in the `CodeHighlight` component: | ||
@@ -219,2 +223,3 @@ ```tsx | ||
const language = match ? match[1] : undefined; | ||
const code = String(children).trim(); | ||
@@ -224,3 +229,3 @@ | ||
<ShikiHighlighter language={language} theme={"houston"} {...props}> | ||
{String(children).trim()} | ||
{code} | ||
</ShikiHighlighter> | ||
@@ -236,20 +241,63 @@ ) : ( | ||
Pass custom TextMate themes as a JSON object: | ||
```tsx | ||
import tokyoNight from '@styles/tokyo-night.mjs'; | ||
import tokyoNight from '../styles/tokyo-night.json'; | ||
// component | ||
<ShikiHighlighter language="tsx" theme={tokyoNight}> | ||
{String(code)} | ||
{String(code).trim()} | ||
</ShikiHighlighter>; | ||
// hook | ||
const highlightedCode = useShikiHighlighter(code, "tsx", tokyoNight); | ||
``` | ||
### Custom languages | ||
Pass custom TextMate languages as a JSON object: | ||
```tsx | ||
import mcfunction from "../langs/mcfunction.tmLanguage.json" | ||
// component | ||
<ShikiHighlighter language={mcfunction} theme="github-dark" > | ||
{String(code).trim()} | ||
</ShikiHighlighter>; | ||
// hook | ||
const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark"); | ||
``` | ||
#### Preloading custom languages | ||
For dynamic highlighting scenarios (like LLM chat apps) where language selection happens at runtime, preload custom languages to make them available when needed: | ||
```tsx | ||
import mcfunction from "../langs/mcfunction.tmLanguage.json" | ||
import bosque from "../langs/bosque.tmLanguage.json" | ||
// component | ||
<ShikiHighlighter language={mcfunction} theme="github-dark" customLanguages={[mcfunction, bosque]} > | ||
{String(code).trim()} | ||
</ShikiHighlighter>; | ||
// hook | ||
const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark", { customLanguages: [mcfunction, bosque] }); | ||
``` | ||
### Custom transformers | ||
```tsx | ||
import { customTransformer } from '@utils/customTransformers'; | ||
import { customTransformer } from '../utils/shikiTransformers'; | ||
// component | ||
<ShikiHighlighter | ||
language="tsx" | ||
transformers={[customTransformer]}> | ||
transformers={[customTransformer]} | ||
> | ||
{String(code).trim()} | ||
</ShikiHighlighter>; | ||
// hook | ||
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", [customTransformer]); | ||
``` | ||
@@ -256,0 +304,0 @@ |
Sorry, the diff of this file is not supported yet
42688
31.3%194
33.79%428
12.63%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated