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.

Preview
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:

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