feat(plugin-md-power): add multiple lines parse for annotation syntax (#496)

* feat(plugin-md-power): add multiple lines parse for `annotation` syntax

* chore: tweak
This commit is contained in:
pengzhanbo 2025-03-01 23:44:21 +08:00 committed by GitHub
parent 2505e7f623
commit b879c62442
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 173 additions and 53 deletions

View File

@ -7,7 +7,7 @@ permalink: /guide/markdown/annotation/
## 描述
Annotation(注释) 是 Markdown 中的一种特殊的语法,用于在文档中添加额外的信息、说明或者提示。
==Annotation(注释)== 是 Markdown 中的一种特殊的语法,用于在文档中添加额外的信息、说明或者提示。
注释不会直接显示在文档中,需要用户手动点击才会显示。
@ -34,49 +34,120 @@ export default defineUserConfig({
## 语法
Annotation(注释) 语法有两个部分组成:
==Annotation(注释)== 语法由两个部分组成:
- **行内注释:** 在行内通过 `[+label]` 语法插入注释。
- **定义注释:** 在文档单独一行中使用 `[+label]: 内容` 语法定义注释。
### 行内注释
在行内通过 `[+label]` 语法插入注释标签。
注释标签由 `[+` + `label` + `]` 组成。为方便与内容做区分,在 `[+label]` 的左边边缘应该有一个空格。
`label` 为注释的标签,可以是任意字符串。
::: important 符号 `+` 是必须的
- 行内注释语法由 `[+` + `label` + `]` 组成。
- 定义注释语法由 `[+` + `label` + `]:` + `内容` 组成。
:::
### 定义注释
在文档的单独区域中使用 `[+label]:` 语法开始定义注释。
注释定义区域由 `[+` + `label` + `]:` + `内容` 组成。
`label` 应该与上述的 `[+label]` 一致,用于标记注释的标签。
**内容** 可以跟随在 `:` 之后开始写:
```md
[+label]: 这里是内容,可以使用 **Markdown** 语法。
```
**内容** 也可以从下一行开始写,但需要添加缩进,多行时应该保持一致的缩进。
```md
[+label]:
这里是内容。
缩进一致,此行也是内容。
即使上一行空行,但此行缩进也是一致的,也是内容。
可以使用 **Markdown** 语法。
此行不再缩进,该标签的注释定义在上一行结束。
```
定义注释的内容不会直接渲染在文档中,而是在 行内注释 的 `[+label]` 被点击后呈现。
## 示例
### 示例一
**输入:**
```md
站点由 VuePress [+vuepress] 驱动。
[+vuepress]: VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) 。 专为构建快速、以内容为中心的站点而设计。
[+vuepress]:
VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) 。
专为构建快速、以内容为中心的站点而设计。
```
**输出:**
站点由 VuePress [+vuepress] 驱动。
[+vuepress]: VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) 。 专为构建快速、以内容为中心的站点而设计。
[+vuepress]:
VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) 。
专为构建快速、以内容为中心的站点而设计。
**还可以为 `label` 定义多个注释,多个定义将会以列表的形式渲染。**
### 示例二
**同一个 `label` 定义多个注释,多个定义以列表的形式渲染。**
**输入:**
```md
中国古代 **四大名著** [+名著] 家喻户晓。
[+名著]: **《三国演义》:** 以三国时期的历史为背景,描写了魏、蜀、吴三国之间的政治、军事斗争,塑造了诸葛亮、曹操、关羽、刘备等众多历史人物形象。
[+名著]: **《西游记》:** 讲述了唐僧师徒四人(孙悟空、猪八戒、沙僧、白龙马)西天取经的故事,充满了神话色彩和奇幻冒险。
[+名著]: **《红楼梦》:** 以贾、史、王、薛四大家族的兴衰为背景,描写了贾宝玉、林黛玉、薛宝钗等人的爱情悲剧,展现了封建社会的腐朽与没落。
[+名著]: **《水浒传》:** 描写了北宋末年以宋江为首的108位好汉在梁山泊聚义反抗朝廷的故事展现了官逼民反的社会现实。
[+名著]:
**《三国演义》:**
以三国时期的历史为背景,描写了魏、蜀、吴三国之间的政治、军事斗争,塑造了诸葛亮、曹操、关羽、刘备等众多历史人物形象。
[+名著]:
**《西游记》:**
讲述了唐僧师徒四人(孙悟空、猪八戒、沙僧、白龙马)西天取经的故事,充满了神话色彩和奇幻冒险。
[+名著]:
**《红楼梦》:**
以贾、史、王、薛四大家族的兴衰为背景,描写了贾宝玉、林黛玉、薛宝钗等人的爱情悲剧,展现了封建社会的腐朽与没落。
[+名著]:
**《水浒传》:**
描写了北宋末年以宋江为首的108位好汉在梁山泊聚义反抗朝廷的故事展现了官逼民反的社会现实。
```
**输出:**
中国古代 **四大名著** [+名著] 家喻户晓。
[+名著]: **《三国演义》:** 以三国时期的历史为背景,描写了魏、蜀、吴三国之间的政治、军事斗争,塑造了诸葛亮、曹操、关羽、刘备等众多历史人物形象。
[+名著]: **《西游记》:** 讲述了唐僧师徒四人(孙悟空、猪八戒、沙僧、白龙马)西天取经的故事,充满了神话色彩和奇幻冒险。
[+名著]: **《红楼梦》:** 以贾、史、王、薛四大家族的兴衰为背景,描写了贾宝玉、林黛玉、薛宝钗等人的爱情悲剧,展现了封建社会的腐朽与没落。
[+名著]: **《水浒传》:** 描写了北宋末年以宋江为首的108位好汉在梁山泊聚义反抗朝廷的故事展现了官逼民反的社会现实。
[+名著]:
**《三国演义》:**
以三国时期的历史为背景,描写了魏、蜀、吴三国之间的政治、军事斗争,塑造了诸葛亮、曹操、关羽、刘备等众多历史人物形象。
[+名著]:
**《西游记》:**
讲述了唐僧师徒四人(孙悟空、猪八戒、沙僧、白龙马)西天取经的故事,充满了神话色彩和奇幻冒险。
[+名著]:
**《红楼梦》:**
以贾、史、王、薛四大家族的兴衰为背景,描写了贾宝玉、林黛玉、薛宝钗等人的爱情悲剧,展现了封建社会的腐朽与没落。
[+名著]:
**《水浒传》:**
描写了北宋末年以宋江为首的108位好汉在梁山泊聚义反抗朝廷的故事展现了官逼民反的社会现实。

View File

@ -24,32 +24,41 @@ function updatePosition() {
return
const { x: _x, y: _y, width: w, height: h } = button.value.getBoundingClientRect()
const x = _x + w / 2
const y = _y + h
const y = _y + h / 2
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
if (y > clientHeight - 16) {
active.value = false
}
else {
position.value.y = y + height + 16 > clientHeight ? clientHeight - y - height - 16 : 0
}
}
watch(active, () => nextTick(updatePosition))
useEventListener('resize', updatePosition)
useEventListener('scroll', updatePosition, { passive: true })
</script>
<template>
<span class="vp-annotation" :class="{ active, [label]: true }" :aria-label="label">
<span class="vp-annotation ignore-header" :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}`" />
<ClientOnly>
<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>
</div>
</Transition>
</Transition>
</ClientOnly>
</span>
</template>
@ -94,13 +103,15 @@ useEventListener('resize', updatePosition)
max-width: min(calc(100vw - 32px), 360px);
max-height: 360px;
padding: 8px 12px;
overflow-y: auto;
overflow: auto;
font-size: 14px;
font-weight: normal;
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));
transform: translateX(var(--vp-annotation-x, 0)) translateY(var(--vp-annotation-y, 0)) translateZ(0);
will-change: transform;
}
.annotations-popover.list {
@ -117,4 +128,25 @@ useEventListener('resize', updatePosition)
border-radius: 4px;
box-shadow: var(--vp-shadow-1);
}
.annotations-popover :deep(p) {
margin: 12px 0;
line-height: 24px;
}
.annotations-popover :deep(:first-child) {
margin-top: 4px;
}
.annotations-popover :deep(:last-child) {
margin-bottom: 4px;
}
.annotations-popover.list :deep(:first-child) {
margin-top: 8px;
}
.annotations-popover.list :deep(:last-child) {
margin-bottom: 8px;
}
</style>

View File

@ -8,12 +8,14 @@ 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[]>
annotations: Record<string, {
sources: string[]
rendered: string[]
}>
}
interface AnnotationStateBlock extends StateBlock {
@ -55,7 +57,7 @@ const annotationDef: RuleBlock = (
}
if (
// empty footnote label
// empty annotation label
pos === start + 2
|| pos + 1 >= max
|| state.src.charAt(++pos) !== ':'
@ -68,16 +70,30 @@ const annotationDef: RuleBlock = (
pos++
state.env.annotations ??= {}
const data = state.env.annotations ??= {}
const label = state.src.slice(start + 2, pos - 2)
const annotation = state.src.slice(pos, max).trim()
state.env.annotations[`:${label}`] ??= []
let annotation = state.src.slice(pos, max).trim()
state.env.annotations[`:${label}`].push(annotation)
// 处理多行注释
let nextLine = startLine + 1
while (nextLine < endLine) {
const nextStart = state.bMarks[nextLine] + state.tShift[nextLine]
const nextMax = state.eMarks[nextLine]
const source = state.src.slice(nextStart, nextMax).trim()
state.line += 1
// 行不为空,且行缩进小于块缩进,则跳出
if (state.sCount[nextLine] < state.blkIndent + 2 && source !== '')
break
annotation += `\n${source}`
nextLine++
}
const current = data[`:${label}`] ??= { sources: [], rendered: [] }
current.sources.push(annotation)
state.line = nextLine
return true
}
@ -120,7 +136,7 @@ const annotationRef: RuleInline = (
pos++
const label = state.src.slice(start + 2, pos - 1)
const annotations = state.env.annotations?.[`:${label}`] ?? []
const annotations = state.env.annotations?.[`:${label}`]?.sources ?? []
if (annotations.length === 0)
return false
@ -128,10 +144,7 @@ const annotationRef: RuleInline = (
if (!silent) {
const refToken = state.push('annotation_ref', '', 0)
refToken.meta = {
label,
annotations,
} as AnnotationToken['meta']
refToken.meta = { label } as AnnotationToken['meta']
}
state.pos = pos
@ -144,13 +157,17 @@ export const annotationPlugin: PluginSimple = (md) => {
md.renderer.rules.annotation_ref = (
tokens: AnnotationToken[],
idx: number,
_,
env: AnnotationEnv,
) => {
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>`
const label = tokens[idx].meta.label
const data = env.annotations[`:${label}`]
return `<Annotation label="${label}" :total="${data.sources.length}">${
data.sources.map((source, i) => {
const annotation = data.rendered[i] ??= md.render(source, env)
return `<template #item-${i}>${annotation}</template>`
}).join('')}</Annotation>`
}
md.inline.ruler.before('image', 'annotation_ref', annotationRef)