@barzhsieh/nuxt-content-mermaid 2.2.3 → 3.0.2

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 (62) hide show
  1. package/README.md +47 -502
  2. package/README.zh-TW.md +46 -509
  3. package/dist/module.d.mts +39 -53
  4. package/dist/module.json +3 -3
  5. package/dist/module.mjs +600 -155
  6. package/dist/runtime/built-in-renderer/BuiltInRenderer.d.vue.ts +0 -0
  7. package/dist/runtime/built-in-renderer/BuiltInRenderer.vue +597 -0
  8. package/dist/runtime/built-in-renderer/BuiltInRenderer.vue.d.ts +0 -0
  9. package/dist/runtime/component-configuration-error.d.ts +3 -0
  10. package/dist/runtime/component-configuration-error.js +7 -0
  11. package/dist/runtime/component-configuration.d.ts +23 -0
  12. package/dist/runtime/component-configuration.js +78 -0
  13. package/dist/runtime/components/ContentMermaidTransport.d.vue.ts +10 -0
  14. package/dist/runtime/components/ContentMermaidTransport.vue +21 -0
  15. package/dist/runtime/components/ContentMermaidTransport.vue.d.ts +10 -0
  16. package/dist/runtime/components/Mermaid.d.vue.ts +0 -31
  17. package/dist/runtime/components/Mermaid.vue +136 -743
  18. package/dist/runtime/components/Mermaid.vue.d.ts +0 -31
  19. package/dist/runtime/components/MermaidExpandOverlay.d.vue.ts +13 -16
  20. package/dist/runtime/components/MermaidExpandOverlay.vue +61 -53
  21. package/dist/runtime/components/MermaidExpandOverlay.vue.d.ts +13 -16
  22. package/dist/runtime/components/MermaidFullscreenPresentation.d.vue.ts +18 -0
  23. package/dist/runtime/components/MermaidFullscreenPresentation.vue +56 -0
  24. package/dist/runtime/components/MermaidFullscreenPresentation.vue.d.ts +18 -0
  25. package/dist/runtime/composables/useMermaidExpand.d.ts +16 -32
  26. package/dist/runtime/composables/useMermaidExpand.js +232 -75
  27. package/dist/runtime/composables/useMermaidFullscreen.d.ts +18 -0
  28. package/dist/runtime/composables/useMermaidFullscreen.js +242 -0
  29. package/dist/runtime/composables/useMermaidZoom.d.ts +5 -1
  30. package/dist/runtime/composables/useMermaidZoom.js +54 -28
  31. package/dist/runtime/configuration/core.d.ts +27 -0
  32. package/dist/runtime/configuration/core.js +340 -0
  33. package/dist/runtime/configuration/module.d.ts +17 -0
  34. package/dist/runtime/configuration/module.js +233 -0
  35. package/dist/runtime/configuration/runtime-options.d.ts +4 -0
  36. package/dist/runtime/configuration/runtime-options.js +57 -0
  37. package/dist/runtime/constants.d.ts +24 -0
  38. package/dist/runtime/constants.js +60 -2
  39. package/dist/runtime/direct-mermaid-config.d.ts +5 -0
  40. package/dist/runtime/direct-mermaid-config.js +216 -0
  41. package/dist/runtime/mermaid-config.d.ts +9 -5
  42. package/dist/runtime/mermaid-config.js +30 -19
  43. package/dist/runtime/mermaid-rendering.d.ts +28 -0
  44. package/dist/runtime/mermaid-rendering.js +140 -0
  45. package/dist/runtime/plugins/content-mermaid-transport.d.ts +2 -0
  46. package/dist/runtime/plugins/content-mermaid-transport.js +5 -0
  47. package/dist/runtime/plugins/mermaid.client.d.ts +0 -3
  48. package/dist/runtime/plugins/mermaid.client.js +14 -32
  49. package/dist/runtime/plugins/runtime-config.d.ts +2 -0
  50. package/dist/runtime/plugins/runtime-config.js +9 -0
  51. package/dist/runtime/rendererSelection.d.ts +43 -0
  52. package/dist/runtime/rendererSelection.js +36 -0
  53. package/dist/runtime/rendererSelectionOrchestration.d.ts +12 -0
  54. package/dist/runtime/rendererSelectionOrchestration.js +13 -0
  55. package/dist/runtime/runtime-snapshot.d.ts +3 -0
  56. package/dist/runtime/runtime-snapshot.js +26 -0
  57. package/dist/runtime/utils/index.d.ts +0 -4
  58. package/dist/runtime/utils/index.js +0 -37
  59. package/dist/types.d.mts +2 -2
  60. package/package.json +38 -32
  61. package/dist/runtime/utils/mermaid-transform.d.ts +0 -18
  62. package/dist/runtime/utils/mermaid-transform.js +0 -345
@@ -1,622 +1,152 @@
1
1
  <script setup>
2
- import { useNuxtApp, useRuntimeConfig } from "#app";
2
+ import { useNuxtApp } from "#app";
3
+ import { computed, shallowRef, watch } from "vue";
3
4
  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 { mergeMermaidConfig, resolveMermaidTheme } from "../mermaid-config";
