diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/createMarkdownFilePathGetter.ts b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createMarkdownFilePathGetter.ts similarity index 91% rename from plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/createMarkdownFilePathGetter.ts rename to plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createMarkdownFilePathGetter.ts index 6971de96f8..1de0a9a8a0 100644 --- a/plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/createMarkdownFilePathGetter.ts +++ b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createMarkdownFilePathGetter.ts @@ -11,7 +11,7 @@ export const createMarkdownFilePathGetter = ( const rawRender = md.render.bind(md) // we need to store file path before each render - md.render = (src, env: MarkdownEnv) => { + md.render = (src, env: MarkdownEnv = {}) => { store.path = env.filePathRelative return rawRender(src, env) diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts new file mode 100644 index 0000000000..5b631b591f --- /dev/null +++ b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts @@ -0,0 +1,27 @@ +import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki' +import { createHighlighter } from 'shiki' +import { bundledLanguageNames } from '../../shiki.js' +import type { ShikiHighlightOptions } from '../../types.js' + +export const createShikiHighlighter = async ({ + langs = bundledLanguageNames, + langAlias = {}, + defaultLang, + shikiSetup, + ...options +}: ShikiHighlightOptions = {}): Promise< + HighlighterGeneric +> => { + const shikiHighlighter = await createHighlighter({ + langs, + langAlias, + themes: + 'themes' in options + ? Object.values(options.themes) + : [options.theme ?? 'nord'], + }) + + await shikiSetup?.(shikiHighlighter) + + return shikiHighlighter +} diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/index.ts b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getHighLightFunction.ts similarity index 56% rename from plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/index.ts rename to plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getHighLightFunction.ts index e29f7683de..ff73439602 100644 --- a/plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/index.ts +++ b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getHighLightFunction.ts @@ -1,56 +1,37 @@ import { transformerCompactLineOptions } from '@shikijs/transformers' -import type MarkdownIt from 'markdown-it' -import { createHighlighter } from 'shiki' -import type { App } from 'vuepress' -import { bundledLanguageNames } from '../../shiki.js' +import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki' import { getTransformers, whitespaceTransformer, } from '../../transformers/getTransformers.js' import type { ShikiHighlightOptions } from '../../types.js' import { attrsToLines } from '../../utils.js' -import { createMarkdownFilePathGetter } from './createMarkdownFilePathGetter.js' +import type { MarkdownFilePathGetter } from './createMarkdownFilePathGetter.js' import { getLanguage } from './getLanguage.js' import { handleMustache } from './handleMustache.js' -export const applyHighlighter = async ( - md: MarkdownIt, - app: App, - { - langs = bundledLanguageNames, - langAlias = {}, - defaultLang, - transformers: userTransformers = [], - ...options - }: ShikiHighlightOptions = {}, -): Promise => { - const logLevel = options.logLevel ?? (app.env.isDebug ? 'debug' : 'warn') - const getMarkdownFilePath = - logLevel === 'debug' ? createMarkdownFilePathGetter(md) : null - - const highlighter = await createHighlighter({ - langs, - langAlias, - themes: - 'themes' in options - ? Object.values(options.themes) - : [options.theme ?? 'nord'], - }) - - await options.shikiSetup?.(highlighter) +type MarkdownItHighlight = ( + content: string, + language: string, + attrs: string, +) => string +export const getHighLightFunction = ( + highlighter: HighlighterGeneric, + options: ShikiHighlightOptions, + markdownFilePathGetter: MarkdownFilePathGetter, +): MarkdownItHighlight => { const transformers = getTransformers(options) const loadedLanguages = highlighter.getLoadedLanguages() - md.options.highlight = (content, language, attrs) => + return (content, language, attrs) => handleMustache(content, (str) => highlighter.codeToHtml(str, { lang: getLanguage( language, loadedLanguages, - defaultLang, - logLevel, - getMarkdownFilePath!, + options, + markdownFilePathGetter, ), meta: { /** @@ -65,7 +46,7 @@ export const applyHighlighter = async ( ? [transformerCompactLineOptions(attrsToLines(attrs))] : []), ...whitespaceTransformer(attrs, options.whitespace), - ...userTransformers, + ...(options.transformers ?? []), ], ...('themes' in options ? { diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/getLanguage.ts b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getLanguage.ts similarity index 83% rename from plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/getLanguage.ts rename to plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getLanguage.ts index 1cb21081e4..36856e1f41 100644 --- a/plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/getLanguage.ts +++ b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getLanguage.ts @@ -1,5 +1,6 @@ import { isSpecialLang } from 'shiki' import { colors } from 'vuepress/utils' +import type { ShikiHighlightOptions } from '../../types.js' import { logger, resolveLanguage } from '../../utils.js' import type { MarkdownFilePathGetter } from './createMarkdownFilePathGetter.js' @@ -8,9 +9,8 @@ const WARNED_LANGS = new Set() export const getLanguage = ( lang: string, loadedLanguages: string[], - defaultLang: string | undefined, - logLevel: string, - getMarkdownFilePath: MarkdownFilePathGetter, + { defaultLang, logLevel }: ShikiHighlightOptions, + markdownFilePathGetter: MarkdownFilePathGetter, ): string => { let result = resolveLanguage(lang) @@ -26,7 +26,7 @@ export const getLanguage = ( // log file path if unknown language is found if (logLevel === 'debug') { logger.info( - `Unknown language ${colors.cyan(result)} found in ${colors.cyan(getMarkdownFilePath())}`, + `Unknown language ${colors.cyan(result)} found in ${colors.cyan(markdownFilePathGetter())}`, ) } diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/handleMustache.ts b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/handleMustache.ts similarity index 100% rename from plugins/markdown/plugin-shiki/src/node/markdown/applyHighlighter/handleMustache.ts rename to plugins/markdown/plugin-shiki/src/node/markdown/highlighter/handleMustache.ts diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/index.ts b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/index.ts new file mode 100644 index 0000000000..f5128a3fd6 --- /dev/null +++ b/plugins/markdown/plugin-shiki/src/node/markdown/highlighter/index.ts @@ -0,0 +1,3 @@ +export * from './createMarkdownFilePathGetter.js' +export * from './createShikiHighlighter.js' +export * from './getHighLightFunction.js' diff --git a/plugins/markdown/plugin-shiki/src/node/markdown/index.ts b/plugins/markdown/plugin-shiki/src/node/markdown/index.ts index d73cd17546..60b473d22c 100644 --- a/plugins/markdown/plugin-shiki/src/node/markdown/index.ts +++ b/plugins/markdown/plugin-shiki/src/node/markdown/index.ts @@ -1,3 +1,3 @@ -export * from './applyHighlighter/index.js' +export * from './highlighter/index.js' export * from './highlightLinesPlugin.js' export * from './preWrapperPlugin.js' diff --git a/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts b/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts index 172c204a6c..6173fbc088 100644 --- a/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts +++ b/plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts @@ -8,50 +8,60 @@ import { } from '@vuepress/highlighter-helper' import type { Plugin } from 'vuepress/core' import { isPlainObject } from 'vuepress/shared' +import { createMarkdownFilePathGetter } from './markdown/highlighter/createMarkdownFilePathGetter.js' import type { MarkdownItPreWrapperOptions } from './markdown/index.js' import { - applyHighlighter, + createShikiHighlighter, + getHighLightFunction, highlightLinesPlugin, preWrapperPlugin, } from './markdown/index.js' import type { ShikiPluginOptions } from './options.js' import { prepareClientConfigFile } from './prepareClientConfigFile.js' -export const shikiPlugin = (options: ShikiPluginOptions = {}): Plugin => { - const opt: ShikiPluginOptions = { - preWrapper: true, - lineNumbers: true, - collapsedLines: 'disable', - ...options, - } +export const shikiPlugin = (_options: ShikiPluginOptions = {}): Plugin => { + return (app) => { + // FIXME: Remove in stable version + // eslint-disable-next-line @typescript-eslint/no-deprecated + const { code } = app.options.markdown + const options = { + ...(isPlainObject(code) ? code : {}), + ..._options, + } + + options.logLevel ??= app.env.isDebug ? 'debug' : 'warn' + options.preWrapper ??= true + options.lineNumbers ??= true + options.collapsedLines ??= 'disable' + + return { + name: '@vuepress/plugin-shiki', + + extendsMarkdown: async (md) => { + const { preWrapper, lineNumbers, collapsedLines } = options + + const markdownFilePathGetter = createMarkdownFilePathGetter(md) + const shikiHighlighter = await createShikiHighlighter(options) + + md.options.highlight = getHighLightFunction( + shikiHighlighter, + options, + markdownFilePathGetter, + ) + + md.use(highlightLinesPlugin) + md.use(preWrapperPlugin, { preWrapper }) + if (preWrapper) { + md.use(lineNumbersPlugin, { + lineNumbers, + }) + md.use(collapsedLinesPlugin, { + collapsedLines, + }) + } + }, - return { - name: '@vuepress/plugin-shiki', - - extendsMarkdown: async (md, app) => { - // FIXME: Remove in stable version - // eslint-disable-next-line @typescript-eslint/no-deprecated - const { code } = app.options.markdown - - await applyHighlighter(md, app, { - ...(isPlainObject(code) ? code : {}), - ...options, - }) - - const { preWrapper, lineNumbers, collapsedLines } = opt - - md.use(highlightLinesPlugin) - md.use(preWrapperPlugin, { preWrapper }) - if (preWrapper) { - md.use(lineNumbersPlugin, { - lineNumbers, - }) - md.use(collapsedLinesPlugin, { - collapsedLines, - }) - } - }, - - clientConfigFile: (app) => prepareClientConfigFile(app, opt), + clientConfigFile: () => prepareClientConfigFile(app, options), + } } } diff --git a/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts b/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts index 9cd08e5287..19c1fa367e 100644 --- a/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts +++ b/plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts @@ -8,24 +8,33 @@ import { } from '@vuepress/highlighter-helper' import MarkdownIt from 'markdown-it' import { describe, expect, it } from 'vitest' -import type { App } from 'vuepress' import type { MarkdownItPreWrapperOptions } from '../src/node/markdown/index.js' import { - applyHighlighter, + createMarkdownFilePathGetter, + createShikiHighlighter, + getHighLightFunction, highlightLinesPlugin, preWrapperPlugin, } from '../src/node/markdown/index.js' import type { ShikiPluginOptions } from '../src/node/options.js' -const createMarkdown = async ({ +const shikiHighlighter = await createShikiHighlighter() + +const createMarkdown = ({ preWrapper = true, lineNumbers = true, collapsedLines = false, ...options -}: ShikiPluginOptions = {}): Promise => { +}: ShikiPluginOptions = {}): MarkdownIt => { const md = MarkdownIt() - await applyHighlighter(md, { env: { isDebug: false } } as App, options) + const markdownFilePathGetter = createMarkdownFilePathGetter(md) + + md.options.highlight = getHighLightFunction( + shikiHighlighter, + options, + markdownFilePathGetter, + ) md.use(highlightLinesPlugin) md.use(preWrapperPlugin, { preWrapper }) @@ -84,50 +93,50 @@ ${codeFence} ${codeFence}{{ inlineCode }}${codeFence} ` - it('should process code fences with default options', async () => { - const md = await createMarkdown() + it('should process code fences with default options', () => { + const md = createMarkdown() expect(md.render(source)).toMatchSnapshot() }) - it('should disable `highlightLines`', async () => { - const md = await createMarkdown({ + it('should disable `highlightLines`', () => { + const md = createMarkdown({ highlightLines: false, }) expect(md.render(source)).toMatchSnapshot() }) - it('should disable `lineNumbers`', async () => { - const md = await createMarkdown({ + it('should disable `lineNumbers`', () => { + const md = createMarkdown({ lineNumbers: false, }) expect(md.render(source)).toMatchSnapshot() }) - it('should enable `lineNumbers` according to number of code lines', async () => { - const md = await createMarkdown({ + it('should enable `lineNumbers` according to number of code lines', () => { + const md = createMarkdown({ lineNumbers: 4, }) expect(md.render(source)).toMatchSnapshot() }) - it('should disable `preWrapper`', async () => { - const md = await createMarkdown({ + it('should disable `preWrapper`', () => { + const md = createMarkdown({ preWrapper: false, }) expect(md.render(source)).toMatchSnapshot() }) - it('should always disable `lineNumbers` if `preWrapper` is disabled', async () => { - const mdWithLineNumbers = await createMarkdown({ + it('should always disable `lineNumbers` if `preWrapper` is disabled', () => { + const mdWithLineNumbers = createMarkdown({ lineNumbers: true, preWrapper: false, }) - const mdWithoutLineNumbers = await createMarkdown({ + const mdWithoutLineNumbers = createMarkdown({ lineNumbers: false, preWrapper: false, }) @@ -137,12 +146,12 @@ ${codeFence}{{ inlineCode }}${codeFence} ) }) - it('should always disable `collapsedLines` if `preWrapper` is disabled', async () => { - const mdWithCollapsedLines = await createMarkdown({ + it('should always disable `collapsedLines` if `preWrapper` is disabled', () => { + const mdWithCollapsedLines = createMarkdown({ collapsedLines: 3, preWrapper: false, }) - const mdWithoutCollapsedLines = await createMarkdown({ + const mdWithoutCollapsedLines = createMarkdown({ collapsedLines: 'disable', preWrapper: false, }) @@ -216,24 +225,24 @@ function bar () { ${codeFence} ` - it('should work properly if `lineNumbers` is enabled by default', async () => { - const md = await createMarkdown({ + it('should work properly if `lineNumbers` is enabled by default', () => { + const md = createMarkdown({ lineNumbers: true, }) expect(md.render(source)).toMatchSnapshot() }) - it('should work properly if `lineNumbers` is disabled by default', async () => { - const md = await createMarkdown({ + it('should work properly if `lineNumbers` is disabled by default', () => { + const md = createMarkdown({ lineNumbers: false, }) expect(md.render(source)).toMatchSnapshot() }) - it('should work properly if `lineNumbers` is set to a number by default', async () => { - const md = await createMarkdown({ + it('should work properly if `lineNumbers` is set to a number by default', () => { + const md = createMarkdown({ lineNumbers: 4, }) @@ -259,24 +268,24 @@ const line10 = 'line 10' const line11 = 'line 11' ${codeFence} ` - it('should work properly if `lineNumbers` is enabled by default', async () => { - const md = await createMarkdown({ + it('should work properly if `lineNumbers` is enabled by default', () => { + const md = createMarkdown({ lineNumbers: true, }) expect(md.render(source)).toMatchSnapshot() }) - it('should work properly if `lineNumbers` is disabled by default', async () => { - const md = await createMarkdown({ + it('should work properly if `lineNumbers` is disabled by default', () => { + const md = createMarkdown({ lineNumbers: false, }) expect(md.render(source)).toMatchSnapshot() }) - it('should work properly if `lineNumbers` is set to a number by default', async () => { - const md = await createMarkdown({ + it('should work properly if `lineNumbers` is set to a number by default', () => { + const md = createMarkdown({ lineNumbers: 4, }) @@ -313,8 +322,8 @@ console.log(msg) console.log(msg) // prints Hello World ${codeFence} ` - it('should work notation enabled', async () => { - const md = await createMarkdown({ + it('should work notation enabled', () => { + const md = createMarkdown({ notationDiff: true, notationErrorLevel: true, notationFocus: true, @@ -354,28 +363,28 @@ function foo () { const foo = 'foo' \n return 'foo' } ` - it('should work whitespace with default options', async () => { - const md = await createMarkdown() + it('should work whitespace with default options', () => { + const md = createMarkdown() expect(md.render(source)).toMatchSnapshot() }) - it('should work whitespace with `all` option', async () => { - const md = await createMarkdown({ whitespace: 'all' }) + it('should work whitespace with `all` option', () => { + const md = createMarkdown({ whitespace: 'all' }) expect(md.render(source)).toMatchSnapshot() }) - it('should work whitespace with `boundary` option', async () => { - const md = await createMarkdown({ whitespace: 'boundary' }) + it('should work whitespace with `boundary` option', () => { + const md = createMarkdown({ whitespace: 'boundary' }) expect(md.render(source)).toMatchSnapshot() }) - it('should work whitespace with `trailing` option', async () => { - const md = await createMarkdown({ whitespace: 'trailing' }) + it('should work whitespace with `trailing` option', () => { + const md = createMarkdown({ whitespace: 'trailing' }) expect(md.render(source)).toMatchSnapshot() }) - it('should work whitespace with `false` option', async () => { - const md = await createMarkdown({ whitespace: false }) + it('should work whitespace with `false` option', () => { + const md = createMarkdown({ whitespace: false }) expect(md.render(source)).toMatchSnapshot() }) }) @@ -407,18 +416,18 @@ ${codeFence}ts :no-collapsed-lines=12 ${genLines(20)} ${codeFence} ` - it('should work properly if `collapsedLines` is disabled by default', async () => { - const md = await createMarkdown({ collapsedLines: false }) + it('should work properly if `collapsedLines` is disabled by default', () => { + const md = createMarkdown({ collapsedLines: false }) expect(md.render(source)).toMatchSnapshot() }) - it('should work properly if `collapsedLines` is enabled', async () => { - const md = await createMarkdown({ collapsedLines: true }) + it('should work properly if `collapsedLines` is enabled', () => { + const md = createMarkdown({ collapsedLines: true }) expect(md.render(source)).toMatchSnapshot() }) - it('should work properly if `collapsedLines` is set to a number', async () => { - const md = await createMarkdown({ collapsedLines: 10 }) + it('should work properly if `collapsedLines` is set to a number', () => { + const md = createMarkdown({ collapsedLines: 10 }) expect(md.render(source)).toMatchSnapshot() }) }) diff --git a/plugins/tools/plugin-cache/src/node/renderCache.ts b/plugins/tools/plugin-cache/src/node/renderCache.ts index 6e798f5a2b..8aa4dd1ff7 100644 --- a/plugins/tools/plugin-cache/src/node/renderCache.ts +++ b/plugins/tools/plugin-cache/src/node/renderCache.ts @@ -63,7 +63,7 @@ export const renderCacheWithMemory = async ( // eslint-disable-next-line @typescript-eslint/unbound-method const rawRender = md.render - md.render = (input, env: MarkdownEnv) => { + md.render = (input, env: MarkdownEnv = {}) => { const filepath = env.filePathRelative if (!filepath) { @@ -120,7 +120,7 @@ export const renderCacheWithFilesystem = async ( // eslint-disable-next-line @typescript-eslint/unbound-method const rawRender = md.render - md.render = (input, env: MarkdownEnv) => { + md.render = (input, env: MarkdownEnv = {}) => { const filepath = env.filePathRelative if (!filepath) {