avatar

Code Highlight 测试

最后编辑于 2023年3月11日

What's this?

This package is a Rehype plugin that provides beautiful code blocks for your MD/MDX docs. It has advantages over other solutions such as Prism. View on GitHub.

VS Code's highlighting

Leverage the accuracy of VS Code's syntax highlighting engine and the popularity of its themes ecosystem, powered by Shiki. No missing tokens or unexpected broken syntax highlighting.

pages/_document.js
import Document, { Html, Head, Main, NextScript } from "next/document";
 
// 🔥 Super granular and accurate highlighting
export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }
 
  render() {
    return (
      <Html>
        <Head />
        <body className="bg-zinc-800 text-zinc-200">
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

The theme is Moonlight II with a custom background color.

There's no runtime or bundle size cost

Shiki does its highlighting at build-time — there's no perf hit caused by runtime syntax highlighting or kilobytes of library code sent over the wire on the client. Your docs will be lightning fast.

Line numbers and line highlighting are supported

import { useFloating } from "@floating-ui/react";
 
function MyComponent() {
  const { x, y, refs } = useFloating();
 
  return (
    <>
      <div ref={refs.setReference} />
      <div ref={refs.setFloating} />
    </>
  );
}

Word highlighting too

import { useFloating } from "@floating-ui/react";
 
function MyComponent() {
  const { x, y, refs } = useFloating();
 
  return (
    <>
      <div ref={refs.setReference} />
      <div ref={refs.setFloating} />
    </>
  );
}

Even inline code!

The result of [1, 2, 3].join('-') is '1-2-3'.

Context-aware inline code

For instance, if you had the following code block:

function getStringLength(str) {
  return str.length;
}

When we refer to getStringLength as a plain variable, we can color it as a function. Same with function, or str vs. str, etc. This allows you to semantically tie inline code with the nearest code block it's referring to.

ANSI highlighting

  vite v2.8.6 dev server running at:
 
  > Local: http://localhost:3000/
  > Network: use `--host` to expose
 
  ready in 125ms.
 
8:38:02 PM [vite] hmr update /src/App.jsx

Inline ANSI: > Local: http://localhost:3000/

Code blocks are unstyled

No need to fight CSS rules, build the look from scratch. We recommend the following styles as a base though:

pre > code {
  display: grid;
}

This allows line highlighting to span the width of a horizontally-scrollable code block on small viewport widths.

Installation

Install via your terminal:

npm install rehype-pretty-code shiki

Usage

The following example shows how to use this package with Next.js and MDX (this website's stack).

const rehypePrettyCode = require("rehype-pretty-code");
const fs = require("fs");
 
const options = {
  // Use one of Shiki's packaged themes
  theme: "one-dark-pro",
  // Or your own JSON theme
  theme: JSON.parse(
    fs.readFileSync(require.resolve("./themes/dark.json"), "utf-8")
  ),
 
  // Keep the background or use a custom background color?
  keepBackground: true,
 
  // Callback hooks to add custom logic to nodes when visiting
  // them.
  onVisitLine(node) {
    // Prevent lines from collapsing in `display: grid` mode, and
    // allow empty lines to be copy/pasted
    if (node.children.length === 0) {
      node.children = [{ type: "text", value: " " }];
    }
  },
  onVisitHighlightedLine(node) {
    // Each line node by default has `class="line"`.
    node.properties.className.push("highlighted");
  },
  onVisitHighlightedWord(node) {
    // Each word node has no className by default.
    node.properties.className = ["word"];
  },
};
 
const withMDX = require("@next/mdx")({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [],
    rehypePlugins: [[rehypePrettyCode, options]],
  },
});

Meta strings

Code blocks are configured via the meta string on the top codeblock fence.

Highlight lines

Place a numeric range inside {}.

```js {1-3,4}
 
```

Highlight words

Literal text, like a regex. Note regex itself is not yet supported.

```js /carrot/
 
```
```js /carrot/ /apple/
 
```

Highlight only the third to fifth instances of carrot. Any numeric range can be placed after the last /.

```js /carrot/3-5
 
```

Highlight only the third to fifth instances of carrot and any instances of apple.

```js /carrot/3-5 /apple/
 
```

Group highlighted words by id

Place an id after # after the literal text. This allows you to color words differently based on their id.

```js /age/#v /name/#v /setAge/#s /setName/#s /50/#i /'Taylor'/#i
const [age, setAge] = useState(50);
const [name, setName] = useState("Taylor");
```
const [age, setAge] = useState(50);
const [name, setName] = useState("Taylor");
onVisitHighlightedWord(node, id) {
  node.properties.className = ['word'];
 
  // `id` will be either 'v', 's', or 'i'.
  // State (v)alue, (s)etter, and (i)nitial value
  if (id) {
    const backgroundColor = {
      v: 'rgb(196 42 94 / 59%)',
      s: 'rgb(0 103 163 / 56%)',
      i: 'rgb(100 50 255 / 35%)',
    }[id];
 
    const color = {
      v: 'rgb(255 225 225 / 100%)',
      s: 'rgb(175 255 255 / 100%)',
      i: 'rgb(225 200 255 / 100%)',
    }[id];
 
    // If the word spans across syntax boundaries (e.g. punctuation), remove
    // colors from the child nodes.
    if (node.properties['data-rehype-pretty-code-wrapper']) {
      node.children.forEach((childNode) => {
        childNode.properties.style = '';
      });
    }
 
    node.properties.style =
      `background-color: ${backgroundColor}; color: ${color};`;
  }
}

Highlight inline code

Append {:lang}‎ (e.g. {:js}‎) to the end of inline code to highlight it like a regular code block.

This is an array `[1, 2, 3]{:js}` of numbers 1 through 3.

Highlight plain text

Append {:.token}‎ to the end of the inline code to highlight it based on a token specified in your VS Code theme. Tokens start with a . to differentiate them from a language.

The name of the function is `getStringLength{:.entity.name.function}`.

You can create a map of tokens to shorten this usage throughout your docs:

const options = {
  tokensMap: {
    fn: "entity.name.function",
  },
};
The name of the function is `getStringLength{:.fn}`.

Titles

Add a file title to your code block, with text inside double quotes (""):

```js title="..."
 
```

Line numbers

CSS counters can be used to add line numbers.

code {
  counter-reset: line;
}
 
code > .line::before {
  counter-increment: line;
  content: counter(line);
 
  /* Other styling */
  display: inline-block;
  width: 1rem;
  margin-right: 2rem;
  text-align: right;
  color: gray;
}
 
code[data-line-numbers-max-digits="2"] > .line::before {
  width: 2rem;
}
 
code[data-line-numbers-max-digits="3"] > .line::before {
  width: 3rem;
}

If you want to conditionally show them, use showLineNumbers:

```js showLineNumbers
// <code> will have attributes `data-line-numbers` and
// `data-line-numbers-max-digits="n"`
```

If you want to start line number at specific number, use showLineNumbers{number}:

```js showLineNumbers{number}
// the first line of this code block will start at {number}
```

Multiple themes (dark/light mode)

Because Shiki generates themes at build time, client-side theme switching support is not built in. There are two popular options for supporting something like Dark Mode with Shiki. See the Shiki docs for more info.

Pass your themes to theme, where the keys represent the color mode:

const options = {
  theme: {
    dark: JSON.parse(
      fs.readFileSync(require.resolve("./themes/dark.json"), "utf-8")
    ),
    light: JSON.parse(
      fs.readFileSync(require.resolve("./themes/light.json"), "utf-8")
    ),
  },
};

The <code> and <pre> element will have a data attribute data-theme="[key]", e.g data-theme="light".

Now, you can use CSS to display the desired theme:

@media (prefers-color-scheme: dark) {
  pre[data-theme="light"],
  code[data-theme="light"] {
    display: none;
  }
}
 
@media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) {
  pre[data-theme="dark"],
  code[data-theme="dark"] {
    display: none;
  }
}

Custom highlighter

To completely configure the highlighter, use the getHighlighter option to provide a Shiki highlighter instance.

In order to support light and dark modes, Rehype Pretty Code provides an options object for you to extend. The theme property is preconfigured with the light or dark theme based on your theme options.

This is helpful if you'd like to configure other Shiki options, such as langs.

const { getHighlighter, BUNDLED_LANGUAGES } = require("shiki");
 
const options = {
  theme: {
    dark: JSON.parse(
      fs.readFileSync(require.resolve("./themes/dark.json"), "utf-8")
    ),
    light: JSON.parse(
      fs.readFileSync(require.resolve("./themes/light.json"), "utf-8")
    ),
  },
  getHighlighter: (options) =>
    getHighlighter({
      ...options,
      langs: [
        ...BUNDLED_LANGUAGES,
        {
          id: "groq",
          scopeName: "source.groq",
          path: "./langs/vscode-sanity/grammars/groq.json",
        },
      ],
    }),
};

Filter meta string

If your library also parses code blocks' meta strings, it may cause conflicts with rehype-pretty-code. This option allows you to filter out some part of the meta string before the library starts parsing it.

const options = {
  filterMetaString: (string) => string.replace(/filename="[^"]*"/, ""),
};

License

MIT • View on GitHub