15
- import { enqueueRender, parseSizeToPx, isRecord } from "../utils";
16
- import { useMermaidTheme } from "../composables/useMermaidTheme";
17
- import { useMermaidExpand } from "../composables/useMermaidExpand";
18
- import { useMermaidCursors } from "../composables/useMermaidCursors";
19
- import { useFullscreen } from "../composables/useFullscreen";
20
- import { useMermaidZoom } from "../composables/useMermaidZoom";
21
- import { useEventListener } from "../composables/useEventListener";
22
- import { DEFAULT_TOOLBAR_OPTIONS, DEFAULT_EXPAND_OPTIONS } from "../constants";
5
+ collectMermaidComponentSourceDependencies,
6
+ resolveMermaidComponentSource
7
+ } from "../component-configuration";
8
+ import { MERMAID_LOG_PREFIX } from "../constants";
9
+ import BuiltInRenderer from "../built-in-renderer/BuiltInRenderer.vue";
10
+ import {
11
+ createRendererResolutionFailureHandoff,
12
+ selectRenderer
13
+ } from "../rendererSelection";
14
+ import { createRendererSelectionAttemptCoordinator } from "../rendererSelectionOrchestration";
15
+ import { getRuntimeMermaidSnapshot } from "../runtime-snapshot";
23
16
  import Spinner from "./Spinner.vue";
24
- import IconClipboard from "./icons/IconClipboard.vue";
25
- import IconClipboardHover from "./icons/IconClipboardHover.vue";
26
- import IconClipboardSuccess from "./icons/IconClipboardSuccess.vue";
27
- import IconFullScreen from "./icons/iconFullScreen.vue";
28
- import IconFullScreenExit from "./icons/iconFullScreenExit.vue";
29
- import MermaidExpandOverlay from "./MermaidExpandOverlay.vue";
30
- import IconExpand from "./icons/IconExpand.vue";
31
- import IconCollapse from "./icons/IconCollapse.vue";
32
- import MermaidZoomToolbar from "./MermaidZoomToolbar.vue";
33
- const isMac = import.meta.client ? /Mac|iPhone|iPad|iPod/.test(navigator.userAgent) : false;
34
17
  const props = defineProps({
35
- config: { type: null, required: false },
36
18
  toolbar: { type: Object, required: false },
37
- code: { type: String, required: false }
38
- });
19
+ code: { type: String, required: false },
20
+ pageConfig: { type: null, required: false },
21
+ config: { type: null, required: false }
22
+ });
23
+ function resolveCurrentComponentSource() {
24
+ return resolveMermaidComponentSource({
25
+ pageConfig: props.pageConfig,
26
+ config: props.config
27
+ });
28
+ }
29
+ const initialComponentSource = resolveCurrentComponentSource();
30
+ if (initialComponentSource.kind === "conflict") throw initialComponentSource.error;
31
+ const componentSource = shallowRef(initialComponentSource);
39
32
  const nuxtApp = useNuxtApp();
40
- const runtimeConfig = useRuntimeConfig();
41
- const contentMermaidOptions = runtimeConfig.public?.contentMermaid || runtimeConfig.public?.mermaidContent || {};
42
- const isEnabled = contentMermaidOptions.enabled !== false;
43
- const debug = contentMermaidOptions.debug || false;
44
- const loaderOptions = contentMermaidOptions.loader || {};
45
- const themeOptions = contentMermaidOptions.theme || {};
33
+ const contentMermaidOptions = getRuntimeMermaidSnapshot(nuxtApp);
46
34
  const componentOptions = contentMermaidOptions.components || {};
47
- function resolveExpandOptions(expand) {
48
- if (expand === false)
49
- return { ...DEFAULT_EXPAND_OPTIONS, enabled: false };
50
- if (expand === true || !expand || typeof expand !== "object")
51
- return DEFAULT_EXPAND_OPTIONS;
52
- return defu({}, expand, DEFAULT_EXPAND_OPTIONS);
53
- }
54
- const expandOptions = resolveExpandOptions(contentMermaidOptions.expand);
55
- const expandEnabled = expandOptions.enabled !== false;
56
- const expandOpenOptions = expandOptions.invokeOpenOn || {};
57
- const expandCloseOptions = expandOptions.invokeCloseOn || {};
58
- const expandMargin = typeof expandOptions.margin === "number" && Number.isFinite(expandOptions.margin) ? Math.max(0, expandOptions.margin) : 0;
59
- const allowOpenDiagramClick = expandOpenOptions.diagramClick !== false;
60
- const allowOverlayClose = expandCloseOptions.overlayClick !== false;
61
- const allowCloseButtonClose = expandCloseOptions.closeButtonClick !== false;
62
- const allowEscClose = expandCloseOptions.esc !== false;
63
- const allowWheelClose = expandCloseOptions.wheel !== false;
64
- const allowSwipeClose = expandCloseOptions.swipe !== false;
65
- const lazyOption = loaderOptions.lazy;
66
- const isLazy = lazyOption !== false;
67
- const lazyThreshold = typeof lazyOption === "object" && typeof lazyOption.threshold === "number" ? lazyOption.threshold : 0.1;
68
- const baseMermaidInit = loaderOptions.init || {};
69
- const lightTheme = themeOptions.light;
70
- const darkTheme = themeOptions.dark;
71
- const colorMode = nuxtApp.$colorMode;
72
- const { $mermaid } = nuxtApp;
73
- const { currentTheme: manualThemeMode } = useMermaidTheme();
74
- const mermaidBlock = useTemplateRef("mermaidBlock");
75
- const mermaidContainer = useTemplateRef("mermaidContainer");
76
- const hasRenderedOnce = ref(false);
77
- const isLoading = ref(false);
78
- const hasError = ref(false);
79
- const errorContent = shallowRef(null);
80
35
  const decodedCode = computed(() => props.code ? decodeURIComponent(props.code) : "");
