@barzhsieh/nuxt-content-mermaid 1.0.1 → 1.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  [English](./README.md) | [中文](./README.zh-TW.md)
2
2
 
3
- # nuxt-mermaid-content
3
+ # nuxt-content-mermaid
4
4
 
5
5
  [![npm version][npm-version-src]][npm-version-href]
6
6
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
@@ -16,7 +16,7 @@ A Nuxt module designed for integrating Mermaid with `@nuxt/content`. It automati
16
16
  - **Automatic conversion**: Parses Markdown code blocks and replaces them with a `<Mermaid>` rendering component.
17
17
  - **Performance friendly**: Supports lazy loading — Mermaid core and related resources are only loaded when the component mounts.
18
18
  - **Theme integration**: Integrates with `@nuxtjs/color-mode` to automatically switch between light and dark Mermaid themes.
19
- - **Highly customizable**: Allows custom wrapper components, loading spinners, and custom CDN or local import sources.
19
+ - **Highly customizable**: Allows custom wrapper components, loading spinners, error views, and custom CDN or local import sources.
20
20
  - **Runtime config**: Settings can be overridden at deployment time through runtime config / environment variables.
21
21
 
22
22
  ## Requirements
@@ -84,6 +84,7 @@ export default defineNuxtConfig({
84
84
  components: {
85
85
  renderer: undefined,
86
86
  spinner: undefined,
87
+ error: undefined,
87
88
  },
88
89
  },
89
90
  });
