From 1111e855cb4f9818decc4ebec356a332a5283d35 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Sat, 23 Dec 2023 09:15:00 +0800 Subject: [PATCH] feat: add `plugin-shikiji` --- plugins/plugin-shikiji/LICENSE | 21 +++ plugins/plugin-shikiji/README.md | 67 ++++++++ plugins/plugin-shikiji/package.json | 45 ++++++ plugins/plugin-shikiji/src/node/highlight.ts | 150 ++++++++++++++++++ plugins/plugin-shikiji/src/node/index.ts | 6 + .../plugin-shikiji/src/node/shikijiPlugin.ts | 19 +++ plugins/plugin-shikiji/src/node/types.ts | 56 +++++++ plugins/plugin-shikiji/tsconfig.build.json | 8 + 8 files changed, 372 insertions(+) create mode 100644 plugins/plugin-shikiji/LICENSE create mode 100644 plugins/plugin-shikiji/README.md create mode 100644 plugins/plugin-shikiji/package.json create mode 100644 plugins/plugin-shikiji/src/node/highlight.ts create mode 100644 plugins/plugin-shikiji/src/node/index.ts create mode 100644 plugins/plugin-shikiji/src/node/shikijiPlugin.ts create mode 100644 plugins/plugin-shikiji/src/node/types.ts create mode 100644 plugins/plugin-shikiji/tsconfig.build.json diff --git a/plugins/plugin-shikiji/LICENSE b/plugins/plugin-shikiji/LICENSE new file mode 100644 index 000000000..9f677c905 --- /dev/null +++ b/plugins/plugin-shikiji/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (C) 2021 - PRESENT by pengzhanbo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/plugin-shikiji/README.md b/plugins/plugin-shikiji/README.md new file mode 100644 index 000000000..3963abbd6 --- /dev/null +++ b/plugins/plugin-shikiji/README.md @@ -0,0 +1,67 @@ +# `@vuepress-plume/plugin-shikiji` + +使用 [`shikiji`](https://shikiji.netlify.app/) 来为 Markdown 代码块启用代码高亮。 + +## Install +``` +yarn add @vuepress-plume/plugin-shikiji +``` +## Usage +``` js +// .vuepress/config.js +const shikijiPlugin = require('@vuepress-plume/plugin-shikiji') +module.exports = { + //... + plugins: [ + shikijiPlugin() + ] + // ... +} +``` + +## Options + +```ts +interface ShikijiOptions { + /** + * Custom theme for syntax highlighting. + * + * You can also pass an object with `light` and `dark` themes to support dual themes. + * + * @example { theme: 'github-dark' } + * @example { theme: { light: 'github-light', dark: 'github-dark' } } + * + * You can use an existing theme. + * @see https://github.com/antfu/shikiji/blob/main/docs/themes.md#all-themes + * Or add your own theme. + * @see https://github.com/antfu/shikiji/blob/main/docs/themes.md#load-custom-themes + */ + theme?: ThemeOptions + /** + * Languages for syntax highlighting. + * @see https://github.com/antfu/shikiji/blob/main/docs/languages.md#all-themes + */ + languages?: LanguageInput[] + /** + * Custom language aliases. + * + * @example { 'my-lang': 'js' } + * @see https://github.com/antfu/shikiji/tree/main#custom-language-aliases + */ + languageAlias?: Record + /** + * Setup Shikiji instance + */ + shikijiSetup?: (shikiji: Highlighter) => void | Promise + /** + * Fallback language when the specified language is not available. + */ + defaultHighlightLang?: string + /** + * Transformers applied to code blocks + * @see https://github.com/antfu/shikiji#hast-transformers + */ + codeTransformers?: ShikijiTransformer[] +} + +``` diff --git a/plugins/plugin-shikiji/package.json b/plugins/plugin-shikiji/package.json new file mode 100644 index 000000000..8f2cb7516 --- /dev/null +++ b/plugins/plugin-shikiji/package.json @@ -0,0 +1,45 @@ +{ + "name": "@vuepress-plume/plugin-shikiji", + "version": "1.0.0-beta.89", + "description": "The Plugin for VuePres 2", + "homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme", + "bugs": { + "url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git" + }, + "license": "MIT", + "author": "pengzhanbo ", + "type": "module", + "exports": { + ".": "./lib/node/index.js", + "./package.json": "./package.json" + }, + "main": "lib/node/index.js", + "types": "./lib/node/index.d.ts", + "scripts": { + "build": "pnpm run clean && pnpm run copy && pnpm run ts", + "clean": "rimraf lib *.tsbuildinfo", + "copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib", + "ts": "tsc -b tsconfig.build.json" + }, + "dependencies": { + "@vuepress/core": "2.0.0-rc.0", + "@vuepress/utils": "2.0.0-rc.0", + "nanoid": "^5.0.4", + "picocolors": "^1.0.0", + "shikiji": "^0.9.11", + "shikiji-transformers": "^0.9.11" + }, + "publishConfig": { + "access": "public" + }, + "keyword": [ + "VuePress", + "vuepress plugin", + "shikiji", + "vuepress-plugin-shikiji" + ] +} diff --git a/plugins/plugin-shikiji/src/node/highlight.ts b/plugins/plugin-shikiji/src/node/highlight.ts new file mode 100644 index 000000000..de50dc9da --- /dev/null +++ b/plugins/plugin-shikiji/src/node/highlight.ts @@ -0,0 +1,150 @@ +import { logger } from '@vuepress/utils' +import { customAlphabet } from 'nanoid' +import c from 'picocolors' +import type { ShikijiTransformer } from 'shikiji' +import { + addClassToHast, + bundledLanguages, + getHighlighter, + isPlaintext as isPlainLang, + isSpecialLang +} from 'shikiji' +import { + transformerNotationDiff, + transformerNotationErrorLevel, + transformerNotationFocus, + transformerNotationHighlight +} from 'shikiji-transformers' +import type { HighlighterOptions, ThemeOptions } from './types.js' + +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) + +export async function highlight( + theme: ThemeOptions, + options: HighlighterOptions, +): Promise<(str: string, lang: string, attrs: string) => string> { + const { + defaultHighlightLang: defaultLang = '', + codeTransformers: userTransformers = [] + } = options + + const highlighter = await getHighlighter({ + themes: + typeof theme === 'string' || 'name' in theme + ? [theme] + : [theme.light, theme.dark], + langs: [...Object.keys(bundledLanguages), ...(options.languages || [])], + langAlias: options.languageAlias + }) + + await options?.shikijiSetup?.(highlighter) + + const transformers: ShikijiTransformer[] = [ + transformerNotationDiff(), + transformerNotationFocus({ + classActiveLine: 'has-focus', + classActivePre: 'has-focused-lines' + }), + transformerNotationHighlight(), + transformerNotationErrorLevel(), + { + name: 'vuepress:add-class', + pre(node) { + addClassToHast(node, 'vp-code') + } + }, + { + name: 'vuepress:clean-up', + pre(node) { + delete node.properties.tabindex + delete node.properties.style + } + } + ] + + const vueRE = /-vue$/ + const lineNoStartRE = /=(\d*)/ + 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 + + if (lang) { + const langLoaded = highlighter.getLoadedLanguages().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 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 restoreMustache = (s: string) => { + mustaches.forEach((marker, match) => { + s = s.replaceAll(marker, match) + }) + return s + } + + const fillEmptyHighlightedLine = (s: string) => { + return s.replace( + /()(<\/span>)/g, + '$1$2' + ) + '\n' + } + + 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'] = '' + // } + // }, + ...userTransformers + ], + meta: { + __raw: attrs + }, + ...(typeof theme === 'string' || 'name' in theme + ? { theme } + : { + themes: theme, + defaultColor: false + }) + }) + + return fillEmptyHighlightedLine(restoreMustache(highlighted)) + } +} diff --git a/plugins/plugin-shikiji/src/node/index.ts b/plugins/plugin-shikiji/src/node/index.ts new file mode 100644 index 000000000..6aca3b80d --- /dev/null +++ b/plugins/plugin-shikiji/src/node/index.ts @@ -0,0 +1,6 @@ +import { shikijiPlugin } from './shikijiPlugin.js' + +export * from './shikijiPlugin.js' +export * from './types.js' + +export default shikijiPlugin diff --git a/plugins/plugin-shikiji/src/node/shikijiPlugin.ts b/plugins/plugin-shikiji/src/node/shikijiPlugin.ts new file mode 100644 index 000000000..8183b7d4e --- /dev/null +++ b/plugins/plugin-shikiji/src/node/shikijiPlugin.ts @@ -0,0 +1,19 @@ +import type { Plugin } from '@vuepress/core' +import { highlight } from './highlight.js' +import type { HighlighterOptions } from './types' +/** + * Options of @vuepress/plugin-shiki + */ +export type ShikijiPluginOptions = HighlighterOptions + +export const shikijiPlugin = ( + options: ShikijiPluginOptions = {}): Plugin => ({ + name: '@vuepress-plume/plugin-shikiji', + + extendsMarkdown: async (md) => { + const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' } + const highlighter = await highlight(theme, options) + + md.options.highlight = highlighter + }, +}) diff --git a/plugins/plugin-shikiji/src/node/types.ts b/plugins/plugin-shikiji/src/node/types.ts new file mode 100644 index 000000000..49e6ab57c --- /dev/null +++ b/plugins/plugin-shikiji/src/node/types.ts @@ -0,0 +1,56 @@ +import type { + BuiltinTheme, + Highlighter, + LanguageInput, + ShikijiTransformer, + ThemeRegistration, +} from 'shikiji' +export type ThemeOptions = + | ThemeRegistration + | BuiltinTheme + | { + light: ThemeRegistration | BuiltinTheme + dark: ThemeRegistration | BuiltinTheme + } + +export interface HighlighterOptions { + /** + * Custom theme for syntax highlighting. + * + * You can also pass an object with `light` and `dark` themes to support dual themes. + * + * @example { theme: 'github-dark' } + * @example { theme: { light: 'github-light', dark: 'github-dark' } } + * + * You can use an existing theme. + * @see https://github.com/antfu/shikiji/blob/main/docs/themes.md#all-themes + * Or add your own theme. + * @see https://github.com/antfu/shikiji/blob/main/docs/themes.md#load-custom-themes + */ + theme?: ThemeOptions + /** + * Languages for syntax highlighting. + * @see https://github.com/antfu/shikiji/blob/main/docs/languages.md#all-themes + */ + languages?: LanguageInput[] + /** + * Custom language aliases. + * + * @example { 'my-lang': 'js' } + * @see https://github.com/antfu/shikiji/tree/main#custom-language-aliases + */ + languageAlias?: Record + /** + * Setup Shikiji instance + */ + shikijiSetup?: (shikiji: Highlighter) => void | Promise + /** + * Fallback language when the specified language is not available. + */ + defaultHighlightLang?: string + /** + * Transformers applied to code blocks + * @see https://github.com/antfu/shikiji#hast-transformers + */ + codeTransformers?: ShikijiTransformer[] +} diff --git a/plugins/plugin-shikiji/tsconfig.build.json b/plugins/plugin-shikiji/tsconfig.build.json new file mode 100644 index 000000000..6bf67375e --- /dev/null +++ b/plugins/plugin-shikiji/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["./src"] +}