81
- const mermaidDefinition = ref(decodedCode.value);
82
- let observer = null;
83
- const defaultToolbarTitle = DEFAULT_TOOLBAR_OPTIONS.title ?? "mermaid";
84
- const defaultToolbarFontSize = DEFAULT_TOOLBAR_OPTIONS.fontSize ?? "14px";
85
- const defaultFullscreenToolbarScale = DEFAULT_TOOLBAR_OPTIONS.fullscreenToolbarScale ?? 1.25;
86
- const baseToolbarDefaults = DEFAULT_TOOLBAR_OPTIONS;
87
- const runtimeToolbarDefaults = computed(() => {
88
- const toolbar = contentMermaidOptions.toolbar;
89
- return toolbar && typeof toolbar === "object" ? toolbar : {};
90
- });
91
- const resolvedToolbar = computed(() => {
92
- return defu({}, props.toolbar ?? {}, runtimeToolbarDefaults.value, baseToolbarDefaults);
93
- });
94
- const toolbarButtons = computed(() => {
95
- const buttons = resolvedToolbar.value.buttons;
96
- return isRecord(buttons) ? buttons : {};
97
- });
98
- const toolbarTitle = computed(() => resolvedToolbar.value.title ?? defaultToolbarTitle);
99
- const toolbarFontSize = computed(() => {
100
- const value = resolvedToolbar.value.fontSize ?? defaultToolbarFontSize;
101
- if (typeof value === "number")
102
- return `${value}px`;
103
- if (typeof value === "string") {
104
- const trimmed = value.trim();
105
- if (/^\d+(?:\.\d+)?$/.test(trimmed))
106
- return `${trimmed}px`;
107
- return trimmed;
108
- }
109
- return defaultToolbarFontSize;
110
- });
111
- const showCopyButton = computed(() => toolbarButtons.value.copy !== false);
112
- const copySource = computed(() => decodedCode.value || mermaidDefinition.value || "");
113
- const hasCopySource = computed(() => !!copySource.value);
114
- const copyState = ref("idle");
115
- const copyLabel = computed(() => {
116
- if (copyState.value === "copied") return "Copied";
117
- if (copyState.value === "error") return "Copy failed";
118
- return "Copy";
119
- });
120
- let copyResetTimer = null;
121
- const isCopyHovered = ref(false);
122
- const copyIcon = computed(() => {
123
- if (copyState.value === "copied") return IconClipboardSuccess;
124
- if (isCopyHovered.value && hasCopySource.value) return IconClipboardHover;
125
- return IconClipboard;
126
- });
127
- const { isSupported: isFullscreenSupported, isFullscreen, toggle: toggleFullscreen } = useFullscreen(mermaidBlock, {
128
- autoExit: true
129
- });
130
- const showFullscreenButton = computed(() => toolbarButtons.value.fullscreen !== false && isFullscreenSupported.value);
131
- const fullscreenLabel = computed(() => isFullscreen.value ? "Exit fullscreen" : "Enter fullscreen");
132
- const fullscreenToolbarScale = computed(() => {
133
- const raw = resolvedToolbar.value.fullscreenToolbarScale ?? defaultFullscreenToolbarScale;
134
- if (typeof raw === "number" && Number.isFinite(raw))
135
- return raw;
136
- if (typeof raw === "string") {
137
- const parsed = Number.parseFloat(raw);
138
- if (Number.isFinite(parsed))
139
- return parsed;
140
- }
141
- return defaultFullscreenToolbarScale;
142
- });
143
- const toolbarFontSizePx = computed(() => {
144
- return parseSizeToPx(resolvedToolbar.value.fontSize ?? defaultToolbarFontSize, 14);
145
- });
146
- const fullscreenToolbarFontSize = computed(() => {
147
- const scaled = toolbarFontSizePx.value * fullscreenToolbarScale.value;
148
- return `${Math.round(scaled * 100) / 100}px`;
149
- });
150
- const effectiveToolbarFontSize = computed(() => {
151
- return isFullscreen.value ? fullscreenToolbarFontSize.value : toolbarFontSize.value;
152
- });
153
- const effectiveToolbarFontSizePx = computed(() => {
154
- const baseSize = toolbarFontSizePx.value;
155
- return isFullscreen.value ? baseSize * fullscreenToolbarScale.value : baseSize;
156
- });
157
- const pageConfig = computed(() => {
158
- const value = props.config;
159
- if (!value)
160
- return void 0;
161
- if (typeof value !== "object") {
162
- if (import.meta.dev && value !== null && value !== void 0) {
163
- console.warn(
164
- "[nuxt-content-mermaid] Received non-object `config` prop on <Mermaid> component. This usually means your `content.config.ts` collection schema did not declare `config` as a JSON field. See README section about per-page overrides via frontmatter.",
165
- value
166
- );
167
- }
168
- return void 0;
169
- }
170
- return value;
171
- });
172
- const mermaidTheme = computed(() => {
173
- return resolveMermaidTheme({
174
- colorModeValue: colorMode?.value,
175
- manualThemeMode: manualThemeMode.value,
176
- frontmatterTheme: pageConfig.value?.theme,
177
- baseTheme: baseMermaidInit.theme,
178
- lightTheme,
179
- darkTheme
180
- });
181
- });
182
- const effectiveMermaidInit = computed(() => {
183
- return mergeMermaidConfig({
184
- baseConfig: baseMermaidInit,
185
- overrideConfig: pageConfig.value,
186
- theme: mermaidTheme.value
187
- });
188
- });
189
36
  const configuredSpinnerName = computed(() => componentOptions.spinner?.trim() || "");
