Javascript is required
ยท
7 min read

Focus & Code Diff in Nuxt Content Code Blocks

Focus & Code Diff in Nuxt Content Code Blocks Image

Custom code blocks are essential for my blog as my articles usually contain a lot of code snippets. My blog is powered by Nuxt Content v2, which is a Nuxt 3 module. I already wrote an article about how you can create custom code blocks using Nuxt Content v2.

In this article, I'll show you how to focus certain lines of your code or highlight a diff inside a custom code block. This feature is adopted from Vitepress which provides similar functionality.

Focus Lines

Sometimes, you want to focus on certain lines of your code. For example, you want to highlight the most important lines of your code snippet.

In our example, we can do this by adding // [!code focus] in the line that should be highlighted:

1```js [focus.js]
2export default {
3  data() {
4    return {
5      msg: 'Focused!', // [!code  focus]
6    }
7  },
8}
9```

The above code results in the following code block:

focus.js
1export default {
2  data() {
3    return {
4      msg: 'Focused!', // [!code focus]
5    }
6  },
7}

If you hover over the code block, you can see that the whole code is visible without any highlighting.

Diff Lines

Another use case is to highlight a diff inside a code block. For example, you want to show the difference between two code snippets.

In our example, we can do this by adding // [!code ++] in the line that should be highlighted as added and // [!code --] in the line that should be highlighted as removed:

1```js [diff.js]
2export default {
3  data () {
4    return {
5      msg: 'Removed' // [!code  --]
6      msg: 'Added' // [!code  ++]
7    }
8  }
9}
10```

The above code results in the following code block:

diff.js
1export default {
2  data () {
3    return {
4      msg: 'Removed' // [!code --]
5      msg: 'Added' // [!code ++]
6    }
7  }
8}

Implementation

Info

Update from January 2024: The following implementation is only necessary if you don't use shikiji, which provides transformers for the focus and diff syntax.

Let's now take a look at the implementation of this feature.

We opt out of the default code highlighting provided by Nuxt Content and handle it ourselves. We use Shiki to highlight the code, which is also used by Nuxt Content under the hood. The process of custom rendering of code blocks is documented in the Shiki README.

Let's start by writing a composable that returns a Shiki highlighter instance. As we'll have multiple Shiki instances on the same page we need to make sure that we only create one instance and reuse it. This is achieved by putting the highlighter instance in a ref outside of the composable.

Additionally, the composable exports the renderToHtml function from Shiki which we use later to render the highlighted code to HTML:

composables/useShikiHighlighter.ts
1import { getHighlighter, Highlighter, renderToHtml } from 'shiki-es'
2
3const highlighter = ref<Highlighter | null>(null)
4
5export const useShikiHighlighter = () => {
6  if (highlighter.value === null) {
7    getHighlighter({
8      theme: 'dark-plus',
9      themes: ['dark-plus'],
10      langs: ['css', 'scss', 'js', 'ts', 'groovy', 'java', 'diff', 'vue', 'html', 'json', 'xml'],
11    }).then((_highlighter) => {
12      highlighter.value = _highlighter
13    })
14  }
15
16  return { highlighter, renderToHtml }
17}

Now it's time to create the custom code block component.

Info

If you never did this before, I'd recommend you to read my article about how to create a custom code block with Nuxt Content v2 first.

The basic structure of our custom ProseCode component looks like this:

components/content/ProseCode.vue
1<script setup lang="ts">
2interface Props {
3  code?: string
4  language?: string | null
5  filename?: string | null
6  highlights?: Array<number>
7}
8const props = withDefaults(defineProps<Props>(), {
9  code: '',
10  language: null,
11  filename: null,
12  highlights: () => [],
13})
14</script>
15
16<template>
17  <div class="mb-8 mt-4 rounded-md bg-[#1e1e1e]">
18    {{ code }}
19  </div>
20</template>

Let's extend that component by using our useShikiHighlighter composable to highlight the code:

components/content/ProseCode.vue
1<script setup lang="ts">
2// ...
3
4const html = ref(null)
5const shiki = useShikiHighlighter()
6
7watch(
8  shiki.highlighter,
9  (newHighlighter) => {
10    if (!newHighlighter || !props.code) {
11      return
12    }
13    const tokens = newHighlighter.codeToThemedTokens(props.code.trim(), props.language ?? undefined)
14    html.value = shiki.renderToHtml(tokens, {
15      fg: newHighlighter.getForegroundColor('dark-plus'),
16      bg: newHighlighter.getBackgroundColor('dark-plus'),
17      // custom element renderer
18      elements: {
19        pre({ className, style, children }) {
20          return `<pre tabindex="1" class="${className} bg-[#1e1e1e] style="${style}">${children}</pre>`
21        },
22        code({ children, className, style }) {
23          return `<code class="${className}" style="${style}">${children}</code>`
24        },