HTML Code Snippets with Syntax Highlighting

When switching to the new post layout, I took into account preserving syntax-highlighted code snippets. I planned to use a semi-manual method I described in How to Highlight Code Syntax in Google Slides. I assumed that the output of the Shiki playground will be pasted into a text editor as HTML. Guess what? That didn't work.

Before I dove into debugging, I realized that the playground output has only a single theme. The post layout uses both light and dark theme. After taking a look at a possibility of manually merging colors in the snippet, I scrapped this idea.

That's when I came up with an idea to use Shiki directly1—not through the playground.

The Script

After some tinkering, I came up with a JavaScript script to run with Node. Nothing too fancy. Use filename and language arguments to highlight file's contents with a given language's parser.

// Usage:
//    node highlight.js FILE [LANG] | pbcopy
import { readFileSync } from "node:fs";
import { codeToHtml } from "shiki";

const fileName = process.argv[2];
const lang = process.argv[3] ?? "text";
const code = readFileSync(fileName, { encoding: "utf-8" }).replace(/\n$/, "");

const result = await codeToHtml(code, {
  lang: lang,
  themes: {
    light: "github-light",
    dark: "github-dark",
  },
  defaultColor: false,
});

console.log(result);

That's not the end of it, though! By default, Shiki outputs an HTML snippet that has light theme's colors and dark theme's CSS variables. To make the dark theme work an additional CSS code is required.

The CSS to Enable the Dual Theme

Browsing through the documentation, I didn't like the fact that CSS snippets used !important to enable the dark theme. Luckily, I have spotted a defaultColor option. Using it moves light theme color's to CSS variables. This allowed me to write the following code that doesn't use !important! 😎

.shiki,
.shiki span {
  color: var(--shiki);
  background-color: var(--shiki-bg);

  @media (prefers-color-scheme: light) {
    --shiki: var(--shiki-light);
    --shiki-bg: var(--shiki-light-bg);
  }

  @media (prefers-color-scheme: dark) {
    --shiki: var(--shiki-dark);
    --shiki-bg: var(--shiki-dark-bg);
  }
}

Bonus: CSS to Add Line Numbers

If you would like to add line numbers like the ones I have, then this is the snippet to do that:

.shiki {
  counter-reset: line;

  span.line {
    counter-increment: line;

    &:nth-child(-n + 9) {
      padding-left: 0.5rem;
    }

    &::before {
      border-right: 1px solid var(--line-counter);
      color: var(--line-counter);
      content: counter(line);
      margin-right: 0.75rem;
      padding-right: 0.5rem;
    }

    @media (prefers-color-scheme: light) {
      --line-counter: rgb(111, 111, 111);
    }

    @media (prefers-color-scheme: dark) {
      --line-counter: hsl(210 10.5% 56.4%);
    }
  }
}

Optionally, you can move --line-counter declarations to the snippet enabling dark mode; that's how I have it!

Footnotes

  1. I know Shiki has a CLI. However, the CLI doesn't support the dual theming. ↩ī¸Ž