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
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
42688
31.3%194
33.79%428
12.63%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated