@docfuse/plugins 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/README.zh-CN.md +52 -0
  4. package/dist/client/kroki.js +221 -0
  5. package/dist/client/mermaid.js +366 -0
  6. package/dist/client/plantuml.js +221 -0
  7. package/dist/diagram.css +195 -0
  8. package/dist/external-links.d.ts +10 -0
  9. package/dist/external-links.js +49 -0
  10. package/dist/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  11. package/dist/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  12. package/dist/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  13. package/dist/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  14. package/dist/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  15. package/dist/fonts/KaTeX_Main-Bold.woff2 +0 -0
  16. package/dist/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  17. package/dist/fonts/KaTeX_Main-Italic.woff2 +0 -0
  18. package/dist/fonts/KaTeX_Main-Regular.woff2 +0 -0
  19. package/dist/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  20. package/dist/fonts/KaTeX_Math-Italic.woff2 +0 -0
  21. package/dist/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  22. package/dist/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  23. package/dist/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  24. package/dist/fonts/KaTeX_Script-Regular.woff2 +0 -0
  25. package/dist/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  26. package/dist/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  27. package/dist/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  28. package/dist/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  29. package/dist/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  30. package/dist/index.d.ts +9 -0
  31. package/dist/index.js +757 -0
  32. package/dist/kroki.d.ts +12 -0
  33. package/dist/kroki.js +293 -0
  34. package/dist/link-card.d.ts +9 -0
  35. package/dist/link-card.js +87 -0
  36. package/dist/math.css +3 -0
  37. package/dist/math.d.ts +19 -0
  38. package/dist/math.js +140 -0
  39. package/dist/mermaid.d.ts +10 -0
  40. package/dist/mermaid.js +290 -0
  41. package/dist/pagefind.d.ts +66 -0
  42. package/dist/pagefind.js +84 -0
  43. package/dist/plantuml.d.ts +10 -0
  44. package/dist/plantuml.js +297 -0
  45. package/dist/reading-time.d.ts +20 -0
  46. package/dist/reading-time.js +90 -0
  47. package/package.json +122 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Docfuse Contributors
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,52 @@
1
+ # @docfuse/plugins
2
+
3
+ English | [简体中文](./README.zh-CN.md)
4
+
5
+ `@docfuse/plugins` contains the official Markdown plugins and search providers for Docfuse. All factories share one package version; normal site configuration imports them from the package root.
6
+
7
+ ## Install for a Docfuse site
8
+
9
+ Install the package as a development dependency:
10
+
11
+ ```bash
12
+ pnpm add -D @docfuse/plugins
13
+ ```
14
+
15
+ `mermaid` and `pagefind` are optional peers. Install either one only when its capability is enabled:
16
+
17
+ ```bash
18
+ pnpm add -D mermaid pagefind
19
+ ```
20
+
21
+ ```ts
22
+ import { externalLinks, math, mermaid, pagefind } from '@docfuse/plugins'
23
+ import { defineConfig } from 'docfuse'
24
+
25
+ export default defineConfig({
26
+ search: { provider: pagefind() },
27
+ markdown: {
28
+ plugins: [math(), mermaid(), externalLinks()]
29
+ }
30
+ })
31
+ ```
32
+
33
+ ## Available factories
34
+
35
+ | Factory | Contract | Purpose |
36
+ |---|---|---|
37
+ | `externalLinks(options?)` | Markdown plugin | Adds safe attributes to external HTTP(S) links |
38
+ | `readingTime(options?)` | Markdown plugin | Adds localized reading time |
39
+ | `linkCard(options?)` | Markdown plugin | Converts a standalone link into a link card |
40
+ | `kroki(options?)` | Markdown plugin | Renders Graphviz, D2, and other Kroki languages |
41
+ | `math(options?)` | Markdown plugin | Renders formulas with remark-math and KaTeX |
42
+ | `mermaid(options?)` | Markdown plugin | Renders Mermaid fences in the browser |
43
+ | `plantUml(options?)` | Markdown plugin | Renders PlantUML when a trusted server is configured |
44
+ | `pagefind(options?)` | Search provider | Builds a Pagefind index from final static HTML |
45
+
46
+ When using the plugins directly with `@docfuse/markdown` in a React application, install them as regular application dependencies. A React host that enables `math()` must also import `@docfuse/plugins/math.css`.
47
+
48
+ Package-root and focused factory entries such as `@docfuse/plugins/math` are public. Browser and CSS entries are consumed by generated sites and should not be treated as plugin factories.
49
+
50
+ See the [official plugin guide](https://docfuse.dev/en/guide/site/plugins/) for options, examples, lifecycle differences, and verification steps.
51
+
52
+ License: MIT
@@ -0,0 +1,52 @@
1
+ # @docfuse/plugins
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ `@docfuse/plugins` 提供 Docfuse 官方维护的 Markdown 插件和搜索 Provider。所有工厂共用一个包版本,常规站点配置从包根导入。
6
+
7
+ ## 在 Docfuse 站点中安装
8
+
9
+ 把插件包安装为开发依赖:
10
+
11
+ ```bash
12
+ pnpm add -D @docfuse/plugins
13
+ ```
14
+
15
+ `mermaid` 和 `pagefind` 是可选 Peer,只在启用对应能力时安装:
16
+
17
+ ```bash
18
+ pnpm add -D mermaid pagefind
19
+ ```
20
+
21
+ ```ts
22
+ import { externalLinks, math, mermaid, pagefind } from '@docfuse/plugins'
23
+ import { defineConfig } from 'docfuse'
24
+
25
+ export default defineConfig({
26
+ search: { provider: pagefind() },
27
+ markdown: {
28
+ plugins: [math(), mermaid(), externalLinks()]
29
+ }
30
+ })
31
+ ```
32
+
33
+ ## 可用工厂
34
+
35
+ | 工厂 | 契约 | 作用 |
36
+ |---|---|---|
37
+ | `externalLinks(options?)` | Markdown 插件 | 为站外 HTTP(S) 链接添加安全属性 |
38
+ | `readingTime(options?)` | Markdown 插件 | 添加本地化阅读时长 |
39
+ | `linkCard(options?)` | Markdown 插件 | 把独占段落的链接转换为链接卡片 |
40
+ | `kroki(options?)` | Markdown 插件 | 渲染 Graphviz、D2 和其他 Kroki 图表 |
41
+ | `math(options?)` | Markdown 插件 | 使用 remark-math 和 KaTeX 渲染公式 |
42
+ | `mermaid(options?)` | Markdown 插件 | 在浏览器中渲染 Mermaid 围栏 |
43
+ | `plantUml(options?)` | Markdown 插件 | 配置可信服务后渲染 PlantUML |
44
+ | `pagefind(options?)` | Search Provider | 根据最终静态 HTML 生成 Pagefind 索引 |
45
+
46
+ 在 React 应用中直接配合 `@docfuse/markdown` 使用时,应把插件安装为普通应用依赖。启用 `math()` 的 React 宿主还需要导入 `@docfuse/plugins/math.css`。
47
+
48
+ 包根入口和 `@docfuse/plugins/math` 等工厂子路径属于公共 API。浏览器与 CSS 入口由生成站点消费,不是插件工厂。
49
+
50
+ 选项、完整示例、生命周期区别和验证方法见[官方插件指南](https://docfuse.dev/guide/site/plugins/)。
51
+
52
+ 许可证:MIT
@@ -0,0 +1,221 @@
1
+ // src/client/shared.ts
2
+ var ICONS = {
3
+ copy: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
4
+ check: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg>',
5
+ source: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 8-4 4 4 4M18 8l4 4-4 4M14.5 4l-5 16"/></svg>',
6
+ expand: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>',
7
+ "zoom-out": '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg>',
8
+ "zoom-reset": '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>',
9
+ "zoom-in": '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>'
10
+ };
11
+ var DIAGRAM_SCALE_MIN = 0.5;
12
+ var DIAGRAM_SCALE_MAX = 2;
13
+ var DIAGRAM_SCALE_STEP = 0.25;
14
+ async function copyText(value) {
15
+ if (navigator.clipboard) {
16
+ try {
17
+ await navigator.clipboard.writeText(value);
18
+ return true;
19
+ } catch {
20
+ }
21
+ }
22
+ if (typeof document.execCommand !== "function") return false;
23
+ const textarea = document.createElement("textarea");
24
+ textarea.value = value;
25
+ textarea.setAttribute("readonly", "");
26
+ textarea.style.position = "fixed";
27
+ textarea.style.opacity = "0";
28
+ document.body.append(textarea);
29
+ textarea.select();
30
+ try {
31
+ return document.execCommand("copy");
32
+ } finally {
33
+ textarea.remove();
34
+ }
35
+ }
36
+ function setupActions(figure) {
37
+ const controller = new AbortController();
38
+ const copyTimers = /* @__PURE__ */ new Map();
39
+ const dialogs = /* @__PURE__ */ new Set();
40
+ const source = figure.dataset.dfSource ?? "";
41
+ const preview = figure.querySelector(".df-diagram-preview");
42
+ const sourcePanel = figure.querySelector(".df-diagram-source");
43
+ const zoomControls = figure.querySelector(".df-diagram-zoom-controls");
44
+ const buttons = figure.querySelectorAll("[data-df-diagram-action]");
45
+ const previewHidden = preview?.hidden;
46
+ const sourcePanelHidden = sourcePanel?.hidden;
47
+ const zoomControlsHidden = zoomControls?.hidden;
48
+ const previewZoomWidth = preview?.style.getPropertyValue("--df-diagram-zoom-width") ?? "";
49
+ const previewZoomMaxWidth = preview?.style.getPropertyValue("--df-diagram-zoom-max-width") ?? "";
50
+ const previewScale = preview?.getAttribute("data-df-scale");
51
+ let scale = 1;
52
+ const buttonStates = [...buttons].map((button) => ({
53
+ button,
54
+ innerHTML: button.innerHTML,
55
+ ariaLabel: button.getAttribute("aria-label"),
56
+ copied: button.getAttribute("data-copied"),
57
+ actionError: button.getAttribute("data-df-action-error"),
58
+ disabled: button.disabled
59
+ }));
60
+ const updateScale = () => {
61
+ if (!preview) return;
62
+ preview.style.setProperty("--df-diagram-zoom-width", `${scale * 100}%`);
63
+ preview.style.setProperty("--df-diagram-zoom-max-width", `${56.25 * scale}rem`);
64
+ preview.dataset.dfScale = String(scale);
65
+ for (const button of buttons) {
66
+ const action = button.dataset.dfDiagramAction;
67
+ if (action === "zoom-out") button.disabled = scale <= DIAGRAM_SCALE_MIN;
68
+ if (action === "zoom-in") button.disabled = scale >= DIAGRAM_SCALE_MAX;
69
+ if (action === "zoom-reset") button.disabled = scale === 1;
70
+ }
71
+ };
72
+ if (zoomControls) updateScale();
73
+ for (const button of buttons) {
74
+ const action = button.dataset.dfDiagramAction;
75
+ button.innerHTML = ICONS[action] ?? "";
76
+ button.addEventListener(
77
+ "click",
78
+ async () => {
79
+ try {
80
+ if (action === "copy") {
81
+ if (!await copyText(source)) throw new Error("Copy command failed");
82
+ button.innerHTML = ICONS.check;
83
+ button.dataset.copied = "true";
84
+ const activeTimer = copyTimers.get(button);
85
+ if (activeTimer !== void 0) window.clearTimeout(activeTimer);
86
+ const timer = window.setTimeout(() => {
87
+ if (copyTimers.get(button) !== timer) return;
88
+ copyTimers.delete(button);
89
+ button.innerHTML = ICONS.copy;
90
+ delete button.dataset.copied;
91
+ }, 1500);
92
+ copyTimers.set(button, timer);
93
+ } else if (action === "source" && preview && sourcePanel) {
94
+ const showingSource = !sourcePanel.hidden;
95
+ sourcePanel.hidden = showingSource;
96
+ preview.hidden = !showingSource;
97
+ if (zoomControls) zoomControls.hidden = !showingSource;
98
+ button.setAttribute(
99
+ "aria-label",
100
+ showingSource ? buttonStates.find((state) => state.button === button)?.ariaLabel ?? "Show source" : button.dataset.dfDiagramPreviewLabel ?? "Show preview"
101
+ );
102
+ } else if (action === "expand" && preview) {
103
+ const dialog = document.createElement("dialog");
104
+ dialogs.add(dialog);
105
+ dialog.className = "df-diagram-dialog";
106
+ const close = document.createElement("button");
107
+ close.type = "button";
108
+ close.className = "df-diagram-dialog-close";
109
+ close.setAttribute("aria-label", button.dataset.dfDiagramCloseLabel ?? "Close expanded diagram");
110
+ close.textContent = "\xD7";
111
+ close.addEventListener("click", () => dialog.close(), { signal: controller.signal });
112
+ const expandedPreview = preview.cloneNode(true);
113
+ expandedPreview.style.removeProperty("--df-diagram-zoom-width");
114
+ expandedPreview.style.removeProperty("--df-diagram-zoom-max-width");
115
+ delete expandedPreview.dataset.dfScale;
116
+ dialog.append(close, expandedPreview);
117
+ dialog.addEventListener(
118
+ "close",
119
+ () => {
120
+ dialogs.delete(dialog);
121
+ dialog.remove();
122
+ },
123
+ { signal: controller.signal }
124
+ );
125
+ document.body.append(dialog);
126
+ dialog.showModal();
127
+ } else if (action === "zoom-out") {
128
+ scale = Math.max(DIAGRAM_SCALE_MIN, scale - DIAGRAM_SCALE_STEP);
129
+ updateScale();
130
+ } else if (action === "zoom-reset") {
131
+ scale = 1;
132
+ updateScale();
133
+ } else if (action === "zoom-in") {
134
+ scale = Math.min(DIAGRAM_SCALE_MAX, scale + DIAGRAM_SCALE_STEP);
135
+ updateScale();
136
+ }
137
+ } catch (error) {
138
+ button.dataset.dfActionError = "true";
139
+ console.error("[docfuse] Diagram action failed:", error);
140
+ }
141
+ },
142
+ { signal: controller.signal }
143
+ );
144
+ }
145
+ return () => {
146
+ controller.abort();
147
+ copyTimers.forEach((timer) => window.clearTimeout(timer));
148
+ dialogs.forEach((dialog) => dialog.remove());
149
+ copyTimers.clear();
150
+ dialogs.clear();
151
+ if (preview && previewHidden !== void 0) preview.hidden = previewHidden;
152
+ if (sourcePanel && sourcePanelHidden !== void 0) sourcePanel.hidden = sourcePanelHidden;
153
+ if (zoomControls && zoomControlsHidden !== void 0) zoomControls.hidden = zoomControlsHidden;
154
+ if (preview) {
155
+ if (previewZoomWidth) preview.style.setProperty("--df-diagram-zoom-width", previewZoomWidth);
156
+ else preview.style.removeProperty("--df-diagram-zoom-width");
157
+ if (previewZoomMaxWidth) preview.style.setProperty("--df-diagram-zoom-max-width", previewZoomMaxWidth);
158
+ else preview.style.removeProperty("--df-diagram-zoom-max-width");
159
+ if (previewScale == null) preview.removeAttribute("data-df-scale");
160
+ else preview.setAttribute("data-df-scale", previewScale);
161
+ }
162
+ buttonStates.forEach(({ button, innerHTML, ariaLabel, copied, actionError, disabled }) => {
163
+ button.innerHTML = innerHTML;
164
+ button.disabled = disabled;
165
+ if (ariaLabel === null) button.removeAttribute("aria-label");
166
+ else button.setAttribute("aria-label", ariaLabel);
167
+ if (copied === null) button.removeAttribute("data-copied");
168
+ else button.setAttribute("data-copied", copied);
169
+ if (actionError === null) button.removeAttribute("data-df-action-error");
170
+ else button.setAttribute("data-df-action-error", actionError);
171
+ });
172
+ };
173
+ }
174
+ function enhanceDiagrams(root, kind, render) {
175
+ let active = true;
176
+ let disposed = false;
177
+ const figures = Array.from(root.querySelectorAll(`[data-df-plugin-diagram="${kind}"]`)).filter(
178
+ (figure) => figure.dataset.dfEnhanced !== "true"
179
+ );
180
+ const renderErrorStates = figures.map(
181
+ (figure) => [figure, figure.getAttribute("data-df-render-error")]
182
+ );
183
+ const cleanups = [];
184
+ const renders = [];
185
+ for (const figure of figures) {
186
+ figure.dataset.dfEnhanced = "true";
187
+ cleanups.push(setupActions(figure));
188
+ if (render) {
189
+ renders.push(
190
+ render(figure).catch((error) => {
191
+ if (!active) return;
192
+ figure.dataset.dfRenderError = "true";
193
+ console.error(`[docfuse] ${kind} render failed:`, error);
194
+ })
195
+ );
196
+ }
197
+ }
198
+ const dispose = () => {
199
+ if (disposed) return;
200
+ disposed = true;
201
+ active = false;
202
+ cleanups.reverse().forEach((cleanup) => cleanup());
203
+ renderErrorStates.forEach(([figure, renderError]) => {
204
+ delete figure.dataset.dfEnhanced;
205
+ if (renderError === null) figure.removeAttribute("data-df-render-error");
206
+ else figure.setAttribute("data-df-render-error", renderError);
207
+ });
208
+ };
209
+ return Object.assign(dispose, {
210
+ ready: Promise.all(renders).then(() => void 0),
211
+ dispose
212
+ });
213
+ }
214
+
215
+ // src/client/kroki.ts
216
+ function enhance(root = document) {
217
+ return enhanceDiagrams(root, "kroki");
218
+ }
219
+ export {
220
+ enhance
221
+ };