vitepress-plugin-plantuml 0.2.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 +141 -0
- package/assets/fallback.png +0 -0
- package/assets/fallback.svg +1 -0
- package/dist/client/browser/index.d.ts +24 -0
- package/dist/client/browser/index.js +94 -0
- package/dist/client/ssr/index.d.ts +24 -0
- package/dist/client/ssr/index.js +75 -0
- package/dist/client/style.css +176 -0
- package/dist/node/index.d.ts +35 -0
- package/dist/node/index.js +336 -0
- package/package.json +56 -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,141 @@
|
|
|
1
|
+
# vitepress-plugin-plantuml
|
|
2
|
+
|
|
3
|
+
Render PlantUML diagrams in your VitePress site.
|
|
4
|
+
|
|
5
|
+
在 VitePress 中渲染 PlantUML 图表。
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### With Vitepress-tuck
|
|
10
|
+
|
|
11
|
+
**Installation:**
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# npm
|
|
15
|
+
npm install -D vitepress-tuck vitepress-plugin-plantuml
|
|
16
|
+
# pnpm
|
|
17
|
+
pnpm add -D vitepress-tuck vitepress-plugin-plantuml
|
|
18
|
+
# yarn
|
|
19
|
+
yarn add -D vitepress-tuck vitepress-plugin-plantuml
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Configuration:**
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// .vitepress/config.ts
|
|
26
|
+
import plantuml from 'vitepress-plugin-plantuml'
|
|
27
|
+
import { defineConfig } from 'vitepress-tuck'
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
plugins: [plantuml()],
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// .vitepress/theme/index.ts
|
|
36
|
+
import type { Theme } from 'vitepress'
|
|
37
|
+
import enhanceApp from 'virtual:enhance-app'
|
|
38
|
+
import DefaultTheme from 'vitepress/theme'
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
extends: DefaultTheme,
|
|
42
|
+
enhanceApp(ctx) {
|
|
43
|
+
enhanceApp(ctx)
|
|
44
|
+
},
|
|
45
|
+
} satisfies Theme
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### With Vitepress
|
|
49
|
+
|
|
50
|
+
**Installation:**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# npm
|
|
54
|
+
npm install -D vitepress-plugin-plantuml
|
|
55
|
+
# pnpm
|
|
56
|
+
pnpm add -D vitepress-plugin-plantuml
|
|
57
|
+
# yarn
|
|
58
|
+
yarn add -D vitepress-plugin-plantuml
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Configuration:**
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// .vitepress/config.ts
|
|
65
|
+
import { defineConfig } from 'vitepress'
|
|
66
|
+
import { plantumlMarkdownPlugin, plantumlVitePlugin } from 'vitepress-plugin-plantuml'
|
|
67
|
+
|
|
68
|
+
export default defineConfig({
|
|
69
|
+
markdown: {
|
|
70
|
+
config: (md) => {
|
|
71
|
+
md.use(plantumlMarkdownPlugin)
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
vite: {
|
|
75
|
+
plugins: [plantumlVitePlugin()],
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// .vitepress/theme/index.ts
|
|
82
|
+
import type { Theme } from 'vitepress'
|
|
83
|
+
import { enhanceAppWithPlantuml } from 'vitepress-plugin-plantuml/client'
|
|
84
|
+
import DefaultTheme from 'vitepress/theme'
|
|
85
|
+
|
|
86
|
+
export default {
|
|
87
|
+
extends: DefaultTheme,
|
|
88
|
+
enhanceApp(ctx) {
|
|
89
|
+
enhanceAppWithPlantuml(ctx)
|
|
90
|
+
},
|
|
91
|
+
} satisfies Theme
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Syntax
|
|
95
|
+
|
|
96
|
+
Use `plantuml` code blocks to render diagrams.
|
|
97
|
+
|
|
98
|
+
````md
|
|
99
|
+
```plantuml
|
|
100
|
+
@startuml
|
|
101
|
+
Alice -> Bob: Authentication Request
|
|
102
|
+
Bob --> Alice: Authentication Response
|
|
103
|
+
|
|
104
|
+
Alice -> Bob: Another authentication Request
|
|
105
|
+
Alice <-- Bob: Another authentication Response
|
|
106
|
+
@enduml
|
|
107
|
+
```
|
|
108
|
+
````
|
|
109
|
+
|
|
110
|
+
### Output Format
|
|
111
|
+
|
|
112
|
+
You can specify the output format (`svg` or `png`) for individual diagrams:
|
|
113
|
+
|
|
114
|
+
````md
|
|
115
|
+
```plantuml png
|
|
116
|
+
@startuml
|
|
117
|
+
class Example {
|
|
118
|
+
+attribute: string
|
|
119
|
+
+method(): void
|
|
120
|
+
}
|
|
121
|
+
@enduml
|
|
122
|
+
```
|
|
123
|
+
````
|
|
124
|
+
|
|
125
|
+
Or set a global default when configuring the plugin:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
plantuml('png') // default is 'svg'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Supported formats: `svg`, `png`
|
|
132
|
+
|
|
133
|
+
## Features
|
|
134
|
+
|
|
135
|
+
- **Dark / Light mode** — Automatically generates both dark and light variants of diagrams, matching the current VitePress theme.
|
|
136
|
+
- **Chart / Source tabs** — Toggle between the rendered diagram and its source code.
|
|
137
|
+
- **Fullscreen mode** — Click the fullscreen button to view the diagram in a full-screen overlay.
|
|
138
|
+
- **Download** — Download the current diagram as an image file.
|
|
139
|
+
- **Multi-language** — Built-in support for English, Chinese, Japanese, Korean, Spanish, French, Russian, German, and Portuguese.
|
|
140
|
+
- **SVG optimization** — SVGs are automatically optimized via SVGO for cleaner output.
|
|
141
|
+
- **Caching** — Rendered diagrams are cached to disk for fast rebuilds.
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200" viewBox="0 0 400 200"><rect width="100%" height="100%" fill="#f8f9fa" rx="8" ry="8"/><circle cx="200" cy="70" r="24" fill="#ffc107" stroke="#e5a800" stroke-width="2"/><text x="200" y="78" text-anchor="middle" font-size="28" font-family="Arial, sans-serif" fill="#5a3e00" font-weight="bold">!</text><text x="200" y="130" text-anchor="middle" font-size="18" font-family="Arial, Helvetica, sans-serif" fill="#333" font-weight="500">Load Fail</text><text x="200" y="155" text-anchor="middle" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="#666">Please try again</text></svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EnhanceAppContext } from "vitepress";
|
|
2
|
+
|
|
3
|
+
//#region src/client/VPPlantUML.vue.d.ts
|
|
4
|
+
declare var __VLS_6: {}, __VLS_8: {};
|
|
5
|
+
type __VLS_Slots = {} & {
|
|
6
|
+
default?: (props: typeof __VLS_6) => any;
|
|
7
|
+
} & {
|
|
8
|
+
source?: (props: typeof __VLS_8) => any;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, 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
|
+
declare function enhanceAppWithPlantuml({
|
|
21
|
+
app
|
|
22
|
+
}: EnhanceAppContext): void;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { _default as VPPlantUML, enhanceAppWithPlantuml };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import "virtual:tuck-icons.css";
|
|
2
|
+
import "../style.css";
|
|
3
|
+
import { computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, isRef, nextTick, normalizeClass, normalizeStyle, onMounted, openBlock, ref, renderSlot, toDisplayString, unref, useTemplateRef, vShow, watch, withDirectives } from "vue";
|
|
4
|
+
import { useFullscreen } from "@vueuse/core";
|
|
5
|
+
import { VPTabSwitch, useZoomAndDrag } from "vitepress-plugin-toolkit/client";
|
|
6
|
+
import { useData } from "vitepress/client";
|
|
7
|
+
import { locales } from "virtual:vitepress-plantuml";
|
|
8
|
+
//#region src/client/composables/locales.ts
|
|
9
|
+
function useLocale() {
|
|
10
|
+
const { localeIndex } = useData();
|
|
11
|
+
return computed(() => locales[localeIndex.value] || locales.root || {});
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/client/composables/tabs.ts
|
|
15
|
+
function useTabs() {
|
|
16
|
+
const locale = useLocale();
|
|
17
|
+
return {
|
|
18
|
+
tab: ref("chart"),
|
|
19
|
+
tabs: computed(() => [{
|
|
20
|
+
label: locale.value.chart,
|
|
21
|
+
value: "chart"
|
|
22
|
+
}, {
|
|
23
|
+
label: locale.value.source,
|
|
24
|
+
value: "source"
|
|
25
|
+
}])
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/client/VPPlantUML.vue
|
|
30
|
+
const _hoisted_1 = { class: "vp-plantuml" };
|
|
31
|
+
const _hoisted_2 = { class: "plantuml-header" };
|
|
32
|
+
const _hoisted_3 = { class: "plantuml-actions" };
|
|
33
|
+
const _hoisted_4 = { class: "plantuml-source" };
|
|
34
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
35
|
+
__name: "VPPlantUML",
|
|
36
|
+
setup(__props) {
|
|
37
|
+
const { isDark } = useData();
|
|
38
|
+
const { tab, tabs } = useTabs();
|
|
39
|
+
const locale = useLocale();
|
|
40
|
+
const el = useTemplateRef("el");
|
|
41
|
+
const { actorStyle, reset, zoom, zoomIn, zoomOut, resetZoom } = useZoomAndDrag(el);
|
|
42
|
+
const { isFullscreen, isSupported, enter } = useFullscreen(el);
|
|
43
|
+
watch(isFullscreen, (newVal) => reset(newVal));
|
|
44
|
+
onMounted(() => watch(isDark, () => {
|
|
45
|
+
const img = el.value?.querySelector(isDark.value ? ".dark" : ".light");
|
|
46
|
+
if (!img) return;
|
|
47
|
+
img.onload = () => nextTick(reset);
|
|
48
|
+
}, { immediate: true }));
|
|
49
|
+
function download() {
|
|
50
|
+
const img = el.value?.querySelector(isDark.value ? ".dark" : ".light");
|
|
51
|
+
if (!img) return;
|
|
52
|
+
const url = img.src;
|
|
53
|
+
const a = document.createElement("a");
|
|
54
|
+
a.href = url;
|
|
55
|
+
a.download = "";
|
|
56
|
+
a.click();
|
|
57
|
+
a.remove();
|
|
58
|
+
}
|
|
59
|
+
return (_ctx, _cache) => {
|
|
60
|
+
return openBlock(), createElementBlock("div", _hoisted_1, [
|
|
61
|
+
createElementVNode("div", _hoisted_2, [createVNode(unref(VPTabSwitch), {
|
|
62
|
+
modelValue: unref(tab),
|
|
63
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => isRef(tab) ? tab.value = $event : null),
|
|
64
|
+
items: unref(tabs)
|
|
65
|
+
}, null, 8, ["modelValue", "items"]), createElementVNode("div", _hoisted_3, [createElementVNode("button", { onClick: download }, [_cache[5] || (_cache[5] = createElementVNode("span", { class: "vpi-download" }, null, -1)), createTextVNode(" " + toDisplayString(unref(locale).download), 1)]), unref(isSupported) ? (openBlock(), createElementBlock("button", {
|
|
66
|
+
key: 0,
|
|
67
|
+
class: "fullscreen",
|
|
68
|
+
onClick: _cache[1] || (_cache[1] = (...args) => unref(enter) && unref(enter)(...args))
|
|
69
|
+
}, [_cache[6] || (_cache[6] = createElementVNode("span", { class: "vpi-fullscreen" }, null, -1)), createTextVNode(" " + toDisplayString(unref(locale).fullscreen), 1)])) : createCommentVNode("v-if", true)])]),
|
|
70
|
+
withDirectives(createElementVNode("div", {
|
|
71
|
+
ref_key: "el",
|
|
72
|
+
ref: el,
|
|
73
|
+
class: "plantuml-view"
|
|
74
|
+
}, [createElementVNode("div", {
|
|
75
|
+
class: "content",
|
|
76
|
+
style: normalizeStyle(unref(actorStyle))
|
|
77
|
+
}, [renderSlot(_ctx.$slots, "default")], 4), createElementVNode("div", { class: normalizeClass(["plantuml-zoom", { fullscreen: unref(isFullscreen) }]) }, [
|
|
78
|
+
createElementVNode("button", { onClick: _cache[2] || (_cache[2] = (...args) => unref(zoomOut) && unref(zoomOut)(...args)) }, [..._cache[7] || (_cache[7] = [createElementVNode("span", { class: "vpi-zoom-out" }, null, -1)])]),
|
|
79
|
+
createElementVNode("span", null, toDisplayString(unref(zoom)), 1),
|
|
80
|
+
createElementVNode("button", { onClick: _cache[3] || (_cache[3] = (...args) => unref(zoomIn) && unref(zoomIn)(...args)) }, [..._cache[8] || (_cache[8] = [createElementVNode("span", { class: "vpi-zoom-in" }, null, -1)])]),
|
|
81
|
+
createElementVNode("button", { onClick: _cache[4] || (_cache[4] = (...args) => unref(resetZoom) && unref(resetZoom)(...args)) }, [..._cache[9] || (_cache[9] = [createElementVNode("span", { class: "vpi-zoom-reset" }, null, -1)])])
|
|
82
|
+
], 2)], 512), [[vShow, unref(tab) === "chart"]]),
|
|
83
|
+
withDirectives(createElementVNode("div", _hoisted_4, [renderSlot(_ctx.$slots, "source")], 512), [[vShow, unref(tab) === "source"]])
|
|
84
|
+
]);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/client/index.ts
|
|
90
|
+
function enhanceAppWithPlantuml({ app }) {
|
|
91
|
+
app.component("VPPlantUML", _sfc_main);
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { _sfc_main as VPPlantUML, enhanceAppWithPlantuml };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EnhanceAppContext } from "vitepress";
|
|
2
|
+
|
|
3
|
+
//#region src/client/VPPlantUML.vue.d.ts
|
|
4
|
+
declare var __VLS_6: {}, __VLS_8: {};
|
|
5
|
+
type __VLS_Slots = {} & {
|
|
6
|
+
default?: (props: typeof __VLS_6) => any;
|
|
7
|
+
} & {
|
|
8
|
+
source?: (props: typeof __VLS_8) => any;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, 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
|
+
declare function enhanceAppWithPlantuml({
|
|
21
|
+
app
|
|
22
|
+
}: EnhanceAppContext): void;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { _default as VPPlantUML, enhanceAppWithPlantuml };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { computed, defineComponent, isRef, mergeProps, nextTick, onMounted, ref, unref, useSSRContext, useTemplateRef, watch } from "vue";
|
|
2
|
+
import { ssrInterpolate, ssrRenderAttrs, ssrRenderClass, ssrRenderComponent, ssrRenderSlot, ssrRenderStyle } from "vue/server-renderer";
|
|
3
|
+
import { useFullscreen } from "@vueuse/core";
|
|
4
|
+
import { VPTabSwitch, useZoomAndDrag } from "vitepress-plugin-toolkit/client";
|
|
5
|
+
import { useData } from "vitepress/client";
|
|
6
|
+
import { locales } from "virtual:vitepress-plantuml";
|
|
7
|
+
//#region src/client/composables/locales.ts
|
|
8
|
+
function useLocale() {
|
|
9
|
+
const { localeIndex } = useData();
|
|
10
|
+
return computed(() => locales[localeIndex.value] || locales.root || {});
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/client/composables/tabs.ts
|
|
14
|
+
function useTabs() {
|
|
15
|
+
const locale = useLocale();
|
|
16
|
+
return {
|
|
17
|
+
tab: ref("chart"),
|
|
18
|
+
tabs: computed(() => [{
|
|
19
|
+
label: locale.value.chart,
|
|
20
|
+
value: "chart"
|
|
21
|
+
}, {
|
|
22
|
+
label: locale.value.source,
|
|
23
|
+
value: "source"
|
|
24
|
+
}])
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/client/VPPlantUML.vue
|
|
29
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
30
|
+
__name: "VPPlantUML",
|
|
31
|
+
__ssrInlineRender: true,
|
|
32
|
+
setup(__props) {
|
|
33
|
+
const { isDark } = useData();
|
|
34
|
+
const { tab, tabs } = useTabs();
|
|
35
|
+
const locale = useLocale();
|
|
36
|
+
const el = useTemplateRef("el");
|
|
37
|
+
const { actorStyle, reset, zoom, zoomIn, zoomOut, resetZoom } = useZoomAndDrag(el);
|
|
38
|
+
const { isFullscreen, isSupported, enter } = useFullscreen(el);
|
|
39
|
+
watch(isFullscreen, (newVal) => reset(newVal));
|
|
40
|
+
onMounted(() => watch(isDark, () => {
|
|
41
|
+
const img = el.value?.querySelector(isDark.value ? ".dark" : ".light");
|
|
42
|
+
if (!img) return;
|
|
43
|
+
img.onload = () => nextTick(reset);
|
|
44
|
+
}, { immediate: true }));
|
|
45
|
+
return (_ctx, _push, _parent, _attrs) => {
|
|
46
|
+
_push(`<div${ssrRenderAttrs(mergeProps({ class: "vp-plantuml" }, _attrs))}><div class="plantuml-header">`);
|
|
47
|
+
_push(ssrRenderComponent(unref(VPTabSwitch), {
|
|
48
|
+
modelValue: unref(tab),
|
|
49
|
+
"onUpdate:modelValue": ($event) => isRef(tab) ? tab.value = $event : null,
|
|
50
|
+
items: unref(tabs)
|
|
51
|
+
}, null, _parent));
|
|
52
|
+
_push(`<div class="plantuml-actions"><button><span class="vpi-download"></span> ${ssrInterpolate(unref(locale).download)}</button>`);
|
|
53
|
+
if (unref(isSupported)) _push(`<button class="fullscreen"><span class="vpi-fullscreen"></span> ${ssrInterpolate(unref(locale).fullscreen)}</button>`);
|
|
54
|
+
else _push(`<!---->`);
|
|
55
|
+
_push(`</div></div><div class="plantuml-view" style="${ssrRenderStyle(unref(tab) === "chart" ? null : { display: "none" })}"><div class="content" style="${ssrRenderStyle(unref(actorStyle))}">`);
|
|
56
|
+
ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent);
|
|
57
|
+
_push(`</div><div class="${ssrRenderClass([{ fullscreen: unref(isFullscreen) }, "plantuml-zoom"])}"><button><span class="vpi-zoom-out"></span></button><span>${ssrInterpolate(unref(zoom))}</span><button><span class="vpi-zoom-in"></span></button><button><span class="vpi-zoom-reset"></span></button></div></div><div class="plantuml-source" style="${ssrRenderStyle(unref(tab) === "source" ? null : { display: "none" })}">`);
|
|
58
|
+
ssrRenderSlot(_ctx.$slots, "source", {}, null, _push, _parent);
|
|
59
|
+
_push(`</div></div>`);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const _sfc_setup = _sfc_main.setup;
|
|
64
|
+
_sfc_main.setup = (props, ctx) => {
|
|
65
|
+
const ssrContext = useSSRContext();
|
|
66
|
+
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/VPPlantUML.vue");
|
|
67
|
+
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/client/index.ts
|
|
71
|
+
function enhanceAppWithPlantuml({ app }) {
|
|
72
|
+
app.component("VPPlantUML", _sfc_main);
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { _sfc_main as VPPlantUML, enhanceAppWithPlantuml };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
@import url("vitepress-plugin-toolkit/styles/tab-switch.css");
|
|
2
|
+
|
|
3
|
+
.vp-plantuml {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
margin-block: 16px;
|
|
7
|
+
margin-inline: -24px;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
background: var(--vp-code-block-bg);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media (min-width: 640px) {
|
|
13
|
+
.vp-plantuml {
|
|
14
|
+
margin-inline: 0;
|
|
15
|
+
border-radius: 8px;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html:not(.dark) .vp-plantuml img.dark,
|
|
20
|
+
html.dark .vp-plantuml img.light {
|
|
21
|
+
display: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.vp-plantuml .plantuml-header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: flex-start;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
padding-block: 4px;
|
|
29
|
+
padding-inline: 8px;
|
|
30
|
+
background: var(--vp-code-block-bg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.vp-plantuml .plantuml-actions {
|
|
34
|
+
display: flex;
|
|
35
|
+
gap: 4px;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
opacity: 1;
|
|
39
|
+
transition: opacity 0.25s ease-in-out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media (min-width: 640px) {
|
|
43
|
+
.vp-plantuml .plantuml-actions {
|
|
44
|
+
gap: 8px;
|
|
45
|
+
opacity: 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.vp-plantuml:hover .plantuml-actions {
|
|
50
|
+
opacity: 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.vp-plantuml .plantuml-actions button {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
min-width: 20px;
|
|
58
|
+
height: 20px;
|
|
59
|
+
padding-inline: 2px;
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
background-color: transparent;
|
|
62
|
+
border-radius: 12px;
|
|
63
|
+
transition: background-color 0.25s ease-in-out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.vp-plantuml .plantuml-actions button:hover {
|
|
67
|
+
background-color: var(--vp-c-gray-soft);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@media (min-width: 640px) {
|
|
71
|
+
.vp-plantuml .plantuml-actions button {
|
|
72
|
+
gap: 2px;
|
|
73
|
+
min-width: 24px;
|
|
74
|
+
height: 24px;
|
|
75
|
+
padding-inline: 4px;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@media (max-width: 639px) {
|
|
81
|
+
.vp-plantuml .plantuml-actions button.fullscreen {
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.vp-plantuml .plantuml-view {
|
|
87
|
+
position: relative;
|
|
88
|
+
max-width: 100%;
|
|
89
|
+
min-height: 150px;
|
|
90
|
+
max-height: 50vh;
|
|
91
|
+
padding-block: 12px;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
text-align: center;
|
|
94
|
+
background: var(--vp-code-block-bg);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (min-width: 640px) {
|
|
98
|
+
.vp-plantuml .plantuml-view {
|
|
99
|
+
max-height: calc(100vh - 58px - var(--vp-nav-height) - var(--vp-layout-top-height, 0px));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.vp-plantuml .plantuml-view .content {
|
|
104
|
+
position: absolute;
|
|
105
|
+
width: max-content;
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
will-change: width, height, top, left;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.vp-plantuml .plantuml-view .content.zooming {
|
|
111
|
+
transition: 0.25s ease-in-out;
|
|
112
|
+
transition-property: width, height, top, left;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.vp-plantuml .plantuml-view .content img {
|
|
116
|
+
width: 100%;
|
|
117
|
+
height: 100%;
|
|
118
|
+
margin-block: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.vp-plantuml .plantuml-zoom {
|
|
122
|
+
position: absolute;
|
|
123
|
+
right: 8px;
|
|
124
|
+
bottom: 8px;
|
|
125
|
+
display: flex;
|
|
126
|
+
gap: 4px;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
padding-block: 4px;
|
|
130
|
+
padding-inline: 8px;
|
|
131
|
+
background-color: var(--vp-c-bg);
|
|
132
|
+
border-radius: 4px;
|
|
133
|
+
box-shadow: var(--vp-shadow-3);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.vp-plantuml .plantuml-zoom.fullscreen {
|
|
137
|
+
right: 50%;
|
|
138
|
+
bottom: 24px;
|
|
139
|
+
font-size: 18px;
|
|
140
|
+
transform: translateX(50%);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.vp-plantuml .plantuml-zoom > :where(button, span) {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
min-width: 20px;
|
|
148
|
+
height: 20px;
|
|
149
|
+
padding-inline: 2px;
|
|
150
|
+
font-size: inherit;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.vp-plantuml .plantuml-zoom > span {
|
|
154
|
+
width: 48px;
|
|
155
|
+
text-align: right;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.vp-plantuml .plantuml-zoom.fullscreen > span {
|
|
159
|
+
width: 64px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.vp-plantuml .plantuml-source div[class*="language-"] {
|
|
163
|
+
margin: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
.vp-plantuml [class*="vpi-"] {
|
|
168
|
+
display: inline-block;
|
|
169
|
+
width: 1em;
|
|
170
|
+
height: 1em;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.vp-plantuml .vpi-download {
|
|
174
|
+
width: 1.1em;
|
|
175
|
+
height: 1.1em;
|
|
176
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PluginWithOptions } from "markdown-it";
|
|
2
|
+
import { Plugin } from "vitepress";
|
|
3
|
+
|
|
4
|
+
//#region src/node/plugin.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { plantuml } from 'vitepress-plugin-plantuml'
|
|
9
|
+
* import { defineConfig } from 'vitepress-tuck'
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* plugins: [plantuml()],
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
declare const plantuml: (options?: "svg" | "png" | undefined) => import("vitepress-tuck").VitepressPlugin;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/node/types.d.ts
|
|
19
|
+
type PlantumlFormat = 'svg' | 'png';
|
|
20
|
+
interface PlantumlLocaleData extends Record<string, unknown> {
|
|
21
|
+
chart: string;
|
|
22
|
+
source: string;
|
|
23
|
+
fullscreen: string;
|
|
24
|
+
download: string;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/node/markdown.d.ts
|
|
28
|
+
declare const plantumlMarkdownPlugin: PluginWithOptions<PlantumlFormat>;
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/node/vite.d.ts
|
|
31
|
+
declare function plantumlVitePlugin(options?: {
|
|
32
|
+
locales?: Record<string, PlantumlLocaleData>;
|
|
33
|
+
}): Plugin[];
|
|
34
|
+
//#endregion
|
|
35
|
+
export { plantuml as default, plantuml, plantumlMarkdownPlugin, plantumlVitePlugin };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { definePlugin } from "vitepress-tuck";
|
|
2
|
+
import ansis from "ansis";
|
|
3
|
+
import { createLocales, genHash, getVitepressConfig, iconPlugin, isBuild } from "vitepress-plugin-toolkit";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { deflateRawSync } from "node:zlib";
|
|
8
|
+
import { LRUCache, attemptAsync, combineURLs, indent, remove } from "@pengzhanbo/utils";
|
|
9
|
+
import fs, { createReadStream } from "node:fs";
|
|
10
|
+
import { optimize } from "svgo";
|
|
11
|
+
//#region src/node/constants.ts
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.join(path.dirname(__filename), "../");
|
|
14
|
+
const OUTPUT_DIR = "plantuml";
|
|
15
|
+
const SERVER_PREFIX = "/vitepress-plantuml/";
|
|
16
|
+
const plantumlUrl = "https://www.plantuml.com/plantuml";
|
|
17
|
+
const fallbackSVG = path.join(__dirname, "../assets", "fallback.svg");
|
|
18
|
+
const fallbackPNG = path.join(__dirname, "../assets", "fallback.png");
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/node/utils.ts
|
|
21
|
+
const cache = new LRUCache({ maxSize: 1024 });
|
|
22
|
+
function getFilename(hash, format, isDark) {
|
|
23
|
+
return `${hash}.${isDark ? "dark" : "light"}.${format}`;
|
|
24
|
+
}
|
|
25
|
+
function getOutputPath(dir, filename) {
|
|
26
|
+
return combineURLs(dir, OUTPUT_DIR, filename);
|
|
27
|
+
}
|
|
28
|
+
function parseFilename(filename) {
|
|
29
|
+
const [hash, isDark, format] = filename.split(".");
|
|
30
|
+
return {
|
|
31
|
+
hash,
|
|
32
|
+
format,
|
|
33
|
+
isDark: isDark === "dark"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function deflate(data) {
|
|
37
|
+
return deflateRawSync(Buffer.from(data, "utf-8"), { level: 9 }).toString("binary");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @param byte - 6-bit byte value
|
|
41
|
+
* @returns Encoded character
|
|
42
|
+
* @see https://plantuml.com/en/text-encoding
|
|
43
|
+
*
|
|
44
|
+
* PlantUML uses a custom Base64 encoding scheme for text data.
|
|
45
|
+
*/
|
|
46
|
+
function encode6bit(byte) {
|
|
47
|
+
return byte < 10 ? String.fromCharCode(48 + byte) : byte < 36 ? String.fromCharCode(65 + byte - 10) : byte < 62 ? String.fromCharCode(97 + byte - 36) : byte === 62 ? "-" : "_";
|
|
48
|
+
}
|
|
49
|
+
function append3bytes(b1, b2, b3) {
|
|
50
|
+
const c1 = b1 >> 2;
|
|
51
|
+
const c2 = (b1 & 3) << 4 | b2 >> 4;
|
|
52
|
+
const c3 = (b2 & 15) << 2 | b3 >> 6;
|
|
53
|
+
const c4 = b3 & 63;
|
|
54
|
+
return encode6bit(c1 & 63) + encode6bit(c2 & 63) + encode6bit(c3 & 63) + encode6bit(c4 & 63);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Custom Base64 encoding for PlantUML
|
|
58
|
+
*
|
|
59
|
+
* Mapping: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_
|
|
60
|
+
*
|
|
61
|
+
* @param data The input string to encode
|
|
62
|
+
* @returns The Base64 encoded string
|
|
63
|
+
*/
|
|
64
|
+
function customEncodeBase64(data) {
|
|
65
|
+
let result = "";
|
|
66
|
+
for (let i = 0; i < data.length; i += 3) if (i + 2 === data.length) result += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0);
|
|
67
|
+
else if (i + 1 === data.length) result += append3bytes(data.charCodeAt(i), 0, 0);
|
|
68
|
+
else result += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2));
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function encodePlantuml(data) {
|
|
72
|
+
return customEncodeBase64(deflate(data));
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/node/markdown.ts
|
|
76
|
+
const plantumlMarkdownPlugin = (md, format = "svg") => {
|
|
77
|
+
const config = getVitepressConfig();
|
|
78
|
+
const cacheDir = config.cacheDir;
|
|
79
|
+
const name = "plantuml";
|
|
80
|
+
const supportedFormats = ["svg", "png"];
|
|
81
|
+
const rawFence = md.renderer.rules.fence;
|
|
82
|
+
md.renderer.rules.fence = (...args) => {
|
|
83
|
+
const [tokens, idx, _, env] = args;
|
|
84
|
+
const { content, info } = tokens[idx];
|
|
85
|
+
const code = rawFence(...args);
|
|
86
|
+
if (!info.trim().startsWith(name)) return code;
|
|
87
|
+
const resolvedFormat = info.trim().slice(8).trim() || format;
|
|
88
|
+
if (resolvedFormat && !supportedFormats.includes(resolvedFormat)) {
|
|
89
|
+
config.logger.warn(`PlantUML format ${ansis.red(resolvedFormat)} is not supported, please use ${ansis.green(supportedFormats.join(", "))}`);
|
|
90
|
+
return code;
|
|
91
|
+
}
|
|
92
|
+
const hash = genHash(content, 12);
|
|
93
|
+
const light = getFilename(hash, resolvedFormat, false);
|
|
94
|
+
const dark = getFilename(hash, resolvedFormat, true);
|
|
95
|
+
let cached = cache.get(light);
|
|
96
|
+
if (!cached) cache.set(light, cached = {
|
|
97
|
+
content,
|
|
98
|
+
paths: /* @__PURE__ */ new Set()
|
|
99
|
+
});
|
|
100
|
+
cached.paths.add(env.path);
|
|
101
|
+
cached = cache.get(dark);
|
|
102
|
+
if (!cached) cache.set(dark, cached = {
|
|
103
|
+
content,
|
|
104
|
+
paths: /* @__PURE__ */ new Set()
|
|
105
|
+
});
|
|
106
|
+
cached.paths.add(env.path);
|
|
107
|
+
return `<VPPlantUML><img ${isBuild ? `src="${getOutputPath(cacheDir, light)}"` : `:src="\`${SERVER_PREFIX}${light}\`"`} alt="PlantUML light" class="light"><img ${isBuild ? `src="${getOutputPath(cacheDir, dark)}"` : `:src="\`${SERVER_PREFIX}${dark}\`"`} alt="PlantUML dark" class="dark"><template #source>${code}</template></VPPlantUML>`;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/node/locales.ts
|
|
112
|
+
const builtinLocales = [
|
|
113
|
+
[["en", "en-US"], {
|
|
114
|
+
chart: "Chart",
|
|
115
|
+
source: "Code",
|
|
116
|
+
fullscreen: "Fullscreen",
|
|
117
|
+
download: "Download"
|
|
118
|
+
}],
|
|
119
|
+
[["zh", "zh-CN"], {
|
|
120
|
+
chart: "图表",
|
|
121
|
+
source: "代码",
|
|
122
|
+
fullscreen: "全屏",
|
|
123
|
+
download: "下载"
|
|
124
|
+
}],
|
|
125
|
+
[["ja", "ja-JP"], {
|
|
126
|
+
chart: "グラフ",
|
|
127
|
+
source: "コード",
|
|
128
|
+
fullscreen: "全画面",
|
|
129
|
+
download: "ダウンロード"
|
|
130
|
+
}],
|
|
131
|
+
[["ko", "ko-KR"], {
|
|
132
|
+
chart: "차트",
|
|
133
|
+
source: "코드",
|
|
134
|
+
fullscreen: "전체화면",
|
|
135
|
+
download: "다운로드"
|
|
136
|
+
}],
|
|
137
|
+
[["es", "es-ES"], {
|
|
138
|
+
chart: "Gráfico",
|
|
139
|
+
source: "Código",
|
|
140
|
+
fullscreen: "Pantalla completa",
|
|
141
|
+
download: "Descargar"
|
|
142
|
+
}],
|
|
143
|
+
[["fr", "fr-FR"], {
|
|
144
|
+
chart: "Graphique",
|
|
145
|
+
source: "Code",
|
|
146
|
+
fullscreen: "Écran plein",
|
|
147
|
+
download: "Télécharger"
|
|
148
|
+
}],
|
|
149
|
+
[["ru", "ru-RU"], {
|
|
150
|
+
chart: "График",
|
|
151
|
+
source: "Код",
|
|
152
|
+
fullscreen: "Полный экран",
|
|
153
|
+
download: "Скачать"
|
|
154
|
+
}],
|
|
155
|
+
[["de", "de-DE"], {
|
|
156
|
+
chart: "Diagramm",
|
|
157
|
+
source: "Code",
|
|
158
|
+
fullscreen: "Vollbild",
|
|
159
|
+
download: "Herunterladen"
|
|
160
|
+
}],
|
|
161
|
+
[["pt", "pt-BR"], {
|
|
162
|
+
chart: "Gráfico",
|
|
163
|
+
source: "Código",
|
|
164
|
+
fullscreen: "Tela cheia",
|
|
165
|
+
download: "Baixar"
|
|
166
|
+
}]
|
|
167
|
+
];
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/node/vite.ts
|
|
170
|
+
function plantumlVitePlugin(options = {}) {
|
|
171
|
+
const moduleId = "virtual:vitepress-plantuml";
|
|
172
|
+
const resolveId = `\0${moduleId}`;
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
name: "vitepress:plantuml-locales",
|
|
176
|
+
resolveId(id) {
|
|
177
|
+
if (id === moduleId) return resolveId;
|
|
178
|
+
},
|
|
179
|
+
load(id) {
|
|
180
|
+
if (id === resolveId) return `export const locales = ${JSON.stringify(createLocales(builtinLocales, options.locales))}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
iconPlugin([{
|
|
185
|
+
name: "download",
|
|
186
|
+
svg: `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' d='m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z'/%3E%3C/svg%3E")`
|
|
187
|
+
}, {
|
|
188
|
+
name: "fullscreen",
|
|
189
|
+
svg: `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' d='M3 21v-5h2v3h3v2zm13 0v-2h3v-3h2v5zM3 8V3h5v2H5v3zm16 0V5h-3V3h5v5z'/%3E%3C/svg%3E")`
|
|
190
|
+
}]),
|
|
191
|
+
isBuild ? plantumlVitePluginWithBuild() : plantumlVitePluginWithServer()
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
function plantumlVitePluginWithServer() {
|
|
195
|
+
let config;
|
|
196
|
+
return {
|
|
197
|
+
name: "vitepress:plantuml",
|
|
198
|
+
configResolved(_config) {
|
|
199
|
+
config = _config;
|
|
200
|
+
fs.mkdirSync(path.join(config.cacheDir, OUTPUT_DIR), { recursive: true });
|
|
201
|
+
},
|
|
202
|
+
configureServer(server) {
|
|
203
|
+
server.middlewares.use(async (req, res, next) => {
|
|
204
|
+
const url = req.url;
|
|
205
|
+
if (!url.startsWith("/vitepress-plantuml/")) return next();
|
|
206
|
+
const filename = url.slice(20);
|
|
207
|
+
const { format, isDark } = parseFilename(filename);
|
|
208
|
+
const cached = cache.get(filename);
|
|
209
|
+
if (!cached) return next();
|
|
210
|
+
const type = format === "svg" ? "image/svg+xml" : `image/${format}`;
|
|
211
|
+
const outputPath = getOutputPath(config.cacheDir, filename);
|
|
212
|
+
if (fs.existsSync(outputPath)) {
|
|
213
|
+
res.setHeader("Content-Type", type);
|
|
214
|
+
return createReadStream(outputPath).pipe(res);
|
|
215
|
+
}
|
|
216
|
+
const [, buffer] = await (cached.promise ?? attemptAsync(async () => {
|
|
217
|
+
const buffer = await fetchPlantuml(cached.content, (isDark ? "d" : "") + format);
|
|
218
|
+
buffer && await fs.promises.writeFile(outputPath, buffer);
|
|
219
|
+
return buffer;
|
|
220
|
+
}));
|
|
221
|
+
if (!buffer) {
|
|
222
|
+
config.logger.error(`Failed to render: \n${indent(cached.content, " ".repeat(4))}`);
|
|
223
|
+
cached.promise = null;
|
|
224
|
+
return next();
|
|
225
|
+
}
|
|
226
|
+
res.setHeader("Content-Type", type);
|
|
227
|
+
return createReadStream(outputPath).pipe(res);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function plantumlVitePluginWithBuild() {
|
|
233
|
+
let config;
|
|
234
|
+
return {
|
|
235
|
+
name: "vitepress:plantuml",
|
|
236
|
+
enforce: "post",
|
|
237
|
+
configResolved(_config) {
|
|
238
|
+
config = _config;
|
|
239
|
+
fs.mkdirSync(path.join(config.cacheDir, OUTPUT_DIR), { recursive: true });
|
|
240
|
+
},
|
|
241
|
+
transform: {
|
|
242
|
+
filter: { id: /\.md($|\?.*)/ },
|
|
243
|
+
async handler(code, id) {
|
|
244
|
+
const pagePath = id.split("?")[0];
|
|
245
|
+
const tasks = [];
|
|
246
|
+
for (const [filename, cached] of cache.entries()) {
|
|
247
|
+
if (cached.loaded && cached.paths.has(pagePath)) continue;
|
|
248
|
+
const outputPath = getOutputPath(config.cacheDir, filename);
|
|
249
|
+
if (fs.existsSync(outputPath)) {
|
|
250
|
+
cached.loaded = true;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const { isDark, format } = parseFilename(filename);
|
|
254
|
+
const promise = cached.promise ?? attemptAsync(async () => {
|
|
255
|
+
const buffer = await fetchPlantuml(cached.content, (isDark ? "d" : "") + format);
|
|
256
|
+
if (!buffer) {
|
|
257
|
+
config.logger.error(`Failed to render: \n${indent(cached.content, " ")}`);
|
|
258
|
+
await fs.promises.copyFile(format === "png" ? fallbackPNG : fallbackSVG, outputPath);
|
|
259
|
+
cached.promise = null;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await fs.promises.writeFile(outputPath, buffer);
|
|
263
|
+
cached.loaded = true;
|
|
264
|
+
});
|
|
265
|
+
cached.promise = promise;
|
|
266
|
+
tasks.push(promise);
|
|
267
|
+
}
|
|
268
|
+
await Promise.all(tasks);
|
|
269
|
+
return code;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const RE_PLANTUML_TAG = /<\?plantuml.*?\?>/g;
|
|
275
|
+
async function fetchPlantuml(source, format) {
|
|
276
|
+
const url = `${plantumlUrl}/${format}/${encodePlantuml(source)}`;
|
|
277
|
+
const res = await fetch(url);
|
|
278
|
+
if (!res.ok) return null;
|
|
279
|
+
if (format === "svg" || format === "dsvg") {
|
|
280
|
+
const data = svgo(await res.text());
|
|
281
|
+
return Buffer.from(data);
|
|
282
|
+
}
|
|
283
|
+
return Buffer.from(await res.arrayBuffer());
|
|
284
|
+
}
|
|
285
|
+
function svgo(svg) {
|
|
286
|
+
return optimize(svg, {
|
|
287
|
+
multipass: true,
|
|
288
|
+
plugins: [{
|
|
289
|
+
name: "removeRootStyle",
|
|
290
|
+
fn: () => {
|
|
291
|
+
let width;
|
|
292
|
+
let height;
|
|
293
|
+
let background;
|
|
294
|
+
return { element: { enter(element, parent) {
|
|
295
|
+
const attrs = element.attributes;
|
|
296
|
+
if (element.name === "svg") {
|
|
297
|
+
width = `${Number.parseInt(attrs.width)}`;
|
|
298
|
+
height = `${Number.parseInt(attrs.height)}`;
|
|
299
|
+
background = attrs.style?.split(";").filter((item) => item.trim().startsWith("background"))[0]?.split(":")[1]?.trim() || "";
|
|
300
|
+
delete attrs.style;
|
|
301
|
+
}
|
|
302
|
+
if (element.name === "rect" && attrs.fill === background && attrs.width === width && attrs.height === height && attrs.x === "0" && attrs.y === "0") remove(parent.children, element);
|
|
303
|
+
} } };
|
|
304
|
+
}
|
|
305
|
+
}, "preset-default"]
|
|
306
|
+
}).data.replaceAll(RE_PLANTUML_TAG, "");
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/node/plugin.ts
|
|
310
|
+
/**
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* import { plantuml } from 'vitepress-plugin-plantuml'
|
|
314
|
+
* import { defineConfig } from 'vitepress-tuck'
|
|
315
|
+
*
|
|
316
|
+
* export default defineConfig({
|
|
317
|
+
* plugins: [plantuml()],
|
|
318
|
+
* })
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
const plantuml = definePlugin((format) => ({
|
|
322
|
+
name: "vitepress-plugin-plantuml",
|
|
323
|
+
client: { enhance: "enhanceAppWithPlantuml" },
|
|
324
|
+
markdown: {
|
|
325
|
+
config(md) {
|
|
326
|
+
md.use(plantumlMarkdownPlugin, format);
|
|
327
|
+
},
|
|
328
|
+
languageAlias: { plantuml: "txt" }
|
|
329
|
+
},
|
|
330
|
+
vite: { plugins: [plantumlVitePlugin()] }
|
|
331
|
+
}));
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/node/index.ts
|
|
334
|
+
var node_default = plantuml;
|
|
335
|
+
//#endregion
|
|
336
|
+
export { node_default as default, plantuml, plantumlMarkdownPlugin, plantumlVitePlugin };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vitepress-plugin-plantuml",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Render PlantUML diagrams in your VitePress site.",
|
|
6
|
+
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/pengzhanbo/vitepress-tuck.git",
|
|
11
|
+
"directory": "packages/plugin-plantuml"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"vitepress",
|
|
15
|
+
"vitepress-plugin",
|
|
16
|
+
"plantuml"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/node/index.js",
|
|
20
|
+
"./client": {
|
|
21
|
+
"browser": "./dist/client/browser/index.js",
|
|
22
|
+
"default": "./dist/client/ssr/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./style.css": "./dist/client/style.css"
|
|
25
|
+
},
|
|
26
|
+
"module": "./dist/node/index.js",
|
|
27
|
+
"types": "./dist/node/index.d.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"assets",
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"vitepress": "^1.6.4 || ^2.0.0-alpha.17",
|
|
34
|
+
"vue": "^3.5.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@pengzhanbo/utils": "^3.7.3",
|
|
38
|
+
"@vueuse/core": "^14.3.0",
|
|
39
|
+
"ansis": "^4.3.1",
|
|
40
|
+
"svgo": "^4.0.1",
|
|
41
|
+
"vitepress-plugin-toolkit": "0.2.0",
|
|
42
|
+
"vitepress-tuck": "0.2.0"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"clean": "rimraf --glob ./dist",
|
|
49
|
+
"dev": "pnpm '/(tsdown|copy):watch/'",
|
|
50
|
+
"build": "pnpm tsdown && pnpm copy",
|
|
51
|
+
"copy": "cpx \"src/**/*.css\" dist",
|
|
52
|
+
"copy:watch": "pnpm copy -w",
|
|
53
|
+
"tsdown": "tsdown --config-loader unrun",
|
|
54
|
+
"tsdown:watch": "pnpm tsdown -w"
|
|
55
|
+
}
|
|
56
|
+
}
|