@@ -118,6 +119,7 @@ export default defineNuxtConfig({
118
119
  | :------------------------ | :------- | :---------- | :----------------------------------------------------------------------- |
119
120
  | `components.renderer` | `string` | `undefined` | Optional: custom Mermaid renderer component name. |
120
121
  | `components.spinner` | `string` | `undefined` | Optional: global loading spinner component name. |
122
+ | `components.error` | `string` | `undefined` | Optional: global error component name when Mermaid rendering fails. |
121
123
 
122
124
  > **Note**: All options can be overridden at runtime via `runtimeConfig.public.mermaidContent`.
123
125
 
@@ -164,6 +166,117 @@ If you want full control over rendering (for example to add a border, zoom contr
164
166
  </template>
165
167
  ```
166
168
 
169
+ #### Loading Indicator
170
+
171
+ The module renders a built-in spinner before the first Mermaid render. If you set `components.spinner`, the named component will replace the default spinner and will be passed into your custom renderer as a `spinner` prop.
172
+
173
+ Minimal example that uses the provided spinner while waiting for your own loading logic:
174
+
175
+ ```ts
176
+ // nuxt.config.ts
177
+ export default defineNuxtConfig({
178
+ mermaidContent: {
179
+ components: {
180
+ renderer: 'MyCustomMermaid',
181
+ spinner: 'MySpinner', // optional: globally replace the default spinner
182
+ },
183
+ },
184
+ })
185
+ ```
186
+
187
+ ```vue
188
+ <!-- components/MyCustomMermaid.vue -->
189
+ <script setup lang="ts">
190
+ import { ref, onMounted } from 'vue'
191
+ import type { Component } from 'vue'
192
+
193
+ const props = defineProps<{ spinner: Component | string }>()
194
+ const loading = ref(true)
195
+ onMounted(() => { loading.value = false })
196
+ </script>
197
+
198
+ <template>
199
+ <div class="my-mermaid">
200
+ <component v-if="loading" :is="props.spinner" />
201
+ <Mermaid v-else>
202
+ <slot />
203
+ </Mermaid>
204
+ </div>
205
+ </template>
206
+ ```
207
+
208
+ ### Wrapper Example
209
+
210
+ You can wrap `<Mermaid>` inside your own Vue component.
211
+ For example, you can bundle a title, loading state, and error/fallback UI, then drop it anywhere in templates:
212
+
213
+ ```vue
214
+ <!-- WrapperMermaid.vue -->
215
+ <template>
216
+ <section>
217
+ <header v-if="title">{{ title }}</header>
218
+
219
+ <Mermaid>
220
+ <slot>
221
+ <pre><code>{{ code }}</code></pre>
222
+ </slot>
223
+
224
+ <template #loading>
225
+ <component :is="spinner" v-if="spinner" />
226
+ <p v-else>Diagram loading…</p>
227
+ </template>
228
+
229
+ <template #error="{ error, source }">
230
+ <p>Render failed: {{ error instanceof Error ? error.message : String(error) }}</p>
231
+ <pre><code>{{ source }}</code></pre>
232
+ </template>
233
+ </Mermaid>
234
+ </section>
235
+ </template>
236
+ ```
237
+
238
+ ```vue
239
+ <!-- usage -->
240
+ <WrapperMermaid
241
+ title="Demo Diagram"
242
+ spinner="MySpinner"
243
+ >
244
+ <pre><code>graph TD; A-->B; B-->C; C-->A</code></pre>
245
+ </WrapperMermaid>
246
+ ```
247
+
248
+ Copy or adapt this pattern to centralize the common slots you want to reuse.
249
+
250
+ ### Error Handling
251
+
252
+ When Mermaid parsing/rendering fails, the component exposes an `error` slot and supports a global error component via `components.error`. Both receive the thrown error and the original Mermaid source for inspection.
253
+
254
+ ```vue
255
+ <Mermaid>
256
+ <pre><code>graph TD; A-->B; B-->C; C-->A</code></pre>
257
+
258
+ <template #error="{ error, source }">
259
+ <p>Render failed: {{ error instanceof Error ? error.message : String(error) }}</p>
260
+ <details>
261
+ <summary>Show definition</summary>
262
+ <pre><code>{{ source }}</code></pre>
263
+ </details>
264
+ </template>
265
+ </Mermaid>
266
+ ```
267
+
268
+ To reuse a global error view everywhere, register it once and reference it in config:
269
+
270
+ ```ts
271
+ // nuxt.config.ts
272
+ export default defineNuxtConfig({
273
+ mermaidContent: {
274
+ components: {
275
+ error: 'MermaidError', // globally registered component name
276
+ },
277
+ },
278
+ })
279
+ ```
167
280
  ## Contribution
168
281
 
169
282
  <details>
package/README.zh-TW.md CHANGED
@@ -1,6 +1,6 @@
1
1
  [English](./README.md) | [中文](./README.zh-TW.md)
2
2
 
3
- # nuxt-mermaid-content
3
+ # nuxt-content-mermaid
4
4
 
5
5
  [![npm version][npm-version-src]][npm-version-href]
6
6
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
@@ -16,7 +16,7 @@
16
16
  - **自動轉換**:解析 Markdown 代碼區塊並替換為 `<Mermaid>` 渲染元件。
17
17
  - **效能優化**:支援 Lazy Loading,僅在元件掛載時載入 Mermaid 核心與資源。
18
18
  - **主題整合**:無縫整合 `@nuxtjs/color-mode`,自動切換 Light/Dark 對應主題。
19
- - **高度客製**:支援自訂渲染元件(Wrapper)、Loading Spinner 及 CDN 來源。
19
+ - **高度客製**:支援自訂渲染元件(Wrapper)、Loading Spinner、錯誤畫面以及 CDN 來源。
20
20
  - **Runtime Config**:支援透過環境變數動態覆寫設定。
21
21
 
22
22
  ## 使用前提
@@ -84,6 +84,7 @@ export default defineNuxtConfig({
84
84
  components: {
85
85
  renderer: undefined,
86
86
  spinner: undefined,
87
+ error: undefined,
87
88
  },
88
89
  },
89
90
  });
@@ -118,6 +119,7 @@ export default defineNuxtConfig({
118
119
  | :----------------------- | :------- | :--------- | :----------------------------------------------------- |
119
120
  | `components.renderer` | `string` | `undefined` | 指定自訂的 Mermaid 實作元件名稱(見進階用法)。 |
120
121
  | `components.spinner` | `string` | `undefined` | 指定全域的 Loading 元件名稱。 |
122
+ | `components.error` | `string` | `undefined` | 指定全域的錯誤顯示元件名稱,渲染失敗時會使用。 |
121
123
 
122
124
  > **Note**: 所有設定皆可透過 `runtimeConfig.public.mermaidContent` 在部署時進行覆寫。
123
125
 
@@ -164,6 +166,117 @@ export default defineNuxtConfig({
164
166
  </template>
165
167
  ```
166
168
 
169
+ #### 載入中指示
170
+
171
+ 模組在首次渲染前會使用內建的 spinner,如需替換可在 `components.spinner` 指定元件名稱;該元件也會以 `spinner` prop 傳入你的自訂渲染元件,方便直接渲染。
172
+
173
+ 以下是極簡的設定與元件示例:
174
+
175
+ ```ts
176
+ // nuxt.config.ts
177
+ export default defineNuxtConfig({
178
+ mermaidContent: {
179
+ components: {
180
+ renderer: 'MyCustomMermaid',
181
+ spinner: 'MySpinner', // 選填:改用自己的全域 Loading 元件
182
+ }
183
+ }
184
+ })
185
+ ```
186
+
187
+ ```vue
188
+ <!-- components/MyCustomMermaid.vue -->
189
+ <script setup lang="ts">
190
+ import { ref, onMounted } from 'vue'
191
+ import type { Component } from 'vue'
192
+
193
+ const props = defineProps<{ spinner: Component | string }>()
194
+ const loading = ref(true)
195
+ onMounted(() => { loading.value = false })
196
+ </script>
197
+
198
+ <template>
199
+ <div class="my-mermaid">
200
+ <component v-if="loading" :is="props.spinner" />
201
+ <Mermaid v-else>
202
+ <slot />
203
+ </Mermaid>
204
+ </div>
205
+ </template>
206
+ ```
207
+
208
+ ### 元件使用方式
209
+
210
+ 可以用 `<Mermaid>` 自己包一個的 Vue 元件 。
211
+ 例如,你可以同時放入標題、Loading 與錯誤顯示,之後就能在任意模板重複使用:
212
+ ```vue
213
+ <!-- WrapperMermaid.vue -->
214
+ <template>
215
+ <section>
216
+ <header v-if="title">{{ title }}</header>
217
+
218
+ <Mermaid>
219
+ <slot>
220
+ <pre><code>{{ code }}</code></pre>
221
+ </slot>
222
+
223
+ <template #loading>
224
+ <component :is="spinner" v-if="spinner" />
225
+ <p v-else>Diagram loading…</p>
226
+ </template>
227
+
228
+ <template #error="{ error, source }">
229
+ <p>渲染失敗:{{ error instanceof Error ? error.message : String(error) }}</p>
230
+ <pre><code>{{ source }}</code></pre>
231
+ </template>
232
+ </Mermaid>
233
+ </section>
234
+ </template>
235
+ ```
236
+
237
+ ```vue
238
+ <!-- 使用範例 -->
239
+ <WrapperMermaid
240
+ title="Demo Diagram"
241
+ spinner="MySpinner"
242
+ >
243
+ <pre><code>graph TD; A-->B; B-->C; C-->A</code></pre>
244
+ </WrapperMermaid>
245
+ ```
246
+
247
+ 可依需求調整此模式,把常用的 slot 寫在一個可重用的包裝元件中。
248
+
249
+ ### 錯誤處理
250
+
251
+ 當 Mermaid 解析或渲染失敗時,`<Mermaid>` 會觸發 `error` slot,並可透過 `components.error` 指定全域錯誤元件。兩者都會拿到錯誤內容與原始 Mermaid 定義,方便除錯。
252
+
253
+ ```vue
254
+ <Mermaid>
255
+ <pre><code>graph TD; A-->B; B-->C; C-->A</code></pre>
256
+
257
+ <template #error="{ error, source }">
258
+ <p>渲染失敗:{{ error instanceof Error ? error.message : String(error) }}</p>
259
+ <details>
260
+ <summary>查看原始定義</summary>
261
+ <pre><code>{{ source }}</code></pre>
262
+ </details>
263
+ </template>
264
+ </Mermaid>
265
+ ```
266
+
267
+ 若想一次註冊、全域套用自訂錯誤畫面,可在設定中指定元件名稱:
268
+
269
+ ```ts
270
+ // nuxt.config.ts
271
+ export default defineNuxtConfig({
272
+ mermaidContent: {
273
+ components: {
274
+ error: 'MermaidError', // 全域註冊的元件名稱
275
+ },
276
+ },
277
+ })
278
+ ```
279
+
167
280
  ## Contribution
168
281
 
169
282
  <details>
package/dist/module.d.mts CHANGED
@@ -19,6 +19,14 @@ interface ModuleOptions {
19
19
  * Pass-through to `mermaid.initialize`
20
20
  */
21
21
  init?: MermaidConfig;
22
+ /**
23
+ * Whether to lazy load the diagram when it enters the viewport.
24
+ * Can be a boolean or an object with IntersectionObserver options.
25
+ * @default true
26
+ */
27
+ lazy?: boolean | {
28
+ threshold?: number;
29
+ };
22
30
  };
23
31
  /**
24
32
  * Options related to theme handling
@@ -48,6 +56,12 @@ interface ModuleOptions {
48
56
  * If not provided, the module's built-in `Spinner.vue` will be used.
49
57
  */
50
58
  spinner?: string;
59
+ /**
60
+ * Error component name to display when Mermaid rendering fails.
61
+ * This should be a globally available component name (e.g., under the project `components/` directory).
62
+ * If not provided, the module's built-in fallback message will be used.
63
+ */
64
+ error?: string;
51
65
  };
52
66
  }
53
67
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^4.0.0"
6
6
  },
