Skip to content

Commit 2ffcdcd

Browse files
authored
feat: Add support for custom regex engines with engine param (#100)
This PR adds support for custom regex engines in the highlighter factory functions. It: - Adds an optional `engine` parameter to `createFullHighlighter` and `createWebHighlighter` functions - Defaults to using the Oniguruma engine when no custom engine is provided - Re-exports JavaScript regex engines from Shiki in the main bundles for convenience - Adds the `engine` option to the `HighlighterOptions` type These changes allow users to customize the regex engine used for syntax highlighting, providing more flexibility in different environments.
1 parent 35b52ac commit 2ffcdcd

File tree

13 files changed

+244
-44
lines changed

13 files changed

+244
-44
lines changed

.changeset/breezy-jeans-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-shiki": minor
3+
---
4+
5+
feat: support custom regex engine selection for full (main) and web bundles

package/README.md

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,17 @@ import ShikiHighlighter from 'react-shiki';
9797
```
9898
- **Size**: ~6.4MB minified, ~1.2MB gzipped (includes ~12KB react-shiki)
9999
- **Languages**: All Shiki languages and themes
100+
- **Exported engines**: `createJavaScriptRegexEngine`, `createJavaScriptRawEngine`
100101
- **Use case**: Unknown language requirements, maximum language support
101102
- **Setup**: Zero configuration required
102103

103-
### `react-shiki/web` (Web Bundle)
104+
### `react-shiki/web` (Web Bundle)
104105
```tsx
105106
import ShikiHighlighter from 'react-shiki/web';
106107
```
107108
- **Size**: ~3.8MB minified, ~707KB gzipped (includes ~12KB react-shiki)
108109
- **Languages**: Web-focused languages (HTML, CSS, JS, TS, JSON, Markdown, Vue, JSX, Svelte)
110+
- **Exported engines**: `createJavaScriptRegexEngine`, `createJavaScriptRawEngine`
109111
- **Use case**: Web applications with balanced size/functionality
110112
- **Setup**: Drop-in replacement for main entry point
111113

@@ -137,11 +139,59 @@ const highlighter = await createHighlighterCore({
137139

138140
### RegExp Engines
139141

140-
Shiki offers two built-in engines:
141-
- **Oniguruma** - default, uses the compiled Oniguruma WebAssembly, and offer maximum language support
142-
- **JavaScript** - smaller bundle, faster startup, recommended when running highlighting on the client
142+
Shiki offers three built-in engines for syntax highlighting:
143+
- **Oniguruma** - Default engine using compiled WebAssembly, offers maximum language support
144+
- **JavaScript RegExp** - Smaller bundle, faster startup, compiles patterns on-the-fly, recommended for client-side highlighting
145+
- **JavaScript Raw** - For [pre-compiled languages](https://shiki.style/guide/regex-engines#pre-compiled-languages), skips transpilation step for best performance
143146

144-
Unlike the Oniguruma engine, the JavaScript engine is [strict by default](https://shiki.style/guide/regex-engines#use-with-unsupported-languages). It will throw an error if it encounters an invalid Oniguruma pattern or a pattern that it cannot convert. If you want best-effort results for unsupported grammars, you can enable the forgiving option to suppress any conversion errors:
147+
#### Using Engines with Full and Web Bundles
148+
149+
The full and web bundles use Oniguruma by default, but you can override this with the `engine` option:
150+
151+
```tsx
152+
import {
153+
useShikiHighlighter,
154+
createJavaScriptRegexEngine,
155+
createJavaScriptRawEngine
156+
} from 'react-shiki';
157+
158+
// Hook with JavaScript RegExp engine
159+
const highlightedCode = useShikiHighlighter(code, 'typescript', 'github-dark', {
160+
engine: createJavaScriptRegexEngine()
161+
});
162+
163+
// Component with JavaScript Raw engine (for pre-compiled languages)
164+
// See https://shiki.style/guide/regex-engines#pre-compiled-languages
165+
<ShikiHighlighter
166+
language="typescript"
167+
theme="github-dark"
168+
engine={createJavaScriptRawEngine()}
169+
>
170+
{code}
171+
</ShikiHighlighter>
172+
```
173+
174+
#### Using Engines with Core Bundle
175+
176+
When using the core bundle, you must specify an engine:
177+
178+
```tsx
179+
import {
180+
createHighlighterCore,
181+
createOnigurumaEngine,
182+
createJavaScriptRegexEngine
183+
} from 'react-shiki/core';
184+
185+
const highlighter = await createHighlighterCore({
186+
themes: [import('@shikijs/themes/nord')],
187+
langs: [import('@shikijs/langs/typescript')],
188+
engine: createJavaScriptRegexEngine() // or createOnigurumaEngine(import('shiki/wasm'))
189+
});
190+
```
191+
192+
#### Engine Options
193+
194+
The JavaScript RegExp engine is [strict by default](https://shiki.style/guide/regex-engines#use-with-unsupported-languages). For best-effort results with unsupported grammars, enable the `forgiving` option:
145195

146196
```tsx
147197
createJavaScriptRegexEngine({ forgiving: true });
@@ -163,6 +213,7 @@ See [Shiki - RegExp Engines](https://shiki.style/guide/regex-engines) for more i
163213
| `delay` | `number` | `0` | Delay between highlights (in milliseconds) |
164214
| `customLanguages` | `array` | `[]` | Array of custom languages to preload |
165215
| `langAlias` | `object` | `{}` | Map of language aliases |
216+
| `engine` | `RegexEngine` | Oniguruma | RegExp engine for syntax highlighting (Oniguruma, JavaScript RegExp, or JavaScript Raw) |
166217
| `showLineNumbers` | `boolean` | `false` | Display line numbers alongside code |
167218
| `startingLineNumber` | `number` | `1` | Starting line number when line numbers are enabled |
168219
| `transformers` | `array` | `[]` | Custom Shiki transformers for modifying the highlighting output |

package/package.json

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,8 @@
2626
"type": "module",
2727
"main": "./dist/index.js",
2828
"types": "./dist/index.d.ts",
29-
"files": [
30-
"dist",
31-
"src/lib/styles.css"
32-
],
33-
"sideEffects": [
34-
"src/lib/styles.css"
35-
],
29+
"files": ["dist", "src/lib/styles.css"],
30+
"sideEffects": ["src/lib/styles.css"],
3631
"exports": {
3732
".": {
3833
"types": "./dist/index.d.ts",

package/src/bundles/full.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { getSingletonHighlighter, type Highlighter } from 'shiki';
1+
import {
2+
getSingletonHighlighter,
3+
type Highlighter,
4+
type Awaitable,
5+
type RegexEngine,
6+
} from 'shiki';
7+
import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
28
import type { ShikiLanguageRegistration } from '../lib/extended-types';
39
import type { Theme } from '../lib/types';
410

@@ -8,18 +14,21 @@ import type { Theme } from '../lib/types';
814
*/
915
export async function createFullHighlighter(
1016
langsToLoad: ShikiLanguageRegistration,
11-
themesToLoad: Theme[]
17+
themesToLoad: Theme[],
18+
engine?: Awaitable<RegexEngine>
1219
): Promise<Highlighter> {
1320
try {
1421
return await getSingletonHighlighter({
1522
langs: [langsToLoad],
1623
themes: themesToLoad,
24+
engine: engine ?? createOnigurumaEngine(import('shiki/wasm')),
1725
});
1826
} catch (error) {
1927
if (error instanceof Error && error.message.includes('Language')) {
2028
return await getSingletonHighlighter({
2129
langs: ['plaintext'],
2230
themes: themesToLoad,
31+
engine: engine ?? createOnigurumaEngine(import('shiki/wasm')),
2332
});
2433
}
2534
throw error;

package/src/bundles/web.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import {
22
getSingletonHighlighter,
33
type Highlighter,
4+
type Awaitable,
5+
type RegexEngine,
46
} from 'shiki/bundle/web';
7+
import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
58
import type { ShikiLanguageRegistration } from '../lib/extended-types';
69
import type { Theme } from '../lib/types';
710

@@ -12,18 +15,21 @@ import type { Theme } from '../lib/types';
1215
*/
1316
export async function createWebHighlighter(
1417
langsToLoad: ShikiLanguageRegistration,
15-
themesToLoad: Theme[]
18+
themesToLoad: Theme[],
19+
engine?: Awaitable<RegexEngine>
1620
): Promise<Highlighter> {
1721
try {
1822
return await getSingletonHighlighter({
1923
langs: [langsToLoad],
2024
themes: themesToLoad,
25+
engine: engine ?? createOnigurumaEngine(import('shiki/wasm')),
2126
});
2227
} catch (error) {
2328
if (error instanceof Error && error.message.includes('Language')) {
2429
return await getSingletonHighlighter({
2530
langs: ['plaintext'],
2631
themes: themesToLoad,
32+
engine: engine ?? createOnigurumaEngine(import('shiki/wasm')),
2733
});
2834
}
2935
throw error;

package/src/core.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,43 @@ export type {
2222

2323
export { createHighlighterCore } from 'shiki/core';
2424
export { createOnigurumaEngine } from 'shiki/engine/oniguruma';
25-
export { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
25+
export {
26+
createJavaScriptRegexEngine,
27+
createJavaScriptRawEngine,
28+
} from 'shiki/engine/javascript';
2629

2730
/**
28-
* A React hook that provides syntax highlighting using Shiki with a custom highlighter.
29-
* Requires a highlighter to be provided in options for minimal bundle size.
31+
* Highlight code with shiki (core bundle)
32+
*
33+
* @param code - Code to highlight
34+
* @param lang - Language (bundled or custom)
35+
* @param theme - Theme (bundled, multi-theme, or custom)
36+
* @param options - react-shiki options + shiki options
37+
* @returns Highlighted code as React elements or HTML string
3038
*
3139
* @example
32-
* ```ts
40+
* ```tsx
3341
* import { createHighlighterCore, createOnigurumaEngine } from 'react-shiki/core';
3442
*
43+
*
3544
* const highlighter = await createHighlighterCore({
36-
* themes: [import('@shikijs/themes/nord')],
45+
* themes: [import('@shikijs/themes/github-light'), import('@shikijs/themes/github-dark')],
3746
* langs: [import('@shikijs/langs/typescript')],
3847
* engine: createOnigurumaEngine(import('shiki/wasm'))
3948
* });
4049
*
41-
* const code = useShikiHighlighter(code, 'typescript', 'nord', { highlighter });
50+
* const highlighted = useShikiHighlighter(
51+
* 'const x = 1;',
52+
* 'typescript',
53+
* {
54+
* light: 'github-light',
55+
* dark: 'github-dark'
56+
* },
57+
* { highlighter }
58+
* );
4259
* ```
4360
*
44-
* For plug-and-play usage, consider:
45-
* - `react-shiki` for full shiki bundle (~6.4MB minified, 1.2MB gzipped)
46-
* - `react-shiki/web` for smaller shiki web bundle (~3.8MB minified, 695KB gzipped)
61+
* Core bundle (minimal). For plug-and-play: `react-shiki` or `react-shiki/web`
4762
*/
4863
export const useShikiHighlighter: UseShikiHighlighter = (
4964
code,

package/src/index.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,33 @@ export type {
2020
HighlighterOptions,
2121
} from './lib/types';
2222

23+
export {
24+
createJavaScriptRegexEngine,
25+
createJavaScriptRawEngine,
26+
} from 'shiki/engine/javascript';
27+
2328
/**
24-
* A React hook that provides syntax highlighting using Shiki with the full bundle.
25-
* Includes all languages and themes for maximum compatibility.
29+
* Highlight code with shiki (full bundle)
30+
*
31+
* @param code - Code to highlight
32+
* @param lang - Language (bundled or custom)
33+
* @param theme - Theme (bundled, multi-theme, or custom)
34+
* @param options - react-shiki options + shiki options
35+
* @returns Highlighted code as React elements or HTML string
2636
*
27-
* Bundle size: ~6.4MB minified (1.2MB gzipped)
37+
* @example
38+
* ```tsx
39+
* const highlighted = useShikiHighlighter(
40+
* 'const x = 1;',
41+
* 'typescript',
42+
* {
43+
* light: 'github-light',
44+
* dark: 'github-dark'
45+
* }
46+
* );
47+
* ```
2848
*
29-
* For smaller bundles, consider:
30-
* - `react-shiki/web` for smaller shiki web bundle (~3.8MB minified, 695KB gzipped)
31-
* - `react-shiki/core` for custom fine-grained bundle
49+
* Full bundle (~6.4MB minified, 1.2MB gzipped). For smaller bundles: `react-shiki/web` or `react-shiki/core`
3250
*/
3351
export const useShikiHighlighter: UseShikiHighlighter = (
3452
code,

package/src/lib/hook.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import type {
1515
CodeOptionsMultipleThemes,
1616
Highlighter,
1717
HighlighterCore,
18+
Awaitable,
19+
RegexEngine,
20+
BundledTheme,
1821
} from 'shiki';
1922

2023
import type { ShikiLanguageRegistration } from './extended-types';
@@ -53,7 +56,8 @@ export const useShikiHighlighter = (
5356
options: HighlighterOptions = {},
5457
highlighterFactory: (
5558
langsToLoad: ShikiLanguageRegistration,
56-
themesToLoad: Theme[]
59+
themesToLoad: Theme[],
60+
engine?: Awaitable<RegexEngine>
5761
) => Promise<Highlighter | HighlighterCore>
5862
) => {
5963
const [highlightedCode, setHighlightedCode] = useState<
@@ -99,10 +103,10 @@ export const useShikiHighlighter = (
99103
themes: multiTheme || DEFAULT_THEMES,
100104
defaultColor,
101105
cssVariablePrefix,
102-
} as CodeOptionsMultipleThemes)
106+
} as CodeOptionsMultipleThemes<BundledTheme>)
103107
: ({
104108
theme: singleTheme || DEFAULT_THEMES.dark,
105-
} as CodeOptionsSingleTheme);
109+
} as CodeOptionsSingleTheme<BundledTheme>);
106110

107111
const transformers = restOptions.transformers || [];
108112
if (showLineNumbers) {
@@ -127,7 +131,8 @@ export const useShikiHighlighter = (
127131
? stableOpts.highlighter
128132
: await highlighterFactory(
129133
langsToLoad as ShikiLanguageRegistration,
130-
themesToLoad
134+
themesToLoad,
135+
stableOpts.engine
131136
);
132137

133138
const loadedLanguages = highlighter.getLoadedLanguages();

package/src/lib/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
Highlighter,
1010
HighlighterCore,
1111
BundledHighlighterOptions,
12+
Awaitable,
13+
RegexEngine,
1214
} from 'shiki';
1315

1416
import type { ReactNode } from 'react';
@@ -133,7 +135,10 @@ interface HighlighterOptions
133135
'defaultColor' | 'cssVariablePrefix'
134136
>,
135137
Omit<CodeToHastOptions, 'lang' | 'theme' | 'themes'>,
136-
Pick<BundledHighlighterOptions<string, string>, 'langAlias'> {}
138+
Pick<
139+
BundledHighlighterOptions<string, string>,
140+
'langAlias' | 'engine'
141+
> {}
137142

138143
/**
139144
* State for the throttling logic
@@ -151,7 +156,6 @@ interface TimeoutState {
151156

152157
/**
153158
* Public API signature for the useShikiHighlighter hook.
154-
* This ensures all entry points have consistent signatures.
155159
*/
156160
export type UseShikiHighlighter = (
157161
code: string,

package/src/web.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,33 @@ export type {
2020
HighlighterOptions,
2121
} from './lib/types';
2222

23+
export {
24+
createJavaScriptRegexEngine,
25+
createJavaScriptRawEngine,
26+
} from 'shiki/engine/javascript';
27+
2328
/**
24-
* A React hook that provides syntax highlighting using Shiki with the web bundle.
25-
* Includes web-focused languages (HTML, CSS, JS, TS, JSON, Markdown, Astro, JSX, Svelte, Vue etc.)
29+
* Highlight code with shiki (web bundle)
30+
*
31+
* @param code - Code to highlight
32+
* @param lang - Language (bundled or custom)
33+
* @param theme - Theme (bundled, multi-theme, or custom)
34+
* @param options - react-shiki options + shiki options
35+
* @returns Highlighted code as React elements or HTML string
2636
*
27-
* Bundle size: ~3.8MB minified (695KB gzipped)
37+
* @example
38+
* ```tsx
39+
* const highlighted = useShikiHighlighter(
40+
* 'const x = 1;',
41+
* 'typescript',
42+
* {
43+
* light: 'github-light',
44+
* dark: 'github-dark'
45+
* }
46+
* );
47+
* ```
2848
*
29-
* For other options, consider:
30-
* - `react-shiki` for full shiki bundle (~6.4MB minified, 1.2MB gzipped)
31-
* - `react-shiki/core` for custom fine-grained bundle
49+
* Web bundle (~3.8MB minified, 695KB gzipped). For other bundles: `react-shiki` or `react-shiki/core`
3250
*/
3351
export const useShikiHighlighter: UseShikiHighlighter = (
3452
code,

0 commit comments

Comments
 (0)