vitepress-plugin-annotation 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/client/browser/index.d.ts +47 -0
- package/dist/client/browser/index.js +126 -0
- package/dist/client/ssr/index.d.ts +47 -0
- package/dist/client/ssr/index.js +146 -0
- package/dist/client/style.css +110 -0
- package/dist/node/index.d.ts +104 -0
- package/dist/node/index.js +238 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pengzhanbo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# vitepress-plugin-annotation
|
|
2
|
+
|
|
3
|
+
Add annotation support to your VitePress site, rendering annotation references as interactive
|
|
4
|
+
markers that reveal a popover with the annotation content on click.
|
|
5
|
+
|
|
6
|
+
为 VitePress 添加注释支持,将注释引用渲染为可交互的标记,点击后弹出包含注释内容的浮层。
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
### With Vitepress-tuck
|
|
11
|
+
|
|
12
|
+
**Installation:**
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# npm
|
|
16
|
+
npm install -D vitepress-tuck vitepress-plugin-annotation
|
|
17
|
+
# pnpm
|
|
18
|
+
pnpm add -D vitepress-tuck vitepress-plugin-annotation
|
|
19
|
+
# yarn
|
|
20
|
+
yarn add -D vitepress-tuck vitepress-plugin-annotation
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Configuration:**
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// .vitepress/config.ts
|
|
27
|
+
import annotation from 'vitepress-plugin-annotation'
|
|
28
|
+
import { defineConfig } from 'vitepress-tuck'
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
plugins: [annotation()],
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// .vitepress/theme/index.ts
|
|
37
|
+
import type { Theme } from 'vitepress'
|
|
38
|
+
import enhanceApp from 'virtual:enhance-app'
|
|
39
|
+
import DefaultTheme from 'vitepress/theme'
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
extends: DefaultTheme,
|
|
43
|
+
enhanceApp(ctx) {
|
|
44
|
+
enhanceApp(ctx)
|
|
45
|
+
},
|
|
46
|
+
} satisfies Theme
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### With Vitepress
|
|
50
|
+
|
|
51
|
+
**Installation:**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# npm
|
|
55
|
+
npm install -D vitepress-plugin-annotation
|
|
56
|
+
# pnpm
|
|
57
|
+
pnpm add -D vitepress-plugin-annotation
|
|
58
|
+
# yarn
|
|
59
|
+
yarn add -D vitepress-plugin-annotation
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Configuration:**
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// .vitepress/config.ts
|
|
66
|
+
import { defineConfig } from 'vitepress'
|
|
67
|
+
import { annotationMarkdownPlugin } from 'vitepress-plugin-annotation'
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
markdown: {
|
|
71
|
+
config: (md) => {
|
|
72
|
+
md.use(annotationMarkdownPlugin)
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// .vitepress/theme/index.ts
|
|
80
|
+
import type { Theme } from 'vitepress'
|
|
81
|
+
import { enhanceAppWithAnnotation } from 'vitepress-plugin-annotation/client'
|
|
82
|
+
import DefaultTheme from 'vitepress/theme'
|
|
83
|
+
|
|
84
|
+
export default {
|
|
85
|
+
extends: DefaultTheme,
|
|
86
|
+
enhanceApp(ctx) {
|
|
87
|
+
enhanceAppWithAnnotation(ctx)
|
|
88
|
+
},
|
|
89
|
+
} satisfies Theme
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Syntax
|
|
93
|
+
|
|
94
|
+
Define an annotation with the `[+label]: annotation content` syntax, then reference it anywhere
|
|
95
|
+
in your markdown content using `[+label]`. The reference is rendered as a small interactive
|
|
96
|
+
marker that, on click, reveals a popover displaying the annotation content.
|
|
97
|
+
|
|
98
|
+
使用 `[+标签]: 注释内容` 语法定义注释,然后在 markdown 内容的任意位置使用 `[+标签]`
|
|
99
|
+
进行引用。引用会渲染为一个小的可交互标记,点击后弹出显示注释内容的浮层。
|
|
100
|
+
|
|
101
|
+
```md
|
|
102
|
+
The **four great classical novels** [+novels] of Chinese literature are widely known.
|
|
103
|
+
|
|
104
|
+
[+novels]:
|
|
105
|
+
**Romance of the Three Kingdoms** — a historical novel set in the Three Kingdoms period.
|
|
106
|
+
|
|
107
|
+
[+novels]:
|
|
108
|
+
**Journey to the West** — a mythological tale of a pilgrimage to the West.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The annotation content supports full block-level markdown, including headings, paragraphs,
|
|
112
|
+
lists, code blocks, and more.
|
|
113
|
+
|
|
114
|
+
注释内容支持完整的块级 markdown 语法,包括标题、段落、列表、代码块等。
|
|
115
|
+
|
|
116
|
+
```md
|
|
117
|
+
The framework is built on [+vue].
|
|
118
|
+
|
|
119
|
+
[+vue]:
|
|
120
|
+
## Vue
|
|
121
|
+
|
|
122
|
+
A progressive framework for building user interfaces.
|
|
123
|
+
|
|
124
|
+
- Reactive data binding
|
|
125
|
+
- Component-based architecture
|
|
126
|
+
- [Learn more](https://vuejs.org/)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Multi-line Definitions
|
|
130
|
+
|
|
131
|
+
Annotation definitions can span multiple lines. Continuation lines must be indented by at
|
|
132
|
+
least two spaces relative to the definition. Empty lines within the indented block are
|
|
133
|
+
preserved as part of the annotation content. A non-indented content line (such as the next
|
|
134
|
+
definition or a new paragraph) terminates the multi-line definition.
|
|
135
|
+
|
|
136
|
+
注释定义可以跨多行。续行必须相对于定义至少缩进两个空格。缩进块内的空行会作为
|
|
137
|
+
注释内容的一部分被保留。非缩进的内容行(如下一个定义或新段落)会终止多行定义。
|
|
138
|
+
|
|
139
|
+
```md
|
|
140
|
+
This concept is explained in [+detail].
|
|
141
|
+
|
|
142
|
+
[+detail]:
|
|
143
|
+
This is the first paragraph of the annotation.
|
|
144
|
+
|
|
145
|
+
This is the second paragraph, separated by an empty line
|
|
146
|
+
but still part of the same annotation.
|
|
147
|
+
|
|
148
|
+
The annotation definition ends here because this line is not indented.
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Multiple Definitions per Label
|
|
152
|
+
|
|
153
|
+
A single label can be defined multiple times. All definitions are collected and displayed
|
|
154
|
+
as separate items in the popover, allowing you to provide multiple pieces of related
|
|
155
|
+
information for the same reference.
|
|
156
|
+
|
|
157
|
+
同一标签可以多次定义。所有定义会被收集并作为独立条目展示在浮层中,允许你为同一
|
|
158
|
+
引用提供多条相关信息。
|
|
159
|
+
|
|
160
|
+
```md
|
|
161
|
+
JavaScript has multiple [+js] versions.
|
|
162
|
+
|
|
163
|
+
[+js]:
|
|
164
|
+
**ES2015 (ES6)** — Introduced classes, modules, arrows, and more.
|
|
165
|
+
|
|
166
|
+
[+js]:
|
|
167
|
+
**ES2020** — Added optional chaining, nullish coalescing, and BigInt.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Global Annotations
|
|
171
|
+
|
|
172
|
+
Instead of (or in addition to) defining annotations inline in each markdown file, you can
|
|
173
|
+
provide a global preset of annotations through the plugin options. This is useful for terms
|
|
174
|
+
that should be annotated consistently across all pages.
|
|
175
|
+
|
|
176
|
+
可以在每个 markdown 文件中内联定义注释,也可以通过插件选项提供全局注释预设。
|
|
177
|
+
这对于需要在所有页面中一致注释的术语很有用。
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// .vitepress/config.ts
|
|
181
|
+
import annotation from 'vitepress-plugin-annotation'
|
|
182
|
+
import { defineConfig } from 'vitepress-tuck'
|
|
183
|
+
|
|
184
|
+
export default defineConfig({
|
|
185
|
+
plugins: [
|
|
186
|
+
annotation({
|
|
187
|
+
HTML: 'HyperText Markup Language',
|
|
188
|
+
CSS: ['Cascading Style Sheets', 'A style sheet language used for describing the presentation of a document.'],
|
|
189
|
+
}),
|
|
190
|
+
],
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Each global annotation value can be a string or an array of strings. When an array is
|
|
195
|
+
provided, each element becomes a separate item in the popover, just like multiple inline
|
|
196
|
+
definitions for the same label.
|
|
197
|
+
|
|
198
|
+
每个全局注释值可以是字符串或字符串数组。当提供数组时,每个元素成为浮层中的
|
|
199
|
+
一个独立条目,就像同一标签的多个内联定义一样。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EnhanceAppContext } from "vitepress";
|
|
2
|
+
|
|
3
|
+
//#region src/client/VPAnnotation.vue.d.ts
|
|
4
|
+
type __VLS_Props = {
|
|
5
|
+
label: string;
|
|
6
|
+
total: number;
|
|
7
|
+
};
|
|
8
|
+
declare var __VLS_20: `item-${number}`, __VLS_21: {};
|
|
9
|
+
type __VLS_Slots = {} & { [K in NonNullable<typeof __VLS_20>]?: (props: typeof __VLS_21) => any };
|
|
10
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
12
|
+
declare const _default: typeof __VLS_export;
|
|
13
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
14
|
+
new (): {
|
|
15
|
+
$slots: S;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/client/index.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Enhances the VitePress application by registering the `VPAnnotation`
|
|
22
|
+
* component globally under the name `VPAnnotation`, so it can be used
|
|
23
|
+
* directly in Vue templates or rendered markdown content.
|
|
24
|
+
*
|
|
25
|
+
* 通过全局注册 `VPAnnotation` 组件来增强 VitePress 应用,
|
|
26
|
+
* 使其可在 Vue 模板或渲染的 markdown 内容中直接使用。
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* `.vitepress/theme/index.ts`
|
|
30
|
+
* ```ts
|
|
31
|
+
* import type { Theme } from 'vitepress'
|
|
32
|
+
* import { enhanceAppWithAnnotation } from 'vitepress-plugin-annotation/client'
|
|
33
|
+
* import DefaultTheme from 'vitepress/theme'
|
|
34
|
+
*
|
|
35
|
+
* export default {
|
|
36
|
+
* extends: DefaultTheme,
|
|
37
|
+
* enhanceApp(ctx) {
|
|
38
|
+
* enhanceAppWithAnnotation(ctx)
|
|
39
|
+
* },
|
|
40
|
+
* } satisfies Theme
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function enhanceAppWithAnnotation({
|
|
44
|
+
app
|
|
45
|
+
}: EnhanceAppContext): void;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { _default as VPAnnotation, enhanceAppWithAnnotation };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import "../style.css";
|
|
2
|
+
import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, mergeProps, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, resolveComponent, unref, useTemplateRef, withCtx } from "vue";
|
|
3
|
+
import { autoUpdate, flip, shift, useFloating } from "@floating-ui/vue";
|
|
4
|
+
import { onClickOutside, useIntersectionObserver } from "@vueuse/core";
|
|
5
|
+
//#region src/client/VPAnnotation.vue
|
|
6
|
+
const _hoisted_1 = ["aria-label", "aria-expanded"];
|
|
7
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
8
|
+
inheritAttrs: false,
|
|
9
|
+
__name: "VPAnnotation",
|
|
10
|
+
props: {
|
|
11
|
+
label: {},
|
|
12
|
+
total: {}
|
|
13
|
+
},
|
|
14
|
+
setup(__props) {
|
|
15
|
+
/**
|
|
16
|
+
* VPAnnotation component for rendering annotation references with an
|
|
17
|
+
* interactive popover in VitePress.
|
|
18
|
+
*
|
|
19
|
+
* VPAnnotation 组件,用于在 VitePress 中渲染带有可交互浮层的注释引用。
|
|
20
|
+
*
|
|
21
|
+
* The annotation marker is rendered as a small inline span (styled via CSS as
|
|
22
|
+
* a superscript-like icon). When the user clicks or focuses the marker, a
|
|
23
|
+
* floating popover anchored to the marker is shown, displaying the annotation
|
|
24
|
+
* content provided via the `#item-0`, `#item-1`, … named slots. Each slot
|
|
25
|
+
* corresponds to one definition of the annotation label. The popover is
|
|
26
|
+
* teleported to `body` and positioned with `@floating-ui/vue`, applying
|
|
27
|
+
* offset, flip, and shift middleware for robust placement near viewport edges.
|
|
28
|
+
*
|
|
29
|
+
* 注释标记渲染为一个小型行内 span(通过 CSS 样式为类上标图标)。当用户点击或
|
|
30
|
+
* 聚焦该标记时,会显示一个锚定到标记的浮动浮层,展示通过 `#item-0`、
|
|
31
|
+
* `#item-1`、… 具名插槽提供的注释内容。每个插槽对应注释标签的一个定义。
|
|
32
|
+
* 浮层被传送到 `body`,并使用 `@floating-ui/vue` 定位,应用 offset、flip 和
|
|
33
|
+
* shift 中间件以保证在视口边缘附近的稳定定位。
|
|
34
|
+
*
|
|
35
|
+
* The popover automatically closes when:
|
|
36
|
+
* - The user clicks outside the marker and popover.
|
|
37
|
+
* - The marker scrolls out of the viewport.
|
|
38
|
+
*
|
|
39
|
+
* 浮层在以下情况下自动关闭:
|
|
40
|
+
* - 用户点击标记和浮层之外的区域。
|
|
41
|
+
* - 标记滚动出视口。
|
|
42
|
+
*/
|
|
43
|
+
const active = ref(false);
|
|
44
|
+
const group = computed(() => Array.from({ length: __props.total }, (_, i) => i));
|
|
45
|
+
const annotation = useTemplateRef("annotation");
|
|
46
|
+
const tooltip = useTemplateRef("tooltip");
|
|
47
|
+
onClickOutside(annotation, () => {
|
|
48
|
+
active.value = false;
|
|
49
|
+
}, { ignore: [tooltip] });
|
|
50
|
+
useIntersectionObserver(annotation, ([entry]) => {
|
|
51
|
+
if (!entry.isIntersecting && active.value) active.value = false;
|
|
52
|
+
}, { rootMargin: "-64px 0px 0px 0px" });
|
|
53
|
+
const { floatingStyles, placement } = useFloating(annotation, tooltip, {
|
|
54
|
+
whileElementsMounted: autoUpdate,
|
|
55
|
+
middleware: [flip(), shift({ padding: {
|
|
56
|
+
top: 80,
|
|
57
|
+
left: 16,
|
|
58
|
+
bottom: 16
|
|
59
|
+
} })]
|
|
60
|
+
});
|
|
61
|
+
const inset = computed(() => placement.value.split("-")[0]);
|
|
62
|
+
return (_ctx, _cache) => {
|
|
63
|
+
const _component_ClientOnly = resolveComponent("ClientOnly");
|
|
64
|
+
return openBlock(), createElementBlock(Fragment, null, [createElementVNode("button", mergeProps(_ctx.$attrs, {
|
|
65
|
+
ref_key: "annotation",
|
|
66
|
+
ref: annotation,
|
|
67
|
+
type: "button",
|
|
68
|
+
class: ["vp-annotation", {
|
|
69
|
+
[__props.label]: true,
|
|
70
|
+
[inset.value]: true,
|
|
71
|
+
active: active.value
|
|
72
|
+
}],
|
|
73
|
+
"aria-label": __props.label,
|
|
74
|
+
"aria-expanded": active.value,
|
|
75
|
+
onClick: _cache[0] || (_cache[0] = ($event) => active.value = !active.value)
|
|
76
|
+
}), [..._cache[1] || (_cache[1] = [createElementVNode("span", { class: "vpi-annotation" }, null, -1)])], 16, _hoisted_1), createVNode(_component_ClientOnly, null, {
|
|
77
|
+
default: withCtx(() => [(openBlock(), createBlock(Teleport, { to: "body" }, [createVNode(Transition, { name: "fade-in" }, {
|
|
78
|
+
default: withCtx(() => [active.value ? (openBlock(), createElementBlock("div", {
|
|
79
|
+
key: 0,
|
|
80
|
+
ref_key: "tooltip",
|
|
81
|
+
ref: tooltip,
|
|
82
|
+
class: normalizeClass(["vp-annotation-popover", { group: group.value.length > 1 }]),
|
|
83
|
+
style: normalizeStyle(unref(floatingStyles))
|
|
84
|
+
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(group.value, (i) => {
|
|
85
|
+
return openBlock(), createElementBlock("div", {
|
|
86
|
+
key: __props.label + i,
|
|
87
|
+
class: "annotation"
|
|
88
|
+
}, [renderSlot(_ctx.$slots, `item-${i}`)]);
|
|
89
|
+
}), 128))], 6)) : createCommentVNode("v-if", true)]),
|
|
90
|
+
_: 3
|
|
91
|
+
})]))]),
|
|
92
|
+
_: 3
|
|
93
|
+
})], 64);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/client/index.ts
|
|
99
|
+
/**
|
|
100
|
+
* Enhances the VitePress application by registering the `VPAnnotation`
|
|
101
|
+
* component globally under the name `VPAnnotation`, so it can be used
|
|
102
|
+
* directly in Vue templates or rendered markdown content.
|
|
103
|
+
*
|
|
104
|
+
* 通过全局注册 `VPAnnotation` 组件来增强 VitePress 应用,
|
|
105
|
+
* 使其可在 Vue 模板或渲染的 markdown 内容中直接使用。
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* `.vitepress/theme/index.ts`
|
|
109
|
+
* ```ts
|
|
110
|
+
* import type { Theme } from 'vitepress'
|
|
111
|
+
* import { enhanceAppWithAnnotation } from 'vitepress-plugin-annotation/client'
|
|
112
|
+
* import DefaultTheme from 'vitepress/theme'
|
|
113
|
+
*
|
|
114
|
+
* export default {
|
|
115
|
+
* extends: DefaultTheme,
|
|
116
|
+
* enhanceApp(ctx) {
|
|
117
|
+
* enhanceAppWithAnnotation(ctx)
|
|
118
|
+
* },
|
|
119
|
+
* } satisfies Theme
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
function enhanceAppWithAnnotation({ app }) {
|
|
123
|
+
app.component("VPAnnotation", _sfc_main);
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { _sfc_main as VPAnnotation, enhanceAppWithAnnotation };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EnhanceAppContext } from "vitepress";
|
|
2
|
+
|
|
3
|
+
//#region src/client/VPAnnotation.vue.d.ts
|
|
4
|
+
type __VLS_Props = {
|
|
5
|
+
label: string;
|
|
6
|
+
total: number;
|
|
7
|
+
};
|
|
8
|
+
declare var __VLS_20: `item-${number}`, __VLS_21: {};
|
|
9
|
+
type __VLS_Slots = {} & { [K in NonNullable<typeof __VLS_20>]?: (props: typeof __VLS_21) => any };
|
|
10
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
12
|
+
declare const _default: typeof __VLS_export;
|
|
13
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
14
|
+
new (): {
|
|
15
|
+
$slots: S;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/client/index.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Enhances the VitePress application by registering the `VPAnnotation`
|
|
22
|
+
* component globally under the name `VPAnnotation`, so it can be used
|
|
23
|
+
* directly in Vue templates or rendered markdown content.
|
|
24
|
+
*
|
|
25
|
+
* 通过全局注册 `VPAnnotation` 组件来增强 VitePress 应用,
|
|
26
|
+
* 使其可在 Vue 模板或渲染的 markdown 内容中直接使用。
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* `.vitepress/theme/index.ts`
|
|
30
|
+
* ```ts
|
|
31
|
+
* import type { Theme } from 'vitepress'
|
|
32
|
+
* import { enhanceAppWithAnnotation } from 'vitepress-plugin-annotation/client'
|
|
33
|
+
* import DefaultTheme from 'vitepress/theme'
|
|
34
|
+
*
|
|
35
|
+
* export default {
|
|
36
|
+
* extends: DefaultTheme,
|
|
37
|
+
* enhanceApp(ctx) {
|
|
38
|
+
* enhanceAppWithAnnotation(ctx)
|
|
39
|
+
* },
|
|
40
|
+
* } satisfies Theme
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function enhanceAppWithAnnotation({
|
|
44
|
+
app
|
|
45
|
+
}: EnhanceAppContext): void;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { _default as VPAnnotation, enhanceAppWithAnnotation };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createVNode, defineComponent, mergeProps, openBlock, ref, renderList, renderSlot, resolveComponent, unref, useSSRContext, useTemplateRef, withCtx } from "vue";
|
|
2
|
+
import { ssrRenderAttrs, ssrRenderClass, ssrRenderComponent, ssrRenderList, ssrRenderSlot, ssrRenderStyle, ssrRenderTeleport } from "vue/server-renderer";
|
|
3
|
+
import { autoUpdate, flip, shift, useFloating } from "@floating-ui/vue";
|
|
4
|
+
import { onClickOutside, useIntersectionObserver } from "@vueuse/core";
|
|
5
|
+
//#region src/client/VPAnnotation.vue
|
|
6
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
7
|
+
inheritAttrs: false,
|
|
8
|
+
__name: "VPAnnotation",
|
|
9
|
+
__ssrInlineRender: true,
|
|
10
|
+
props: {
|
|
11
|
+
label: {},
|
|
12
|
+
total: {}
|
|
13
|
+
},
|
|
14
|
+
setup(__props) {
|
|
15
|
+
/**
|
|
16
|
+
* VPAnnotation component for rendering annotation references with an
|
|
17
|
+
* interactive popover in VitePress.
|
|
18
|
+
*
|
|
19
|
+
* VPAnnotation 组件,用于在 VitePress 中渲染带有可交互浮层的注释引用。
|
|
20
|
+
*
|
|
21
|
+
* The annotation marker is rendered as a small inline span (styled via CSS as
|
|
22
|
+
* a superscript-like icon). When the user clicks or focuses the marker, a
|
|
23
|
+
* floating popover anchored to the marker is shown, displaying the annotation
|
|
24
|
+
* content provided via the `#item-0`, `#item-1`, … named slots. Each slot
|
|
25
|
+
* corresponds to one definition of the annotation label. The popover is
|
|
26
|
+
* teleported to `body` and positioned with `@floating-ui/vue`, applying
|
|
27
|
+
* offset, flip, and shift middleware for robust placement near viewport edges.
|
|
28
|
+
*
|
|
29
|
+
* 注释标记渲染为一个小型行内 span(通过 CSS 样式为类上标图标)。当用户点击或
|
|
30
|
+
* 聚焦该标记时,会显示一个锚定到标记的浮动浮层,展示通过 `#item-0`、
|
|
31
|
+
* `#item-1`、… 具名插槽提供的注释内容。每个插槽对应注释标签的一个定义。
|
|
32
|
+
* 浮层被传送到 `body`,并使用 `@floating-ui/vue` 定位,应用 offset、flip 和
|
|
33
|
+
* shift 中间件以保证在视口边缘附近的稳定定位。
|
|
34
|
+
*
|
|
35
|
+
* The popover automatically closes when:
|
|
36
|
+
* - The user clicks outside the marker and popover.
|
|
37
|
+
* - The marker scrolls out of the viewport.
|
|
38
|
+
*
|
|
39
|
+
* 浮层在以下情况下自动关闭:
|
|
40
|
+
* - 用户点击标记和浮层之外的区域。
|
|
41
|
+
* - 标记滚动出视口。
|
|
42
|
+
*/
|
|
43
|
+
const active = ref(false);
|
|
44
|
+
const group = computed(() => Array.from({ length: __props.total }, (_, i) => i));
|
|
45
|
+
const annotation = useTemplateRef("annotation");
|
|
46
|
+
const tooltip = useTemplateRef("tooltip");
|
|
47
|
+
onClickOutside(annotation, () => {
|
|
48
|
+
active.value = false;
|
|
49
|
+
}, { ignore: [tooltip] });
|
|
50
|
+
useIntersectionObserver(annotation, ([entry]) => {
|
|
51
|
+
if (!entry.isIntersecting && active.value) active.value = false;
|
|
52
|
+
}, { rootMargin: "-64px 0px 0px 0px" });
|
|
53
|
+
const { floatingStyles, placement } = useFloating(annotation, tooltip, {
|
|
54
|
+
whileElementsMounted: autoUpdate,
|
|
55
|
+
middleware: [flip(), shift({ padding: {
|
|
56
|
+
top: 80,
|
|
57
|
+
left: 16,
|
|
58
|
+
bottom: 16
|
|
59
|
+
} })]
|
|
60
|
+
});
|
|
61
|
+
const inset = computed(() => placement.value.split("-")[0]);
|
|
62
|
+
return (_ctx, _push, _parent, _attrs) => {
|
|
63
|
+
const _component_ClientOnly = resolveComponent("ClientOnly");
|
|
64
|
+
_push(`<!--[--><button${ssrRenderAttrs(mergeProps(_ctx.$attrs, {
|
|
65
|
+
ref_key: "annotation",
|
|
66
|
+
ref: annotation,
|
|
67
|
+
type: "button",
|
|
68
|
+
class: ["vp-annotation", {
|
|
69
|
+
[__props.label]: true,
|
|
70
|
+
[inset.value]: true,
|
|
71
|
+
active: active.value
|
|
72
|
+
}],
|
|
73
|
+
"aria-label": __props.label,
|
|
74
|
+
"aria-expanded": active.value
|
|
75
|
+
}))}><span class="vpi-annotation"></span></button>`);
|
|
76
|
+
_push(ssrRenderComponent(_component_ClientOnly, null, {
|
|
77
|
+
default: withCtx((_, _push, _parent, _scopeId) => {
|
|
78
|
+
if (_push) ssrRenderTeleport(_push, (_push) => {
|
|
79
|
+
if (active.value) {
|
|
80
|
+
_push(`<div class="${ssrRenderClass([{ group: group.value.length > 1 }, "vp-annotation-popover"])}" style="${ssrRenderStyle(unref(floatingStyles))}"${_scopeId}><!--[-->`);
|
|
81
|
+
ssrRenderList(group.value, (i) => {
|
|
82
|
+
_push(`<div class="annotation"${_scopeId}>`);
|
|
83
|
+
ssrRenderSlot(_ctx.$slots, `item-${i}`, {}, null, _push, _parent, _scopeId);
|
|
84
|
+
_push(`</div>`);
|
|
85
|
+
});
|
|
86
|
+
_push(`<!--]--></div>`);
|
|
87
|
+
} else _push(`<!---->`);
|
|
88
|
+
}, "body", false, _parent);
|
|
89
|
+
else return [(openBlock(), createBlock(Teleport, { to: "body" }, [createVNode(Transition, { name: "fade-in" }, {
|
|
90
|
+
default: withCtx(() => [active.value ? (openBlock(), createBlock("div", {
|
|
91
|
+
key: 0,
|
|
92
|
+
ref_key: "tooltip",
|
|
93
|
+
ref: tooltip,
|
|
94
|
+
class: ["vp-annotation-popover", { group: group.value.length > 1 }],
|
|
95
|
+
style: unref(floatingStyles)
|
|
96
|
+
}, [(openBlock(true), createBlock(Fragment, null, renderList(group.value, (i) => {
|
|
97
|
+
return openBlock(), createBlock("div", {
|
|
98
|
+
key: __props.label + i,
|
|
99
|
+
class: "annotation"
|
|
100
|
+
}, [renderSlot(_ctx.$slots, `item-${i}`)]);
|
|
101
|
+
}), 128))], 6)) : createCommentVNode("v-if", true)]),
|
|
102
|
+
_: 3
|
|
103
|
+
})]))];
|
|
104
|
+
}),
|
|
105
|
+
_: 3
|
|
106
|
+
}, _parent));
|
|
107
|
+
_push(`<!--]-->`);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const _sfc_setup = _sfc_main.setup;
|
|
112
|
+
_sfc_main.setup = (props, ctx) => {
|
|
113
|
+
const ssrContext = useSSRContext();
|
|
114
|
+
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/VPAnnotation.vue");
|
|
115
|
+
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/client/index.ts
|
|
119
|
+
/**
|
|
120
|
+
* Enhances the VitePress application by registering the `VPAnnotation`
|
|
121
|
+
* component globally under the name `VPAnnotation`, so it can be used
|
|
122
|
+
* directly in Vue templates or rendered markdown content.
|
|
123
|
+
*
|
|
124
|
+
* 通过全局注册 `VPAnnotation` 组件来增强 VitePress 应用,
|
|
125
|
+
* 使其可在 Vue 模板或渲染的 markdown 内容中直接使用。
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* `.vitepress/theme/index.ts`
|
|
129
|
+
* ```ts
|
|
130
|
+
* import type { Theme } from 'vitepress'
|
|
131
|
+
* import { enhanceAppWithAnnotation } from 'vitepress-plugin-annotation/client'
|
|
132
|
+
* import DefaultTheme from 'vitepress/theme'
|
|
133
|
+
*
|
|
134
|
+
* export default {
|
|
135
|
+
* extends: DefaultTheme,
|
|
136
|
+
* enhanceApp(ctx) {
|
|
137
|
+
* enhanceAppWithAnnotation(ctx)
|
|
138
|
+
* },
|
|
139
|
+
* } satisfies Theme
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
function enhanceAppWithAnnotation({ app }) {
|
|
143
|
+
app.component("VPAnnotation", _sfc_main);
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { _sfc_main as VPAnnotation, enhanceAppWithAnnotation };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
@import url("vitepress-plugin-toolkit/styles/transition/fade-in.css");
|
|
2
|
+
|
|
3
|
+
.vp-annotation {
|
|
4
|
+
position: relative;
|
|
5
|
+
top: -2px;
|
|
6
|
+
z-index: 10;
|
|
7
|
+
display: inline-block;
|
|
8
|
+
width: 1.5em;
|
|
9
|
+
height: 1.5em;
|
|
10
|
+
color: currentcolor;
|
|
11
|
+
vertical-align: middle;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
user-select: none;
|
|
14
|
+
opacity: 0.5;
|
|
15
|
+
transition: 0.25s ease-in-out;
|
|
16
|
+
transition-property: color, opacity, transform;
|
|
17
|
+
transform: rotate(0deg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media print {
|
|
21
|
+
.vp-annotation {
|
|
22
|
+
display: none;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.vp-annotation:where(:hover, .active) {
|
|
27
|
+
color: var(--vp-c-brand-2) !important;
|
|
28
|
+
opacity: 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.vp-annotation.active.top {
|
|
32
|
+
transform: rotate(135deg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.vp-annotation.active.left {
|
|
36
|
+
transform: rotate(45deg);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.vp-annotation.active.bottom {
|
|
40
|
+
transform: rotate(-45deg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.vp-annotation.active.right {
|
|
44
|
+
transform: rotate(-135deg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.vp-annotation .vpi-annotation {
|
|
48
|
+
--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");
|
|
49
|
+
|
|
50
|
+
display: inline-block;
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.vp-annotation-popover {
|
|
56
|
+
position: absolute;
|
|
57
|
+
top: 0;
|
|
58
|
+
left: 0;
|
|
59
|
+
z-index: 1;
|
|
60
|
+
width: max-content;
|
|
61
|
+
max-width: min(calc(100vw - 32px), 360px);
|
|
62
|
+
max-height: 360px;
|
|
63
|
+
padding-block: 8px;
|
|
64
|
+
padding-inline: 12px;
|
|
65
|
+
overflow: auto;
|
|
66
|
+
font-size: 14px;
|
|
67
|
+
font-weight: normal;
|
|
68
|
+
background-color: var(--vp-c-bg);
|
|
69
|
+
border: solid 1px var(--vp-c-divider);
|
|
70
|
+
border-radius: 8px;
|
|
71
|
+
box-shadow: var(--vp-shadow-2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.vp-annotation-popover.group {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 12px;
|
|
78
|
+
padding: 12px;
|
|
79
|
+
background-color: var(--vp-c-bg-soft);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.vp-annotation-popover.group .annotation {
|
|
83
|
+
padding-block: 4px;
|
|
84
|
+
padding-inline: 12px;
|
|
85
|
+
background-color: var(--vp-c-bg);
|
|
86
|
+
border-radius: 4px;
|
|
87
|
+
box-shadow: var(--vp-shadow-1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.vp-annotation-popover p {
|
|
91
|
+
margin-block: 12px;
|
|
92
|
+
margin-inline: 0;
|
|
93
|
+
line-height: 24px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.vp-annotation-popover :first-child {
|
|
97
|
+
margin-block-start: 4px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.vp-annotation-popover :last-child {
|
|
101
|
+
margin-block-end: 4px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.vp-annotation-popover.group :first-child {
|
|
105
|
+
margin-block-start: 8px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.vp-annotation-popover.group :last-child {
|
|
109
|
+
margin-block-end: 8px;
|
|
110
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { PluginWithOptions } from "markdown-it";
|
|
2
|
+
|
|
3
|
+
//#region src/node/plugin.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* VitePress plugin for annotation support in markdown content.
|
|
6
|
+
*
|
|
7
|
+
* VitePress 插件,用于在 markdown 内容中支持注释功能。
|
|
8
|
+
*
|
|
9
|
+
* The plugin registers annotation definition and reference rules in markdown-it,
|
|
10
|
+
* and wires up the client-side `VPAnnotation` component via `componentResolver`
|
|
11
|
+
* so that annotation references render as interactive markers that, on click,
|
|
12
|
+
* reveal a popover with the annotation content.
|
|
13
|
+
*
|
|
14
|
+
* 该插件在 markdown-it 中注册注释定义与引用规则,并通过 `componentResolver`
|
|
15
|
+
* 接入客户端 `VPAnnotation` 组件,使注释引用渲染为可交互的标记,点击后弹出
|
|
16
|
+
* 包含注释内容的浮层。
|
|
17
|
+
*
|
|
18
|
+
* Annotations can be defined inline in markdown using the syntax
|
|
19
|
+
* `[+label]: annotation content`, or provided globally through the `annotations`
|
|
20
|
+
* option. A single label can be defined multiple times — all definitions are
|
|
21
|
+
* collected and displayed as separate items in the popover. Annotation content
|
|
22
|
+
* supports full block-level markdown.
|
|
23
|
+
*
|
|
24
|
+
* 注释可以在 markdown 中通过 `[+标签]: 注释内容` 语法内联定义,也可以通过
|
|
25
|
+
* `annotations` 选项全局提供。同一标签可多次定义 —— 所有定义会被收集并作为
|
|
26
|
+
* 独立条目展示在浮层中。注释内容支持完整的块级 markdown 语法。
|
|
27
|
+
*
|
|
28
|
+
* @param annotations - Global annotations preset, a map of label to annotation
|
|
29
|
+
* content (string or string array) / 全局注释预设,标签到注释内容
|
|
30
|
+
* (字符串或字符串数组)的映射
|
|
31
|
+
* @example
|
|
32
|
+
* `.vitepress/config.ts` without global annotations
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { defineConfig } from 'vitepress-tuck'
|
|
35
|
+
* import annotation from 'vitepress-plugin-annotation'
|
|
36
|
+
*
|
|
37
|
+
* export default defineConfig({
|
|
38
|
+
* plugins: [annotation()],
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
* @example
|
|
42
|
+
* `.vitepress/config.ts` with global annotations
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { defineConfig } from 'vitepress-tuck'
|
|
45
|
+
* import annotation from 'vitepress-plugin-annotation'
|
|
46
|
+
*
|
|
47
|
+
* export default defineConfig({
|
|
48
|
+
* plugins: [
|
|
49
|
+
* annotation({
|
|
50
|
+
* HTML: 'HyperText Markup Language',
|
|
51
|
+
* }),
|
|
52
|
+
* ],
|
|
53
|
+
* })
|
|
54
|
+
* ```
|
|
55
|
+
* @example
|
|
56
|
+
* // Markdown usage
|
|
57
|
+
* ```md
|
|
58
|
+
* The **four great classical novels** [+novels] of Chinese literature.
|
|
59
|
+
*
|
|
60
|
+
* [+novels]:
|
|
61
|
+
* **Romance of the Three Kingdoms** — a historical novel.
|
|
62
|
+
*
|
|
63
|
+
* [+novels]:
|
|
64
|
+
* **Journey to the West** — a mythological adventure.
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare const annotation: (options?: Record<string, string | string[]> | undefined) => import("vitepress-tuck").VitepressPlugin;
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/node/markdown.d.ts
|
|
70
|
+
/**
|
|
71
|
+
* Annotation plugin - Enable annotation syntax in markdown-it.
|
|
72
|
+
*
|
|
73
|
+
* 注释插件 - 在 markdown-it 中启用注释语法。
|
|
74
|
+
*
|
|
75
|
+
* Registers two rules:
|
|
76
|
+
* - A block rule (`annotationDef`) that recognizes annotation definitions of
|
|
77
|
+
* the form `[+label]: content` and stores them in `env.annotations`.
|
|
78
|
+
* Definitions support multi-line content via indented continuation lines,
|
|
79
|
+
* and multiple definitions for the same label are accumulated.
|
|
80
|
+
* - An inline rule (`annotationRef`) that recognizes annotation references of
|
|
81
|
+
* the form `[+label]` and pushes an `annotation_ref` token to be rendered
|
|
82
|
+
* as a `<VPAnnotation>` component.
|
|
83
|
+
*
|
|
84
|
+
* 注册两条规则:
|
|
85
|
+
* - 块规则(`annotationDef`)识别 `[+标签]: 内容` 形式的注释定义,并将其
|
|
86
|
+
* 存储到 `env.annotations` 中。定义通过缩进续行支持多行内容,同一标签的
|
|
87
|
+
* 多次定义会被累加。
|
|
88
|
+
* - 行内规则(`annotationRef`)识别 `[+标签]` 形式的注释引用,并推入
|
|
89
|
+
* `annotation_ref` 令牌,最终渲染为 `<VPAnnotation>` 组件。
|
|
90
|
+
*
|
|
91
|
+
* Definition syntax: `[+label]: annotation content`
|
|
92
|
+
* Reference syntax: `[+label]`
|
|
93
|
+
*
|
|
94
|
+
* 定义语法:`[+标签]: 注释内容`
|
|
95
|
+
* 引用语法:`[+标签]`
|
|
96
|
+
*
|
|
97
|
+
* @param md - Markdown-it instance / Markdown-it 实例
|
|
98
|
+
* @param globalAnnotations - Global annotations preset, a map of label to
|
|
99
|
+
* annotation content (string or string array) / 全局注释预设,标签到
|
|
100
|
+
* 注释内容(字符串或字符串数组)的映射
|
|
101
|
+
*/
|
|
102
|
+
declare const annotationMarkdownPlugin: PluginWithOptions<Record<string, string | string[]>>;
|
|
103
|
+
//#endregion
|
|
104
|
+
export { annotation, annotation as default, annotationMarkdownPlugin };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { definePlugin } from "vitepress-tuck";
|
|
2
|
+
import { objectMap, toArray } from "@pengzhanbo/utils";
|
|
3
|
+
import { cleanMarkdownEnv } from "vitepress-plugin-toolkit";
|
|
4
|
+
//#region src/node/markdown.ts
|
|
5
|
+
/**
|
|
6
|
+
* Annotation definition rule
|
|
7
|
+
*
|
|
8
|
+
* 注释定义规则
|
|
9
|
+
*
|
|
10
|
+
* @param state - State block / 状态块
|
|
11
|
+
* @param startLine - Start line number / 开始行号
|
|
12
|
+
* @param endLine - End line number / 结束行号
|
|
13
|
+
* @param silent - Silent mode / 静默模式
|
|
14
|
+
* @returns Whether matched / 是否匹配
|
|
15
|
+
*/
|
|
16
|
+
const annotationDef = (state, startLine, endLine, silent) => {
|
|
17
|
+
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
18
|
+
const max = state.eMarks[startLine];
|
|
19
|
+
if (start + 4 > max || state.src.charAt(start) !== "[" || state.src.charAt(start + 1) !== "+") return false;
|
|
20
|
+
let pos = start + 2;
|
|
21
|
+
while (pos < max) {
|
|
22
|
+
if (state.src.charAt(pos) === " ") return false;
|
|
23
|
+
if (state.src.charAt(pos) === "]") break;
|
|
24
|
+
pos++;
|
|
25
|
+
}
|
|
26
|
+
if (pos === start + 2 || pos + 1 >= max || state.src.charAt(++pos) !== ":") return false;
|
|
27
|
+
/* istanbul ignore if -- @preserve */
|
|
28
|
+
if (silent) return true;
|
|
29
|
+
pos++;
|
|
30
|
+
const data = state.env.annotations ??= {};
|
|
31
|
+
const label = state.src.slice(start + 2, pos - 2);
|
|
32
|
+
let annotation = state.src.slice(pos, max).trim();
|
|
33
|
+
let nextLine = startLine + 1;
|
|
34
|
+
while (nextLine < endLine) {
|
|
35
|
+
const nextStart = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
36
|
+
const nextMax = state.eMarks[nextLine];
|
|
37
|
+
const source = state.src.slice(nextStart, nextMax).slice(state.blkIndent).trimEnd();
|
|
38
|
+
if (state.sCount[nextLine] < state.blkIndent + 2 && source !== "") break;
|
|
39
|
+
annotation += `\n${source}`;
|
|
40
|
+
nextLine++;
|
|
41
|
+
}
|
|
42
|
+
(data[`:${label}`] ??= {
|
|
43
|
+
sources: [],
|
|
44
|
+
rendered: []
|
|
45
|
+
}).sources.push(annotation);
|
|
46
|
+
state.line = nextLine;
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Annotation reference rule
|
|
51
|
+
*
|
|
52
|
+
* 注释引用规则
|
|
53
|
+
*
|
|
54
|
+
* @param state - State inline / 行内状态
|
|
55
|
+
* @param silent - Silent mode / 静默模式
|
|
56
|
+
* @returns Whether matched / 是否匹配
|
|
57
|
+
*/
|
|
58
|
+
const annotationRef = (state, silent) => {
|
|
59
|
+
const start = state.pos;
|
|
60
|
+
const max = state.posMax;
|
|
61
|
+
if (start + 3 > max || typeof state.env.annotations === "undefined" || state.src.charAt(start) !== "[" || state.src.charAt(start + 1) !== "+") return false;
|
|
62
|
+
let pos = start + 2;
|
|
63
|
+
while (pos < max) {
|
|
64
|
+
if (state.src.charAt(pos) === " " || state.src.charAt(pos) === "\n") return false;
|
|
65
|
+
if (state.src.charAt(pos) === "]") break;
|
|
66
|
+
pos++;
|
|
67
|
+
}
|
|
68
|
+
if (pos === start + 2 || pos >= max) return false;
|
|
69
|
+
pos++;
|
|
70
|
+
const label = state.src.slice(start + 2, pos - 1);
|
|
71
|
+
if ((state.env.annotations?.[`:${label}`]?.sources ?? []).length === 0) return false;
|
|
72
|
+
/* istanbul ignore if -- @preserve */
|
|
73
|
+
if (!silent) {
|
|
74
|
+
const refToken = state.push("annotation_ref", "", 0);
|
|
75
|
+
refToken.meta = { label };
|
|
76
|
+
}
|
|
77
|
+
state.pos = pos;
|
|
78
|
+
state.posMax = max;
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Annotation plugin - Enable annotation syntax in markdown-it.
|
|
83
|
+
*
|
|
84
|
+
* 注释插件 - 在 markdown-it 中启用注释语法。
|
|
85
|
+
*
|
|
86
|
+
* Registers two rules:
|
|
87
|
+
* - A block rule (`annotationDef`) that recognizes annotation definitions of
|
|
88
|
+
* the form `[+label]: content` and stores them in `env.annotations`.
|
|
89
|
+
* Definitions support multi-line content via indented continuation lines,
|
|
90
|
+
* and multiple definitions for the same label are accumulated.
|
|
91
|
+
* - An inline rule (`annotationRef`) that recognizes annotation references of
|
|
92
|
+
* the form `[+label]` and pushes an `annotation_ref` token to be rendered
|
|
93
|
+
* as a `<VPAnnotation>` component.
|
|
94
|
+
*
|
|
95
|
+
* 注册两条规则:
|
|
96
|
+
* - 块规则(`annotationDef`)识别 `[+标签]: 内容` 形式的注释定义,并将其
|
|
97
|
+
* 存储到 `env.annotations` 中。定义通过缩进续行支持多行内容,同一标签的
|
|
98
|
+
* 多次定义会被累加。
|
|
99
|
+
* - 行内规则(`annotationRef`)识别 `[+标签]` 形式的注释引用,并推入
|
|
100
|
+
* `annotation_ref` 令牌,最终渲染为 `<VPAnnotation>` 组件。
|
|
101
|
+
*
|
|
102
|
+
* Definition syntax: `[+label]: annotation content`
|
|
103
|
+
* Reference syntax: `[+label]`
|
|
104
|
+
*
|
|
105
|
+
* 定义语法:`[+标签]: 注释内容`
|
|
106
|
+
* 引用语法:`[+标签]`
|
|
107
|
+
*
|
|
108
|
+
* @param md - Markdown-it instance / Markdown-it 实例
|
|
109
|
+
* @param globalAnnotations - Global annotations preset, a map of label to
|
|
110
|
+
* annotation content (string or string array) / 全局注释预设,标签到
|
|
111
|
+
* 注释内容(字符串或字符串数组)的映射
|
|
112
|
+
*/
|
|
113
|
+
const annotationMarkdownPlugin = (md, globalAnnotations = {}) => {
|
|
114
|
+
const annotations = objectMap(globalAnnotations, (key, value) => {
|
|
115
|
+
return [key.startsWith(":") ? key : `:${key}`, {
|
|
116
|
+
sources: toArray(value),
|
|
117
|
+
rendered: []
|
|
118
|
+
}];
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* Custom renderer for the `annotation_ref` token.
|
|
122
|
+
*
|
|
123
|
+
* `annotation_ref` 令牌的自定义渲染器。
|
|
124
|
+
*
|
|
125
|
+
* Renders the annotation reference as a `<VPAnnotation>` component tag. Each
|
|
126
|
+
* annotation source is rendered as block-level markdown and placed into a
|
|
127
|
+
* named slot (`#item-0`, `#item-1`, …). Rendered results are cached on the
|
|
128
|
+
* environment so that repeated references to the same label reuse the
|
|
129
|
+
* rendered output without re-rendering. The environment passed to the nested
|
|
130
|
+
* render is cleaned via `cleanMarkdownEnv` to strip `references` and
|
|
131
|
+
* `annotations`, preventing recursive annotation processing inside annotation
|
|
132
|
+
* content.
|
|
133
|
+
*
|
|
134
|
+
* 将注释引用渲染为 `<VPAnnotation>` 组件标签。每个注释源作为块级 markdown
|
|
135
|
+
* 渲染,并放入具名插槽(`#item-0`、`#item-1`、…)。渲染结果缓存在环境对象上,
|
|
136
|
+
* 使得对同一标签的重复引用可复用已渲染的结果而无需重复渲染。传递给嵌套渲染的
|
|
137
|
+
* 环境通过 `cleanMarkdownEnv` 清理,去除 `references` 和 `annotations`,
|
|
138
|
+
* 防止在注释内容中递归处理注释。
|
|
139
|
+
*
|
|
140
|
+
* When a label is not found in the inline (env) annotations, the renderer
|
|
141
|
+
* falls back to the global annotations preset.
|
|
142
|
+
*
|
|
143
|
+
* 当标签在内联(环境)注释中未找到时,渲染器回退到全局注释预设。
|
|
144
|
+
*
|
|
145
|
+
* @param tokens - Token array / token 数组
|
|
146
|
+
* @param idx - Current token index / 当前 token 索引
|
|
147
|
+
* @param _ - Render options (unused) / 渲染选项(未使用)
|
|
148
|
+
* @param env - Render environment containing annotations / 包含注释的渲染环境
|
|
149
|
+
* @returns Rendered HTML string / 渲染后的 HTML 字符串
|
|
150
|
+
*/
|
|
151
|
+
md.renderer.rules.annotation_ref = (tokens, idx, _, env) => {
|
|
152
|
+
const label = tokens[idx].meta.label;
|
|
153
|
+
/* istanbul ignore next -- @preserve */
|
|
154
|
+
const data = env.annotations[`:${label}`] || annotations[`:${label}`];
|
|
155
|
+
return `<VPAnnotation label="${md.utils.escapeHtml(label)}" :total="${data.sources.length}">${data.sources.map((source, i) => {
|
|
156
|
+
return `<template #item-${i}>${data.rendered[i] ??= md.render(source, cleanMarkdownEnv(env, ["references", "annotations"]))}</template>`;
|
|
157
|
+
}).join("\n")}</VPAnnotation>`;
|
|
158
|
+
};
|
|
159
|
+
md.inline.ruler.before("image", "annotation_ref", annotationRef);
|
|
160
|
+
md.block.ruler.before("reference", "annotation", annotationDef, { alt: ["paragraph", "reference"] });
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/node/plugin.ts
|
|
164
|
+
/**
|
|
165
|
+
* VitePress plugin for annotation support in markdown content.
|
|
166
|
+
*
|
|
167
|
+
* VitePress 插件,用于在 markdown 内容中支持注释功能。
|
|
168
|
+
*
|
|
169
|
+
* The plugin registers annotation definition and reference rules in markdown-it,
|
|
170
|
+
* and wires up the client-side `VPAnnotation` component via `componentResolver`
|
|
171
|
+
* so that annotation references render as interactive markers that, on click,
|
|
172
|
+
* reveal a popover with the annotation content.
|
|
173
|
+
*
|
|
174
|
+
* 该插件在 markdown-it 中注册注释定义与引用规则,并通过 `componentResolver`
|
|
175
|
+
* 接入客户端 `VPAnnotation` 组件,使注释引用渲染为可交互的标记,点击后弹出
|
|
176
|
+
* 包含注释内容的浮层。
|
|
177
|
+
*
|
|
178
|
+
* Annotations can be defined inline in markdown using the syntax
|
|
179
|
+
* `[+label]: annotation content`, or provided globally through the `annotations`
|
|
180
|
+
* option. A single label can be defined multiple times — all definitions are
|
|
181
|
+
* collected and displayed as separate items in the popover. Annotation content
|
|
182
|
+
* supports full block-level markdown.
|
|
183
|
+
*
|
|
184
|
+
* 注释可以在 markdown 中通过 `[+标签]: 注释内容` 语法内联定义,也可以通过
|
|
185
|
+
* `annotations` 选项全局提供。同一标签可多次定义 —— 所有定义会被收集并作为
|
|
186
|
+
* 独立条目展示在浮层中。注释内容支持完整的块级 markdown 语法。
|
|
187
|
+
*
|
|
188
|
+
* @param annotations - Global annotations preset, a map of label to annotation
|
|
189
|
+
* content (string or string array) / 全局注释预设,标签到注释内容
|
|
190
|
+
* (字符串或字符串数组)的映射
|
|
191
|
+
* @example
|
|
192
|
+
* `.vitepress/config.ts` without global annotations
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { defineConfig } from 'vitepress-tuck'
|
|
195
|
+
* import annotation from 'vitepress-plugin-annotation'
|
|
196
|
+
*
|
|
197
|
+
* export default defineConfig({
|
|
198
|
+
* plugins: [annotation()],
|
|
199
|
+
* })
|
|
200
|
+
* ```
|
|
201
|
+
* @example
|
|
202
|
+
* `.vitepress/config.ts` with global annotations
|
|
203
|
+
* ```ts
|
|
204
|
+
* import { defineConfig } from 'vitepress-tuck'
|
|
205
|
+
* import annotation from 'vitepress-plugin-annotation'
|
|
206
|
+
*
|
|
207
|
+
* export default defineConfig({
|
|
208
|
+
* plugins: [
|
|
209
|
+
* annotation({
|
|
210
|
+
* HTML: 'HyperText Markup Language',
|
|
211
|
+
* }),
|
|
212
|
+
* ],
|
|
213
|
+
* })
|
|
214
|
+
* ```
|
|
215
|
+
* @example
|
|
216
|
+
* // Markdown usage
|
|
217
|
+
* ```md
|
|
218
|
+
* The **four great classical novels** [+novels] of Chinese literature.
|
|
219
|
+
*
|
|
220
|
+
* [+novels]:
|
|
221
|
+
* **Romance of the Three Kingdoms** — a historical novel.
|
|
222
|
+
*
|
|
223
|
+
* [+novels]:
|
|
224
|
+
* **Journey to the West** — a mythological adventure.
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
const annotation = definePlugin((annotations) => ({
|
|
228
|
+
name: "vitepress-plugin-annotation",
|
|
229
|
+
componentResolver: ["VPAnnotation"],
|
|
230
|
+
markdown: { config(md) {
|
|
231
|
+
md.use(annotationMarkdownPlugin, annotations);
|
|
232
|
+
} }
|
|
233
|
+
}));
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/node/index.ts
|
|
236
|
+
var node_default = annotation;
|
|
237
|
+
//#endregion
|
|
238
|
+
export { annotation, annotationMarkdownPlugin, node_default as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vitepress-plugin-annotation",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.7.0",
|
|
5
|
+
"description": "Add annotation support to VitePress",
|
|
6
|
+
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://tuck.pengzhanbo.cn/plugins/annotation",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/pengzhanbo/vitepress-tuck.git",
|
|
12
|
+
"directory": "packages/plugin-annotation"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/pengzhanbo/vitepress-tuck/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"vitepress",
|
|
19
|
+
"vitepress-plugin",
|
|
20
|
+
"annotation"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/node/index.js",
|
|
24
|
+
"./client": {
|
|
25
|
+
"browser": "./dist/client/browser/index.js",
|
|
26
|
+
"default": "./dist/client/ssr/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./style.css": "./dist/client/style.css"
|
|
29
|
+
},
|
|
30
|
+
"module": "./dist/node/index.js",
|
|
31
|
+
"types": "./dist/node/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"vitepress": "^1.6.4 || ^2.0.0-alpha.17",
|
|
37
|
+
"vue": "^3.5.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@floating-ui/vue": "^2.0.0",
|
|
41
|
+
"@pengzhanbo/utils": "^3.7.3",
|
|
42
|
+
"@vueuse/core": "^14.3.0",
|
|
43
|
+
"vitepress-tuck": "0.7.0",
|
|
44
|
+
"vitepress-plugin-toolkit": "0.7.0"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"clean": "rimraf --glob ./dist",
|
|
51
|
+
"dev": "pnpm '/(tsdown|copy):watch/'",
|
|
52
|
+
"build": "pnpm tsdown && pnpm copy",
|
|
53
|
+
"copy": "cpx \"src/**/*.css\" dist",
|
|
54
|
+
"copy:watch": "pnpm copy -w",
|
|
55
|
+
"tsdown": "tsdown --config-loader unrun",
|
|
56
|
+
"tsdown:watch": "pnpm tsdown -w"
|
|
57
|
+
}
|
|
58
|
+
}
|