🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more →

react-shiki

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-shiki - npm Package Compare versions

Comparing version

to
0.4.0

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