perf(plugin-shikiji): improve highlight
This commit is contained in:
parent
4a49b9f027
commit
199bbd9a9a
@ -1,183 +0,0 @@
|
||||
import { colors as c, logger } from 'vuepress/utils'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import type { ShikiTransformer } from 'shiki'
|
||||
import {
|
||||
addClassToHast,
|
||||
bundledLanguages,
|
||||
createHighlighter,
|
||||
isPlainLang,
|
||||
isSpecialLang,
|
||||
} from 'shiki'
|
||||
import {
|
||||
transformerCompactLineOptions,
|
||||
transformerNotationDiff,
|
||||
transformerNotationErrorLevel,
|
||||
transformerNotationFocus,
|
||||
transformerNotationHighlight,
|
||||
transformerNotationWordHighlight,
|
||||
transformerRemoveNotationEscape,
|
||||
transformerRenderWhitespace,
|
||||
} from '@shikijs/transformers'
|
||||
import type { HighlighterOptions, ThemeOptions } from './types.js'
|
||||
import { attrsToLines, resolveLanguage } from './utils/index.js'
|
||||
import { defaultHoverInfoProcessor, transformerTwoslash } from './twoslash/rendererTransformer.js'
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
|
||||
|
||||
const vueRE = /-vue$/
|
||||
const mustacheRE = /\{\{.*?\}\}/g
|
||||
const decorationsRE = /^\/\/ @decorations:(.*)\n/
|
||||
|
||||
export async function highlight(
|
||||
theme: ThemeOptions,
|
||||
options: HighlighterOptions,
|
||||
): Promise<(str: string, lang: string, attrs: string) => string> {
|
||||
const {
|
||||
defaultHighlightLang: defaultLang = '',
|
||||
codeTransformers: userTransformers = [],
|
||||
whitespace = false,
|
||||
languages = Object.keys(bundledLanguages),
|
||||
} = options
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes:
|
||||
typeof theme === 'object' && 'light' in theme && 'dark' in theme
|
||||
? [theme.light, theme.dark]
|
||||
: [theme],
|
||||
langs: languages,
|
||||
langAlias: options.languageAlias,
|
||||
})
|
||||
|
||||
await options?.shikiSetup?.(highlighter)
|
||||
|
||||
const transformers: ShikiTransformer[] = [
|
||||
transformerNotationDiff(),
|
||||
transformerNotationFocus({
|
||||
classActiveLine: 'has-focus',
|
||||
classActivePre: 'has-focused-lines',
|
||||
}),
|
||||
transformerNotationHighlight(),
|
||||
transformerNotationErrorLevel(),
|
||||
transformerNotationWordHighlight(),
|
||||
{
|
||||
name: 'vuepress:add-class',
|
||||
pre(node) {
|
||||
addClassToHast(node, 'vp-code')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'vuepress:clean-up',
|
||||
pre(node) {
|
||||
delete node.properties.tabindex
|
||||
delete node.properties.style
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'shiki:inline-decorations',
|
||||
preprocess(code, options) {
|
||||
code = code.replace(decorationsRE, (match, decorations) => {
|
||||
options.decorations ||= []
|
||||
options.decorations.push(...JSON.parse(decorations))
|
||||
return ''
|
||||
})
|
||||
return code
|
||||
},
|
||||
},
|
||||
transformerRemoveNotationEscape(),
|
||||
]
|
||||
|
||||
const loadedLanguages = highlighter.getLoadedLanguages()
|
||||
|
||||
return (str: string, language: string, attrs: string) => {
|
||||
attrs = attrs || ''
|
||||
let lang = resolveLanguage(language) || defaultLang
|
||||
const vPre = vueRE.test(lang) ? '' : 'v-pre'
|
||||
|
||||
if (lang) {
|
||||
const langLoaded = loadedLanguages.includes(lang as any)
|
||||
if (!langLoaded && !isPlainLang(lang) && !isSpecialLang(lang)) {
|
||||
logger.warn(
|
||||
c.yellow(
|
||||
`\nThe language '${lang}' is not loaded, falling back to '${defaultLang || 'txt'
|
||||
}' for syntax highlighting.`,
|
||||
),
|
||||
)
|
||||
lang = defaultLang
|
||||
}
|
||||
}
|
||||
// const { attrs: attributes, rawAttrs } = resolveAttrs(attrs || '')
|
||||
const enabledTwoslash = attrs.includes('twoslash')
|
||||
const mustaches = new Map<string, string>()
|
||||
|
||||
const removeMustache = (s: string) => {
|
||||
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)
|
||||
})
|
||||
|
||||
if (enabledTwoslash && options.twoslash)
|
||||
s = s.replace(/\{/g, '{')
|
||||
|
||||
return `${s}\n`
|
||||
}
|
||||
|
||||
str = removeMustache(str).trimEnd()
|
||||
|
||||
const inlineTransformers: ShikiTransformer[] = [
|
||||
transformerCompactLineOptions(attrsToLines(attrs)),
|
||||
]
|
||||
|
||||
if (enabledTwoslash && options.twoslash) {
|
||||
inlineTransformers.push(transformerTwoslash({
|
||||
processHoverInfo(info) {
|
||||
return defaultHoverInfoProcessor(info)
|
||||
},
|
||||
}))
|
||||
}
|
||||
else {
|
||||
inlineTransformers.push({
|
||||
name: 'vuepress:v-pre',
|
||||
pre(node) {
|
||||
if (vPre)
|
||||
node.properties['v-pre'] = ''
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (attrs.includes('whitespace') || whitespace)
|
||||
inlineTransformers.push(transformerRenderWhitespace({ position: 'boundary' }))
|
||||
|
||||
try {
|
||||
const highlighted = highlighter.codeToHtml(str, {
|
||||
lang,
|
||||
transformers: [
|
||||
...transformers,
|
||||
...inlineTransformers,
|
||||
...userTransformers,
|
||||
],
|
||||
meta: { __raw: attrs },
|
||||
...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
|
||||
? { themes: theme, defaultColor: false }
|
||||
: { theme }),
|
||||
})
|
||||
|
||||
const rendered = restoreMustache(highlighted)
|
||||
|
||||
return rendered
|
||||
}
|
||||
catch (e) {
|
||||
logger.error(e)
|
||||
return str
|
||||
}
|
||||
}
|
||||
}
|
||||
25
plugins/plugin-shikiji/src/node/highlight/getLanguage.ts
Normal file
25
plugins/plugin-shikiji/src/node/highlight/getLanguage.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { isPlainLang, isSpecialLang } from 'shiki'
|
||||
import { colors as c, logger } from 'vuepress/utils'
|
||||
import { resolveLanguage } from '../utils/index.js'
|
||||
|
||||
export function getLanguage(
|
||||
loadedLanguages: string[],
|
||||
language: string,
|
||||
defaultLang: string,
|
||||
): string {
|
||||
let lang = resolveLanguage(language) || defaultLang
|
||||
|
||||
if (lang) {
|
||||
const langLoaded = loadedLanguages.includes(lang as any)
|
||||
if (!langLoaded && !isPlainLang(lang) && !isSpecialLang(lang)) {
|
||||
logger.warn(
|
||||
c.yellow(
|
||||
`\nThe language '${lang}' is not loaded, falling back to '${defaultLang || 'txt'
|
||||
}' for syntax highlighting.`,
|
||||
),
|
||||
)
|
||||
lang = defaultLang
|
||||
}
|
||||
}
|
||||
return lang
|
||||
}
|
||||
87
plugins/plugin-shikiji/src/node/highlight/highlight.ts
Normal file
87
plugins/plugin-shikiji/src/node/highlight/highlight.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { logger } from 'vuepress/utils'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { bundledLanguages, createHighlighter } from 'shiki'
|
||||
import type { HighlighterOptions, ThemeOptions } from '../types.js'
|
||||
import { baseTransformers, getInlineTransformers } from './transformers.js'
|
||||
import { getLanguage } from './getLanguage.js'
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
|
||||
const mustacheRE = /\{\{.*?\}\}/g
|
||||
|
||||
export async function highlight(
|
||||
theme: ThemeOptions,
|
||||
options: HighlighterOptions,
|
||||
): Promise<(str: string, lang: string, attrs: string) => string> {
|
||||
const {
|
||||
defaultHighlightLang: defaultLang = '',
|
||||
codeTransformers: userTransformers = [],
|
||||
whitespace = false,
|
||||
languages = Object.keys(bundledLanguages),
|
||||
} = options
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes:
|
||||
typeof theme === 'object' && 'light' in theme && 'dark' in theme
|
||||
? [theme.light, theme.dark]
|
||||
: [theme],
|
||||
langs: languages,
|
||||
langAlias: options.languageAlias,
|
||||
})
|
||||
|
||||
await options.shikiSetup?.(highlighter)
|
||||
|
||||
const loadedLanguages = highlighter.getLoadedLanguages()
|
||||
const removeMustache = (s: string, mustaches: Map<string, string>) => {
|
||||
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: Map<string, string>, twoslash: boolean) => {
|
||||
mustaches.forEach((marker, match) => {
|
||||
s = s.replaceAll(marker, match)
|
||||
})
|
||||
|
||||
if (twoslash)
|
||||
s = s.replace(/\{/g, '{')
|
||||
|
||||
return `${s}\n`
|
||||
}
|
||||
|
||||
return (str: string, language: string, attrs: string = '') => {
|
||||
const lang = getLanguage(loadedLanguages, language, defaultLang)
|
||||
|
||||
const enabledTwoslash = attrs.includes('twoslash') && !!options.twoslash
|
||||
|
||||
const mustaches = new Map<string, string>()
|
||||
str = removeMustache(str, mustaches).trimEnd()
|
||||
|
||||
try {
|
||||
const highlighted = highlighter.codeToHtml(str, {
|
||||
lang,
|
||||
transformers: [
|
||||
...baseTransformers,
|
||||
...getInlineTransformers({ attrs, lang, enabledTwoslash, whitespace }),
|
||||
...userTransformers,
|
||||
],
|
||||
meta: { __raw: attrs },
|
||||
...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
|
||||
? { themes: theme, defaultColor: false }
|
||||
: { theme }),
|
||||
})
|
||||
|
||||
const rendered = restoreMustache(highlighted, mustaches, enabledTwoslash)
|
||||
|
||||
return rendered
|
||||
}
|
||||
catch (e) {
|
||||
logger.error(e)
|
||||
return str
|
||||
}
|
||||
}
|
||||
}
|
||||
1
plugins/plugin-shikiji/src/node/highlight/index.ts
Normal file
1
plugins/plugin-shikiji/src/node/highlight/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './highlight.js'
|
||||
89
plugins/plugin-shikiji/src/node/highlight/transformers.ts
Normal file
89
plugins/plugin-shikiji/src/node/highlight/transformers.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import type { ShikiTransformer } from 'shiki'
|
||||
import { addClassToHast } from 'shiki'
|
||||
import {
|
||||
transformerCompactLineOptions,
|
||||
transformerNotationDiff,
|
||||
transformerNotationErrorLevel,
|
||||
transformerNotationFocus,
|
||||
transformerNotationHighlight,
|
||||
transformerNotationWordHighlight,
|
||||
transformerRemoveNotationEscape,
|
||||
transformerRenderWhitespace,
|
||||
} from '@shikijs/transformers'
|
||||
import type { WhitespacePosition } from '../utils/index.js'
|
||||
import { attrsToLines, resolveWhitespacePosition } from '../utils/index.js'
|
||||
import { defaultHoverInfoProcessor, transformerTwoslash } from '../twoslash/rendererTransformer.js'
|
||||
|
||||
const decorationsRE = /^\/\/ @decorations:(.*)\n/
|
||||
|
||||
export const baseTransformers: ShikiTransformer[] = [
|
||||
transformerNotationDiff(),
|
||||
transformerNotationFocus({
|
||||
classActiveLine: 'has-focus',
|
||||
classActivePre: 'has-focused-lines',
|
||||
}),
|
||||
transformerNotationHighlight(),
|
||||
transformerNotationErrorLevel(),
|
||||
transformerNotationWordHighlight(),
|
||||
{
|
||||
name: 'vuepress:add-class',
|
||||
pre(node) {
|
||||
addClassToHast(node, 'vp-code')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'vuepress:clean-up',
|
||||
pre(node) {
|
||||
delete node.properties.tabindex
|
||||
delete node.properties.style
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'shiki:inline-decorations',
|
||||
preprocess(code, options) {
|
||||
code = code.replace(decorationsRE, (match, decorations) => {
|
||||
options.decorations ||= []
|
||||
options.decorations.push(...JSON.parse(decorations))
|
||||
return ''
|
||||
})
|
||||
return code
|
||||
},
|
||||
},
|
||||
transformerRemoveNotationEscape(),
|
||||
]
|
||||
|
||||
const vueRE = /-vue$/
|
||||
export function getInlineTransformers({ attrs, lang, enabledTwoslash, whitespace }: {
|
||||
attrs: string
|
||||
lang: string
|
||||
enabledTwoslash: boolean
|
||||
whitespace: boolean | WhitespacePosition
|
||||
}): ShikiTransformer[] {
|
||||
const vPre = vueRE.test(lang) ? '' : 'v-pre'
|
||||
const inlineTransformers: ShikiTransformer[] = [
|
||||
transformerCompactLineOptions(attrsToLines(attrs)),
|
||||
]
|
||||
|
||||
if (enabledTwoslash) {
|
||||
inlineTransformers.push(transformerTwoslash({
|
||||
processHoverInfo(info) {
|
||||
return defaultHoverInfoProcessor(info)
|
||||
},
|
||||
}))
|
||||
}
|
||||
else {
|
||||
inlineTransformers.push({
|
||||
name: 'vuepress:v-pre',
|
||||
pre(node) {
|
||||
if (vPre)
|
||||
node.properties['v-pre'] = ''
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const position = resolveWhitespacePosition(attrs, whitespace)
|
||||
if (position)
|
||||
inlineTransformers.push(transformerRenderWhitespace({ position }))
|
||||
|
||||
return inlineTransformers
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user