feat(plugin-md-power): add support for abbr (#477)

This commit is contained in:
pengzhanbo 2025-02-23 02:01:01 +08:00 committed by GitHub
parent fded7e807a
commit 1f0ec7feaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 300 additions and 0 deletions

View File

@ -25,6 +25,7 @@ export const theme: Theme = plumeTheme({
flowchart: true,
},
markdownPower: {
abbr: true,
imageSize: 'all',
pdf: true,
caniuse: true,

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { nextTick, ref, useTemplateRef, watch } from 'vue'
const show = ref(false)
const tooltip = useTemplateRef<HTMLSpanElement>('tooltip')
const styles = ref<CSSProperties>()
watch(show, () => nextTick(() => {
if (__VUEPRESS_SSR__)
return
if (show.value && tooltip.value) {
const { x, width } = tooltip.value.getBoundingClientRect()
const innerWidth = window.innerWidth
const space = 16
let translate = 0
if (x - space < 0)
translate = Math.abs(x) + space
else if (x + width + space > innerWidth)
translate = innerWidth - x - width - space
if (translate !== 0) {
styles.value = {
'--vp-abbr-transform': `translateX(${translate}px) translateX(-50%)`,
'--vp-abbr-space-transform': `translateX(${-translate}px) translateX(-50%)`,
}
}
}
}))
</script>
<template>
<span class="vp-abbr" @mouseenter="show = true" @mouseleave="show = false">
<slot />
<Transition name="fade">
<span v-show="show" ref="tooltip" class="vp-abbr-tooltip" :style="styles">
<slot name="tooltip" />
</span>
</Transition>
</span>
</template>
<style>
:root {
--vp-abbr-bg: var(--vp-c-bg);
--vp-abbr-text: var(--vp-c-text-2);
--vp-abbr-border: var(--vp-c-divider);
--vp-abbr-transform: translateX(-50%);
--vp-abbr-space-transform: translateX(-50%);
}
.vp-abbr {
position: relative;
text-decoration: underline dotted currentcolor;
text-underline-offset: 4px;
cursor: help;
}
.vp-abbr .vp-abbr-tooltip {
position: absolute;
top: calc(100% + 12px);
left: 50%;
z-index: 1;
width: max-content;
max-width: min(calc(100vw - 32px), 360px);
padding: 8px 14px;
font-size: 0.875em;
line-height: 1.7;
color: var(--vp-abbr-text);
cursor: auto;
background-color: var(--vp-abbr-bg);
border: solid 1px var(--vp-abbr-border);
border-radius: 4px;
box-shadow: var(--vp-shadow-2);
transform: var(--vp-abbr-transform);
}
.vp-abbr .vp-abbr-tooltip::before,
.vp-abbr .vp-abbr-tooltip::after {
position: absolute;
top: -16px;
left: 50%;
display: block;
width: 0;
height: 0;
content: "";
border: 8px solid transparent;
border-bottom-color: var(--vp-abbr-bg);
transform: var(--vp-abbr-space-transform);
}
.vp-abbr .vp-abbr-tooltip::before {
top: -17px;
border-bottom-color: var(--vp-abbr-border);
}
</style>

View File

@ -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<string, string>
}
}
interface AbbrStateCore extends StateCore {
env: {
abbreviations?: Record<string, string>
}
}
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 `<Abbreviation>
${content}
${info ? `<template #tooltip>${md.renderInline(info)}</template>` : ''}
</Abbreviation>`
}
}

View File

@ -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 : {})

View File

@ -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',
`\

View File

@ -8,6 +8,11 @@ import type { PlotOptions } from './plot.js'
import type { ReplOptions } from './repl.js'
export interface MarkdownPowerPluginOptions {
/**
* abbr
* @default false
*/
abbr?: boolean
/**
*
*/