@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 +115 -2
- package/README.zh-TW.md +115 -2
- package/dist/module.d.mts +14 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +4 -2
- package/dist/runtime/components/Mermaid.d.vue.ts +9 -6
- package/dist/runtime/components/Mermaid.vue +156 -140
- package/dist/runtime/components/Mermaid.vue.d.ts +9 -6
- package/dist/runtime/plugins/mermaid.client.js +1 -1
- package/dist/runtime/utils.d.ts +1 -0
- package/dist/runtime/utils.js +7 -0
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[English](./README.md) | [中文](./README.zh-TW.md)
|
|
2
2
|
|
|
3
|
-
# nuxt-mermaid
|
|
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
|
|
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
|
|
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
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
|
|
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
|
|
6
|
+
default?: (props: typeof __VLS_1) => any;
|
|
4
7
|
} & {
|
|
5
|
-
default?: (props: typeof
|
|
8
|
+
default?: (props: typeof __VLS_9) => any;
|
|
6
9
|
} & {
|
|
7
|
-
default?: (props: typeof
|
|
10
|
+
default?: (props: typeof __VLS_11) => any;
|
|
8
11
|
} & {
|
|
9
|
-
loading?: (props: typeof
|
|
12
|
+
loading?: (props: typeof __VLS_13) => any;
|
|
10
13
|
} & {
|
|
11
|
-
|
|
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
|
|
16
|
-
const isEnabled =
|
|
17
|
-
const loaderOptions =
|
|
18
|
-
const themeOptions =
|
|
19
|
-
const componentOptions =
|
|
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 =
|
|
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
|
|
62
|
+
`[nuxt-content-mermaid] Cannot find ${label} component:`,
|
|
58
63
|
name
|
|
59
64
|
);
|
|
60
65
|
return null;
|
|
61
66
|
}
|
|
62
67
|
try {
|
|
63
|
-
const [,
|
|
64
|
-
const mod = await
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
182
|
+
onMounted(() => {
|
|
184
183
|
if (!isEnabled) return;
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
219
|
-
class="mermaid"
|
|
217
|
+
v-else
|
|
218
|
+
class="mermaid-wrapper"
|
|
220
219
|
>
|
|
221
|
-
<
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
|
6
|
+
default?: (props: typeof __VLS_1) => any;
|
|
4
7
|
} & {
|
|
5
|
-
default?: (props: typeof
|
|
8
|
+
default?: (props: typeof __VLS_9) => any;
|
|
6
9
|
} & {
|
|
7
|
-
default?: (props: typeof
|
|
10
|
+
default?: (props: typeof __VLS_11) => any;
|
|
8
11
|
} & {
|
|
9
|
-
loading?: (props: typeof
|
|
12
|
+
loading?: (props: typeof __VLS_13) => any;
|
|
10
13
|
} & {
|
|
11
|
-
|
|
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>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function enqueueRender(task: () => Promise<void>): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barzhsieh/nuxt-content-mermaid",
|
|
3
|
-
"version": "1.0.
|
|
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",
|