Ferriki 1.3, a syntax highlighter for Node.js, Rust and Vite
Your editor’s colors, on your page.Highlighted natively, with Shiki’s familiar calls.
Ferriki turns source code into colored HTML for documentation sites, blogs, component libraries and build tools. It reads the TextMate grammars and themes that VS Code uses, follows languages nested inside one another, and runs in native Rust: from Node.js, from a Rust program, or inside your Vite build.
- 14 of 14repository files with the same HTML as Shiki
- 94grammars in the standard packages
- 65themes, as inline styles or as CSS
- Node.js 22.13+Rust 1.94+, MIT or Apache-2.0
Code is easier to read in color.
Every code example in documentation, a blog post or a product page reaches its reader as plain characters. Color shows where a string ends, which word is a keyword and which line is only a comment. Editors have done this for decades. A web page has to do it too, for every language on the page, including the CSS and JavaScript that live inside an HTML file.
Most editors learned their languages from TextMate grammars. A grammar reads code the way a person skims it and gives every piece a name, such as string.quoted.double.js. A theme then maps those names to colors. Shiki brought the same grammars and themes to the web, so a code block on a page could look the way it does in VS Code.
Ferriki is a highlighter built on that machinery. It reads the same grammars and themes, follows one language into the next and returns HTML that is ready for the page. Its engine is written in Rust; Node.js and Vite use the same engine through a compiled module.
<!doctype html><html lang="en"> <head> <style> button { color: rebeccapurple; } </style> </head> <body> <button data-name="Ferriki">Say hello</button> <script type="module"> const button = document.querySelector("button"); button.addEventListener("click", () => { const name = button.dataset.name ?? "you"; alert(`Hello, ${name}!`); }); </script> </body></html>
name
- text.html.basic
- meta.embedded.block.html
- source.js
- meta.arrow.js
- meta.block.js
- string.template.js
- meta.template.expression.js
- meta.embedded.line.js
- variable.other.readwrite.js
github-light-default gives it #1F2328
The template expression is selected to begin with. The grammar knows that name sits in a placeholder, inside a template string, inside JavaScript, inside a script element, inside HTML. That nesting is why the variable keeps its own color while the text around it is colored as a string.
Less time for every page of code.
On 78 files in 34 languages, Ferriki needed less time than Shiki with either of its engines. Prism was faster still, with simpler grammars that see less of the code.
A documentation build highlights every code block on every page, often thousands of them, and does it again after each change. A server that renders articles on request highlights while a reader waits. Every millisecond saved is saved many times over.
Ferriki does the work Shiki does, with the same grammars and themes. The difference is where the matching runs: in native Rust, rather than in WebAssembly or in regular expressions translated to JavaScript. Prism takes another road. Its grammars are written for speed and recognize fewer details, so a parameter or an embedded language may stay plain; the Prism comparison shows the same examples side by side.
Time for the same files
Ferriki = 1×, the line through every row. A bar past it took longer; a bar short of it was faster.
Every contender, language and size, with startup and memory: the benchmark report and the run that produced it.
Show the figures as a table
| Highlighter | Linux x86-64 | macOS ARM64 | Files |
|---|---|---|---|
| Shiki, WASM engine | 4.96× | 5.90× | 78 |
| Shiki, JavaScript engine | 3.91× | 4.66× | 76 |
| Prism | 0.25× | 0.30× | 70 |
Memory tells a similar story. After 600 renders of 12 repository files, the whole Ferriki process used about 45% less memory than the same work with Shiki’s WASM engine, on both machines.
We ship the preparation.
Standard language packages arrive with their patterns already compiled, so a new process can start highlighting instead of preparing.
A grammar is mostly regular expressions, hundreds of them for a language like TypeScript. Before the first line gets its colors, each pattern has to be compiled, and the patterns that compete at each position are gathered into scanners. Most highlighters repeat that work in every new process: on every build, in every serverless function, in every test run.
Ferriki moves it earlier. Ferroni, its regular-expression engine, compiles the static patterns and prepares their scanners when a language package is built. The package carries that work to your machine, the disk cache keeps it, and a fresh process restores it instead of rebuilding it. Patterns that depend on text captured earlier in the line still compile on the spot, as TextMate requires.
- Compile before delivery
- Ferroni compiles the static patterns and prepares their scanners.
- Deliver the package
- Each requested grammar arrives with that work inside it.
- Keep it on disk
- Verified downloads stay in your cache from one build to the next.
- Restore, then highlight
- A fresh process restores the scanners and starts on your code.
The engine reports what it did. Here is the HTML file from above, highlighted twice by fresh highlighters: once with the prepared packages, once from the grammar source.
With prepared packages, the default
getRegexDiagnostics()
- regex modeprepared
- prepared archives opened7
- prepared groups restored43
- scanners in use48
- groups rejected0
- fallbacks to source0
7 archives opened, 43 prepared groups restored from them.
From the grammar source
regexMode: "auto"
- regex modeauto
- prepared archives opened0
- prepared groups restored0
- scanners in use48
- groups rejected0
- fallbacks to source0
No archive opened: the scanners come from the grammar source, built in this process.
Real output of @ferriki/core 1.3.0 for the index.html file above. Both highlighters returned the same HTML.
The difference shows when a process starts cold. With the downloads already in the cache, a fresh process highlights the repository files sooner with the prepared packages than when it prepares the same scanners itself, and much sooner than Shiki.
| Highlighter | Linux x86-64 | macOS ARM64 |
|---|---|---|
| Ferriki, prepared packages (the default) | 227.9 ms | 194.8 ms |
| Ferriki, preparing its scanners at startup | 368.6 ms | 318.5 ms |
| Shiki, WASM engine | 954.1 ms | 1072.4 ms |
Preparation has a price: a prepared package is a larger download than its grammar alone, and the unpacked data takes disk space. Only the languages you request are fetched, and a missing package falls back to the grammar source. Download sizes for each language
Sponsored by Sebastian Software, which currently covers Ferriki’s CDN costs.
Finished before the page ships.
Keep a code example beside the React component that explains it. While Vite builds the site, Ferriki turns the example into highlighted HTML and CSS.
Highlighting in the browser means sending a highlighter, its grammars and its themes to every visitor, then waiting while they load. For examples that never change, that work can happen once, while the site is built. The @ferriki/vite adapter finds the code() calls and <Code /> elements in your components, highlights them during the build and leaves finished markup in their place.
import { Code } from "@ferriki/core/react/macro";
export function Answer() {
return (
<Code
language="ts"
source={`export const answer = 42;`}
/>
);
}
export const answer = 42;The adapter works with Vite 8. Other build tools have no adapter yet; they can call Ferriki from Node.js like any other script. Set up the Vite adapter
Style once. Switch themes freely.
Class-based output keeps the token names in the markup and the colors in a stylesheet. Another theme is another rule, not another highlighting run.
Inline colors are the simplest output: every token carries its color in a style attribute. That works until a site needs a dark mode, a second theme or one color of its own. Then every code block has to be highlighted again for each look.
With codeToHtmlWithCss, Ferriki writes each token’s scope into its classes and returns the themes as CSS. Dark mode becomes one attribute on the page, and your own stylesheet can restyle a single kind of token, as the second switch shows.
;
Familiar calls, native underneath.
Start with Shiki-style calls in Node.js, or use the engine directly from Rust.
Coming from Shiki, the calls will look familiar: createHighlighter, codeToHtml, the themes map, transformers and decorations. Ferriki covers that HTML API with its own native engine. It runs in Node.js and in Rust, not in the browser; by the time a page loads, its work is done.
The npm package brings a compiled binary for Linux, macOS on Apple Silicon and Windows. The Rust crate needs no Node.js at all, and its optional remote feature downloads the same release-pinned grammars and themes.
Node.js package
npm install @ferriki/core
import { createHighlighter } from "@ferriki/core";
const highlighter = await createHighlighter({
langs: ["typescript"],
themes: ["github-light-default"],
});
try {
const html = highlighter.codeToHtml("const answer = 42;", {
lang: "typescript",
theme: "github-light-default",
});
console.log(html);
} finally {
highlighter.dispose();
}const answer = 42;Node.js 22.13 or newer, on Linux, macOS and Windows.
Rust crate
cargo add ferriki --features remote
use ferriki::{
Highlighter, RemoteAssets, RenderOptions,
StandardAssetCatalogs, render_html,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let assets = StandardAssetCatalogs::remote(RemoteAssets::default())?;
let mut highlighter = Highlighter::builder()
.with_assets(assets)
.load_languages(["rust"])
.load_themes(["github-light-default"])
.build()?;
let tokens = highlighter.highlight(
"fn main() { println!(\"Hello, Ferriki!\"); }",
"rust",
"github-light-default",
)?;
let html = render_html(&tokens, &RenderOptions::default());
println!("{html}");
Ok(())
}Rust 1.94 or newer, with no Node.js in the process.
Both programs are highlighted by @ferriki/core 1.3.0 in github-light-default; so is the block the Node.js program prints.
The same HTML as Shiki.
On our 14 repository files, Ferriki’s HTML matched Shiki’s byte for byte.
Compatibility is checked, not claimed. Shiki’s own test suite runs unchanged against Ferriki’s engine from a pinned copy in the repository. The tokenizer is a mechanical port of vscode-textmate, the library inside VS Code, and is checked against its test cases. Tests outside Ferriki’s scope are listed with their reasons instead of being skipped quietly.
- Repository files with HTML identical to Shiki’s14 / 14
Measured 2026-10-24 with Ferriki 1.2.0 and Shiki 4.5.0, using Oniguruma in WebAssembly as Shiki’s engine. What compatibility covers
Built for the code you publish.
94 grammars and 65 themes ship as standard packages, from TypeScript and Rust to Vue, Svelte and Astro.
Component formats are where highlighters tend to struggle. A Vue or Svelte file holds markup, styles and script; an Astro file adds expressions inside the markup. TextMate grammars describe those languages inside one another, and Ferriki follows them. Grammars that only serve as building blocks, such as the expressions inside an Angular template, load with the language that needs them.
- Documents and configuration, 9
- Documentation, content, package metadata, application settings, and data exchange.
- Web languages and components, 12
- Browser and server JavaScript, React syntax, PHP, and component frameworks.
- Styles and templates, 9
- Stylesheets, preprocessors, Handlebars and Liquid templates, CSS-in-JS, and Lit HTML/SVG templates.
- General-purpose programming, 21
- Backend services, systems software, desktop and mobile apps, scripting, and scientific computing.
- Infrastructure and automation, 9
- Shell scripts, build files, containers, infrastructure definitions, and code changes.
- Game scripting, 2
- GDScript for Godot and Luau for Roblox and other Luau runtimes.
- Queries and patterns, 3
- Database queries, GraphQL operations and schemas, and regular expressions.
- WebAssembly and graphics, 2
- WebAssembly text and WGSL shaders for WebGPU.
Your own grammars and themes register as JSON, with embedded languages and injections. Every language and theme
Good company.
Ferriki works on its own. These are the places where it meets the rest of the workshop.
Each engine in the workshop has its own job and its own API, and none needs another beside it. Where two fit together, the cards below say how.
Ferroni
Ferriki runs on it
A regex engine. Runs Oniguruma's regular expressions in Rust, lookbehind, backreferences, named groups and Unicode classes included, with the multi-pattern scanner that editors and highlighters drive built in.
Ferromark
Often used with Ferriki
A Markdown renderer. Turns Markdown into HTML, sanitized by default, and is among the fastest engines for the job.
Ardo
A documentation framework for React. Builds static documentation sites from Markdown and MDX with React components, navigation and search, using Ferromark for Markdown rendering and Ferriki for syntax highlighting.
Runs on FerrikiShiki brought editor-grade highlighting to the web. Ferriki keeps its familiar HTML API and moves the engine to native Rust, without WebAssembly or regex translation, so Rust programs can use it directly and a Vite build can ship highlighted HTML instead of a highlighter. Ferriki checks itself against Shiki’s tests, and its tokenizer is a port of Microsoft’s vscode-textmate. It is an independent project, one of the Ferramenta engines, made by Sebastian Software in Mainz, Germany.
Give your next build a head start.
Choose a language and a theme. Ferriki brings the prepared scanners and turns your code into HTML and CSS, ready for the page.
npmcrates.ioGitHub repositoryDocumentationIntegration and support