From 1f0ec7feaff8282a0db71372d69b6c6d68e489c2 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Sun, 23 Feb 2025 02:01:01 +0800 Subject: [PATCH] feat(plugin-md-power): add support for `abbr` (#477) --- docs/.vuepress/theme.ts | 1 + .../src/client/components/Abbreviation.vue | 98 ++++++++++ .../plugin-md-power/src/node/inline/abbr.ts | 184 ++++++++++++++++++ .../plugin-md-power/src/node/inline/index.ts | 7 + .../src/node/prepareConfigFile.ts | 5 + plugins/plugin-md-power/src/shared/plugin.ts | 5 + 6 files changed, 300 insertions(+) create mode 100644 plugins/plugin-md-power/src/client/components/Abbreviation.vue create mode 100644 plugins/plugin-md-power/src/node/inline/abbr.ts diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 93b5b76e..bfd932df 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -25,6 +25,7 @@ export const theme: Theme = plumeTheme({ flowchart: true, }, markdownPower: { + abbr: true, imageSize: 'all', pdf: true, caniuse: true, diff --git a/plugins/plugin-md-power/src/client/components/Abbreviation.vue b/plugins/plugin-md-power/src/client/components/Abbreviation.vue new file mode 100644 index 00000000..cceb7d54 --- /dev/null +++ b/plugins/plugin-md-power/src/client/components/Abbreviation.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/plugins/plugin-md-power/src/node/inline/abbr.ts b/plugins/plugin-md-power/src/node/inline/abbr.ts new file mode 100644 index 00000000..b2436eab --- /dev/null +++ b/plugins/plugin-md-power/src/node/inline/abbr.ts @@ -0,0 +1,184 @@ +/** + * Forked and modified from https://github.com/markdown-it/markdown-it-abbr/blob/master/index.mjs + */ + +import type { PluginSimple } from 'markdown-it' +import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs' +import type { RuleCore } from 'markdown-it/lib/parser_core.mjs' +import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs' +import type StateCore from 'markdown-it/lib/rules_core/state_core.mjs' +import type Token from 'markdown-it/lib/token.mjs' + +interface AbbrStateBlock extends StateBlock { + env: { + abbreviations?: Record + } +} + +interface AbbrStateCore extends StateCore { + env: { + abbreviations?: Record + } +} + +export const abbrPlugin: PluginSimple = (md) => { + const { arrayReplaceAt, escapeRE, lib } = md.utils + + // ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on; + // you can check character classes here: + // http://www.unicode.org/Public/UNIDATA/UnicodeData.txt + const OTHER_CHARS = ' \r\n$+<=>^`|~' + const UNICODE_PUNCTUATION_REGEXP = (lib.ucmicro.P as RegExp).source + const UNICODE_SPACE_REGEXP = (lib.ucmicro.Z as RegExp).source + const WORDING_REGEXP_TEXT = `${UNICODE_PUNCTUATION_REGEXP}|${UNICODE_SPACE_REGEXP}|[${OTHER_CHARS.split('').map(escapeRE).join('')}]` + + const abbrDefinition: RuleBlock = ( + state: AbbrStateBlock, + startLine, + _endLine, + silent, + ) => { + let labelEnd = -1 + let pos = state.bMarks[startLine] + state.tShift[startLine] + const max = state.eMarks[startLine] + + if ( + pos + 2 >= max + || state.src.charAt(pos++) !== '*' + || state.src.charAt(pos++) !== '[' + ) { + return false + } + + const labelStart = pos + + while (pos < max) { + const ch = state.src.charAt(pos) + + if (ch === '[') + return false + if (ch === ']') { + labelEnd = pos + break + } + if (ch === '\\') + pos++ + pos++ + } + + if (labelEnd < 0 || state.src.charAt(labelEnd + 1) !== ':') + return false + if (silent) + return true + + const label = state.src.slice(labelStart, labelEnd).replace(/\\(.)/g, '$1') + const title = state.src.slice(labelEnd + 2, max).trim() + + if (!label.length || !title.length) + return false; + + // prepend ':' to avoid conflict with Object.prototype members + (state.env.abbreviations ??= {})[`:${label}`] ??= title + + state.line = startLine + 1 + + return true + } + + const abbrReplace: RuleCore = (state: AbbrStateCore) => { + const tokens = state.tokens + const { abbreviations } = state.env + + if (!abbreviations) + return + + const abbreviationsRegExpText = Object.keys(abbreviations) + .map(x => x.substring(1)) + .sort((a, b) => b.length - a.length) + .map(escapeRE) + .join('|') + + const regexpSimple = new RegExp(`(?:${abbreviationsRegExpText})`) + + const regExp = new RegExp( + `(^|${WORDING_REGEXP_TEXT})(${abbreviationsRegExpText})($|${WORDING_REGEXP_TEXT})`, + 'g', + ) + + for (const token of tokens) { + if (token.type !== 'inline') + continue + + let children = token.children! + + // We scan from the end, to keep position when new tags added. + for (let index = children.length - 1; index >= 0; index--) { + const currentToken = children[index] + + if (currentToken.type !== 'text') + continue + + const text = currentToken.content + + regExp.lastIndex = 0 + + // fast regexp run to determine whether there are any abbreviated words + // in the current token + if (!regexpSimple.test(text)) + continue + + const nodes: Token[] = [] + let match: RegExpExecArray | null + let pos = 0 + + // eslint-disable-next-line no-cond-assign + while ((match = regExp.exec(text))) { + const [, before, word, after] = match + + if (match.index > 0 || before.length > 0) { + const token = new state.Token('text', '', 0) + + token.content = text.slice(pos, match.index + before.length) + nodes.push(token) + } + + const abbrToken = new state.Token('abbreviation', 'Abbreviation', 0) + abbrToken.content = word + abbrToken.info = abbreviations[`:${word}`] + + nodes.push(abbrToken) + + regExp.lastIndex -= after.length + pos = regExp.lastIndex + } + + if (!nodes.length) + continue + + if (pos < text.length) { + const token = new state.Token('text', '', 0) + + token.content = text.slice(pos) + nodes.push(token) + } + + // replace current node + token.children = children = arrayReplaceAt(children, index, nodes) + } + } + } + + md.block.ruler.before('reference', 'abbr_definition', abbrDefinition, { + alt: ['paragraph', 'reference'], + }) + + md.core.ruler.after('linkify', 'abbr_replace', abbrReplace) + + md.renderer.rules.abbreviation = (tokens, idx) => { + const { content, info } = tokens[idx] + return ` + ${content} + ${info ? `` : ''} + ` + } +} diff --git a/plugins/plugin-md-power/src/node/inline/index.ts b/plugins/plugin-md-power/src/node/inline/index.ts index 92e52d62..e4893608 100644 --- a/plugins/plugin-md-power/src/node/inline/index.ts +++ b/plugins/plugin-md-power/src/node/inline/index.ts @@ -7,6 +7,7 @@ import { sub } from '@mdit/plugin-sub' import { sup } from '@mdit/plugin-sup' import { tasklist } from '@mdit/plugin-tasklist' import { isPlainObject } from '@vuepress/helper' +import { abbrPlugin } from './abbr.js' import { iconsPlugin } from './icons.js' import { plotPlugin } from './plot.js' @@ -21,6 +22,12 @@ export function inlineSyntaxPlugin( md.use(footnote) md.use(tasklist) + if (options.abbr) { + // a HTML element + // *[HTML]: A HTML element description + md.use(abbrPlugin) + } + if (options.icons) { // :[collect:name]: md.use(iconsPlugin, isPlainObject(options.icons) ? options.icons : {}) diff --git a/plugins/plugin-md-power/src/node/prepareConfigFile.ts b/plugins/plugin-md-power/src/node/prepareConfigFile.ts index 2ebf288c..ea82bf64 100644 --- a/plugins/plugin-md-power/src/node/prepareConfigFile.ts +++ b/plugins/plugin-md-power/src/node/prepareConfigFile.ts @@ -82,6 +82,11 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp enhances.add(`app.component('VPDemoNormal', VPDemoNormal)`) } + if (options.abbr) { + imports.add(`import Abbreviation from '${CLIENT_FOLDER}components/Abbreviation.vue'`) + enhances.add(`app.component('Abbreviation', Abbreviation)`) + } + return app.writeTemp( 'md-power/config.js', `\ diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts index 88d1a401..e839fdbb 100644 --- a/plugins/plugin-md-power/src/shared/plugin.ts +++ b/plugins/plugin-md-power/src/shared/plugin.ts @@ -8,6 +8,11 @@ import type { PlotOptions } from './plot.js' import type { ReplOptions } from './repl.js' export interface MarkdownPowerPluginOptions { + /** + * 是否启用 abbr 语法 + * @default false + */ + abbr?: boolean /** * 配置代码块分组 */