190
37
  const customSpinner = shallowRef(null);
191
38
  const configuredMermaidImplName = computed(() => componentOptions.renderer?.trim() || "");
192
- const customMermaidImpl = shallowRef(null);
193
- const configuredErrorName = computed(() => componentOptions.error?.trim() || "");
194
- const errorComponent = shallowRef(null);
39
+ const rendererSelectionState = shallowRef(
40
+ configuredMermaidImplName.value ? { status: "pending" } : { status: "built-in" }
41
+ );
195
42
  const spinnerComponent = computed(() => customSpinner.value || Spinner);
196
- const iconSize = computed(() => {
197
- return Math.round(effectiveToolbarFontSizePx.value * 1.2);
198
- });
199
- const isExpandBlocked = computed(() => !isEnabled || !expandEnabled || isLoading.value || hasError.value || isFullscreen.value);
200
- const fullscreenZoom = useMermaidZoom({
201
- active: isFullscreen,
202
- minScale: 0.1,
203
- maxScale: 10
204
- });
205
- const showFullscreenZoomHint = ref(false);
206
- const fullscreenHintDurationMs = 3e3;
207
- let fullscreenHintTimeout;
208
- let hasShownFullscreenZoomHint = false;
209
- watch(isFullscreen, (value) => {
210
- if (value) {
211
- hasShownFullscreenZoomHint = false;
212
- nextTick(() => {
213
- const el = mermaidContainer.value;
214
- if (el) {
215
- const rect = el.getBoundingClientRect();
216
- fullscreenZoom.init({
217
- scale: 1,
218
- translateX: 0,
219
- translateY: 0,
220
- top: rect.top,
221
- left: rect.left
222
- });
223
- }
224
- });
225
- } else {
226
- showFullscreenZoomHint.value = false;
227
- clearTimeout(fullscreenHintTimeout);
228
- }
229
- });
230
- function refreshFullscreenOrigin() {
231
- if (!import.meta.client || !isFullscreen.value) return;
232
- const el = mermaidContainer.value;
233
- if (!el) return;
234
- requestAnimationFrame(() => {
235
- requestAnimationFrame(() => {
236
- const rect = el.getBoundingClientRect();
237
- fullscreenZoom.setMetrics({
238
- scale: fullscreenZoom.scale.value,
239
- translateX: fullscreenZoom.translateX.value,
240
- translateY: fullscreenZoom.translateY.value,
241
- top: rect.top,
242
- left: rect.left
243
- });
244
- });
245
- });
246
- }
247
- function handleFullscreenWheel(event) {
248
- if (!isFullscreen.value) return;
249
- const handled = fullscreenZoom.handleWheel(event);
250
- if (handled) {
251
- if (fullscreenHintTimeout && showFullscreenZoomHint.value) {
252
- showFullscreenZoomHint.value = false;
253
- clearTimeout(fullscreenHintTimeout);
254
- }
255
- return;
256
- }
257
- if (!hasShownFullscreenZoomHint) {
258
- hasShownFullscreenZoomHint = true;
259
- showFullscreenZoomHint.value = true;
260
- clearTimeout(fullscreenHintTimeout);
261
- fullscreenHintTimeout = setTimeout(() => {
262
- showFullscreenZoomHint.value = false;
263
- }, fullscreenHintDurationMs);
264
- }
265
- }
266
- function handleFullscreenKeyDown(event) {
267
- if (!isFullscreen.value) return;
268
- if (event.defaultPrevented) return;
269
- const target = event.target;
270
- if (target) {
271
- const tagName = target.tagName;
272
- if (target.isContentEditable || tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT")
273
- return;
274
- }
275
- const browserZoomKeys = /* @__PURE__ */ new Set(["+", "=", "-", "_"]);
276
- if (event.ctrlKey || event.metaKey) {
277
- if (browserZoomKeys.has(event.key))
278
- event.preventDefault();
279
- return;
280
- }
281
- const moveStep = 20 / fullscreenZoom.scale.value;
282
- const keyHandlers = {
283
- "+": () => fullscreenZoom.zoomIn(),
284
- "=": () => fullscreenZoom.zoomIn(),
285
- "-": () => fullscreenZoom.zoomOut(),
286
- "_": () => fullscreenZoom.zoomOut(),
287
- "0": () => fullscreenZoom.reset(),
288
- "ArrowUp": () => {
289
- fullscreenZoom.translateY.value += moveStep;
290
- },
291
- "ArrowDown": () => {
292
- fullscreenZoom.translateY.value -= moveStep;
293
- },
294
- "ArrowLeft": () => {
295
- fullscreenZoom.translateX.value += moveStep;
296
- },
297
- "ArrowRight": () => {
298
- fullscreenZoom.translateX.value -= moveStep;
299
- }
300
- };
301
- const handler = keyHandlers[event.key];
302
- if (handler) {
303
- event.preventDefault();
304
- handler();
305
- }
306
- }
307
- if (import.meta.client) {
308
- const fullscreenDocument = computed(() => isFullscreen.value ? document : null);
309
- const fullscreenWindow = computed(() => isFullscreen.value ? window : null);
310
- const fullscreenVisualViewport = computed(() => isFullscreen.value ? window.visualViewport : null);
311
- useEventListener(fullscreenWindow, "wheel", handleFullscreenWheel, { passive: false });
312
- useEventListener(fullscreenDocument, "keydown", handleFullscreenKeyDown);
313
- useEventListener(fullscreenWindow, "focus", refreshFullscreenOrigin);
314
- useEventListener(fullscreenDocument, "fullscreenchange", refreshFullscreenOrigin);
315
- useEventListener(fullscreenVisualViewport, "resize", refreshFullscreenOrigin, { passive: true });
316
- useEventListener(fullscreenDocument, "visibilitychange", () => {
317
- if (document.visibilityState === "visible") refreshFullscreenOrigin();
318
- });
319
- }
320
- const {
321
- setExpandTargetWrap,
322
- expandTargetStyle,
323
- isExpandActive,
324
- isVisible,
325
- closeExpand,
326
- toggleExpand,
327
- handleExpandTransitionEnd,
328
- handleMermaidClick,
329
- resetExpand,
330
- zoom,
331
- showZoomHint
332
- } = useMermaidExpand({
333
- getExpandTarget: getMermaidSvg,
334
- expandMargin,
335
- isBlocked: isExpandBlocked,
336
- invokeOpenOn: {
337
- diagramClick: allowOpenDiagramClick
338
- },
339
- invokeCloseOn: {
340
- esc: allowEscClose,
341
- wheel: allowWheelClose,
342
- swipe: allowSwipeClose
343
- }
344
- });
345
- const showExpandToolbarButton = computed(() => {
346
- if (!expandEnabled) return false;
347
- if (toolbarButtons.value.expand === false) return false;
348
- if (isExpandActive.value) return false;
349
- if (isFullscreen.value) return false;
350
- return true;
351
- });
352
43
  function normalizeIdentifier(value) {
353
44
  return value.replace(/\.vue$/i, "").split(/[\\/]/).pop()?.replace(/[\s_-]+/g, "").toLowerCase() || "";
354
45
  }
355
- async function resolveAppComponent(name, appComponents, label) {
46
+ async function loadAppComponent(name, appComponents) {
356
47
  const target = normalizeIdentifier(name);
357
48
  const matchEntry = Object.entries(appComponents).find(([path]) => {
358
49
  const base = path.split(/[\\/]/).pop() || "";
359
- const normalized = normalizeIdentifier(base);
360
- return normalized === target;
50
+ return normalizeIdentifier(base) === target;
361
51
  });
362
- if (!matchEntry) {
363
- console.warn(
364
- `[nuxt-content-mermaid] Cannot find ${label} component:`,
365
- name
366
- );
367
- return null;
368
- }
52
+ if (!matchEntry) return null;
53
+ const [, componentLoader] = matchEntry;
54
+ const mod = await componentLoader();
55
+ return mod.default || mod;
56
+ }
57
+ async function resolveSpinnerAppComponent(name, appComponents) {
369
58
  try {
370
- const [, componentLoader] = matchEntry;
371
- const mod = await componentLoader();
372
- return mod.default || mod;
59
+ const component = await loadAppComponent(name, appComponents);
60
+ if (!component) {
61
+ console.warn(
62
+ "[nuxt-content-mermaid] Cannot find spinner component:",
63
+ name
64
+ );
65
+ }
66
+ return component;
373
67
  } catch (error) {
374
68
  console.error(
375
- `[nuxt-content-mermaid] Failed to load ${label} component:`,
69
+ "[nuxt-content-mermaid] Failed to load spinner component:",
376
70
  error
377
71
  );
378
72
  return null;
379
73
  }
380
74
  }
381
- if (import.meta.client && isEnabled) {
382
- let watchCustomAppComponent = function(nameRef, targetRef, label) {
383
- watch(
384
- nameRef,
385
- async (name) => {
386
- if (!name) {
387
- targetRef.value = null;
388
- return;
389
- }
390
- targetRef.value = await resolveAppComponent(
391
- name,
392
- appComponents,
393
- label
394
- );
395
- },
396
- { immediate: true }
397
- );
398
- };
75
+ if (import.meta.client) {
399
76
  const appComponents = import.meta.glob("~/components/**/*.{vue,js,ts}");
400
- watchCustomAppComponent(configuredSpinnerName, customSpinner, "spinner");
401
- watchCustomAppComponent(configuredMermaidImplName, customMermaidImpl, "mermaid");
402
- watchCustomAppComponent(configuredErrorName, errorComponent, "error");
403
- }
404
- function serializeMermaidFromNode(root) {
405
- const TEXT_NODE = 3;
406
- const ELEMENT_NODE = 1;
407
- if (root.nodeType === TEXT_NODE) {
408
- return root.data;
409
- }
410
- if (root.nodeType === ELEMENT_NODE) {
411
- const el = root;
412
- const tag = el.tagName?.toUpperCase();
413
- if (tag === "BR") return "\n";
414
- const blockTags = [
415
- "DIV",
416
- "P",
417
- "LI",
418
- "TR",
419
- "H1",
420
- "H2",
421
- "H3",
422
- "H4",
423
- "H5",
424
- "H6",
425
- "BLOCKQUOTE",
426
- "PRE",
427
- "UL",
428
- "OL",
429
- "DL",
430
- "DT",
431
- "DD",
432
- "TABLE",
433
- "THEAD",
434
- "TBODY",
435
- "TFOOT",
436
- "SECTION",
437
- "ARTICLE",
438
- "ASIDE",
439
- "HEADER",
440
- "FOOTER",
441
- "MAIN"
442
- ];
443
- const isBlock = blockTags.includes(tag) || el.classList.contains("line") || el.classList.contains("code-line");
444
- let out = "";
445
- el.childNodes.forEach((child) => {
446
- out += serializeMermaidFromNode(child);
447
- });
448
- if (isBlock) return "\n" + out + "\n";
449
- return out;
450
- }
451
- return "";
452
- }
453
- function extractMermaidDefinition(container) {
454
- const codeNodes = Array.from(container.querySelectorAll("code"));
455
- if (codeNodes.length > 0) {
456
- return codeNodes.map((node) => serializeMermaidFromNode(node)).join("\n").trim();
457
- }
458
- const pre = container.querySelector("pre");
459
- if (pre) return serializeMermaidFromNode(pre).trim();
460
- return serializeMermaidFromNode(container).trim();
461
- }
462
- function getMermaidSvg() {
463
- if (!mermaidContainer.value) return null;
464
- const svg = mermaidContainer.value.querySelector("svg");
465
- return svg instanceof SVGSVGElement ? svg : null;
466
- }
467
- async function renderMermaid() {
468
- if (!mermaidContainer.value || !mermaidDefinition.value) return;
469
- isLoading.value = true;
470
- const performRender = async () => {
471
- if (!mermaidContainer.value) return;
472
- if (import.meta.client && isExpandActive.value) {
473
- resetExpand();
474
- }
475
- hasError.value = false;
476
- errorContent.value = null;
477
- const startTime = performance.now();
478
- try {
479
- const mermaid = await $mermaid();
480
- mermaid.initialize(effectiveMermaidInit.value);
481
- mermaidContainer.value.removeAttribute("data-processed");
482
- mermaidContainer.value.textContent = mermaidDefinition.value;
483
- await nextTick();
484
- await mermaid.run({
485
- nodes: [mermaidContainer.value],
486
- suppressErrors: !debug
487
- });
488
- hasRenderedOnce.value = true;
489
- const svg = getMermaidSvg();
490
- if (svg) ensureViewBox(svg);
491
- if (debug) {
492
- const endTime = performance.now();
493
- console.log(`[nuxt-content-mermaid] \u23F1\uFE0F Rendered in ${(endTime - startTime).toFixed(2)}ms`);
77
+ watch(
78
+ configuredSpinnerName,
79
+ async (name) => {
80
+ if (!name) {
81
+ customSpinner.value = null;
82
+ return;
494
83
  }
495
- } catch (error) {
496
- console.error("[nuxt-content-mermaid]", error);
497
- hasError.value = true;
498
- errorContent.value = error;
499
- if (mermaidContainer.value)
500
- mermaidContainer.value.innerHTML = "";
501
- } finally {
502
- isLoading.value = false;
503
- }
504
- };
505
- await enqueueRender(performRender, debug);
506
- }
507
- function setupMermaidContainer() {
508
- const container = mermaidContainer.value;
509
- if (!container) return;
510
- if (!isLazy) {
511
- renderMermaid();
512
- return;
513
- }
514
- observer = new IntersectionObserver(
515
- (entries) => {
516
- if (entries[0]?.isIntersecting && !hasRenderedOnce.value) {
517
- renderMermaid();
518
- if (observer) observer.disconnect();
84
+ customSpinner.value = await resolveSpinnerAppComponent(name, appComponents);
85
+ },
86
+ { immediate: true }
87
+ );
88
+ const beginRendererSelectionAttempt = createRendererSelectionAttemptCoordinator();
89
+ watch(
90
+ configuredMermaidImplName,
91
+ async (name) => {
92
+ const commitRendererSelectionAttempt = beginRendererSelectionAttempt();
93
+ const outcome = selectRenderer(name, {
94
+ loadComponent: (candidate) => loadAppComponent(candidate, appComponents)
95
+ });
96
+ if (outcome.status === "no-candidate") {
97
+ rendererSelectionState.value = { status: "built-in" };
98
+ return;
519
99
  }
100
+ rendererSelectionState.value = { status: "pending" };
101
+ const commitResolutionFailure = createRendererResolutionFailureHandoff({
102
+ reportDiagnostic: (diagnostic) => {
103
+ const failureContext = diagnostic.reason === "load-failed" ? [diagnostic.error] : [];
104
+ console.warn(
105
+ MERMAID_LOG_PREFIX,
106
+ `Custom Renderer Candidate "${diagnostic.candidate}" resolution failed (${diagnostic.reason}).`,
107
+ diagnostic,
108
+ ...failureContext
109
+ );
110
+ },
111
+ commitBuiltInOwnership: () => {
112
+ rendererSelectionState.value = { status: "built-in" };
113
+ }
114
+ });
115
+ const resolvedOutcome = await outcome.resolution;
116
+ commitRendererSelectionAttempt(resolvedOutcome, {
117
+ commitCustomOwnership: (component) => {
118
+ rendererSelectionState.value = {
119
+ status: "custom",
120
+ component
121
+ };
122
+ },
123
+ commitResolutionFailure
124
+ });
520
125
  },
521
- { threshold: lazyThreshold }
126
+ { immediate: true }
522
127
  );
523
- observer.observe(container);
524
128
  }
525
- async function copyMermaidSource() {
526
- if (!copySource.value) return;
527
- const copied = await copyToClipboard(copySource.value);
528
- copyState.value = copied ? "copied" : "error";
529
- if (copyResetTimer)
530
- clearTimeout(copyResetTimer);
531
- copyResetTimer = setTimeout(() => {
532
- copyState.value = "idle";
533
- }, 2e3);
534
- }
535
- async function copyToClipboard(text) {
536
- if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
537
- try {
538
- await navigator.clipboard.writeText(text);
539
- return true;
540
- } catch {
541
- }
542
- }
543
- if (typeof document === "undefined")
544
- return false;
545
- const textarea = document.createElement("textarea");
546
- textarea.value = text;
547
- textarea.setAttribute("readonly", "");
548
- textarea.style.position = "fixed";
549
- textarea.style.top = "-9999px";
550
- textarea.style.opacity = "0";
551
- document.body.appendChild(textarea);
552
- textarea.select();
553
- let copied = false;
554
- try {
555
- copied = document.execCommand("copy");
556
- } catch {
557
- copied = false;
558
- } finally {
559
- document.body.removeChild(textarea);
560
- }
561
- return copied;
562
- }
563
- onMounted(() => {
564
- if (!isEnabled) return;
565
- if (!mermaidDefinition.value && mermaidContainer.value)
566
- mermaidDefinition.value = extractMermaidDefinition(mermaidContainer.value);
567
- nextTick(() => setupMermaidContainer());
568
- });
569
- onUnmounted(() => {
570
- if (observer) observer.disconnect();
571
- if (copyResetTimer) clearTimeout(copyResetTimer);
572
- });
573
- watch(mermaidTheme, () => {
574
- if (!isEnabled) return;
575
- if (hasRenderedOnce.value) renderMermaid();
576
- });
577
129
  watch(
578
- pageConfig,
130
+ () => collectMermaidComponentSourceDependencies({
131
+ pageConfig: props.pageConfig,
132
+ config: props.config
133
+ }),
579
134
  () => {
580
- if (!isEnabled) return;
581
- if (hasRenderedOnce.value) renderMermaid();
135
+ const wasConflict = componentSource.value.kind === "conflict";
136
+ const source = resolveCurrentComponentSource();
137
+ componentSource.value = source;
138
+ if (source.kind === "conflict" && !wasConflict)
139
+ throw source.error;
582
140
  },
583
- { deep: true }
141
+ { flush: "post" }
584
142
  );
585
- watch(decodedCode, (newCode) => {
586
- if (!isEnabled) return;
587
- mermaidDefinition.value = newCode;
588
- if (hasRenderedOnce.value) renderMermaid();
589
- });
590
- function ensureViewBox(svg) {
591
- if (svg.hasAttribute("viewBox"))
592
- return;
593
- try {
594
- const bbox = svg.getBBox();
595
- if (bbox.width > 0 && bbox.height > 0)
596
- svg.setAttribute("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);
597
- } catch {
598
- }
599
- }
600
- const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
601
143
  </script>
602
144
 
603
145
  <template>
604
146
  <div class="mermaid-outer-wrapper">
605
- <div
606
- v-if="!isEnabled"
607
- class="mermaid-wrapper"
608
- >
609
- <div
610
- ref="mermaidContainer"
611
- class="mermaid"
612
- >
613
- <slot />
614
- </div>
615
- </div>
616
-
617
147
  <component
618
- :is="customMermaidImpl"
619
- v-else-if="customMermaidImpl"
148
+ :is="rendererSelectionState?.component"
149
+ v-if="rendererSelectionState?.status === 'custom'"
620
150
  :spinner="spinnerComponent"
621
151
  :code="decodedCode"
622
152
  >
@@ -625,174 +155,37 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
625
155
  </slot>
626
156
  </component>
627
157
 
628
- <div
158
+ <template v-else-if="rendererSelectionState?.status === 'pending'">
159
+ <slot>
160
+ <pre v-if="decodedCode"><code>{{ decodedCode }}</code></pre>
161
+ </slot>
162
+ </template>
163
+
164
+ <BuiltInRenderer
629
165
  v-else
630
- ref="mermaidBlock"
631
- class="mermaid-block"
632
- :style="{ '--ncm-icon-size': `${iconSize}px` }"
166
+ v-bind="props"
167
+ :component-source="componentSource"
168
+ :spinner-component="spinnerComponent"
633
169
  >
634
- <!-- Toolbar: Title and Actions -->
635
- <div class="mermaid-toolbar">
636
- <div
637
- class="mermaid-title"
638
- :style="{ fontSize: effectiveToolbarFontSize }"
639
- >
640
- <span v-if="toolbarTitle">{{ toolbarTitle }}</span>
641
- </div>
642
- <div class="mermaid-actions">
643
- <button
644
- v-if="showCopyButton"
645
- type="button"
646
- class="mermaid-btn"
647
- :title="copyLabel"
648
- :aria-label="copyLabel"
649
- :disabled="!hasCopySource"
650
- @click="copyMermaidSource"
651
- @mouseenter="isCopyHovered = true"
652
- @mouseleave="isCopyHovered = false"
653
- >
654
- <component
655
- :is="copyIcon"
656
- :size="iconSize"
657
- />
658
- </button>
659
- <button
660
- v-if="showExpandToolbarButton"
661
- type="button"
662
- class="mermaid-btn"
663
- :title="isExpandActive ? 'Collapse' : 'Expand'"
664
- :aria-label="isExpandActive ? 'Collapse diagram' : 'Expand diagram'"
665
- @click="toggleExpand"
666
- >
667
- <IconExpand
668
- v-if="!isExpandActive"
669
- :size="iconSize"
670
- />
671
- <IconCollapse
672
- v-else
673
- :size="iconSize"
674
- />
675
- </button>
676
- <button
677
- v-if="showFullscreenButton"
678
- type="button"
679
- class="mermaid-btn"
680
- :title="fullscreenLabel"
681
- :aria-label="fullscreenLabel"
682
- @click="toggleFullscreen"
683
- >
684
- <IconFullScreen
685
- v-if="!isFullscreen"
686
- :size="iconSize"
687
- />
688
- <IconFullScreenExit
689
- v-else
690
- :size="iconSize"
691
- />
692
- </button>
693
- </div>
694
- </div>
695
-
696
- <MermaidZoomToolbar
697
- v-if="isFullscreen"
698
- variant="fullscreen"
699
- :scale="fullscreenZoom.scale.value"
700
- :icon-size="iconSize"
701
- @zoom-out="fullscreenZoom.zoomOut()"
702
- @zoom-in="fullscreenZoom.zoomIn()"
703
- @reset="fullscreenZoom.reset()"
704
- />
705
-
706
- <!-- Fullscreen Zoom Hint -->
707
- <Transition name="ncm-hint-fade">
708
- <div
709
- v-if="showFullscreenZoomHint && isFullscreen"
710
- class="ncm-fullscreen-zoom-hint"
711
- >
712
- {{ isMac ? "\u2318" : "Ctrl" }} + Scroll to zoom
713
- </div>
714
- </Transition>
715
-
716
- <!-- Diagram Container -->
717
- <div
718
- class="mermaid-wrapper"
719
- :class="{
720
- 'ncm-is-loading': isLoading && !hasRenderedOnce,
721
- 'ncm-expand-hidden': isExpandActive,
722
- 'ncm-expand-clickable': expandEnabled && allowOpenDiagramClick && !isFullscreen,
723
- 'ncm-fullscreen-zoom': isFullscreen
724
- }"
725
- :style="cursorVariables"
726
- @click="handleMermaidClick"
727
- @mousedown="isFullscreen && fullscreenZoom.handleDragStart($event)"
728
- @mousemove="isFullscreen && fullscreenZoom.handleDragMove($event)"
729
- @touchstart="isFullscreen && fullscreenZoom.handleDragStart($event)"
730
- @touchmove="isFullscreen && fullscreenZoom.handleDragMove($event)"
731
- @touchend="isFullscreen && fullscreenZoom.handleDragEnd()"
170
+ <slot>
171
+ <pre v-if="decodedCode"><code>{{ decodedCode }}</code></pre>
172
+ </slot>
173
+ <template
174
+ v-if="$slots.loading"
175
+ #loading
732
176
  >
733
- <div
734
- ref="mermaidContainer"
735
- class="mermaid"
736
- :style="isFullscreen ? {
737
- transform: fullscreenZoom.transformStyle.value.transform,
738
- transformOrigin: '0 0',
739
- cursor: fullscreenZoom.cursor.value
740
- } : void 0"
741
- >
742
- <!-- Initially show the slot's <pre><code> for SSR; client-side rendering will replace it with the mermaid SVG -->
743
- <slot>
744
- <pre v-if="decodedCode"><code>{{ decodedCode }}</code></pre>
745
- </slot>
746
- </div>
747
-
748
- <template v-if="isLoading && !hasRenderedOnce">
749
- <slot name="loading">
750
- <component :is="spinnerComponent" />
751
- </slot>
752
- </template>
753
- <template v-else-if="hasError">
754
- <slot
755
- name="error"
756
- :error="errorContent"
757
- :source="mermaidDefinition"
758
- >
759
- <component
760
- :is="errorComponent"
761
- v-if="errorComponent"
762
- :error="errorContent"
763
- :source="mermaidDefinition"
764
- />
765
-
766
- <div
767
- v-else
768
- class="mermaid-error-default"
769
- >
770
- ⚠️ Mermaid Diagram Error
771
- </div>
772
- </slot>
773
- </template>
774
- </div>
775
-
776
- <MermaidExpandOverlay
777
- :active="isExpandActive"
778
- :is-visable="isVisible"
779
- :overlay-style="cursorVariables"
780
- :content-style="cursorVariables"
781
- :target-style="expandTargetStyle"
782
- :target-ref="setExpandTargetWrap"
783
- :allow-overlay-close="allowOverlayClose"
784
- :allow-close-button="allowCloseButtonClose"
785
- :target-has-margin="expandMargin > 0"
786
- :icon-size="iconSize"
787
- :zoom="zoom"
788
- :show-hint="showZoomHint"
789
- @close="closeExpand"
790
- @transitionend="handleExpandTransitionEnd"
791
- />
792
- </div>
177
+ <slot name="loading" />
178
+ </template>
179
+ <template
180
+ v-if="$slots.error"
181
+ #error="{ error, source }"
182
+ >
183
+ <slot
184
+ name="error"
185
+ :error="error"
186
+ :source="source"
187
+ />
188
+ </template>
189
+ </BuiltInRenderer>
793
190
  </div>
794
191
  </template>
795
-
796
- <style scoped>
797
- .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)}.ncm-fullscreen-zoom-hint{display:none}:fullscreen .ncm-fullscreen-zoom-hint{background-color:var(--ncm-hint-bg,rgba(0,0,0,.75));border-radius:var(--ncm-hint-radius,8px);color:var(--ncm-hint-text,#fff);display:block;font-size:20px;font-weight:500;left:50%;padding:12px 24px;pointer-events:none;position:absolute;top:50%;transform:translate(-50%,-50%);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap;z-index:20}.ncm-hint-fade-enter-active,.ncm-hint-fade-leave-active{transition:opacity .2s ease}.ncm-hint-fade-enter-from,.ncm-hint-fade-leave-to{opacity:0}@media (prefers-reduced-motion:reduce){.ncm-hint-fade-enter-active,.ncm-hint-fade-leave-active{transition-duration:.01ms!important}}.mermaid-error-default{color:#f55;font-family:monospace;padding:1rem;white-space:pre-wrap}
798
- </style>