7
- "version": "1.0.1",
7
+ "version": "1.0.2",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,7 +7,8 @@ const DEFAULTS = {
7
7
  enabled: true,
8
8
  loader: {
9
9
  importSource: DEFAULT_IMPORT_SOURCE,
10
- init: { ...DEFAULT_MERMAID_INIT }
10
+ init: { ...DEFAULT_MERMAID_INIT },
11
+ lazy: true
11
12
  },
12
13
  theme: {
13
14
  useColorModeTheme: true,
@@ -16,7 +17,8 @@ const DEFAULTS = {
16
17
  },
17
18
  components: {
18
19
  renderer: void 0,
19
- spinner: void 0
20
+ spinner: void 0,
21
+ error: void 0
20
22
  }
21
23
  };
22
24
  const module$1 = defineNuxtModule({
@@ -1,14 +1,17 @@
1
- declare var __VLS_8: {}, __VLS_10: {}, __VLS_12: {}, __VLS_14: {}, __VLS_22: {};
1
+ declare var __VLS_1: {}, __VLS_9: {}, __VLS_11: {}, __VLS_13: {}, __VLS_21: {
2
+ error: unknown;
3
+ source: string;
4
+ };
2
5
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_8) => any;
6
+ default?: (props: typeof __VLS_1) => any;
4
7
  } & {
5
- default?: (props: typeof __VLS_10) => any;
8
+ default?: (props: typeof __VLS_9) => any;
6
9
  } & {
7
- default?: (props: typeof __VLS_12) => any;
10
+ default?: (props: typeof __VLS_11) => any;
8
11
  } & {
9
- loading?: (props: typeof __VLS_14) => any;
12
+ loading?: (props: typeof __VLS_13) => any;
10
13
  } & {
11
- default?: (props: typeof __VLS_22) => any;
14
+ error?: (props: typeof __VLS_21) => any;
12
15
  };
13
16
  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>;
14
17
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -7,41 +7,46 @@ import {
7
7
  nextTick,
8
8
  computed,
9
9
  watch,
10
- shallowRef
10
+ shallowRef,
11
+ useTemplateRef
11
12
  } from "vue";
13
+ import { enqueueRender } from "../utils";
12
14
  import Spinner from "./Spinner.vue";
13
15
  const nuxtApp = useNuxtApp();
14
16
  const runtimeConfig = useRuntimeConfig();
15
- const mermaidRuntime = runtimeConfig.public?.mermaidContent || {};
16
- const isEnabled = mermaidRuntime.enabled !== false;
17
- const loaderOptions = mermaidRuntime.loader || {};
18
- const themeOptions = mermaidRuntime.theme || {};
19
- const componentOptions = mermaidRuntime.components || {};
17
+ const mermaidContent = runtimeConfig.public?.mermaidContent || {};
18
+ const isEnabled = mermaidContent.enabled !== false;
19
+ const loaderOptions = mermaidContent.loader || {};
20
+ const themeOptions = mermaidContent.theme || {};
21
+ const componentOptions = mermaidContent.components || {};
22
+ const lazyOption = loaderOptions.lazy;
23
+ const isLazy = lazyOption !== false;
24
+ const lazyThreshold = typeof lazyOption === "object" && typeof lazyOption.threshold === "number" ? lazyOption.threshold : 0.1;
25
+ const baseMermaidInit = loaderOptions.init || {};
26
+ const useColorModeTheme = themeOptions.useColorModeTheme;
27
+ const lightTheme = themeOptions.light;
28
+ const darkTheme = themeOptions.dark;
20
29
  const colorMode = nuxtApp.$colorMode;
21
30
  const { $mermaid } = nuxtApp;
22
- const mermaidContainer = ref(null);
23
- const isMounted = ref(false);
31
+ const mermaidContainer = useTemplateRef("mermaidContainer");
24
32
  const hasRenderedOnce = ref(false);
25
33
  const isLoading = ref(false);
34
+ const hasError = ref(false);
35
+ const errorContent = shallowRef(null);
26
36
  let mermaidDefinition = "";
27
37
  let observer = null;
28
- const baseMermaidInit = loaderOptions.init || {};
29
- const useColorModeTheme = themeOptions.useColorModeTheme;
30
- const lightTheme = themeOptions.light;
31
- const darkTheme = themeOptions.dark;
32
38
  const mermaidTheme = computed(() => {
33
39
  if (useColorModeTheme && colorMode)
34
40
  return colorMode.value === "dark" ? darkTheme : lightTheme;
35
41
  return baseMermaidInit.theme ?? lightTheme ?? darkTheme;
36
42
  });
37
- const configuredSpinnerName = computed(
38
- () => componentOptions.spinner?.trim() || ""
39
- );
43
+ const configuredSpinnerName = computed(() => componentOptions.spinner?.trim() || "");
40
44
  const customSpinner = shallowRef(null);
41
- const configuredMermaidImplName = computed(
42
- () => componentOptions.renderer?.trim() || ""
43
- );
45
+ const configuredMermaidImplName = computed(() => componentOptions.renderer?.trim() || "");
44
46
  const customMermaidImpl = shallowRef(null);
47
+ const configuredErrorName = computed(() => componentOptions.error?.trim() || "");
48
+ const errorComponent = shallowRef(null);
49
+ const spinnerComponent = computed(() => customSpinner.value || Spinner);
45
50
  function normalizeIdentifier(value) {
46
51
  return value.replace(/\.vue$/i, "").split(/[\\/]/).pop()?.replace(/[\s_-]+/g, "").toLowerCase() || "";
47
52
  }
@@ -54,59 +59,46 @@ async function resolveAppComponent(name, appComponents, label) {
54
59
  });
