Skip to content

Commit

Permalink
feat: include simple own optimised syntax highlighter
Browse files Browse the repository at this point in the history
  • Loading branch information
ovflowd committed Nov 1, 2023
1 parent aff10dd commit 049ab7a
Show file tree
Hide file tree
Showing 5 changed files with 1,456 additions and 888 deletions.
6 changes: 3 additions & 3 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const nextConfig = {
eslint: { dirs: ['.'], ignoreDuringBuilds: true },
// Next.js WebPack Bundler does not know how to handle `.mjs` files on `node_modules`
// This is not an issue when using TurboPack as it uses SWC and it is ESM-only
// Once we migrate to Next.js 14 we might be able to remove this
// Once Next.js uses Turbopack for their build process we can remove this
webpack: function (config) {
config.module.rules.push({
test: /\.m?js$/,
Expand All @@ -58,8 +58,8 @@ const nextConfig = {
'@radix-ui/react-toast',
'tailwindcss',
],
// Enable concurrent WebPack builds
webpackBuildWorker: true,
// Removes the warning regarding the WebPack Build Worker
webpackBuildWorker: false,
},
};

Expand Down
14 changes: 2 additions & 12 deletions next.mdx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,10 @@

import remarkHeadings from '@vcarl/remark-headings';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeShikiji from 'rehype-shikiji';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';

import { LANGUAGES, DEFAULT_THEME } from './shiki.config.mjs';

// This memoizes Shikiji Syntax Highlighter as `getStaticProps` from within Next.js context
// are called independently for each page, which recreates the Shikiji promise for each call
const memoizedShikiji = rehypeShikiji({
theme: DEFAULT_THEME,
langs: LANGUAGES,
});
import rehypeShikiji from './next.mdx.shiki.mjs';

/**
* Provides all our Rehype Plugins that are used within MDX
Expand All @@ -26,9 +18,7 @@ export const NEXT_REHYPE_PLUGINS = [
// Automatically add anchor links to headings (H1, ...)
[rehypeAutolinkHeadings, { properties: { tabIndex: -1, class: 'anchor' } }],
// Adds our syntax highlighter (Shikiji) to Codeboxes
function rehypeShikiji() {
return memoizedShikiji;
},
rehypeShikiji,
];

/**
Expand Down
83 changes: 83 additions & 0 deletions next.mdx.shiki.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

import classNames from 'classnames';
import { toString } from 'hast-util-to-string';
import { getHighlighterCore } from 'shikiji/core';
import { getWasmInlined } from 'shikiji/wasm';
import { visit } from 'unist-util-visit';

import { LANGUAGES, DEFAULT_THEME } from './shiki.config.mjs';

// This creates a memoized minimal Shikiji Syntax Highlighter
const memoizedShikiji = await getHighlighterCore({
themes: [DEFAULT_THEME],
langs: LANGUAGES,
loadWasm: getWasmInlined,
});

// This is what Remark will use as prefix within a <pre> className
// to attribute the current language of the <pre> element
const languagePrefix = 'language-';

export default function rehypeShikiji() {
return async function (tree) {
visit(tree, 'element', (node, index, parent) => {
// We only want to process <pre>...</pre> elements
if (!parent || index == null || node.tagName !== 'pre') {
return;
}

const preElement = node.children[0];

// If thereÄs nothing inside the <pre> element... What are we doing here?
if (!preElement || !preElement.properties) {
return;
}

// Ensure that we're not visiting a <code> element but it's inner contents
// (keep iterating further down until we reach where we want)
if (preElement.type !== 'element' || preElement.tagName !== 'code') {
return;
}

// Get the <pre> element class names
const preClassNames = preElement.properties.className;

// The current classnames should be an array and it should have a length
if (typeof preClassNames !== 'object' || preClassNames.length === 0) {
return;
}

// We want to retrieve the language class name from the class names
const codeLanguage = preClassNames.find(
c => typeof c === 'string' && c.startsWith(languagePrefix)
);

// If we didn't find any `language-` classname then we shouldn't highlight
if (typeof codeLanguage !== 'string') {
return;
}

// Retrieve the whole <pre> contents as a parsed DOM string
const preElementContents = toString(preElement);

// Grabs the relevant alias/name of the language
const languageId = codeLanguage.slice(languagePrefix.length);

// Parses the <pre> contents and returns a HAST tree with the highlighted code
const { children = [{}] } = memoizedShikiji.codeToHast(
preElementContents,
{ theme: DEFAULT_THEME, lang: languageId }
);

// Adds the original language back to the <pre> element
children[0].properties.class = classNames(
children[0].properties.class,
codeLanguage
);

// Replaces the <pre> element with the updated one
parent.children.splice(index, 1, ...children);
});
};
}
Loading

0 comments on commit 049ab7a

Please sign in to comment.