From 06aa88e5ddd1a2d2ffe8e635074df60ce3fcd7a8 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:25:47 +0530 Subject: [PATCH] update logic to handle config reloads --- src/node/markdown/markdown.ts | 44 +++---- src/node/markdown/plugins/highlight.ts | 167 +++++++++++++------------ src/node/plugin.ts | 2 + 3 files changed, 107 insertions(+), 106 deletions(-) diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index f2706bafafcc..4c89e5ca0f46 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -28,7 +28,7 @@ import type { import type { Logger } from 'vite' import { containerPlugin, type ContainerOptions } from './plugins/containers' import { gitHubAlertsPlugin } from './plugins/githubAlerts' -import { highlight } from './plugins/highlight' +import { highlight as createHighlighter } from './plugins/highlight' import { highlightLinePlugin } from './plugins/highlightLines' import { imagePlugin, type Options as ImageOptions } from './plugins/image' import { lineNumberPlugin } from './plugins/lineNumbers' @@ -192,39 +192,35 @@ export interface MarkdownOptions extends Options { export type MarkdownRenderer = MarkdownIt -/** - * Keep a reference to the highlighter to avoid re-creating. - * - * This highlighter is used in the `createContentLoader` function so every time - * this function is called, the highlighter will be re-created. At the end, - * Shiki will slow down the build process because it must be a singleton. - */ -let highlighter: - | ((str: string, lang: string, attrs: string) => string) - | null - | undefined +let md: MarkdownRenderer | undefined +let _disposeHighlighter: (() => void) | undefined -export const createMarkdownRenderer = async ( +export function disposeMdItInstance() { + if (md) { + md = undefined + _disposeHighlighter?.() + } +} + +export async function createMarkdownRenderer( srcDir: string, options: MarkdownOptions = {}, base = '/', logger: Pick = console -): Promise => { +): Promise { + if (md) return md + const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' } const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code' const hasSingleTheme = typeof theme === 'string' || 'name' in theme - highlighter = - highlighter || - options.highlight || - (await highlight(theme, options, logger)) + let [highlight, dispose] = options.highlight + ? [options.highlight, () => {}] + : await createHighlighter(theme, options, logger) + + _disposeHighlighter = dispose - const md = MarkdownIt({ - html: true, - linkify: true, - highlight: highlighter, - ...options - }) + md = MarkdownIt({ html: true, linkify: true, highlight, ...options }) md.linkify.set({ fuzzyLink: false }) md.use(restoreEntities) diff --git a/src/node/markdown/plugins/highlight.ts b/src/node/markdown/plugins/highlight.ts index 40b0cf9ef014..26c72c221245 100644 --- a/src/node/markdown/plugins/highlight.ts +++ b/src/node/markdown/plugins/highlight.ts @@ -23,7 +23,7 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) * 2. convert line numbers into line options: * [{ line: number, classes: string[] }] */ -const attrsToLines = (attrs: string): TransformerCompactLineOption[] => { +function attrsToLines(attrs: string): TransformerCompactLineOption[] { attrs = attrs.replace(/^(?:\[.*?\])?.*?([\d,-]+).*/, '$1').trim() const result: number[] = [] if (!attrs) { @@ -51,7 +51,7 @@ export async function highlight( theme: ThemeOptions, options: MarkdownOptions, logger: Pick = console -): Promise<(str: string, lang: string, attrs: string) => string> { +): Promise<[(str: string, lang: string, attrs: string) => string, () => void]> { const { defaultHighlightLang: defaultLang = '', codeTransformers: userTransformers = [] @@ -95,93 +95,96 @@ export async function highlight( const lineNoRE = /:(no-)?line-numbers(=\d*)?$/ const mustacheRE = /\{\{.*?\}\}/g - return (str: string, lang: string, attrs: string) => { - const vPre = vueRE.test(lang) ? '' : 'v-pre' - lang = - lang - .replace(lineNoStartRE, '') - .replace(lineNoRE, '') - .replace(vueRE, '') - .toLowerCase() || defaultLang + return [ + (str: string, lang: string, attrs: string) => { + const vPre = vueRE.test(lang) ? '' : 'v-pre' + lang = + lang + .replace(lineNoStartRE, '') + .replace(lineNoRE, '') + .replace(vueRE, '') + .toLowerCase() || defaultLang - if (lang) { - const langLoaded = highlighter.getLoadedLanguages().includes(lang as any) - if (!langLoaded && !isSpecialLang(lang)) { - logger.warn( - c.yellow( - `\nThe language '${lang}' is not loaded, falling back to '${ - defaultLang || 'txt' - }' for syntax highlighting.` + if (lang) { + const langLoaded = highlighter.getLoadedLanguages().includes(lang) + if (!langLoaded && !isSpecialLang(lang)) { + logger.warn( + c.yellow( + `\nThe language '${lang}' is not loaded, falling back to '${ + defaultLang || 'txt' + }' for syntax highlighting.` + ) ) - ) - lang = defaultLang + lang = defaultLang + } } - } - const lineOptions = attrsToLines(attrs) - const mustaches = new Map() + const lineOptions = attrsToLines(attrs) + const mustaches = new Map() - const removeMustache = (s: string) => { - if (vPre) return s - return s.replace(mustacheRE, (match) => { - let marker = mustaches.get(match) - if (!marker) { - marker = nanoid() - mustaches.set(match, marker) - } - return marker - }) - } + const removeMustache = (s: string) => { + if (vPre) return s + return s.replace(mustacheRE, (match) => { + let marker = mustaches.get(match) + if (!marker) { + marker = nanoid() + mustaches.set(match, marker) + } + return marker + }) + } - const restoreMustache = (s: string) => { - mustaches.forEach((marker, match) => { - s = s.replaceAll(marker, match) - }) - return s - } + const restoreMustache = (s: string) => { + mustaches.forEach((marker, match) => { + s = s.replaceAll(marker, match) + }) + return s + } - str = removeMustache(str).trimEnd() + str = removeMustache(str).trimEnd() - const highlighted = highlighter.codeToHtml(str, { - lang, - transformers: [ - ...transformers, - transformerCompactLineOptions(lineOptions), - { - name: 'vitepress:v-pre', - pre(node) { - if (vPre) node.properties['v-pre'] = '' - } - }, - { - name: 'vitepress:empty-line', - code(hast) { - hast.children.forEach((span) => { - if ( - span.type === 'element' && - span.tagName === 'span' && - Array.isArray(span.properties.class) && - span.properties.class.includes('line') && - span.children.length === 0 - ) { - span.children.push({ - type: 'element', - tagName: 'wbr', - properties: {}, - children: [] - }) - } - }) - } - }, - ...userTransformers - ], - meta: { __raw: attrs }, - ...(typeof theme === 'object' && 'light' in theme && 'dark' in theme - ? { themes: theme, defaultColor: false } - : { theme }) - }) + const highlighted = highlighter.codeToHtml(str, { + lang, + transformers: [ + ...transformers, + transformerCompactLineOptions(lineOptions), + { + name: 'vitepress:v-pre', + pre(node) { + if (vPre) node.properties['v-pre'] = '' + } + }, + { + name: 'vitepress:empty-line', + code(hast) { + hast.children.forEach((span) => { + if ( + span.type === 'element' && + span.tagName === 'span' && + Array.isArray(span.properties.class) && + span.properties.class.includes('line') && + span.children.length === 0 + ) { + span.children.push({ + type: 'element', + tagName: 'wbr', + properties: {}, + children: [] + }) + } + }) + } + }, + ...userTransformers + ], + meta: { __raw: attrs }, + ...(typeof theme === 'object' && 'light' in theme && 'dark' in theme + ? { themes: theme, defaultColor: false } + : { theme }) + }) - return restoreMustache(highlighted) - } + return restoreMustache(highlighted) + }, + highlighter.dispose + ] } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 8132ad82616f..7aa0a9ade5f8 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -16,6 +16,7 @@ import { resolveAliases } from './alias' import { resolvePages, resolveUserConfig, type SiteConfig } from './config' +import { disposeMdItInstance } from './markdown/markdown' import { clearCache, createMarkdownToVueRenderFn, @@ -388,6 +389,7 @@ export async function createVitePressPlugin( return } + disposeMdItInstance() clearCache() await recreateServer?.() return