55
60
  if (!matchEntry) {
56
61
  console.warn(
57
- `[nuxt-mermaid-content] Cannot find ${label} component:`,
62
+ `[nuxt-content-mermaid] Cannot find ${label} component:`,
58
63
  name
59
64
  );
60
65
  return null;
61
66
  }
62
67
  try {
63
- const [, loader] = matchEntry;
64
- const mod = await loader();
68
+ const [, componentLoader] = matchEntry;
69
+ const mod = await componentLoader();
65
70
  return mod.default || mod;
66
71
  } catch (error) {
67
72
  console.error(
68
- `[nuxt-mermaid-content] Failed to load ${label} component:`,
73
+ `[nuxt-content-mermaid] Failed to load ${label} component:`,
69
74
  error
70
75
  );
71
76
  return null;
72
77
  }
73
78
  }
74
79
  if (import.meta.client && isEnabled) {
80
+ let watchCustomAppComponent = function(nameRef, targetRef, label) {
81
+ watch(
82
+ nameRef,
83
+ async (name) => {
84
+ if (!name) {
85
+ targetRef.value = null;
86
+ return;
87
+ }
88
+ targetRef.value = await resolveAppComponent(
89
+ name,
90
+ appComponents,
91
+ label
92
+ );
93
+ },
94
+ { immediate: true }
95
+ );
96
+ };
75
97
  const appComponents = import.meta.glob("~/components/**/*.{vue,js,ts}");
76
- watch(
77
- configuredSpinnerName,
78
- async (name) => {
79
- if (!name) {
80
- customSpinner.value = null;
81
- return;
82
- }
83
- customSpinner.value = await resolveAppComponent(
84
- name,
85
- appComponents,
86
- "spinner"
87
- );
88
- },
89
- { immediate: true }
90
- );
91
- watch(
92
- configuredMermaidImplName,
93
- async (name) => {
94
- if (!name) {
95
- customMermaidImpl.value = null;
96
- return;
97
- }
98
- customMermaidImpl.value = await resolveAppComponent(
99
- name,
100
- appComponents,
101
- "mermaid"
102
- );
103
- },
104
- { immediate: true }
105
- );
98
+ watchCustomAppComponent(configuredSpinnerName, customSpinner, "spinner");
99
+ watchCustomAppComponent(configuredMermaidImplName, customMermaidImpl, "mermaid");
100
+ watchCustomAppComponent(configuredErrorName, errorComponent, "error");
106
101
  }
