[EN] Inline code highlighting with Shiki

Code highlighting always makes a difference
Code highlighting always makes a difference

Intro

Code highlighting always makes a difference. Often, when you do not control CMS or plugins, when writing an article for your company, all the previously beautiful formatting is gone. Fortunately, when you DO control, final touches complement content you have written.

Code block highlighting works as expected

On this site I’m using Astro which is integrated with Shiki. By default, code fences (```) are highlighted. Single backtick is not. See for yourself.

When you write

```ts
    function greet(name: string): string {
        return `Hello, ${name}`
    }
```

You will get highlighted syntax

function greet(name: string): string {
  return `Hello, ${name}`;
}

Inline highlighting is dull out of the box

Hello, this should be highlighted `git commit -m "Initial commit"`.

After being processed it looks like this; It’s hard to see difference, but if you look closer backticks are gone.

Hello, this should be highlighted git commit -m “Initial commit”.

First, you may notice that in a code block you can pass a language (sh or ts). Let’s see how to add that information.

Inline highlighting with built-in Code component

You can use <Code /> component, passing inline={true}

import { Code } from "astro:components";

<Code code={`git commit -m "Initial commit"`} lang="sh" inline={true} />;

This will result in correctly formatted code:

git commit -m "Initial commit"

I DO NOT like it. I would like to pass a lang to inline code. Well, you can, with config modification.

Adding rehype plugin + a transformer

Before I got to this point, I was trying to enable inline highlighting via custom remark and rehype plugins. It was hacky and ugly. Then, I’ve found that there is something like 'tailing-curly-colon' option in packages/rehype/src/handlers.ts:

In Shiki it allows you to pass a language using {:lang} format.

export const InlineCodeHandlers: Record<
  Truthy<RehypeShikiCoreOptions["inline"]>,
  RehypeShikiHandler
> = {
  "tailing-curly-colon": (_tree, node) => {
    const raw = toString(node);
    const match = raw.match(RE_TAILING_CURLY_COLON);
    if (!match) return;

    return {
      type: "inline",
      code: match[1] ?? raw,
      lang: match.at(2),
    };
  },
};

In astro.config.mjs you can add that option to rehypeShikiFromHighlighter rehype plugin. Here’s the config:

import mdx from "@astrojs/mdx";
import rehypeShikiFromHighlighter from "@shikijs/rehype";
import { defineConfig } from "astro/config";

export default defineConfig({
  integrations: [mdx()],
  markdown: {
    shikiConfig: {
      theme: "github-dark",
    },
    rehypePlugins: [
      [
        rehypeShikiFromHighlighter,
        {
          theme: "github-dark",
          inline: "tailing-curly-colon",
          transformers: [],
        },
      ],
    ],
  },
});

`git commit -m "Initial commit"{:sh}` would be then highlighted as git commit -m "Initial commit". Generated HTML:

<span
  class="shiki github-dark"
  style="background-color:#24292e;color:#e1e4e8"
  tabindex="0"
  ><code
    ><span class="line"
      ><span style="color:#B392F0">git</span
      ><span style="color:#9ECBFF"> commit</span
      ><span style="color:#79B8FF"> -m</span
      ><span style="color:#9ECBFF"> "Initial commit"</span></span
    ></code
  ></span
>

Class names and style depends on ShikiConfig. You may notice that generated HTML contains <span>…</span>, not <pre>…</pre> as for block codes. If you look at generated class (<span class="shiki github-dark"></span>) it includes shiki. <Code /> would generate astro-code as this component comes from Astro (import { Code } from "astro:components";).

<code
  class="astro-code github-dark"
  style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;"
  tabindex="0"
  data-language="text"
  ><span class="line"><span>git commit -m "Initial commit"</span></span></code
>

I wanted to unify it, and use astro-code in class name. To do that we can use a transformer. Updated config:

import mdx from "@astrojs/mdx";
import rehypeShikiFromHighlighter from "@shikijs/rehype";
import { defineConfig } from "astro/config";

export default defineConfig({
  integrations: [mdx()],
  markdown: {
    shikiConfig: {
      theme: "github-dark",
    },
    rehypePlugins: [
      [
        rehypeShikiFromHighlighter,
        {
          theme: "github-dark",
          inline: "tailing-curly-colon",
          transformers: [
            {
              pre(node) {
                const cls = node.properties.class;
                node.properties.class = cls.replace(
                  /\bshiki\b/,
                  "astro-code-inline",
                );
              },
            },
          ],
        },
      ],
    ],
  },
});

Now whenever I use <Code /> or a preferred {:lang} syntax with the following CSS it renders in the same way.

@layer components {
  span.astro-code-inline,
  code.astro-code {
    white-space: break-spaces;
    border-radius: 0.25rem;
    margin: 0;
    padding: 0.2em 0.6em;
  }
}
SyntaxResult
`echo "hello" {:sh}`echo "hello"
<Code code={`echo "hello"`} lang="sh" inline={true} />echo "hello"