feat(plugin-md-power): add Annotation syntax support (#483)
* feat(plugin-md-power): add `Annotation` syntax support * chore: lint fix
This commit is contained in:
parent
1f0ec7feaf
commit
f8d32835df
@ -25,5 +25,7 @@
|
||||
"no-hard-tabs": {
|
||||
"spaces_per_tab": 2,
|
||||
"ignore_code_languages": ["xml"]
|
||||
}
|
||||
},
|
||||
"link-image-reference-definitions": false,
|
||||
"no-bare-urls": false
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ export const theme: Theme = plumeTheme({
|
||||
flowchart: true,
|
||||
},
|
||||
markdownPower: {
|
||||
annotation: true,
|
||||
abbr: true,
|
||||
imageSize: 'all',
|
||||
pdf: true,
|
||||
|
||||
119
plugins/plugin-md-power/src/client/components/Annotation.vue
Normal file
119
plugins/plugin-md-power/src/client/components/Annotation.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { onClickOutside, useEventListener } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
total: number
|
||||
}>()
|
||||
|
||||
const active = ref(false)
|
||||
const list = computed(() => Array.from({ length: props.total }, (_, i) => i))
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
|
||||
const popover = useTemplateRef<HTMLDivElement>('popover')
|
||||
const button = useTemplateRef<HTMLButtonElement>('button')
|
||||
onClickOutside(popover, () => (active.value = false), {
|
||||
ignore: [button],
|
||||
})
|
||||
|
||||
function updatePosition() {
|
||||
if (__VUEPRESS_SSR__)
|
||||
return
|
||||
if (!active.value || !popover.value || !button.value)
|
||||
return
|
||||
const { x: _x, y: _y, width: w, height: h } = button.value.getBoundingClientRect()
|
||||
const x = _x + w / 2
|
||||
const y = _y + h
|
||||
|
||||
const { width, height } = popover.value.getBoundingClientRect()
|
||||
const { clientWidth, clientHeight } = document.documentElement
|
||||
position.value.x = x + width + 16 > clientWidth ? clientWidth - x - width - 16 : 0
|
||||
position.value.y = y + height + 16 > clientHeight ? clientHeight - y - height - 16 : 0
|
||||
}
|
||||
|
||||
watch(active, () => nextTick(updatePosition))
|
||||
useEventListener('resize', updatePosition)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="vp-annotation" :class="{ active, [label]: true }" :aria-label="label">
|
||||
<span ref="button" class="vpi-annotation" @click="active = !active" />
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-show="active" ref="popover"
|
||||
class="annotations-popover" :class="{ list: list.length > 1 }"
|
||||
:style="{ '--vp-annotation-x': `${position.x}px`, '--vp-annotation-y': `${position.y}px` }"
|
||||
>
|
||||
<div v-for="i in list" :key="label + i" class="annotation">
|
||||
<slot :name="`item-${i}`" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-annotation {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vpi-annotation {
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10a10 10 0 0 1-4.262-.951l-4.537.93a1 1 0 0 1-1.18-1.18l.93-4.537A10 10 0 0 1 2 12m10-4a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2H9a1 1 0 1 1 0-2h2V9a1 1 0 0 1 1-1' clip-rule='evenodd'/%3E%3C/svg%3E");
|
||||
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
color: currentcolor;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: color var(--vp-t-color), opacity var(--vp-t-color), transform var(--vp-t-color);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.vp-annotation.active {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.vp-annotation:where(:hover, .active) .vpi-annotation {
|
||||
color: var(--vp-c-brand-2);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vp-annotation.active .vpi-annotation {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.annotations-popover {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: min(calc(100vw - 32px), 360px);
|
||||
max-height: 360px;
|
||||
padding: 8px 12px;
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
background-color: var(--vp-c-bg);
|
||||
border: solid 1px var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--vp-shadow-2);
|
||||
transform: translateX(var(--vp-annotation-x, 0)) translateY(var(--vp-annotation-y, 0));
|
||||
}
|
||||
|
||||
.annotations-popover.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.annotations-popover.list .annotation {
|
||||
padding: 4px 12px;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
</style>
|
||||
161
plugins/plugin-md-power/src/node/inline/annotation.ts
Normal file
161
plugins/plugin-md-power/src/node/inline/annotation.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import type { PluginSimple } from 'markdown-it'
|
||||
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
|
||||
import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs'
|
||||
import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs'
|
||||
import type StateInline from 'markdown-it/lib/rules_inline/state_inline.mjs'
|
||||
import type Token from 'markdown-it/lib/token.mjs'
|
||||
|
||||
interface AnnotationToken extends Token {
|
||||
meta: {
|
||||
label: string
|
||||
annotations: string[]
|
||||
}
|
||||
}
|
||||
|
||||
interface AnnotationEnv extends Record<string, unknown> {
|
||||
annotations: Record<string, string[]>
|
||||
}
|
||||
|
||||
interface AnnotationStateBlock extends StateBlock {
|
||||
tokens: AnnotationToken[]
|
||||
env: AnnotationEnv
|
||||
}
|
||||
|
||||
interface AnnotationStateInline extends StateInline {
|
||||
tokens: AnnotationToken[]
|
||||
env: AnnotationEnv
|
||||
}
|
||||
|
||||
const annotationDef: RuleBlock = (
|
||||
state: AnnotationStateBlock,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
silent: boolean,
|
||||
) => {
|
||||
const start = state.bMarks[startLine] + state.tShift[startLine]
|
||||
const max = state.eMarks[startLine]
|
||||
|
||||
if (
|
||||
// line should be at least 5 chars - "[+x]:"
|
||||
start + 4 > max
|
||||
|| state.src.charAt(start) !== '['
|
||||
|| state.src.charAt(start + 1) !== '+'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
let pos = start + 2
|
||||
|
||||
while (pos < max) {
|
||||
if (state.src.charAt(pos) === ' ')
|
||||
return false
|
||||
if (state.src.charAt(pos) === ']')
|
||||
break
|
||||
pos++
|
||||
}
|
||||
|
||||
if (
|
||||
// empty footnote label
|
||||
pos === start + 2
|
||||
|| pos + 1 >= max
|
||||
|| state.src.charAt(++pos) !== ':'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (silent)
|
||||
return true
|
||||
|
||||
pos++
|
||||
|
||||
state.env.annotations ??= {}
|
||||
|
||||
const label = state.src.slice(start + 2, pos - 2)
|
||||
const annotation = state.src.slice(pos, max).trim()
|
||||
|
||||
state.env.annotations[`:${label}`] ??= []
|
||||
|
||||
state.env.annotations[`:${label}`].push(annotation)
|
||||
|
||||
state.line += 1
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const annotationRef: RuleInline = (
|
||||
state: AnnotationStateInline,
|
||||
silent: boolean,
|
||||
): boolean => {
|
||||
const start = state.pos
|
||||
const max = state.posMax
|
||||
|
||||
if (
|
||||
// should be at least 4 chars - "[+x]"
|
||||
start + 3 > max
|
||||
|| typeof state.env.annotations === 'undefined'
|
||||
|| state.src.charAt(start) !== '['
|
||||
|| state.src.charAt(start + 1) !== '+'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
let pos = start + 2
|
||||
|
||||
while (pos < max) {
|
||||
if (state.src.charAt(pos) === ' ' || state.src.charAt(pos) === '\n')
|
||||
return false
|
||||
if (state.src.charAt(pos) === ']')
|
||||
break
|
||||
pos++
|
||||
}
|
||||
|
||||
if (
|
||||
// empty annotation labels
|
||||
pos === start + 2
|
||||
|| pos >= max
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
pos++
|
||||
|
||||
const label = state.src.slice(start + 2, pos - 1)
|
||||
const annotations = state.env.annotations?.[`:${label}`] ?? []
|
||||
|
||||
if (annotations.length === 0)
|
||||
return false
|
||||
|
||||
if (!silent) {
|
||||
const refToken = state.push('annotation_ref', '', 0)
|
||||
|
||||
refToken.meta = {
|
||||
label,
|
||||
annotations,
|
||||
} as AnnotationToken['meta']
|
||||
}
|
||||
|
||||
state.pos = pos
|
||||
state.posMax = max
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const annotationPlugin: PluginSimple = (md) => {
|
||||
md.renderer.rules.annotation_ref = (
|
||||
tokens: AnnotationToken[],
|
||||
idx: number,
|
||||
) => {
|
||||
const { label = '', annotations = [] } = tokens[idx].meta ?? {}
|
||||
return `<Annotation label="${label}" :total="${annotations.length}">
|
||||
${annotations.map((annotation, i) => {
|
||||
return `<template #item-${i}>${md.renderInline(annotation)}</template>`
|
||||
}).join('\n')}
|
||||
</Annotation>`
|
||||
}
|
||||
|
||||
md.inline.ruler.before('image', 'annotation_ref', annotationRef)
|
||||
|
||||
md.block.ruler.before('reference', 'annotation', annotationDef, {
|
||||
alt: ['paragraph', 'reference'],
|
||||
})
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { sup } from '@mdit/plugin-sup'
|
||||
import { tasklist } from '@mdit/plugin-tasklist'
|
||||
import { isPlainObject } from '@vuepress/helper'
|
||||
import { abbrPlugin } from './abbr.js'
|
||||
import { annotationPlugin } from './annotation.js'
|
||||
import { iconsPlugin } from './icons.js'
|
||||
import { plotPlugin } from './plot.js'
|
||||
|
||||
@ -22,9 +23,21 @@ export function inlineSyntaxPlugin(
|
||||
md.use(footnote)
|
||||
md.use(tasklist)
|
||||
|
||||
if (options.annotation) {
|
||||
/**
|
||||
* xxx [+foo] xxx
|
||||
*
|
||||
* [+foo]: xxx
|
||||
*/
|
||||
md.use(annotationPlugin)
|
||||
}
|
||||
|
||||
if (options.abbr) {
|
||||
// a HTML element
|
||||
// *[HTML]: A HTML element description
|
||||
/**
|
||||
* a HTML element
|
||||
*
|
||||
* [HTML]: A HTML element description
|
||||
*/
|
||||
md.use(abbrPlugin)
|
||||
}
|
||||
|
||||
|
||||
@ -82,6 +82,11 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
|
||||
enhances.add(`app.component('VPDemoNormal', VPDemoNormal)`)
|
||||
}
|
||||
|
||||
if (options.annotation) {
|
||||
imports.add(`import Annotation from '${CLIENT_FOLDER}components/Annotation.vue'`)
|
||||
enhances.add(`app.component('Annotation', Annotation)`)
|
||||
}
|
||||
|
||||
if (options.abbr) {
|
||||
imports.add(`import Abbreviation from '${CLIENT_FOLDER}components/Abbreviation.vue'`)
|
||||
enhances.add(`app.component('Abbreviation', Abbreviation)`)
|
||||
|
||||
@ -9,6 +9,12 @@ import type { ReplOptions } from './repl.js'
|
||||
|
||||
export interface MarkdownPowerPluginOptions {
|
||||
/**
|
||||
* 是否启用注释
|
||||
* @default false
|
||||
*/
|
||||
annotation?: boolean
|
||||
|
||||
/*
|
||||
* 是否启用 abbr 语法
|
||||
* @default false
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user