@barzhsieh/nuxt-content-mermaid 1.0.1 → 1.0.3

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]
@@ -9,32 +9,50 @@
9
9
  [![Mermaid](https://img.shields.io/badge/mermaid-11.x-0f5b9d?logo=mermaid)](https://mermaid.js.org/)
10
10
  [![Nuxt Content](https://img.shields.io/badge/Nuxt%20Content-3.x-00DC82?logo=nuxt.js)](https://content.nuxt.com/)
11
11
 
12
- 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.
12
+ A Nuxt module designed for integrating [**Mermaid**](https://mermaid.js.org/) with [**Nuxt Content v3**](https://content.nuxt.com/docs/getting-started).
13
+ It automatically converts `mermaid` code blocks in Markdown into responsive chart components, and supports lazy loading and dark/light theme switching.
13
14
 
14
15
  ## Features
15
16
 
16
17
  - **Automatic conversion**: Parses Markdown code blocks and replaces them with a `<Mermaid>` rendering component.
17
18
  - **Performance friendly**: Supports lazy loading — Mermaid core and related resources are only loaded when the component mounts.
18
19
  - **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.
20
+ - **Highly customizable**: Allows custom wrapper components, loading spinners, error views, and custom CDN or local import sources.
20
21
  - **Runtime config**: Settings can be overridden at deployment time through runtime config / environment variables.
21
22
 
22
23
  ## Requirements
23
24
 
24
- - Nuxt 4 (`nuxt@^4.0.0`)
25
- - `@nuxt/content@^3.8.0`
25
+ - Nuxt 3.19+ or 4.1+ (`nuxt@^3.20.1 || ^4.1.0`)
26
+ - `@nuxt/content@>=3.5.0`
26
27
 
27
28
  ## Quick Setup
28
29
 
29
30
  ### 1. Install the module
30
31
 
32
+ **Auto-setup (adds to `modules` automatically):**
33
+
31
34
  ```bash
32
35
  npx nuxi module add @barzhsieh/nuxt-content-mermaid
33
36
  ```
34
37
 
38
+ > Nuxt CLI installs third-party modules as regular `dependencies` by default. If you prefer `devDependencies`, move it after install or use the manual option below.
39
+
40
+ **Install as dev dependency (manual modules entry):**
41
+
42
+ ```bash
43
+ # pnpm
44
+ pnpm add -D @barzhsieh/nuxt-content-mermaid
45
+
46
+ # npm
47
+ npm install -D @barzhsieh/nuxt-content-mermaid
48
+
49
+ # yarn
50
+ yarn add -D @barzhsieh/nuxt-content-mermaid
51
+ ```
52
+
35
53
  ### 2. Configure `nuxt.config.ts`
36
54
 
37
- Make sure the module is included in the `modules` array (if you used `nuxi module add`, this is added automatically):
55
+ Make sure the module is included in the `modules` array (skip if you used `nuxi module add`):
38
56
 
39
57
  ```ts
40
58
  export default defineNuxtConfig({
@@ -84,6 +102,7 @@ export default defineNuxtConfig({
84
102
  components: {
85
103
  renderer: undefined,
86
104
  spinner: undefined,
105
+ error: undefined,
87
106
  },
88
107
  },
89
108
  });
@@ -118,6 +137,7 @@ export default defineNuxtConfig({
118
137
  | :------------------------ | :------- | :---------- | :----------------------------------------------------------------------- |
119
138
  | `components.renderer` | `string` | `undefined` | Optional: custom Mermaid renderer component name. |
120
139
  | `components.spinner` | `string` | `undefined` | Optional: global loading spinner component name. |
140
+ | `components.error` | `string` | `undefined` | Optional: global error component name when Mermaid rendering fails. |
121
141
 
122
142
  > **Note**: All options can be overridden at runtime via `runtimeConfig.public.mermaidContent`.
123
143
 
@@ -164,6 +184,117 @@ If you want full control over rendering (for example to add a border, zoom contr
164
184
  </template>
165
185
  ```
166
186
 
187
+ #### Loading Indicator
188
+
189
+ 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.
190
+
191
+ Minimal example that uses the provided spinner while waiting for your own loading logic:
192
+
193
+ ```ts
194
+ // nuxt.config.ts
195
+ export default defineNuxtConfig({
196
+ mermaidContent: {
197
+ components: {
198
+ renderer: 'MyCustomMermaid',
199
+ spinner: 'MySpinner', // optional: globally replace the default spinner
200
+ },
201
+ },
202
+ })
203
+ ```
204
+
205
+ ```vue
206
+ <!-- components/MyCustomMermaid.vue -->
207
+ <script setup lang="ts">
208
+ import { ref, onMounted } from 'vue'
209
+ import type { Component } from 'vue'
210
+
211
+ const props = defineProps<{ spinner: Component | string }>()
212
+ const loading = ref(true)
213
+ onMounted(() => { loading.value = false })
214
+ </script>
215
+
216
+ <template>
217
+ <div class="my-mermaid">
218
+ <component v-if="loading" :is="props.spinner" />
219
+ <Mermaid v-else>
220
+ <slot />
221
+ </Mermaid>
222
+ </div>
223
+ </template>
224
+ ```
225
+
226
+ ### Wrapper Example
227
+
228
+ You can wrap `<Mermaid>` inside your own Vue component.
229
+ For example, you can bundle a title, loading state, and error/fallback UI, then drop it anywhere in templates:
230
+
231
+ ```vue
232
+ <!-- WrapperMermaid.vue -->
233
+ <template>
234
+ <section>
235
+ <header v-if="title">{{ title }}</header>
236
+
237
+ <Mermaid>
238
+ <slot>
239
+ <pre><code>{{ code }}</code></pre>
240
+ </slot>
241
+
242
+ <template #loading>
243
+ <component :is="spinner" v-if="spinner" />
244
+ <p v-else>Diagram loading…</p>
245
+ </template>
246
+
247
+ <template #error="{ error, source }">
248
+ <p>Render failed: {{ error instanceof Error ? error.message : String(error) }}</p>
249
+ <pre><code>{{ source }}</code></pre>
250
+ </template>
251
+ </Mermaid>
252
+ </section>
253
+ </template>
254
+ ```
255
+
256
+ ```vue
257
+ <!-- usage -->
258
+ <WrapperMermaid
259
+ title="Demo Diagram"
260
+ spinner="MySpinner"
261
+ >
262
+ <pre><code>graph TD; A-->B; B-->C; C-->A</code></pre>
263
+ </WrapperMermaid>
264
+ ```
265
+
266
+ Copy or adapt this pattern to centralize the common slots you want to reuse.
267
+
268
+ ### Error Handling
269
+
270
+ 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.
271
+
272
+ ```vue
273
+ <Mermaid>
274
+ <pre><code>graph TD; A-->B; B-->C; C-->A</code></pre>
275
+
276
+ <template #error="{ error, source }">
277
+ <p>Render failed: {{ error instanceof Error ? error.message : String(error) }}</p>
278
+ <details>
279
+ <summary>Show definition</summary>
280
+ <pre><code>{{ source }}</code></pre>
281
+ </details>
282
+ </template>
283
+ </Mermaid>
284
+ ```
285
+
286
+ To reuse a global error view everywhere, register it once and reference it in config:
287
+
288
+ ```ts
289
+ // nuxt.config.ts
290
+ export default defineNuxtConfig({
291
+ mermaidContent: {
292
+ components: {
293
+ error: 'MermaidError', // globally registered component name
294
+ },
295
+ },
296
+ })
297
+ ```
167
298
  ## Contribution
168
299
 
169
300
  <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]
@@ -9,32 +9,50 @@
9
9
  [![Mermaid](https://img.shields.io/badge/mermaid-11.x-0f5b9d?logo=mermaid)](https://mermaid.js.org/)
10
10
  [![Nuxt Content](https://img.shields.io/badge/Nuxt%20Content-3.x-00DC82?logo=nuxt.js)](https://content.nuxt.com/)
11
11
 
12
- 專為 Nuxt + `@nuxt/content` 設計的 Mermaid 整合模組。能自動將 Markdown 中的 ```mermaid 區塊轉換為響應式的圖表元件,並支援 Lazy Loading 與深色模式切換。
12
+ 專為 [**Nuxt Content v3**](https://content.nuxt.com/docs/getting-started) 整合 [**Mermaid**](https://mermaid.js.org/) 的模組。
13
+ 能自動將 Markdown 中的 ```mermaid 區塊轉換為響應式的圖表元件,並支援 Lazy Loading 與深色模式切換。
13
14
 
14
15
  ## Features
15
16
 
16
17
  - **自動轉換**:解析 Markdown 代碼區塊並替換為 `<Mermaid>` 渲染元件。
17
18
  - **效能優化**:支援 Lazy Loading,僅在元件掛載時載入 Mermaid 核心與資源。
18
19
  - **主題整合**:無縫整合 `@nuxtjs/color-mode`,自動切換 Light/Dark 對應主題。
19
- - **高度客製**:支援自訂渲染元件(Wrapper)、Loading Spinner 及 CDN 來源。
20
+ - **高度客製**:支援自訂渲染元件(Wrapper)、Loading Spinner、錯誤畫面以及 CDN 來源。
20
21
  - **Runtime Config**:支援透過環境變數動態覆寫設定。
21
22
 
22
23
  ## 使用前提
23
24
 
24
- - Nuxt 4 (`nuxt@^4.0.0`)
25
- - `@nuxt/content@^3.8.0`(必須安裝)
25
+ - Nuxt 3.19 以上或 4.1 以上(`nuxt@^3.20.1 || ^4.1.0`)
26
+ - `@nuxt/content@>=3.5.0`
26
27
 
27
28
  ## Quick Setup
28
29
 
29
30
  ### 1. 安裝模組
30
31
 
32
+ **自動設定(自動寫入 `modules`):**
33
+
31
34
  ```bash
32
35
  npx nuxi module add @barzhsieh/nuxt-content-mermaid
33
36
  ```
34
37
 
38
+ > Nuxt CLI 對第三方模組預設會裝到 `dependencies`。若想放 `devDependencies`,請裝完後移動或改用下方手動安裝。
39
+
40
+ **裝到 `devDependencies`(需手動加入 `modules`):**
41
+
42
+ ```bash
43
+ # pnpm
44
+ pnpm add -D @barzhsieh/nuxt-content-mermaid
45
+
46
+ # npm
47
+ npm install -D @barzhsieh/nuxt-content-mermaid
48
+
49
+ # yarn
50
+ yarn add -D @barzhsieh/nuxt-content-mermaid
51
+ ```
52
+
35
53
  ### 2. 配置 `nuxt.config.ts`
36
54
 
37
- 確保模組已加入 `modules` 清單(若使用 `nuxi module add` 則會自動加入):
55
+ 確保模組已加入 `modules` 清單(若使用 `nuxi module add` 則可略過):
38
56
 
39
57
  ```ts
40
58
  export default defineNuxtConfig({
@@ -84,6 +102,7 @@ export default defineNuxtConfig({
84
102
  components: {
85
103
  renderer: undefined,
86
104
  spinner: undefined,
105
+ error: undefined,
87
106
  },
88
107
  },
89
108
  });
@@ -118,6 +137,7 @@ export default defineNuxtConfig({
118
137
  | :----------------------- | :------- | :--------- | :----------------------------------------------------- |
119
138
  | `components.renderer` | `string` | `undefined` | 指定自訂的 Mermaid 實作元件名稱(見進階用法)。 |
120
139
  | `components.spinner` | `string` | `undefined` | 指定全域的 Loading 元件名稱。 |
140
+ | `components.error` | `string` | `undefined` | 指定全域的錯誤顯示元件名稱,渲染失敗時會使用。 |
121
141
 
122
142
  > **Note**: 所有設定皆可透過 `runtimeConfig.public.mermaidContent` 在部署時進行覆寫。
123
143
 
@@ -164,6 +184,117 @@ export default defineNuxtConfig({
164
184
  </template>
165
185
  ```
166
186
 
187
+ #### 載入中指示
188
+
189
+ 模組在首次渲染前會使用內建的 spinner,如需替換可在 `components.spinner` 指定元件名稱;該元件也會以 `spinner` prop 傳入你的自訂渲染元件,方便直接渲染。
190
+
191
+ 以下是極簡的設定與元件示例:
192
+
193
+ ```ts
194
+ // nuxt.config.ts
195
+ export default defineNuxtConfig({
196
+ mermaidContent: {
197
+ components: {
198
+ renderer: 'MyCustomMermaid',
199
+ spinner: 'MySpinner', // 選填:改用自己的全域 Loading 元件
200
+ }
201
+ }
202
+ })
203
+ ```
204
+
205
+ ```vue
206
+ <!-- components/MyCustomMermaid.vue -->
207
+ <script setup lang="ts">
208
+ import { ref, onMounted } from 'vue'
209
+ import type { Component } from 'vue'
210
+
211
+ const props = defineProps<{ spinner: Component | string }>()
212
+ const loading = ref(true)
213
+ onMounted(() => { loading.value = false })
214
+ </script>
215
+
216
+ <template>
217
+ <div class="my-mermaid">
218
+ <component v-if="loading" :is="props.spinner" />
219
+ <Mermaid v-else>
220
+ <slot />
221
+ </Mermaid>
222
+ </div>
223
+ </template>
224
+ ```
225
+
226
+ ### 元件使用方式
227
+
228
+ 可以用 `<Mermaid>` 自己包一個的 Vue 元件 。
229
+ 例如,你可以同時放入標題、Loading 與錯誤顯示,之後就能在任意模板重複使用:
230
+ ```vue
231
+ <!-- WrapperMermaid.vue -->
232
+ <template>
233
+ <section>
234
+ <header v-if="title">{{ title }}</header>
235
+
236
+ <Mermaid>
237
+ <slot>
238
+ <pre><code>{{ code }}</code></pre>
239
+ </slot>
240
+
241
+ <template #loading>
242
+ <component :is="spinner" v-if="spinner" />
243
+ <p v-else>Diagram loading…</p>
244
+ </template>
245
+
246
+ <template #error="{ error, source }">
247
+ <p>渲染失敗:{{ error instanceof Error ? error.message : String(error) }}</p>
248
+ <pre><code>{{ source }}</code></pre>
249
+ </template>
250
+ </Mermaid>
251
+ </section>
252
+ </template>
253
+ ```
254
+
255
+ ```vue
256
+ <!-- 使用範例 -->
257
+ <WrapperMermaid
258
+ title="Demo Diagram"
259
+ spinner="MySpinner"
260
+ >
261
+ <pre><code>graph TD; A-->B; B-->C; C-->A</code></pre>
262
+ </WrapperMermaid>
263
+ ```
264
+
265
+ 可依需求調整此模式,把常用的 slot 寫在一個可重用的包裝元件中。
266
+
267
+ ### 錯誤處理
268
+
269
+ 當 Mermaid 解析或渲染失敗時,`<Mermaid>` 會觸發 `error` slot,並可透過 `components.error` 指定全域錯誤元件。兩者都會拿到錯誤內容與原始 Mermaid 定義,方便除錯。
270
+
271
+ ```vue
272
+ <Mermaid>
273
+ <pre><code>graph TD; A-->B; B-->C; C-->A</code></pre>
274
+
275
+ <template #error="{ error, source }">
276
+ <p>渲染失敗:{{ error instanceof Error ? error.message : String(error) }}</p>
277
+ <details>
278
+ <summary>查看原始定義</summary>
279
+ <pre><code>{{ source }}</code></pre>
280
+ </details>
281
+ </template>
282
+ </Mermaid>
283
+ ```
284
+
285
+ 若想一次註冊、全域套用自訂錯誤畫面,可在設定中指定元件名稱:
286
+
287
+ ```ts
288
+ // nuxt.config.ts
289
+ export default defineNuxtConfig({
290
+ mermaidContent: {
291
+ components: {
292
+ error: 'MermaidError', // 全域註冊的元件名稱
293
+ },
294
+ },
295
+ })
296
+ ```
297
+
167
298
  ## Contribution
168
299
 
169
300
  <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
@@ -2,9 +2,9 @@
2
2
  "name": "@barzhsieh/nuxt-content-mermaid",
3
3
  "configKey": "mermaidContent",
4
4
  "compatibility": {
5
- "nuxt": "^4.0.0"
5
+ "nuxt": "^3.20.1 || ^4.1.0"
6
6
  },
7
- "version": "1.0.1",
7
+ "version": "1.0.3",
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({
@@ -24,7 +26,7 @@ const module$1 = defineNuxtModule({
24
26
  name: "@barzhsieh/nuxt-content-mermaid",
25
27
  configKey: "mermaidContent",
26
28
  compatibility: {
27
- nuxt: "^4.0.0"
29
+ nuxt: "^3.20.1 || ^4.1.0"
28
30
  }
29
31
  },
30
32
  defaults: {
@@ -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,74 @@ 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.removeAttribute("data-processed");
143
+ mermaidContainer.value.textContent = mermaidDefinition;
144
+ await nextTick();
145
+ await mermaid.run({
146
+ nodes: [mermaidContainer.value],
147
+ suppressErrors: true
148
+ });
149
+ hasRenderedOnce.value = true;
150
+ await new Promise((resolve) => setTimeout(resolve, 5));
151
+ } catch (error) {
152
+ console.error("[nuxt-content-mermaid]", error);
153
+ hasError.value = true;
154
+ errorContent.value = error;
155
+ if (mermaidContainer.value)
156
+ mermaidContainer.value.innerHTML = "";
157
+ } finally {
158
+ isLoading.value = false;
159
+ }
160
+ };
161
+ await enqueueRender(performRender);
155
162
  }
156
163
  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);
164
+ const container = mermaidContainer.value;
165
+ if (!container) return;
166
+ if (!mermaidDefinition)
167
+ mermaidDefinition = extractMermaidDefinition(container);
168
+ if (!isLazy) {
169
+ renderMermaid();
170
+ return;
181
171
  }
172
+ observer = new IntersectionObserver(
173
+ (entries) => {
174
+ if (entries[0]?.isIntersecting && !hasRenderedOnce.value) {
175
+ renderMermaid();
176
+ if (observer) observer.disconnect();
177
+ }
178
+ },
179
+ { threshold: lazyThreshold }
180
+ );
181
+ observer.observe(container);
182
182
  }
183
- onMounted(async () => {
183
+ onMounted(() => {
184
184
  if (!isEnabled) return;
185
- isMounted.value = true;
186
- await nextTick();
187
- setupMermaidContainer();
185
+ if (mermaidContainer.value)
186
+ mermaidDefinition = extractMermaidDefinition(mermaidContainer.value);
187
+ nextTick(() => setupMermaidContainer());
188
188
  });
189
189
  onUnmounted(() => {
190
190
  if (observer) observer.disconnect();
@@ -196,46 +196,63 @@ watch(mermaidTheme, () => {
196
196
  </script>
197
197
 
198
198
  <template>
199
- <component
200
- :is="customMermaidImpl"
201
- v-if="customMermaidImpl"
202
- >
203
- <slot />
204
- </component>
199
+ <div class="mermaid-outer-wrapper">
200
+ <div
201
+ v-if="!isEnabled"
202
+ class="mermaid-wrapper"
203
+ >
204
+ <div ref="mermaidContainer">
205
+ <slot />
206
+ </div>
207
+ </div>
205
208
 
206
- <div
207
- v-else-if="!isEnabled"
208
- class="mermaid"
209
- >
210
- <slot />
211
- </div>
209
+ <component
210
+ :is="customMermaidImpl"
211
+ v-else-if="customMermaidImpl"
212
+ :spinner="spinnerComponent"
213
+ >
214
+ <slot />
215
+ </component>
212
216
 
213
- <div
214
- v-else-if="isMounted"
215
- class="mermaid-wrapper"
216
- >
217
217
  <div
218
- ref="mermaidContainer"
219
- class="mermaid"
218
+ v-else
219
+ class="mermaid-wrapper"
220
220
  >
221
- <slot />
222
- </div>
221
+ <div ref="mermaidContainer">
222
+ <!-- Initially show the slot's <pre><code> for SSR; client-side rendering will replace it with the mermaid SVG -->
223
+ <slot />
224
+ </div>
223
225
 
224
- <template v-if="isLoading && !hasRenderedOnce">
225
- <slot name="loading">
226
- <component :is="spinnerComponent" />
227
- </slot>
228
- </template>
229
- </div>
226
+ <template v-if="isLoading && !hasRenderedOnce">
227
+ <slot name="loading">
228
+ <component :is="spinnerComponent" />
229
+ </slot>
230
+ </template>
231
+ <template v-else-if="hasError">
232
+ <slot
233
+ name="error"
234
+ :error="errorContent"
235
+ :source="mermaidDefinition"
236
+ >
237
+ <component
238
+ :is="errorComponent"
239
+ v-if="errorComponent"
240
+ :error="errorContent"
241
+ :source="mermaidDefinition"
242
+ />
230
243
 
231
- <div
232
- v-else
233
- class="mermaid"
234
- >
235
- <slot />
244
+ <div
245
+ v-else
246
+ class="mermaid-error-default"
247
+ >
248
+ ⚠️ Mermaid Diagram Error
249
+ </div>
250
+ </slot>
251
+ </template>
252
+ </div>
236
253
  </div>
237
254
  </template>
238
255
 
239
256
  <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}
257
+ .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
258
  </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,7 +1,17 @@
1
1
  {
2
2
  "name": "@barzhsieh/nuxt-content-mermaid",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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
+ "keywords": [
6
+ "nuxt",
7
+ "nuxt-module",
8
+ "content",
9
+ "nuxt-content",
10
+ "markdown",
11
+ "mermaid",
12
+ "diagram",
13
+ "mdc"
14
+ ],
5
15
  "repository": {
6
16
  "type": "git",
7
17
  "url": "git+https://github.com/andy820621/nuxt-content-mermaid.git"
@@ -31,7 +41,6 @@
31
41
  "dev:build": "nuxi build playground",
32
42
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
33
43
  "release:base": "npm run lint && npm run test && npm run prepack",
34
-
35
44
  "release:patch": "npm run release:base && changelogen --release --patch --push --publish",
36
45
  "release:minor": "npm run release:base && changelogen --release --minor --push --publish",
37
46
  "release:major": "npm run release:base && changelogen --release --major --push --publish",
@@ -42,30 +51,32 @@
42
51
  "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
43
52
  },
44
53
  "dependencies": {
45
- "@nuxt/kit": "^4.2.1",
46
- "defu": "^6.1.4"
54
+ "@nuxt/kit": "catalog:integrations",
55
+ "defu": "catalog:dev"
47
56
  },
48
57
  "peerDependencies": {
49
- "@nuxt/content": "^3.8.0"
58
+ "nuxt": "^3.20.1 || ^4.1.0",
59
+ "@nuxt/content": ">=3.5.0"
50
60
  },
51
61
  "devDependencies": {
52
- "@nuxt/content": "3.8.0",
53
- "@nuxt/devtools": "^3.1.0",
54
- "@nuxt/eslint-config": "^1.10.0",
55
- "@nuxt/module-builder": "^1.0.2",
56
- "@nuxt/schema": "^4.2.1",
57
- "@nuxt/test-utils": "^3.20.1",
58
- "@nuxtjs/color-mode": "^4.0.0",
59
- "@types/node": "latest",
60
- "changelogen": "^0.6.2",
61
- "eslint": "^9.39.1",
62
- "mermaid": "^11.12.1",
63
- "nuxt": "^4.2.1",
64
- "typescript": "~5.9.3",
65
- "vitest": "^4.0.10",
66
- "vue-tsc": "^3.1.4"
62
+ "@nuxt/content": "catalog:integrations",
63
+ "@nuxt/devtools": "catalog:dev",
64
+ "@nuxt/eslint-config": "catalog:dev",
65
+ "@nuxt/module-builder": "catalog:dev",
66
+ "@nuxt/schema": "catalog:integrations",
67
+ "@nuxt/test-utils": "catalog:test",
68
+ "@nuxtjs/color-mode": "catalog:dev",
69
+ "@types/node": "catalog:types",
70
+ "changelogen": "catalog:dev",
71
+ "eslint": "catalog:dev",
72
+ "mermaid": "catalog:integrations",
73
+ "nuxt": "catalog:dev",
74
+ "typescript": "catalog:dev",
75
+ "vitest": "catalog:test",
76
+ "vue-tsc": "catalog:dev"
67
77
  },
68
78
  "volta": {
69
79
  "node": "22.21.1"
70
- }
80
+ },
81
+ "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
71
82
  }