Syntax highlighter
Paste code and convert to highlighted HTML locally in your browser.
Each language is its own small component — TypeScript’s is
12.22 kB of WebAssembly
— chained with an HTML escaper and a Night Owl stylesheet.
Output HTML
The output embeds its own <style>, so it pastes straight into a blog post
or email with no external stylesheet.
What gets parsed #
These components do more than color isolated keywords. Each one keeps enough
context to recognize declarations and nested language constructs while staying
small enough to ship as a language-specific Wasm module. For example:
- function, method, class, struct, and parameter names get their own scopes;
- Swift, C#, JavaScript, Python, and Ruby interpolation is parsed inside strings;
- JavaScript template substitutions and JSX tags keep their nested highlighting;
- Bash strings can contain variables and command substitutions;
- HTML can switch into JavaScript, and CSS distinguishes selectors, properties,
custom properties, and conditional features; and
- C preprocessor directives and WebAssembly instructions retain their internal
structure instead of becoming one uniformly colored token.
The result uses Highlight.js class names such as hljs-keyword, hljs-title,
and hljs-params, so existing Highlight.js themes can style it. It is a compact
contextual parser rather than a compiler front end: malformed code is preserved
as escaped text, and it does not try to validate types or resolve names.
Each language also has a comply component containing source snippets paired
with the HTML produced by Highlight.js. The highlighter and its executable
expectations are built separately and then compared byte for byte. Adding a
special case to the parser therefore requires adding the corresponding output
to the other side of the ledger.
JavaScript #
const [highlighter, escaper, stylesheet] = await Promise.all([
WebAssembly.instantiateStreaming(
fetch("/components/text/html/html-code-syntax-highlight-tsx.wasm"),
),
WebAssembly.instantiateStreaming(
fetch("/components/text/html/html-escape.wasm"),
),
WebAssembly.instantiateStreaming(
fetch("/components/text/html/html-add-highlight-stylesheet-night-owl.wasm"),
),
]);
const encoder = new TextEncoder(), decoder = new TextDecoder();
function run(component, source) {
const {
memory,
input_ptr,
input_utf8_cap,
output_ptr,
render,
} = component.instance.exports;
const input = encoder.encode(source);
if (input.length > input_utf8_cap()) throw new RangeError("Input is too large");
new Uint8Array(memory.buffer, input_ptr(), input.length).set(input);
const outputSize = render(input.length);
return decoder.decode(new Uint8Array(memory.buffer, output_ptr(), outputSize));
}
function highlight(source) {
const escaped = run(escaper, source);
const codeBlock =
'<pre><code class="language-tsx">' + escaped + "</code></pre>";
return run(stylesheet, run(highlighter, codeBlock));
}
const source = document.querySelector("#source").textContent;
document.querySelector("#highlighted").innerHTML = highlight(source);
The language highlighter accepts HTML, so the source is escaped before it is
wrapped in a <code> element. The stylesheet stage makes the returned fragment
self-contained.
Download #
CLI equivalent #
The highlighters transform whole HTML documents: each rewrites
<pre><code class="language-…"> blocks for its language and leaves
everything else untouched, so they chain safely.
qip run components/text/html/html-code-syntax-highlight-zig.wasm \
components/text/html/html-code-syntax-highlight-go.wasm \
components/text/html/html-code-syntax-highlight-swift.wasm \
components/text/html/html-code-syntax-highlight-ruby.wasm \
components/text/html/html-code-syntax-highlight-css.wasm \
components/text/html/html-code-syntax-highlight-bash.wasm \
components/text/html/html-add-highlight-stylesheet-night-owl.wasm \
< page.html > highlighted.html