107
- const spinnerComponent = computed(() => {
108
- return customSpinner.value || Spinner;
109
- });
110
102
  function serializeMermaidFromNode(root) {
111
103
  const TEXT_NODE = 3;
112
104
  const ELEMENT_NODE = 1;
@@ -125,66 +117,73 @@ function serializeMermaidFromNode(root) {
125
117
  }
126
118
  return "";
127
119
  }
120
+ function extractMermaidDefinition(container) {
121
+ const pre = container.querySelector("pre");
122
+ const code = container.querySelector("code");
123
+ if (code) return code.textContent?.trim() || "";
124
+ if (pre) return pre.textContent?.trim() || "";
125
+ return serializeMermaidFromNode(container).trim();
126
+ }
128
127
  async function renderMermaid() {
129
128
  if (!mermaidContainer.value || !mermaidDefinition) return;
130
- try {
131
- isLoading.value = true;
132
- const mermaid = await $mermaid();
133
- const theme = mermaidTheme.value;
134
- const initOptions = {
135
- startOnLoad: false,
136
- ...baseMermaidInit,
137
- theme
138
- };
139
- mermaid.initialize(initOptions);
140
- mermaidContainer.value.removeAttribute("data-processed");
141
- mermaidContainer.value.textContent = mermaidDefinition;
142
- await nextTick();
143
- await mermaid.run({
144
- nodes: [mermaidContainer.value],
145
- suppressErrors: true
146
- });
147
- hasRenderedOnce.value = true;
148
- isLoading.value = false;
149
- } catch (error) {
150
- console.error("[nuxt-mermaid-content]", error);
151
- if (mermaidContainer.value)
152
- mermaidContainer.value.innerHTML = "\u26A0\uFE0F Mermaid Diagram Error";
153
- isLoading.value = false;
154
- }
129
+ isLoading.value = true;
130
+ const performRender = async () => {
131
+ if (!mermaidContainer.value) return;
132
+ hasError.value = false;
133
+ errorContent.value = null;
134
+ try {
135
+ const mermaid = await $mermaid();
136
+ const initOptions = {
137
+ startOnLoad: false,
138
+ ...baseMermaidInit,
139
+ theme: mermaidTheme.value
140
+ };
141
+ mermaid.initialize(initOptions);
142
+ mermaidContainer.value.textContent = mermaidDefinition;
143
+ await nextTick();
144
+ await mermaid.run({
145
+ nodes: [mermaidContainer.value],
146
+ suppressErrors: true
147
+ });
148
+ hasRenderedOnce.value = true;
149
+ await new Promise((resolve) => setTimeout(resolve, 5));
150
+ } catch (error) {
151
+ console.error("[nuxt-content-mermaid]", error);
152
+ hasError.value = true;
153
+ errorContent.value = error;
154
+ if (mermaidContainer.value)
155
+ mermaidContainer.value.innerHTML = "";
156
+ } finally {
157
+ isLoading.value = false;
158
+ }
159
+ };
160
+ await enqueueRender(performRender);
155
161
  }
