@barzhsieh/nuxt-content-mermaid 2.2.2 → 3.0.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/README.md +190 -90
- package/README.zh-TW.md +183 -99
- package/dist/module.d.mts +39 -53
- package/dist/module.json +3 -3
- package/dist/module.mjs +600 -155
- package/dist/runtime/built-in-renderer/BuiltInRenderer.d.vue.ts +0 -0
- package/dist/runtime/built-in-renderer/BuiltInRenderer.vue +597 -0
- package/dist/runtime/built-in-renderer/BuiltInRenderer.vue.d.ts +0 -0
- package/dist/runtime/component-configuration-error.d.ts +3 -0
- package/dist/runtime/component-configuration-error.js +7 -0
- package/dist/runtime/component-configuration.d.ts +23 -0
- package/dist/runtime/component-configuration.js +78 -0
- package/dist/runtime/components/ContentMermaidTransport.d.vue.ts +10 -0
- package/dist/runtime/components/ContentMermaidTransport.vue +21 -0
- package/dist/runtime/components/ContentMermaidTransport.vue.d.ts +10 -0
- package/dist/runtime/components/Mermaid.d.vue.ts +0 -31
- package/dist/runtime/components/Mermaid.vue +136 -743
- package/dist/runtime/components/Mermaid.vue.d.ts +0 -31
- package/dist/runtime/components/MermaidExpandOverlay.d.vue.ts +13 -16
- package/dist/runtime/components/MermaidExpandOverlay.vue +61 -53
- package/dist/runtime/components/MermaidExpandOverlay.vue.d.ts +13 -16
- package/dist/runtime/components/MermaidFullscreenPresentation.d.vue.ts +18 -0
- package/dist/runtime/components/MermaidFullscreenPresentation.vue +56 -0
- package/dist/runtime/components/MermaidFullscreenPresentation.vue.d.ts +18 -0
- package/dist/runtime/composables/useMermaidExpand.d.ts +16 -32
- package/dist/runtime/composables/useMermaidExpand.js +232 -75
- package/dist/runtime/composables/useMermaidFullscreen.d.ts +18 -0
- package/dist/runtime/composables/useMermaidFullscreen.js +242 -0
- package/dist/runtime/composables/useMermaidZoom.d.ts +5 -1
- package/dist/runtime/composables/useMermaidZoom.js +54 -28
- package/dist/runtime/configuration/core.d.ts +27 -0
- package/dist/runtime/configuration/core.js +340 -0
- package/dist/runtime/configuration/module.d.ts +17 -0
- package/dist/runtime/configuration/module.js +233 -0
- package/dist/runtime/configuration/runtime-options.d.ts +4 -0
- package/dist/runtime/configuration/runtime-options.js +57 -0
- package/dist/runtime/constants.d.ts +24 -0
- package/dist/runtime/constants.js +60 -2
- package/dist/runtime/direct-mermaid-config.d.ts +5 -0
- package/dist/runtime/direct-mermaid-config.js +216 -0
- package/dist/runtime/mermaid-config.d.ts +9 -5
- package/dist/runtime/mermaid-config.js +30 -19
- package/dist/runtime/mermaid-rendering.d.ts +28 -0
- package/dist/runtime/mermaid-rendering.js +140 -0
- package/dist/runtime/plugins/content-mermaid-transport.d.ts +2 -0
- package/dist/runtime/plugins/content-mermaid-transport.js +5 -0
- package/dist/runtime/plugins/mermaid.client.d.ts +0 -3
- package/dist/runtime/plugins/mermaid.client.js +14 -32
- package/dist/runtime/plugins/runtime-config.d.ts +2 -0
- package/dist/runtime/plugins/runtime-config.js +9 -0
- package/dist/runtime/rendererSelection.d.ts +43 -0
- package/dist/runtime/rendererSelection.js +36 -0
- package/dist/runtime/rendererSelectionOrchestration.d.ts +12 -0
- package/dist/runtime/rendererSelectionOrchestration.js +13 -0
- package/dist/runtime/runtime-snapshot.d.ts +3 -0
- package/dist/runtime/runtime-snapshot.js +26 -0
- package/dist/runtime/utils/index.d.ts +0 -4
- package/dist/runtime/utils/index.js +0 -37
- package/dist/types.d.mts +2 -2
- package/package.json +36 -32
- package/dist/runtime/utils/mermaid-transform.d.ts +0 -18
- package/dist/runtime/utils/mermaid-transform.js +0 -345
|
File without changes
|
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useNuxtApp } from "#app";
|
|
3
|
+
import {
|
|
4
|
+
ref,
|
|
5
|
+
onMounted,
|
|
6
|
+
onUnmounted,
|
|
7
|
+
nextTick,
|
|
8
|
+
computed,
|
|
9
|
+
watch,
|
|
10
|
+
shallowRef,
|
|
11
|
+
useTemplateRef
|
|
12
|
+
} from "vue";
|
|
13
|
+
import { defu } from "defu";
|
|
14
|
+
import { materializeMermaidConfigForInvocation, resolveMermaidTheme } from "../mermaid-config";
|
|
15
|
+
import { createMermaidRenderer } from "../mermaid-rendering";
|
|
16
|
+
import { getRuntimeMermaidSnapshot } from "../runtime-snapshot";
|
|
17
|
+
import { parseSizeToPx, isRecord } from "../utils";
|
|
18
|
+
import { useMermaidTheme } from "../composables/useMermaidTheme";
|
|
19
|
+
import { useMermaidCursors } from "../composables/useMermaidCursors";
|
|
20
|
+
import IconClipboard from "../components/icons/IconClipboard.vue";
|
|
21
|
+
import IconClipboardHover from "../components/icons/IconClipboardHover.vue";
|
|
22
|
+
import IconClipboardSuccess from "../components/icons/IconClipboardSuccess.vue";
|
|
23
|
+
import IconFullScreen from "../components/icons/iconFullScreen.vue";
|
|
24
|
+
import IconFullScreenExit from "../components/icons/iconFullScreenExit.vue";
|
|
25
|
+
import MermaidExpandOverlay from "../components/MermaidExpandOverlay.vue";
|
|
26
|
+
import IconExpand from "../components/icons/IconExpand.vue";
|
|
27
|
+
import IconCollapse from "../components/icons/IconCollapse.vue";
|
|
28
|
+
import MermaidFullscreenPresentation from "../components/MermaidFullscreenPresentation.vue";
|
|
29
|
+
const props = defineProps({
|
|
30
|
+
toolbar: { type: Object, required: false },
|
|
31
|
+
code: { type: String, required: false },
|
|
32
|
+
pageConfig: { type: null, required: false },
|
|
33
|
+
config: { type: null, required: false },
|
|
34
|
+
componentSource: { type: Object, required: true },
|
|
35
|
+
spinnerComponent: { type: null, required: true }
|
|
36
|
+
});
|
|
37
|
+
const componentSource = computed(() => props.componentSource);
|
|
38
|
+
const nuxtApp = useNuxtApp();
|
|
39
|
+
const contentMermaidOptions = getRuntimeMermaidSnapshot(nuxtApp);
|
|
40
|
+
const isEnabled = true;
|
|
41
|
+
const debug = contentMermaidOptions.debug || false;
|
|
42
|
+
const loaderOptions = contentMermaidOptions.loader || {};
|
|
43
|
+
const themeOptions = contentMermaidOptions.theme || {};
|
|
44
|
+
const componentOptions = contentMermaidOptions.components || {};
|
|
45
|
+
const expandOptions = contentMermaidOptions.expand;
|
|
46
|
+
const expandEnabled = expandOptions.enabled !== false;
|
|
47
|
+
const expandOpenOptions = expandOptions.invokeOpenOn || {};
|
|
48
|
+
const allowOpenDiagramClick = expandOpenOptions.diagramClick !== false;
|
|
49
|
+
const lazyOption = loaderOptions.lazy;
|
|
50
|
+
const isLazy = lazyOption !== false;
|
|
51
|
+
const lazyThreshold = typeof lazyOption === "object" && typeof lazyOption.threshold === "number" ? lazyOption.threshold : 0.1;
|
|
52
|
+
const baseMermaidInit = loaderOptions.init || {};
|
|
53
|
+
const lightTheme = themeOptions.light;
|
|
54
|
+
const darkTheme = themeOptions.dark;
|
|
55
|
+
const colorMode = nuxtApp.$colorMode;
|
|
56
|
+
const { $mermaid } = nuxtApp;
|
|
57
|
+
const { currentTheme: manualThemeMode } = useMermaidTheme();
|
|
58
|
+
const mermaidBlock = useTemplateRef("mermaidBlock");
|
|
59
|
+
const mermaidWrapper = useTemplateRef("mermaidWrapper");
|
|
60
|
+
const mermaidContainer = useTemplateRef("mermaidContainer");
|
|
61
|
+
const fullscreenPresentation = useTemplateRef("fullscreenPresentation");
|
|
62
|
+
const isFullscreenSupported = ref(false);
|
|
63
|
+
const isFullscreen = ref(false);
|
|
64
|
+
const handleFullscreenSupportedChange = (supported) => {
|
|
65
|
+
isFullscreenSupported.value = supported;
|
|
66
|
+
};
|
|
67
|
+
const handleFullscreenActiveChange = (active) => {
|
|
68
|
+
isFullscreen.value = active;
|
|
69
|
+
};
|
|
70
|
+
const toggleFullscreen = () => {
|
|
71
|
+
expandOverlay.value?.endForDiagramReplacement();
|
|
72
|
+
return fullscreenPresentation.value?.toggle();
|
|
73
|
+
};
|
|
74
|
+
const hasRenderStarted = ref(false);
|
|
75
|
+
const hasRenderedOnce = ref(false);
|
|
76
|
+
const isLoading = ref(false);
|
|
77
|
+
const hasError = ref(false);
|
|
78
|
+
const errorContent = shallowRef(null);
|
|
79
|
+
const decodedCode = computed(() => props.code ? decodeURIComponent(props.code) : "");
|
|
80
|
+
const mermaidDefinition = ref(decodedCode.value);
|
|
81
|
+
let observer = null;
|
|
82
|
+
const runtimeToolbarDefaults = contentMermaidOptions.toolbar;
|
|
83
|
+
const defaultToolbarTitle = runtimeToolbarDefaults?.title ?? "mermaid";
|
|
84
|
+
const defaultToolbarFontSize = runtimeToolbarDefaults?.fontSize ?? "14px";
|
|
85
|
+
const defaultFullscreenToolbarScale = runtimeToolbarDefaults?.fullscreenToolbarScale ?? 1.25;
|
|
86
|
+
const resolvedToolbar = computed(() => {
|
|
87
|
+
return defu({}, props.toolbar ?? {}, runtimeToolbarDefaults ?? {});
|
|
88
|
+
});
|
|
89
|
+
const toolbarButtons = computed(() => {
|
|
90
|
+
const buttons = resolvedToolbar.value.buttons;
|
|
91
|
+
return isRecord(buttons) ? buttons : {};
|
|
92
|
+
});
|
|
93
|
+
const toolbarTitle = computed(() => resolvedToolbar.value.title ?? defaultToolbarTitle);
|
|
94
|
+
const toolbarFontSize = computed(() => {
|
|
95
|
+
const value = resolvedToolbar.value.fontSize ?? defaultToolbarFontSize;
|
|
96
|
+
if (typeof value === "number")
|
|
97
|
+
return `${value}px`;
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed))
|
|
101
|
+
return `${trimmed}px`;
|
|
102
|
+
return trimmed;
|
|
103
|
+
}
|
|
104
|
+
return defaultToolbarFontSize;
|
|
105
|
+
});
|
|
106
|
+
const showCopyButton = computed(() => toolbarButtons.value.copy !== false);
|
|
107
|
+
const copySource = computed(() => decodedCode.value || mermaidDefinition.value || "");
|
|
108
|
+
const hasCopySource = computed(() => !!copySource.value);
|
|
109
|
+
const copyState = ref("idle");
|
|
110
|
+
const copyLabel = computed(() => {
|
|
111
|
+
if (copyState.value === "copied") return "Copied";
|
|
112
|
+
if (copyState.value === "error") return "Copy failed";
|
|
113
|
+
return "Copy";
|
|
114
|
+
});
|
|
115
|
+
let copyResetTimer = null;
|
|
116
|
+
const isCopyHovered = ref(false);
|
|
117
|
+
const copyIcon = computed(() => {
|
|
118
|
+
if (copyState.value === "copied") return IconClipboardSuccess;
|
|
119
|
+
if (isCopyHovered.value && hasCopySource.value) return IconClipboardHover;
|
|
120
|
+
return IconClipboard;
|
|
121
|
+
});
|
|
122
|
+
const showFullscreenButton = computed(() => toolbarButtons.value.fullscreen !== false && isFullscreenSupported.value);
|
|
123
|
+
const fullscreenLabel = computed(() => isFullscreen.value ? "Exit fullscreen" : "Enter fullscreen");
|
|
124
|
+
const fullscreenToolbarScale = computed(() => {
|
|
125
|
+
const raw = resolvedToolbar.value.fullscreenToolbarScale ?? defaultFullscreenToolbarScale;
|
|
126
|
+
if (typeof raw === "number" && Number.isFinite(raw))
|
|
127
|
+
return raw;
|
|
128
|
+
if (typeof raw === "string") {
|
|
129
|
+
const parsed = Number.parseFloat(raw);
|
|
130
|
+
if (Number.isFinite(parsed))
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
return defaultFullscreenToolbarScale;
|
|
134
|
+
});
|
|
135
|
+
const toolbarFontSizePx = computed(() => {
|
|
136
|
+
return parseSizeToPx(resolvedToolbar.value.fontSize ?? defaultToolbarFontSize, 14);
|
|
137
|
+
});
|
|
138
|
+
const fullscreenToolbarFontSize = computed(() => {
|
|
139
|
+
const scaled = toolbarFontSizePx.value * fullscreenToolbarScale.value;
|
|
140
|
+
return `${Math.round(scaled * 100) / 100}px`;
|
|
141
|
+
});
|
|
142
|
+
const effectiveToolbarFontSize = computed(() => {
|
|
143
|
+
return isFullscreen.value ? fullscreenToolbarFontSize.value : toolbarFontSize.value;
|
|
144
|
+
});
|
|
145
|
+
const effectiveToolbarFontSizePx = computed(() => {
|
|
146
|
+
const baseSize = toolbarFontSizePx.value;
|
|
147
|
+
return isFullscreen.value ? baseSize * fullscreenToolbarScale.value : baseSize;
|
|
148
|
+
});
|
|
149
|
+
const diagramSourceConfig = computed(() => {
|
|
150
|
+
const source = componentSource.value;
|
|
151
|
+
return source.kind === "runtime-only" || source.kind === "conflict" ? void 0 : source.config;
|
|
152
|
+
});
|
|
153
|
+
const mermaidTheme = computed(() => {
|
|
154
|
+
return resolveMermaidTheme({
|
|
155
|
+
colorModeValue: colorMode?.value,
|
|
156
|
+
manualThemeMode: manualThemeMode.value,
|
|
157
|
+
frontmatterTheme: diagramSourceConfig.value?.theme,
|
|
158
|
+
baseTheme: baseMermaidInit.theme,
|
|
159
|
+
lightTheme,
|
|
160
|
+
darkTheme
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
function materializeEffectiveMermaidInit() {
|
|
164
|
+
const source = componentSource.value;
|
|
165
|
+
if (source.kind === "conflict") throw source.error;
|
|
166
|
+
return materializeMermaidConfigForInvocation({
|
|
167
|
+
runtimeConfig: baseMermaidInit,
|
|
168
|
+
source,
|
|
169
|
+
theme: mermaidTheme.value
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const configuredErrorName = computed(() => componentOptions.error?.trim() || "");
|
|
173
|
+
const errorComponent = shallowRef(null);
|
|
174
|
+
const spinnerComponent = computed(() => props.spinnerComponent);
|
|
175
|
+
const iconSize = computed(() => {
|
|
176
|
+
return Math.round(effectiveToolbarFontSizePx.value * 1.2);
|
|
177
|
+
});
|
|
178
|
+
const isExpandBlocked = computed(() => !isEnabled || !expandEnabled || isLoading.value || hasError.value || isFullscreen.value);
|
|
179
|
+
const expandOverlay = useTemplateRef("expandOverlay");
|
|
180
|
+
const isExpandActive = ref(false);
|
|
181
|
+
const handleExpandActiveChange = (active) => {
|
|
182
|
+
isExpandActive.value = active;
|
|
183
|
+
};
|
|
184
|
+
const toggleExpand = (event) => expandOverlay.value?.toggle(event);
|
|
185
|
+
const handleMermaidClick = (event) => expandOverlay.value?.openFromDiagram(event);
|
|
186
|
+
let requestBuiltInRender;
|
|
187
|
+
function getBuiltInRenderRequest() {
|
|
188
|
+
requestBuiltInRender ??= createMermaidRenderer({
|
|
189
|
+
loadMermaid: $mermaid,
|
|
190
|
+
readRenderData: () => {
|
|
191
|
+
if (componentSource.value.kind === "conflict") {
|
|
192
|
+
return {
|
|
193
|
+
source: null,
|
|
194
|
+
config: {},
|
|
195
|
+
target: mermaidContainer.value
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
source: mermaidDefinition.value,
|
|
200
|
+
config: materializeEffectiveMermaidInit(),
|
|
201
|
+
target: mermaidContainer.value
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
beforeCommit: () => {
|
|
205
|
+
if (import.meta.client) {
|
|
206
|
+
expandOverlay.value?.endForDiagramReplacement();
|
|
207
|
+
void fullscreenPresentation.value?.endForDiagramReplacement();
|
|
208
|
+
}
|
|
209
|
+
hasError.value = false;
|
|
210
|
+
errorContent.value = null;
|
|
211
|
+
},
|
|
212
|
+
debug
|
|
213
|
+
});
|
|
214
|
+
return requestBuiltInRender;
|
|
215
|
+
}
|
|
216
|
+
const showExpandToolbarButton = computed(() => {
|
|
217
|
+
if (!expandEnabled) return false;
|
|
218
|
+
if (toolbarButtons.value.expand === false) return false;
|
|
219
|
+
if (isExpandActive.value) return false;
|
|
220
|
+
if (isFullscreen.value) return false;
|
|
221
|
+
return true;
|
|
222
|
+
});
|
|
223
|
+
function normalizeIdentifier(value) {
|
|
224
|
+
return value.replace(/\.vue$/i, "").split(/[\\/]/).pop()?.replace(/[\s_-]+/g, "").toLowerCase() || "";
|
|
225
|
+
}
|
|
226
|
+
async function loadAppComponent(name, appComponents) {
|
|
227
|
+
const target = normalizeIdentifier(name);
|
|
228
|
+
const matchEntry = Object.entries(appComponents).find(([path]) => {
|
|
229
|
+
const base = path.split(/[\\/]/).pop() || "";
|
|
230
|
+
const normalized = normalizeIdentifier(base);
|
|
231
|
+
return normalized === target;
|
|
232
|
+
});
|
|
233
|
+
if (!matchEntry) return null;
|
|
234
|
+
const [, componentLoader] = matchEntry;
|
|
235
|
+
const mod = await componentLoader();
|
|
236
|
+
return mod.default || mod;
|
|
237
|
+
}
|
|
238
|
+
async function resolveErrorAppComponent(name, appComponents) {
|
|
239
|
+
try {
|
|
240
|
+
const component = await loadAppComponent(name, appComponents);
|
|
241
|
+
if (!component) {
|
|
242
|
+
console.warn(
|
|
243
|
+
"[nuxt-content-mermaid] Cannot find error component:",
|
|
244
|
+
name
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return component;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error(
|
|
250
|
+
"[nuxt-content-mermaid] Failed to load error component:",
|
|
251
|
+
error
|
|
252
|
+
);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (import.meta.client && isEnabled) {
|
|
257
|
+
const appComponents = import.meta.glob("~/components/**/*.{vue,js,ts}");
|
|
258
|
+
watch(
|
|
259
|
+
configuredErrorName,
|
|
260
|
+
async (name) => {
|
|
261
|
+
if (!name) {
|
|
262
|
+
errorComponent.value = null;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
errorComponent.value = await resolveErrorAppComponent(name, appComponents);
|
|
266
|
+
},
|
|
267
|
+
{ immediate: true }
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
function serializeMermaidFromNode(root) {
|
|
271
|
+
const TEXT_NODE = 3;
|
|
272
|
+
const ELEMENT_NODE = 1;
|
|
273
|
+
if (root.nodeType === TEXT_NODE) {
|
|
274
|
+
return root.data;
|
|
275
|
+
}
|
|
276
|
+
if (root.nodeType === ELEMENT_NODE) {
|
|
277
|
+
const el = root;
|
|
278
|
+
const tag = el.tagName?.toUpperCase();
|
|
279
|
+
if (tag === "BR") return "\n";
|
|
280
|
+
const blockTags = [
|
|
281
|
+
"DIV",
|
|
282
|
+
"P",
|
|
283
|
+
"LI",
|
|
284
|
+
"TR",
|
|
285
|
+
"H1",
|
|
286
|
+
"H2",
|
|
287
|
+
"H3",
|
|
288
|
+
"H4",
|
|
289
|
+
"H5",
|
|
290
|
+
"H6",
|
|
291
|
+
"BLOCKQUOTE",
|
|
292
|
+
"PRE",
|
|
293
|
+
"UL",
|
|
294
|
+
"OL",
|
|
295
|
+
"DL",
|
|
296
|
+
"DT",
|
|
297
|
+
"DD",
|
|
298
|
+
"TABLE",
|
|
299
|
+
"THEAD",
|
|
300
|
+
"TBODY",
|
|
301
|
+
"TFOOT",
|
|
302
|
+
"SECTION",
|
|
303
|
+
"ARTICLE",
|
|
304
|
+
"ASIDE",
|
|
305
|
+
"HEADER",
|
|
306
|
+
"FOOTER",
|
|
307
|
+
"MAIN"
|
|
308
|
+
];
|
|
309
|
+
const isBlock = blockTags.includes(tag) || el.classList.contains("line") || el.classList.contains("code-line");
|
|
310
|
+
let out = "";
|
|
311
|
+
el.childNodes.forEach((child) => {
|
|
312
|
+
out += serializeMermaidFromNode(child);
|
|
313
|
+
});
|
|
314
|
+
if (isBlock) return "\n" + out + "\n";
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
function extractMermaidDefinition(container) {
|
|
320
|
+
const codeNodes = Array.from(container.querySelectorAll("code"));
|
|
321
|
+
if (codeNodes.length > 0) {
|
|
322
|
+
return codeNodes.map((node) => serializeMermaidFromNode(node)).join("\n").trim();
|
|
323
|
+
}
|
|
324
|
+
const pre = container.querySelector("pre");
|
|
325
|
+
if (pre) return serializeMermaidFromNode(pre).trim();
|
|
326
|
+
return serializeMermaidFromNode(container).trim();
|
|
327
|
+
}
|
|
328
|
+
function getMermaidSvg() {
|
|
329
|
+
if (!mermaidContainer.value) return null;
|
|
330
|
+
const svg = mermaidContainer.value.querySelector("svg");
|
|
331
|
+
return svg instanceof SVGSVGElement ? svg : null;
|
|
332
|
+
}
|
|
333
|
+
function getMermaidViewport() {
|
|
334
|
+
return mermaidWrapper.value;
|
|
335
|
+
}
|
|
336
|
+
function stopLazyObservation() {
|
|
337
|
+
const activeObserver = observer;
|
|
338
|
+
observer = null;
|
|
339
|
+
activeObserver?.takeRecords();
|
|
340
|
+
activeObserver?.disconnect();
|
|
341
|
+
}
|
|
342
|
+
async function renderMermaid() {
|
|
343
|
+
if (componentSource.value.kind === "conflict") return;
|
|
344
|
+
const request = getBuiltInRenderRequest();
|
|
345
|
+
hasRenderStarted.value = true;
|
|
346
|
+
stopLazyObservation();
|
|
347
|
+
isLoading.value = true;
|
|
348
|
+
const outcome = await request();
|
|
349
|
+
if (outcome.status === "stale")
|
|
350
|
+
return;
|
|
351
|
+
if (outcome.status === "success") {
|
|
352
|
+
hasRenderedOnce.value = true;
|
|
353
|
+
} else if (outcome.status === "failure") {
|
|
354
|
+
console.error("[nuxt-content-mermaid]", outcome.error);
|
|
355
|
+
hasError.value = true;
|
|
356
|
+
errorContent.value = outcome.error;
|
|
357
|
+
}
|
|
358
|
+
isLoading.value = false;
|
|
359
|
+
}
|
|
360
|
+
function setupMermaidContainer() {
|
|
361
|
+
const container = mermaidContainer.value;
|
|
362
|
+
if (!container) return;
|
|
363
|
+
if (!isLazy) {
|
|
364
|
+
renderMermaid();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const nextObserver = new IntersectionObserver(
|
|
368
|
+
(entries) => {
|
|
369
|
+
if (observer !== nextObserver) return;
|
|
370
|
+
if (entries[0]?.isIntersecting && !hasRenderedOnce.value)
|
|
371
|
+
renderMermaid();
|
|
372
|
+
},
|
|
373
|
+
{ threshold: lazyThreshold }
|
|
374
|
+
);
|
|
375
|
+
observer = nextObserver;
|
|
376
|
+
nextObserver.observe(container);
|
|
377
|
+
}
|
|
378
|
+
async function copyMermaidSource() {
|
|
379
|
+
if (!copySource.value) return;
|
|
380
|
+
const copied = await copyToClipboard(copySource.value);
|
|
381
|
+
copyState.value = copied ? "copied" : "error";
|
|
382
|
+
if (copyResetTimer)
|
|
383
|
+
clearTimeout(copyResetTimer);
|
|
384
|
+
copyResetTimer = setTimeout(() => {
|
|
385
|
+
copyState.value = "idle";
|
|
386
|
+
}, 2e3);
|
|
387
|
+
}
|
|
388
|
+
async function copyToClipboard(text) {
|
|
389
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
390
|
+
try {
|
|
391
|
+
await navigator.clipboard.writeText(text);
|
|
392
|
+
return true;
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (typeof document === "undefined")
|
|
397
|
+
return false;
|
|
398
|
+
const textarea = document.createElement("textarea");
|
|
399
|
+
textarea.value = text;
|
|
400
|
+
textarea.setAttribute("readonly", "");
|
|
401
|
+
textarea.style.position = "fixed";
|
|
402
|
+
textarea.style.top = "-9999px";
|
|
403
|
+
textarea.style.opacity = "0";
|
|
404
|
+
document.body.appendChild(textarea);
|
|
405
|
+
textarea.select();
|
|
406
|
+
let copied = false;
|
|
407
|
+
try {
|
|
408
|
+
copied = document.execCommand("copy");
|
|
409
|
+
} catch {
|
|
410
|
+
copied = false;
|
|
411
|
+
} finally {
|
|
412
|
+
document.body.removeChild(textarea);
|
|
413
|
+
}
|
|
414
|
+
return copied;
|
|
415
|
+
}
|
|
416
|
+
onMounted(() => {
|
|
417
|
+
if (!isEnabled) return;
|
|
418
|
+
if (!mermaidDefinition.value && mermaidContainer.value)
|
|
419
|
+
mermaidDefinition.value = extractMermaidDefinition(mermaidContainer.value);
|
|
420
|
+
nextTick(() => setupMermaidContainer());
|
|
421
|
+
});
|
|
422
|
+
onUnmounted(() => {
|
|
423
|
+
requestBuiltInRender?.invalidate();
|
|
424
|
+
stopLazyObservation();
|
|
425
|
+
if (copyResetTimer) clearTimeout(copyResetTimer);
|
|
426
|
+
});
|
|
427
|
+
watch(
|
|
428
|
+
() => ({
|
|
429
|
+
componentSource: componentSource.value,
|
|
430
|
+
code: decodedCode.value,
|
|
431
|
+
colorMode: colorMode?.value,
|
|
432
|
+
manualThemeMode: manualThemeMode.value
|
|
433
|
+
}),
|
|
434
|
+
({ code, componentSource: source }, previousInputs) => {
|
|
435
|
+
if (code !== previousInputs.code)
|
|
436
|
+
mermaidDefinition.value = code;
|
|
437
|
+
const wasConflict = previousInputs.componentSource.kind === "conflict";
|
|
438
|
+
if (source.kind === "conflict") {
|
|
439
|
+
if (wasConflict) return;
|
|
440
|
+
requestBuiltInRender?.invalidate();
|
|
441
|
+
isLoading.value = false;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (!isEnabled) return;
|
|
445
|
+
if (wasConflict || hasRenderStarted.value) renderMermaid();
|
|
446
|
+
},
|
|
447
|
+
{ flush: "post" }
|
|
448
|
+
);
|
|
449
|
+
const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
|
|
450
|
+
</script>
|
|
451
|
+
|
|
452
|
+
<template>
|
|
453
|
+
<div
|
|
454
|
+
ref="mermaidBlock"
|
|
455
|
+
class="mermaid-block"
|
|
456
|
+
:style="{ '--ncm-icon-size': `${iconSize}px` }"
|
|
457
|
+
>
|
|
458
|
+
<!-- Toolbar: Title and Actions -->
|
|
459
|
+
<div class="mermaid-toolbar">
|
|
460
|
+
<div
|
|
461
|
+
class="mermaid-title"
|
|
462
|
+
:style="{ fontSize: effectiveToolbarFontSize }"
|
|
463
|
+
>
|
|
464
|
+
<span v-if="toolbarTitle">{{ toolbarTitle }}</span>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="mermaid-actions">
|
|
467
|
+
<button
|
|
468
|
+
v-if="showCopyButton"
|
|
469
|
+
type="button"
|
|
470
|
+
class="mermaid-btn"
|
|
471
|
+
:title="copyLabel"
|
|
472
|
+
:aria-label="copyLabel"
|
|
473
|
+
:disabled="!hasCopySource"
|
|
474
|
+
@click="copyMermaidSource"
|
|
475
|
+
@mouseenter="isCopyHovered = true"
|
|
476
|
+
@mouseleave="isCopyHovered = false"
|
|
477
|
+
>
|
|
478
|
+
<component
|
|
479
|
+
:is="copyIcon"
|
|
480
|
+
:size="iconSize"
|
|
481
|
+
/>
|
|
482
|
+
</button>
|
|
483
|
+
<button
|
|
484
|
+
v-if="showExpandToolbarButton"
|
|
485
|
+
type="button"
|
|
486
|
+
class="mermaid-btn"
|
|
487
|
+
:title="isExpandActive ? 'Collapse' : 'Expand'"
|
|
488
|
+
:aria-label="isExpandActive ? 'Collapse diagram' : 'Expand diagram'"
|
|
489
|
+
@click="toggleExpand"
|
|
490
|
+
>
|
|
491
|
+
<IconExpand
|
|
492
|
+
v-if="!isExpandActive"
|
|
493
|
+
:size="iconSize"
|
|
494
|
+
/>
|
|
495
|
+
<IconCollapse
|
|
496
|
+
v-else
|
|
497
|
+
:size="iconSize"
|
|
498
|
+
/>
|
|
499
|
+
</button>
|
|
500
|
+
<button
|
|
501
|
+
v-if="showFullscreenButton"
|
|
502
|
+
type="button"
|
|
503
|
+
class="mermaid-btn"
|
|
504
|
+
:title="fullscreenLabel"
|
|
505
|
+
:aria-label="fullscreenLabel"
|
|
506
|
+
@click="toggleFullscreen"
|
|
507
|
+
>
|
|
508
|
+
<IconFullScreen
|
|
509
|
+
v-if="!isFullscreen"
|
|
510
|
+
:size="iconSize"
|
|
511
|
+
/>
|
|
512
|
+
<IconFullScreenExit
|
|
513
|
+
v-else
|
|
514
|
+
:size="iconSize"
|
|
515
|
+
/>
|
|
516
|
+
</button>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
<MermaidFullscreenPresentation
|
|
521
|
+
ref="fullscreenPresentation"
|
|
522
|
+
:fullscreen-target="mermaidBlock"
|
|
523
|
+
:viewport-target="mermaidWrapper"
|
|
524
|
+
:render-target="mermaidContainer"
|
|
525
|
+
:icon-size="iconSize"
|
|
526
|
+
@active-change="handleFullscreenActiveChange"
|
|
527
|
+
@supported-change="handleFullscreenSupportedChange"
|
|
528
|
+
/>
|
|
529
|
+
|
|
530
|
+
<!-- Diagram Container -->
|
|
531
|
+
<div
|
|
532
|
+
ref="mermaidWrapper"
|
|
533
|
+
class="mermaid-wrapper"
|
|
534
|
+
:class="{
|
|
535
|
+
'ncm-is-loading': isLoading && !hasRenderedOnce,
|
|
536
|
+
'ncm-expand-hidden': isExpandActive,
|
|
537
|
+
'ncm-expand-clickable': expandEnabled && allowOpenDiagramClick && !isFullscreen,
|
|
538
|
+
'ncm-fullscreen-zoom': isFullscreen
|
|
539
|
+
}"
|
|
540
|
+
:style="cursorVariables"
|
|
541
|
+
@click="handleMermaidClick"
|
|
542
|
+
>
|
|
543
|
+
<div
|
|
544
|
+
ref="mermaidContainer"
|
|
545
|
+
class="mermaid"
|
|
546
|
+
>
|
|
547
|
+
<!-- Initially show the slot's <pre><code> for SSR; client-side rendering will replace it with the mermaid SVG -->
|
|
548
|
+
<slot>
|
|
549
|
+
<pre v-if="decodedCode"><code>{{ decodedCode }}</code></pre>
|
|
550
|
+
</slot>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<template v-if="isLoading && !hasRenderedOnce">
|
|
554
|
+
<slot name="loading">
|
|
555
|
+
<component :is="spinnerComponent" />
|
|
556
|
+
</slot>
|
|
557
|
+
</template>
|
|
558
|
+
<template v-else-if="hasError">
|
|
559
|
+
<slot
|
|
560
|
+
name="error"
|
|
561
|
+
:error="errorContent"
|
|
562
|
+
:source="mermaidDefinition"
|
|
563
|
+
>
|
|
564
|
+
<component
|
|
565
|
+
:is="errorComponent"
|
|
566
|
+
v-if="errorComponent"
|
|
567
|
+
:error="errorContent"
|
|
568
|
+
:source="mermaidDefinition"
|
|
569
|
+
/>
|
|
570
|
+
|
|
571
|
+
<div
|
|
572
|
+
v-else
|
|
573
|
+
class="mermaid-error-default"
|
|
574
|
+
>
|
|
575
|
+
⚠️ Mermaid Diagram Error
|
|
576
|
+
</div>
|
|
577
|
+
</slot>
|
|
578
|
+
</template>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
<MermaidExpandOverlay
|
|
582
|
+
ref="expandOverlay"
|
|
583
|
+
:options="expandOptions"
|
|
584
|
+
:blocked="isExpandBlocked"
|
|
585
|
+
:get-expand-target="getMermaidSvg"
|
|
586
|
+
:get-expand-viewport="getMermaidViewport"
|
|
587
|
+
:overlay-style="cursorVariables"
|
|
588
|
+
:content-style="cursorVariables"
|
|
589
|
+
:icon-size="iconSize"
|
|
590
|
+
@active-change="handleExpandActiveChange"
|
|
591
|
+
/>
|
|
592
|
+
</div>
|
|
593
|
+
</template>
|
|
594
|
+
|
|
595
|
+
<style scoped>
|
|
596
|
+
.mermaid-block{border:var(--ncm-border);border-radius:4px;flex-direction:column;margin:1rem 0;overflow:hidden;--ncm-toolbar-height:calc(var(--ncm-icon-size, 17px) + 20px)}.mermaid-block,.mermaid-toolbar{background-color:var(--ncm-code-bg);display:flex}.mermaid-toolbar{align-items:center;border-bottom:var(--ncm-border-bottom);border-top-left-radius:4px;border-top-right-radius:4px;justify-content:space-between;padding:.375rem .375rem .375rem .75rem;z-index:20}.mermaid-title{color:var(--ncm-text-muted);font-weight:500;letter-spacing:0;line-height:1.25;max-width:80%;overflow:hidden;padding:4px 0;text-overflow:ellipsis;white-space:nowrap}:fullscreen .mermaid-title{max-width:80%}.mermaid-actions{display:flex;gap:0;justify-self:flex-end}.mermaid-btn{align-items:center;background:var(--ncm-code-bg);border:none;border-radius:4px;color:var(--ncm-text-xmuted);cursor:pointer;display:flex;justify-content:center;line-height:1.25;padding:4px;transition:color .2s,background-color .2s}.mermaid-btn:hover{background-color:var(--ncm-code-bg-hover);color:var(--ncm-text)}.mermaid-btn:disabled{color:var(--ncm-text-xmuted);cursor:not-allowed;opacity:.6}.mermaid-wrapper{align-items:center;display:flex;justify-content:center;overflow:auto;padding:1rem;position:relative;--sb-thumb:rgba(0,0,0,.2);--sb-thumb-hover:rgba(0,0,0,.35)}.mermaid-wrapper>div{height:100%;width:100%}.mermaid-wrapper>div>:deep(svg){margin:0 auto}.mermaid-wrapper :deep(code),.mermaid-wrapper :deep(pre){height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;clip-path:inset(50%);white-space:nowrap}.mermaid:not([data-processed]){color:transparent;min-height:100px}:fullscreen .mermaid-wrapper{flex:1;overflow:hidden;touch-action:none}:fullscreen .mermaid-wrapper::-webkit-scrollbar{display:none}:fullscreen .mermaid-wrapper :deep(svg){height:100%!important;max-height:none!important;max-width:none!important;width:100%!important}:global(.dark .mermaid-wrapper){--sb-thumb:hsla(0,0%,100%,.2);--sb-thumb-hover:hsla(0,0%,100%,.35)}.mermaid-wrapper::-webkit-scrollbar{width:14px}.mermaid-wrapper::-webkit-scrollbar-track{background:hsla(0,0%,94%,.08)}:global(.dark .mermaid-wrapper::-webkit-scrollbar-track){background:hsla(0,0%,9%,.08)}.mermaid-wrapper::-webkit-scrollbar-corner{background:transparent}.mermaid-wrapper::-webkit-scrollbar-thumb{background-clip:content-box;background-color:var(--sb-thumb);border:3px solid transparent;border-radius:10px}.mermaid-wrapper::-webkit-scrollbar-thumb:hover{background-color:var(--sb-thumb-hover)}.mermaid-wrapper.ncm-is-loading{align-items:center;display:flex;justify-content:center;min-height:150px}.mermaid-wrapper.ncm-expand-hidden{opacity:0;pointer-events:none}.mermaid-wrapper.ncm-expand-clickable :deep(svg){cursor:var(--ncm-cursor-expand)}.mermaid-error-default{color:#f55;font-family:monospace;padding:1rem;white-space:pre-wrap}
|
|
597
|
+
</style>
|
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class MermaidComponentConfigurationError extends Error {
|
|
2
|
+
code = "CONTENT_MERMAID_COMPONENT_CONFIGURATION_ERROR";
|
|
3
|
+
name = "MermaidComponentConfigurationError";
|
|
4
|
+
}
|
|
5
|
+
export function createMermaidComponentConfigurationError(message) {
|
|
6
|
+
return new MermaidComponentConfigurationError(message);
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MermaidConfig } from 'mermaid';
|
|
2
|
+
import type { PageMermaidConfig } from '../types/config.js';
|
|
3
|
+
interface MermaidComponentSourceInput {
|
|
4
|
+
readonly pageConfig?: unknown;
|
|
5
|
+
readonly config?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export type MermaidComponentSource = {
|
|
8
|
+
readonly kind: 'runtime-only';
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'page';
|
|
11
|
+
readonly config: PageMermaidConfig;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'direct';
|
|
14
|
+
readonly config: MermaidConfig;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'conflict';
|
|
17
|
+
readonly error: Error & {
|
|
18
|
+
readonly code: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function resolveMermaidComponentSource(input: MermaidComponentSourceInput): MermaidComponentSource;
|
|
22
|
+
export declare function collectMermaidComponentSourceDependencies(input: MermaidComponentSourceInput): readonly unknown[];
|
|
23
|
+
export {};
|