Skip to content

Commit

Permalink
feat: add shiki rehype plugin for code highlight
Browse files Browse the repository at this point in the history
  • Loading branch information
sanyuan0704 committed Sep 11, 2022
1 parent 513c69c commit b6230e3
Show file tree
Hide file tree
Showing 11 changed files with 80 additions and 126 deletions.
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -45,7 +45,6 @@
"rehype-autolink-headings": "^6.1.1",
"rehype-external-links": "^2.0.1",
"rehype-highlight": "^5.0.2",
"rehype-shiki": "^0.0.9",
"rehype-slug": "^5.0.1",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
Expand Down
95 changes: 0 additions & 95 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions src/node/build.ts
Expand Up @@ -247,13 +247,15 @@ class SSGBuilder {
`;
}

#baseBuild(isServer: boolean, options: InlineConfig = {}) {
const resolveViteConfig = (isServer: boolean): InlineConfig => ({
async #baseBuild(isServer: boolean, options: InlineConfig = {}) {
const resolveViteConfig = async (
isServer: boolean
): Promise<InlineConfig> => ({
...options,
mode: 'production',
root: this.#root,
plugins: [
createIslandPlugins(this.#config, isServer),
await createIslandPlugins(this.#config, isServer),
...(options?.plugins || [])
],
esbuild: {
Expand All @@ -275,7 +277,9 @@ class SSGBuilder {
...options?.build
}
});
return viteBuild(resolveViteConfig(isServer)) as Promise<RollupOutput>;
return viteBuild(
await resolveViteConfig(isServer)
) as Promise<RollupOutput>;
}
}

Expand All @@ -287,5 +291,5 @@ export async function build(root: string) {

await builder.renderPages(render as RenderFn, routes as Route[]);

// await builder.end();
await builder.end();
}
20 changes: 7 additions & 13 deletions src/node/constants/index.ts
Expand Up @@ -6,23 +6,15 @@ export const TS_REGEX = /(c|m)?tsx?$/;

export const PACKAGE_ROOT_PATH = join(fileURLToPath(import.meta.url), '../..');

export const CLIENT_PATH = join(PACKAGE_ROOT_PATH, 'src/client/app');
export const CLIENT_PATH = join(PACKAGE_ROOT_PATH, 'src/client/runtime');

export const CLIENT_ENTRY_PATH = join(
PACKAGE_ROOT_PATH,
'src/client/app/client-entry.tsx'
);

export const SERVER_ENTRY_PATH = join(
PACKAGE_ROOT_PATH,
'src/client/app/ssr-entry.tsx'
);
export const CLIENT_ENTRY_PATH = join(CLIENT_PATH, 'client-entry.tsx');

export const DEFAULT_THEME_PATH = join(PACKAGE_ROOT_PATH, 'src/client/theme');
export const SERVER_ENTRY_PATH = join(CLIENT_PATH, 'ssr-entry.tsx');

export const THEME_ISLANDS_PATH = join(
export const DEFAULT_THEME_PATH = join(
PACKAGE_ROOT_PATH,
'src/client/theme/islands.ts'
'src/client/theme-default'
);

export const TEMP_PATH = 'node_modules/.island';
Expand All @@ -46,4 +38,6 @@ export const DEFAULT_EXTERNALS: string[] = [

export const ISLAND_JSX_RUNTIME_PATH = join(PACKAGE_ROOT_PATH, DIST_PATH);

export const ISLAND_CLI_PATH = join(PACKAGE_ROOT_PATH, DIST_PATH, 'cli.js');

export const VENDOR_PATH = join(PACKAGE_ROOT_PATH, 'vendors');
9 changes: 8 additions & 1 deletion src/node/plugin-island/index.ts
Expand Up @@ -17,6 +17,7 @@ import { transformAsync } from '@babel/core';
import babelPluginIsland from '../babel-plugin-island';
import { transformWithEsbuild } from 'vite';
import pc from 'picocolors';
import { ISLAND_CLI_PATH } from '../constants/index';

const { green } = pc;

Expand Down Expand Up @@ -123,7 +124,12 @@ export function pluginIsland(
};
},
async handleHotUpdate(ctx) {
if (config.configPath === ctx.file) {
const customWatchedFiles = [
...(config.configDeps || []),
config.configPath,
ISLAND_CLI_PATH
];
if (customWatchedFiles.includes(ctx.file)) {
console.log(
green(
`\n${relative(config.root, ctx.file)} changed, restarting server...`
Expand All @@ -139,6 +145,7 @@ export function pluginIsland(
config.configDeps?.forEach((dep) => {
server.watcher.add(dep);
});
server.watcher.add(ISLAND_CLI_PATH);
}
return () => {
server.middlewares.use(async (req, res, next) => {
Expand Down
11 changes: 10 additions & 1 deletion src/node/plugin-mdx/index.ts
Expand Up @@ -7,8 +7,11 @@ import rehypePluginSlug from 'rehype-slug';
import rehypePluginExternalLinks from 'rehype-external-links';
import type { Options } from '@mdx-js/rollup';
import { remarkPluginToc } from './remarkPlugins/toc';
import rehypeShiki from '@leafac/rehype-shiki';
import shiki from 'shiki';
import { rehypePluginShiki } from './rehypePlugins/shiki';

export function createMDXOptions(): Options {
export async function createMDXOptions(): Promise<Options> {
return {
remarkPlugins: [
remarkPluginGFM,
Expand Down Expand Up @@ -39,6 +42,12 @@ export function createMDXOptions(): Options {
target: '_blank'
}
],
[
rehypePluginShiki,
{
highlighter: await shiki.getHighlighter({ theme: 'github-dark' })
}
],
rehypePluginPreWrapper
]
};
Expand Down
1 change: 1 addition & 0 deletions src/node/plugin-mdx/rehypePlugins/preWrapper.ts
Expand Up @@ -13,6 +13,7 @@ export const rehypePluginPreWrapper: Plugin<[], import('hast').Root> = () => {
node.children[0].tagName === 'code' &&
!node.data?.isVisited
) {
debugger;
const codeNode = node.children[0];
const codeClassName = codeNode.properties?.className?.toString() || '';
const lang = codeClassName.split('-')[1];
Expand Down
43 changes: 43 additions & 0 deletions src/node/plugin-mdx/rehypePlugins/shiki.ts
@@ -0,0 +1,43 @@
import { visit } from 'unist-util-visit';
import type { Plugin } from 'unified';
import type { Text, Parent } from 'hast';
import { fromHtml } from 'hast-util-from-html';
import shiki from 'shiki';

interface Options {
highlighter: shiki.Highlighter;
}

// https://github.com/leafac/rehype-shiki/blob/41e64054d72ab29d5ad48c4c070499fc075090e9/source/index.ts
// The plugin cannot be used directly because it won't reserve the class name `language-xxx` in the code tag
// It cause conflict with preWrapper plugin, so we should integrate it manually
export const rehypePluginShiki: Plugin<[Options], import('hast').Root> = ({
highlighter
}) => {
return (tree) => {
visit(tree, 'element', (node, index, parent) => {
// <pre><code>...</code></pre>
if (
node.tagName === 'pre' &&
node.children?.[0]?.type === 'element' &&
node.children[0].tagName === 'code'
) {
const codeNode = node.children[0];
const codeContent = (node.children[0].children[0] as Text).value;
const codeClassName = codeNode.properties?.className?.toString() || '';
const lang = codeClassName.split('-')[1];

if (!lang) {
return;
}

const highlightedCode = highlighter.codeToHtml(codeContent, { lang });
const fragmentAst = fromHtml(highlightedCode, { fragment: true });
// @ts-ignore Reserve the class name `language-xxx` in the code tag
fragmentAst.children[0].children[0].properties.className =
codeClassName;
parent?.children.splice(index!, 1, ...fragmentAst.children);
}
});
};
};
Empty file.
3 changes: 1 addition & 2 deletions src/node/plugin.ts
Expand Up @@ -15,13 +15,12 @@ export async function createIslandPlugins(
isServer: boolean = false,
restartServer?: () => Promise<void>
): Promise<PluginOption[]> {
const mdxOptions = createMDXOptions();
const mdxOptions = await createMDXOptions();
return [
// For island internal use
pluginIsland(config, isServer, restartServer),
// React hmr support
pluginReact({
include: [/theme/],
jsxRuntime: 'automatic',
jsxImportSource: isServer ? ISLAND_JSX_RUNTIME_PATH : 'react',
babel: {
Expand Down
9 changes: 1 addition & 8 deletions tsup.config.ts
@@ -1,15 +1,8 @@
import { defineConfig } from 'tsup';

const pkgInfo = require('./package.json');

const external = [
...Object.keys(pkgInfo.dependencies),
...Object.keys(pkgInfo.devDependencies)
];

export default defineConfig({
entry: {
'jsx-runtime': 'src/client/app/island-jsx-runtime.js',
'jsx-runtime': 'src/client/runtime/island-jsx-runtime.js',
cli: 'src/node/cli.ts'
},
bundle: true,
Expand Down

0 comments on commit b6230e3

Please sign in to comment.