156
162
  function setupMermaidContainer() {
157
- if (mermaidContainer.value) {
158
- const pre = mermaidContainer.value.querySelector("pre");
159
- const code = mermaidContainer.value.querySelector("code");
160
- if (code) {
161
- mermaidDefinition = code.textContent?.trim() || "";
162
- } else if (pre) {
163
- mermaidDefinition = pre.textContent?.trim() || "";
164
- } else {
165
- mermaidDefinition = serializeMermaidFromNode(
166
- mermaidContainer.value
167
- ).trim();
168
- }
169
- observer = new IntersectionObserver(
170
- (entries) => {
171
- if (entries[0]?.isIntersecting && !hasRenderedOnce.value) {
172
- renderMermaid();
173
- if (observer) {
174
- observer.disconnect();
175
- }
176
- }
177
- },
178
- { threshold: 0.1 }
179
- );
180
- observer.observe(mermaidContainer.value);
163
+ const container = mermaidContainer.value;
164
+ if (!container) return;
165
+ if (!mermaidDefinition)
166
+ mermaidDefinition = extractMermaidDefinition(container);
167
+ if (!isLazy) {
168
+ renderMermaid();
169
+ return;
181
170
  }
171
+ observer = new IntersectionObserver(
172
+ (entries) => {
173
+ if (entries[0]?.isIntersecting && !hasRenderedOnce.value) {
174
+ renderMermaid();
175
+ if (observer) observer.disconnect();
176
+ }
177
+ },
178
+ { threshold: lazyThreshold }
179
+ );
180
+ observer.observe(container);
182
181
  }
183
- onMounted(async () => {
182
+ onMounted(() => {
184
183
  if (!isEnabled) return;
185
- isMounted.value = true;
186
- await nextTick();
187
- setupMermaidContainer();
184
+ if (mermaidContainer.value)
185
+ mermaidDefinition = extractMermaidDefinition(mermaidContainer.value);
186
+ nextTick(() => setupMermaidContainer());
188
187
  });
189
188
  onUnmounted(() => {
190
189
  if (observer) observer.disconnect();
@@ -196,46 +195,63 @@ watch(mermaidTheme, () => {
196
195
  </script>
197
196
 
198
197
  <template>
199
- <component
200
- :is="customMermaidImpl"
201
- v-if="customMermaidImpl"
202
- >
203
- <slot />
204
- </component>
198
+ <div class="mermaid-outer-wrapper">
199
+ <div
200
+ v-if="!isEnabled"
201
+ class="mermaid-wrapper"
202
+ >
203
+ <div ref="mermaidContainer">
204
+ <slot />
205
+ </div>
206
+ </div>
205
207
 
206
- <div
207
- v-else-if="!isEnabled"
208
- class="mermaid"
209
- >
210
- <slot />
211
- </div>
208
+ <component
209
+ :is="customMermaidImpl"
210
+ v-else-if="customMermaidImpl"
211
+ :spinner="spinnerComponent"
212
+ >
213
+ <slot />
214
+ </component>
212
215
 
213
- <div
214
- v-else-if="isMounted"
215
- class="mermaid-wrapper"
216
- >
217
216
  <div
218
- ref="mermaidContainer"
219
- class="mermaid"
217
+ v-else
218
+ class="mermaid-wrapper"
220
219
  >
221
- <slot />
222
- </div>
220
+ <div ref="mermaidContainer">
221
+ <!-- Initially show the slot's <pre><code> for SSR; client-side rendering will replace it with the mermaid SVG -->
222
+ <slot />
223
+ </div>
223
224
 
224
- <template v-if="isLoading && !hasRenderedOnce">
225
- <slot name="loading">
226
- <component :is="spinnerComponent" />
227
- </slot>
228
- </template>
229
- </div>
225
+ <template v-if="isLoading && !hasRenderedOnce">
226
+ <slot name="loading">
227
+ <component :is="spinnerComponent" />
228
+ </slot>
229
+ </template>
230
+ <template v-else-if="hasError">
231
+ <slot
232
+ name="error"
233
+ :error="errorContent"
234
+ :source="mermaidDefinition"
235
+ >
236
+ <component
237
+ :is="errorComponent"
238
+ v-if="errorComponent"
239
+ :error="errorContent"
240
+ :source="mermaidDefinition"
241
+ />
230
242
 
231
- <div
232
- v-else
233
- class="mermaid"
234
- >
235
- <slot />
243
+ <div
244
+ v-else
245
+ class="mermaid-error-default"
246
+ >
247
+ ⚠️ Mermaid Diagram Error
248
+ </div>
249
+ </slot>
250
+ </template>
251
+ </div>
236
252
  </div>
237
253
  </template>
238
254
 
239
255
  <style scoped>
240
- .mermaid-wrapper{align-items:center;display:flex;justify-content:center;position:relative}.mermaid:not([data-processed]){color:transparent;min-height:10px}.mermaid{display:flex;justify-content:center}.mermaid .edgeLabel,.mermaid .label,.mermaid .nodeLabel{overflow:visible!important;white-space:normal!important}.mermaid foreignObject{overflow:visible!important}
256
+ .mermaid-wrapper{align-items:center;display:flex;justify-content:center;position:relative}.mermaid-wrapper :deep(code),.mermaid-wrapper :deep(pre){margin:0;max-height:160px;opacity:0;overflow:hidden;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.mermaid-error-default{text-align:center}.mermaid:not([data-processed]){color:transparent;min-height:10px}.mermaid{display:flex;justify-content:center}.mermaid .edgeLabel,.mermaid .label,.mermaid .nodeLabel{overflow:visible!important;white-space:normal!important}.mermaid foreignObject{overflow:visible!important}
241
257
  </style>
@@ -1,14 +1,17 @@
1
- declare var __VLS_8: {}, __VLS_10: {}, __VLS_12: {}, __VLS_14: {}, __VLS_22: {};
1
+ declare var __VLS_1: {}, __VLS_9: {}, __VLS_11: {}, __VLS_13: {}, __VLS_21: {
2
+ error: unknown;
3
+ source: string;
4
+ };
2
5
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_8) => any;
6
+ default?: (props: typeof __VLS_1) => any;
4
7
  } & {
5
- default?: (props: typeof __VLS_10) => any;
8
+ default?: (props: typeof __VLS_9) => any;
6
9
  } & {
7
- default?: (props: typeof __VLS_12) => any;
10
+ default?: (props: typeof __VLS_11) => any;
8
11
  } & {
9
- loading?: (props: typeof __VLS_14) => any;
12
+ loading?: (props: typeof __VLS_13) => any;
10
13
  } & {
11
- default?: (props: typeof __VLS_22) => any;
14
+ error?: (props: typeof __VLS_21) => any;
12
15
  };
13
16
  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>;
14
17
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -27,7 +27,7 @@ export default defineNuxtPlugin(() => {
27
27
  provide: {
28
28
  mermaid: async () => {
29
29
  throw new Error(
30
- "[nuxt-mermaid-content] Mermaid is disabled via config."
30
+ "[nuxt-content-mermaid] Mermaid is disabled via config."
31
31
  );
32
32
  }
33
33
  }
@@ -0,0 +1 @@
1
+ export declare function enqueueRender(task: () => Promise<void>): Promise<void>;
@@ -0,0 +1,7 @@
1
+ let renderQueue = Promise.resolve();
2
+ export function enqueueRender(task) {
3
+ renderQueue = renderQueue.then(task).catch((e) => {
4
+ console.error("[nuxt-content-mermaid] Render error:", e);
5
+ });
6
+ return renderQueue;
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barzhsieh/nuxt-content-mermaid",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A Nuxt module designed for integrating Mermaid with `@nuxt/content`. It automatically converts `mermaid` code blocks in Markdown into responsive chart components, and supports lazy loading and dark/light theme switching.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,6 @@
31
31
  "dev:build": "nuxi build playground",
32
32
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
33
33
  "release:base": "npm run lint && npm run test && npm run prepack",
34
-
35
34
  "release:patch": "npm run release:base && changelogen --release --patch --push --publish",
36
35
  "release:minor": "npm run release:base && changelogen --release --minor --push --publish",
37
36
  "release:major": "npm run release:base && changelogen --release --major --push --publish",