feat(plugin-md-power): add support for abbr (#477)
This commit is contained in:
parent
fded7e807a
commit
1f0ec7feaf
@ -25,6 +25,7 @@ export const theme: Theme = plumeTheme({
|
||||
flowchart: true,
|
||||
},
|
||||
markdownPower: {
|
||||
abbr: true,
|
||||
imageSize: 'all',
|
||||
pdf: true,
|
||||
caniuse: true,
|
||||
|
||||
@ -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>
|
||||
184
plugins/plugin-md-power/src/node/inline/abbr.ts
Normal file
184
plugins/plugin-md-power/src/node/inline/abbr.ts
Normal 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>`
|
||||
}
|
||||
}
|
||||
@ -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 : {})
|
||||
|
||||
@ -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',
|
||||
`\
|
||||
|
||||
@ -8,6 +8,11 @@ import type { PlotOptions } from './plot.js'
|
||||
import type { ReplOptions } from './repl.js'
|
||||
|
||||
export interface MarkdownPowerPluginOptions {
|
||||
/**
|
||||
* 是否启用 abbr 语法
|
||||
* @default false
|
||||
*/
|
||||
abbr?: boolean
|
||||
/**
|
||||
* 配置代码块分组
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user