@barzhsieh/nuxt-content-mermaid 3.0.3 → 3.0.5

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/dist/module.d.mts CHANGED
@@ -102,6 +102,7 @@ interface RuntimeOptions {
102
102
  /**
103
103
  * Whether to lazy load the diagram when it enters the viewport.
104
104
  * Can be a boolean or an object with IntersectionObserver options.
105
+ * The threshold must be between 0 and 1, inclusive.
105
106
  * @default true
106
107
  */
107
108
  lazy?: boolean | {
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^4.1.0"
6
6
  },
7
- "version": "3.0.3",
7
+ "version": "3.0.5",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -94,7 +94,9 @@ function extractInlineAttrsBlock(info) {
94
94
  for (let i = startIndex; i < info.length; i++) {
95
95
  const char = info[i];
96
96
  if (quote) {
97
- if (char === quote && info[i - 1] !== "\\")
97
+ if (char === "\\")
98
+ i += 1;
99
+ else if (char === quote)
98
100
  quote = null;
99
101
  continue;
100
102
  }
@@ -126,7 +128,7 @@ function parseInlineAttrsBlock(raw) {
126
128
  index += 1;
127
129
  const key = raw.slice(keyStart, index).trim();
128
130
  if (!key)
129
- break;
131
+ return null;
130
132
  index = skipWhitespace(raw, index);
131
133
  let value = true;
132
134
  if (index < raw.length && (raw[index] === "=" || raw[index] === ":")) {
@@ -206,7 +208,9 @@ function readBalancedValue(input, start) {
206
208
  for (let i = start; i < input.length; i++) {
207
209
  const char = input[i];
208
210
  if (quote) {
209
- if (char === quote && input[i - 1] !== "\\")
211
+ if (char === "\\")
212
+ i += 1;
213
+ else if (char === quote)
210
214
  quote = null;
211
215
  continue;
212
216
  }
@@ -434,7 +438,7 @@ function resolveMarkdownToolbar(layers) {
434
438
  }
435
439
 
436
440
  const MERMAID_COMPONENT_NAME = "ContentMermaidTransport";
437
- const PAGE_MERMAID_CONFIG_EXPRESSION = "config";
441
+ const PAGE_RENDER_DATA_EXPRESSION = "runtimeData";
438
442
  function serializeInlineAttributeRecord(value) {
439
443
  return JSON.stringify(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#39;");
440
444
  }
@@ -484,7 +488,7 @@ function transformMarkdownDiagrams(body) {
484
488
  };
485
489
  const buildComponentTag = (encoded, toolbar) => {
486
490
  const attrs = [
487
- `:page-config="${PAGE_MERMAID_CONFIG_EXPRESSION}"`
491
+ `:page-data="${PAGE_RENDER_DATA_EXPRESSION}"`
488
492
  ];
489
493
  if (toolbar && isNonEmptyRecord(toolbar)) {
490
494
  attrs.push(`:toolbar='${serializeInlineAttributeRecord(toolbar)}'`);
@@ -15,6 +15,7 @@ import { defu } from "defu";
15
15
  import { materializeMermaidConfigForInvocation, resolveMermaidTheme } from "../mermaid-config";
16
16
  import { createMermaidRenderer } from "../mermaid-rendering";
17
17
  import { getRuntimeMermaidSnapshot } from "../runtime-snapshot";
18
+ import { createPngRasterizerLoader } from "../png-rasterizer-loader";
18
19
  import {
19
20
  createSafeStandaloneSvgClone,
20
21
  downloadBlob,
@@ -34,10 +35,7 @@ import IconCollapse from "../components/icons/IconCollapse.vue";
34
35
  import IconDownload from "../components/icons/IconDownload.vue";
35
36
  import MermaidFullscreenPresentation from "../components/MermaidFullscreenPresentation.vue";
36
37
  import { DEFAULT_TOOLBAR_LABELS } from "../constants";
37
- let pngRasterizerModulePromise;
38
- function loadPngRasterizer() {
39
- return pngRasterizerModulePromise ??= import("../png-rasterizer");
40
- }
38
+ const loadPngRasterizer = createPngRasterizerLoader(() => import("../png-rasterizer"));
41
39
  const props = defineProps({
42
40
  toolbar: { type: Object, required: false },
43
41
  code: { type: String, required: false },
@@ -98,7 +96,8 @@ const hasError = ref(false);
98
96
  const errorContent = shallowRef(null);
99
97
  const committedExportSnapshot = shallowRef(null);
100
98
  const isCommittedOutputSvg = ref(false);
101
- const downloadDisclosureId = `ncm-download-${useId()}`;
99
+ const generatedDownloadDisclosureId = `ncm-download-${useId()}`;
100
+ const downloadDisclosureId = ref();
102
101
  const isDownloadDisclosureOpen = ref(false);
103
102
  const isPngDownloadPending = ref(false);
104
103
  const decodedCode = computed(() => props.code ? decodeURIComponent(props.code) : "");
@@ -529,6 +528,7 @@ async function copyToClipboard(text) {
529
528
  return copied;
530
529
  }
531
530
  onMounted(() => {
531
+ downloadDisclosureId.value = generatedDownloadDisclosureId;
532
532
  if (!isEnabled) return;
533
533
  document.addEventListener("pointerdown", handleDocumentPointerDown);
534
534
  if (!mermaidDefinition.value && mermaidContainer.value)
@@ -619,7 +619,7 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
619
619
  <IconDownload :size="iconSize" />
620
620
  </button>
621
621
  <div
622
- v-if="isDownloadDisclosureOpen"
622
+ v-show="isDownloadDisclosureOpen"
623
623
  :id="downloadDisclosureId"
624
624
  ref="downloadDisclosure"
625
625
  class="ncm-download-disclosure"
@@ -1,7 +1,9 @@
1
1
  import type { PageMermaidConfig } from '../../types/config.js';
2
2
  import type { MermaidToolbarOptions } from '../../types/mermaid.js';
3
3
  type __VLS_Props = {
4
- pageConfig?: PageMermaidConfig | null;
4
+ pageData?: {
5
+ config?: PageMermaidConfig | null;
6
+ };
5
7
  toolbar?: MermaidToolbarOptions;
6
8
  code?: string;
7
9
  };
@@ -1,13 +1,14 @@
1
1
  <script setup>
2
2
  import { computed, resolveComponent } from "vue";
3
3
  const props = defineProps({
4
- pageConfig: { type: [Object, null], required: false },
4
+ pageData: { type: Object, required: false },
5
5
  toolbar: { type: Object, required: false },
6
6
  code: { type: String, required: false }
7
7
  });
8
- const normalizedPageConfig = computed(
9
- () => props.pageConfig === null ? void 0 : props.pageConfig
10
- );
8
+ const normalizedPageConfig = computed(() => {
9
+ const config = props.pageData?.config;
10
+ return config === null ? void 0 : config;
11
+ });
11
12
  const MermaidComponent = resolveComponent("Mermaid");
12
13
  </script>
13
14
 
@@ -1,7 +1,9 @@
1
1
  import type { PageMermaidConfig } from '../../types/config.js';
2
2
  import type { MermaidToolbarOptions } from '../../types/mermaid.js';
3
3
  type __VLS_Props = {
4
- pageConfig?: PageMermaidConfig | null;
4
+ pageData?: {
5
+ config?: PageMermaidConfig | null;
6
+ };
5
7
  toolbar?: MermaidToolbarOptions;
6
8
  code?: string;
7
9
  };
@@ -106,6 +106,10 @@ export function validateRuntimeOptions(value, phase) {
106
106
  assertPlainObject(lazy, phase);
107
107
  assertKnownKeys(lazy, /* @__PURE__ */ new Set(["threshold"]), phase, ["loader", "lazy"]);
108
108
  assertNumberProperty(lazy, "threshold", phase, ["loader", "lazy"]);
109
+ const threshold = descriptorValue(lazy, "threshold");
110
+ if (typeof threshold === "number" && (threshold < 0 || threshold > 1)) {
111
+ throwConfigurationIssue(phase, ["loader", "lazy", "threshold"], "INVALID_VALUE", "a number between 0 and 1", "out-of-range-number");
112
+ }
109
113
  }
110
114
  }
111
115
  }
@@ -37,7 +37,7 @@ function validateDirectValue(value, path, ancestors) {
37
37
  if (isFunctionCapability(value, path) || isOpaqueObjectCapability(value, path))
38
38
  return;
39
39
  if (typeof value === "function")
40
- invalidDirectConfig(path, "a supported capability at its exact Mermaid 11.16.1 or DOMPurify 3.4.13 path");
40
+ invalidDirectConfig(path, "a supported capability at its exact Mermaid 11.17.0 or DOMPurify 3.4.13 path");
41
41
  if (typeof value === "symbol" || typeof value === "bigint")
42
42
  invalidDirectConfig(path, "structural data");
43
43
  if (value === null || typeof value !== "object") return;
@@ -0,0 +1,7 @@
1
+ import { getFontEmbedCSS } from 'html-to-image';
2
+ type FontEmbeddingOptions = NonNullable<Parameters<typeof getFontEmbedCSS>[1]>;
3
+ /** @internal */
4
+ export declare function supportsScopedPngFontEmbedding(document: Document): boolean;
5
+ /** @internal */
6
+ export declare function getScopedPngFontEmbedCSS(captureNode: HTMLElement, options: FontEmbeddingOptions): Promise<string>;
7
+ export {};
@@ -0,0 +1,305 @@
1
+ import { getFontEmbedCSS } from "html-to-image";
2
+ const ERROR_PREFIX = "[nuxt-content-mermaid]";
3
+ const CSS_IMPORT_RULE = 3;
4
+ const CSS_FONT_FACE_RULE = 5;
5
+ function normalizeFontFamily(value) {
6
+ const trimmed = value.trim();
7
+ const quote = trimmed[0];
8
+ const unquoted = (quote === '"' || quote === "'") && trimmed.at(-1) === quote ? trimmed.slice(1, -1) : trimmed;
9
+ return unquoted.replace(/\\([\\'" ])/g, "$1").toLocaleLowerCase();
10
+ }
11
+ function splitFontFamilies(value) {
12
+ const families = [];
13
+ let current = "";
14
+ let quote = "";
15
+ let escaped = false;
16
+ for (const character of value) {
17
+ if (escaped) {
18
+ current += character;
19
+ escaped = false;
20
+ continue;
21
+ }
22
+ if (character === "\\") {
23
+ current += character;
24
+ escaped = true;
25
+ continue;
26
+ }
27
+ if (quote) {
28
+ current += character;
29
+ if (character === quote) quote = "";
30
+ continue;
31
+ }
32
+ if (character === '"' || character === "'") {
33
+ current += character;
34
+ quote = character;
35
+ continue;
36
+ }
37
+ if (character === ",") {
38
+ families.push(normalizeFontFamily(current));
39
+ current = "";
40
+ continue;
41
+ }
42
+ current += character;
43
+ }
44
+ families.push(normalizeFontFamily(current));
45
+ return families.filter(Boolean);
46
+ }
47
+ function directTextContent(element) {
48
+ return Array.from(element.childNodes).filter((node) => node.nodeType === 3).map((node) => node.textContent ?? "").join("");
49
+ }
50
+ function collectFontUsages(captureNode) {
51
+ const view = captureNode.ownerDocument.defaultView;
52
+ if (!view) return [];
53
+ const elements = [captureNode, ...Array.from(captureNode.querySelectorAll("*"))];
54
+ return elements.flatMap((element) => {
55
+ const text = directTextContent(element);
56
+ if (!text.trim()) return [];
57
+ const style = view.getComputedStyle(element);
58
+ const families = splitFontFamilies(style.fontFamily);
59
+ if (families.length === 0) return [];
60
+ return [{
61
+ families,
62
+ style: style.fontStyle || "normal",
63
+ weight: style.fontWeight || "400",
64
+ stretch: style.fontStretch || "normal",
65
+ text
66
+ }];
67
+ });
68
+ }
69
+ function normalizeFontStyle(value) {
70
+ return value.trim().toLocaleLowerCase().split(/\s+/)[0] || "normal";
71
+ }
72
+ function parseWeight(value) {
73
+ const normalized = value.trim().toLocaleLowerCase();
74
+ if (normalized === "normal") return 400;
75
+ if (normalized === "bold") return 700;
76
+ const parsed = Number.parseFloat(normalized);
77
+ return Number.isFinite(parsed) ? parsed : null;
78
+ }
79
+ const stretchKeywords = {
80
+ "ultra-condensed": 50,
81
+ "extra-condensed": 62.5,
82
+ "condensed": 75,
83
+ "semi-condensed": 87.5,
84
+ "normal": 100,
85
+ "semi-expanded": 112.5,
86
+ "expanded": 125,
87
+ "extra-expanded": 150,
88
+ "ultra-expanded": 200
89
+ };
90
+ function parseStretch(value) {
91
+ const normalized = value.trim().toLocaleLowerCase();
92
+ if (stretchKeywords[normalized] !== void 0) return stretchKeywords[normalized];
93
+ const parsed = Number.parseFloat(normalized);
94
+ return Number.isFinite(parsed) ? parsed : null;
95
+ }
96
+ function descriptorBounds(value, parser, fallback) {
97
+ const parts = (value.trim() || fallback).split(/\s+/);
98
+ const first = parser(parts[0]);
99
+ const last = parser(parts[1] ?? parts[0]);
100
+ return first === null || last === null ? null : [Math.min(first, last), Math.max(first, last)];
101
+ }
102
+ function numericDescriptorContains(descriptor, actual, parser, fallback) {
103
+ const bounds = descriptorBounds(descriptor, parser, fallback);
104
+ const value = parser(actual);
105
+ return bounds === null || value === null ? (descriptor.trim() || fallback).toLocaleLowerCase() === actual.trim().toLocaleLowerCase() : value >= bounds[0] && value <= bounds[1];
106
+ }
107
+ function numericDescriptorsEqual(left, right, parser, fallback) {
108
+ const leftBounds = descriptorBounds(left, parser, fallback);
109
+ const rightBounds = descriptorBounds(right, parser, fallback);
110
+ if (!leftBounds || !rightBounds)
111
+ return (left.trim() || fallback).toLocaleLowerCase() === (right.trim() || fallback).toLocaleLowerCase();
112
+ return leftBounds[0] === rightBounds[0] && leftBounds[1] === rightBounds[1];
113
+ }
114
+ function parseUnicodeRanges(value) {
115
+ const source = value.trim() || "U+0-10FFFF";
116
+ const ranges = [];
117
+ for (const rawPart of source.split(",")) {
118
+ const part = rawPart.trim().toLocaleUpperCase();
119
+ const match = /^U\+([0-9A-F?]+)(?:-([0-9A-F]+))?$/.exec(part);
120
+ if (!match) return null;
121
+ const startToken = match[1];
122
+ const endToken = match[2];
123
+ const start = Number.parseInt(startToken.replaceAll("?", "0"), 16);
124
+ const end = Number.parseInt(
125
+ endToken ?? startToken.replaceAll("?", "F"),
126
+ 16
127
+ );
128
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
129
+ ranges.push([start, end]);
130
+ }
131
+ return ranges;
132
+ }
133
+ function unicodeRangeKey(value) {
134
+ const ranges = parseUnicodeRanges(value);
135
+ return ranges ? ranges.map(([start, end]) => `${start.toString(16)}-${end.toString(16)}`).join(",") : value.replace(/\s+/g, "").toLocaleLowerCase();
136
+ }
137
+ function unicodeRangeContainsText(value, text) {
138
+ const ranges = parseUnicodeRanges(value);
139
+ if (!ranges) return true;
140
+ return Array.from(text).some((character) => {
141
+ const codePoint = character.codePointAt(0);
142
+ return ranges.some(([start, end]) => codePoint >= start && codePoint <= end);
143
+ });
144
+ }
145
+ function fontFaceMatchesUsage(face, usage) {
146
+ return usage.families.includes(normalizeFontFamily(face.family)) && normalizeFontStyle(face.style) === normalizeFontStyle(usage.style) && numericDescriptorContains(face.weight, usage.weight, parseWeight, "normal") && numericDescriptorContains(face.stretch, usage.stretch, parseStretch, "normal") && unicodeRangeContainsText(face.unicodeRange, usage.text);
147
+ }
148
+ function ruleMatchesUsage(rule, usage) {
149
+ const style = rule.style;
150
+ return usage.families.includes(normalizeFontFamily(style.getPropertyValue("font-family"))) && normalizeFontStyle(style.getPropertyValue("font-style") || "normal") === normalizeFontStyle(usage.style) && numericDescriptorContains(style.getPropertyValue("font-weight"), usage.weight, parseWeight, "normal") && numericDescriptorContains(style.getPropertyValue("font-stretch"), usage.stretch, parseStretch, "normal") && unicodeRangeContainsText(style.getPropertyValue("unicode-range"), usage.text);
151
+ }
152
+ function ruleMatchesFontFace(rule, face) {
153
+ const style = rule.style;
154
+ return normalizeFontFamily(style.getPropertyValue("font-family")) === normalizeFontFamily(face.family) && normalizeFontStyle(style.getPropertyValue("font-style") || "normal") === normalizeFontStyle(face.style) && numericDescriptorsEqual(style.getPropertyValue("font-weight"), face.weight, parseWeight, "normal") && numericDescriptorsEqual(style.getPropertyValue("font-stretch"), face.stretch, parseStretch, "normal") && unicodeRangeKey(style.getPropertyValue("unicode-range")) === unicodeRangeKey(face.unicodeRange);
155
+ }
156
+ function collectFontRules(document) {
157
+ const candidates = [];
158
+ const unreadableStylesheets = [];
159
+ const visited = /* @__PURE__ */ new Set();
160
+ const visitRules = (rules, importHref) => {
161
+ for (const rule of Array.from(rules)) {
162
+ if (rule.type === CSS_FONT_FACE_RULE) {
163
+ candidates.push({ rule, importHref });
164
+ continue;
165
+ }
166
+ if (rule.type === CSS_IMPORT_RULE) {
167
+ const importRule = rule;
168
+ const href = importRule.href || importRule.styleSheet?.href || "<inline stylesheet>";
169
+ if (importRule.styleSheet)
170
+ visitStylesheet(importRule.styleSheet, href);
171
+ else
172
+ unreadableStylesheets.push(href);
173
+ continue;
174
+ }
175
+ if ("cssRules" in rule) {
176
+ try {
177
+ visitRules(rule.cssRules, importHref);
178
+ } catch {
179
+ unreadableStylesheets.push(rule.parentStyleSheet?.href ?? "<inline stylesheet>");
180
+ }
181
+ }
182
+ }
183
+ };
184
+ const visitStylesheet = (stylesheet, importHref) => {
185
+ if (visited.has(stylesheet)) return;
186
+ visited.add(stylesheet);
187
+ try {
188
+ visitRules(stylesheet.cssRules, importHref);
189
+ } catch {
190
+ unreadableStylesheets.push(stylesheet.href ?? importHref ?? "<inline stylesheet>");
191
+ }
192
+ };
193
+ for (const stylesheet of Array.from(document.styleSheets))
194
+ visitStylesheet(stylesheet);
195
+ return { candidates, unreadableStylesheets };
196
+ }
197
+ function resolveFontSourceUrls(value, baseUrl) {
198
+ const lowerValue = value.toLocaleLowerCase();
199
+ let result = "";
200
+ let searchFrom = 0;
201
+ while (searchFrom < value.length) {
202
+ const urlIndex = lowerValue.indexOf("url", searchFrom);
203
+ if (urlIndex < 0) break;
204
+ const previous = value[urlIndex - 1];
205
+ const afterUrl = urlIndex + 3;
206
+ if (previous !== void 0 && /[\w-]/.test(previous) || value[afterUrl] !== void 0 && /[\w-]/.test(value[afterUrl])) {
207
+ result += value.slice(searchFrom, afterUrl);
208
+ searchFrom = afterUrl;
209
+ continue;
210
+ }
211
+ let cursor = afterUrl;
212
+ while (value[cursor] !== void 0 && /[\t\n\f\r ]/.test(value[cursor]))
213
+ cursor++;
214
+ if (value[cursor] !== "(") {
215
+ result += value.slice(searchFrom, afterUrl);
216
+ searchFrom = afterUrl;
217
+ continue;
218
+ }
219
+ cursor++;
220
+ while (value[cursor] !== void 0 && /[\t\n\f\r ]/.test(value[cursor]))
221
+ cursor++;
222
+ const quote = value[cursor] === '"' || value[cursor] === "'" ? value[cursor] : null;
223
+ if (quote) cursor++;
224
+ const valueStart = cursor;
225
+ const valueEnd = quote ? value.indexOf(quote, valueStart) : value.indexOf(")", valueStart);
226
+ if (valueEnd < 0) break;
227
+ const rawUrl = value.slice(valueStart, valueEnd).trim();
228
+ cursor = valueEnd + 1;
229
+ if (quote) {
230
+ while (value[cursor] !== void 0 && /[\t\n\f\r ]/.test(value[cursor]))
231
+ cursor++;
232
+ if (value[cursor] !== ")") break;
233
+ cursor++;
234
+ }
235
+ result += value.slice(searchFrom, urlIndex);
236
+ result += `url("${new URL(rawUrl, baseUrl).href}")`;
237
+ searchFrom = cursor;
238
+ }
239
+ return result + value.slice(searchFrom);
240
+ }
241
+ function serializeFontRule(candidate, document) {
242
+ const descriptors = [];
243
+ const style = candidate.rule.style;
244
+ const baseUrl = candidate.rule.parentStyleSheet?.href ?? document.baseURI;
245
+ for (let index = 0; index < style.length; index++) {
246
+ const name = style.item(index);
247
+ const value = name === "src" ? resolveFontSourceUrls(style.getPropertyValue(name), baseUrl) : style.getPropertyValue(name);
248
+ descriptors.push([name, value, style.getPropertyPriority(name)]);
249
+ }
250
+ descriptors.sort(([left], [right]) => left.localeCompare(right));
251
+ return `@font-face { ${descriptors.map(
252
+ ([name, value, priority]) => `${name}: ${value}${priority ? ` !${priority}` : ""};`
253
+ ).join(" ")} }`;
254
+ }
255
+ export function supportsScopedPngFontEmbedding(document) {
256
+ return Boolean(
257
+ document.defaultView && typeof document.implementation?.createHTMLDocument === "function" && document.fonts && typeof document.fonts[Symbol.iterator] === "function"
258
+ );
259
+ }
260
+ export async function getScopedPngFontEmbedCSS(captureNode, options) {
261
+ const document = captureNode.ownerDocument;
262
+ const usages = collectFontUsages(captureNode);
263
+ const activeFaces = Array.from(document.fonts).filter((face) => face.status !== "unloaded").filter((face) => usages.some((usage) => fontFaceMatchesUsage(face, usage)));
264
+ if (activeFaces.length === 0) return "";
265
+ const { candidates, unreadableStylesheets } = collectFontRules(document);
266
+ const matchingCandidates = candidates.filter(
267
+ (candidate) => usages.some((usage) => ruleMatchesUsage(candidate.rule, usage)) && activeFaces.some((face) => ruleMatchesFontFace(candidate.rule, face))
268
+ );
269
+ for (const face of activeFaces) {
270
+ if (matchingCandidates.some((candidate) => ruleMatchesFontFace(candidate.rule, face))) continue;
271
+ const unreadable = unreadableStylesheets[0];
272
+ if (unreadable) {
273
+ throw new Error(
274
+ `${ERROR_PREFIX} Cannot read stylesheet for PNG rasterization: ${unreadable}`
275
+ );
276
+ }
277
+ throw new Error(
278
+ `${ERROR_PREFIX} Cannot access @font-face for PNG rasterization: ${face.family}`
279
+ );
280
+ }
281
+ const uniqueRules = /* @__PURE__ */ new Map();
282
+ const candidatesByPreference = [...matchingCandidates].sort((left, right) => Number(Boolean(left.importHref)) - Number(Boolean(right.importHref)));
283
+ for (const candidate of candidatesByPreference) {
284
+ const cssText = serializeFontRule(candidate, document);
285
+ if (!uniqueRules.has(cssText)) uniqueRules.set(cssText, candidate);
286
+ }
287
+ for (const candidate of uniqueRules.values()) {
288
+ if (!candidate.importHref) continue;
289
+ throw new Error(
290
+ `${ERROR_PREFIX} CSS @import is not supported for PNG rasterization: ${candidate.importHref}`
291
+ );
292
+ }
293
+ const isolatedDocument = document.implementation.createHTMLDocument("");
294
+ const styleElement = isolatedDocument.createElement("style");
295
+ isolatedDocument.head.appendChild(styleElement);
296
+ const stylesheet = styleElement.sheet;
297
+ if (!stylesheet) {
298
+ throw new Error(`${ERROR_PREFIX} Cannot create isolated stylesheet for PNG rasterization`);
299
+ }
300
+ for (const cssText of uniqueRules.keys())
301
+ stylesheet.insertRule(cssText, stylesheet.cssRules.length);
302
+ const embeddingNode = isolatedDocument.createElement("div");
303
+ isolatedDocument.body.appendChild(embeddingNode);
304
+ return getFontEmbedCSS(embeddingNode, options);
305
+ }
@@ -0,0 +1,3 @@
1
+ type PngRasterizerModule = typeof import('./png-rasterizer');
2
+ export declare function createPngRasterizerLoader(importModule: () => Promise<PngRasterizerModule>): () => Promise<typeof import("./png-rasterizer.js")>;
3
+ export {};
@@ -0,0 +1,7 @@
1
+ export function createPngRasterizerLoader(importModule) {
2
+ let modulePromise;
3
+ return () => modulePromise ??= importModule().catch((error) => {
4
+ modulePromise = void 0;
5
+ throw error;
6
+ });
7
+ }
@@ -1,4 +1,8 @@
1
1
  import { getFontEmbedCSS, toBlob } from "html-to-image";
2
+ import {
3
+ getScopedPngFontEmbedCSS,
4
+ supportsScopedPngFontEmbedding
5
+ } from "./png-font-embedding.js";
2
6
  const ERROR_PREFIX = "[nuxt-content-mermaid]";
3
7
  function validateDimensions(width, height) {
4
8
  if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
@@ -112,7 +116,8 @@ export async function rasterizePngSnapshot(input) {
112
116
  validateDimensions(input.width, input.height);
113
117
  const document = input.svg.ownerDocument;
114
118
  await document.fonts?.ready;
115
- assertStylesheetsReadable(document);
119
+ const useScopedFontEmbedding = supportsScopedPngFontEmbedding(document);
120
+ if (!useScopedFontEmbedding) assertStylesheetsReadable(document);
116
121
  const captureOptions = {
117
122
  width: input.width,
118
123
  height: input.height,
@@ -127,7 +132,8 @@ export async function rasterizePngSnapshot(input) {
127
132
  input.height
128
133
  );
129
134
  try {
130
- const fontEmbedCSS = await getFontEmbedCSS(captureNode, captureOptions);
135
+ if (useScopedFontEmbedding) await document.fonts.ready;
136
+ const fontEmbedCSS = useScopedFontEmbedding ? await getScopedPngFontEmbedCSS(captureNode, captureOptions) : await getFontEmbedCSS(captureNode, captureOptions);
131
137
  assertFontsEmbedded(fontEmbedCSS);
132
138
  const blob = await toBlob(captureNode, {
133
139
  ...captureOptions,
@@ -0,0 +1,91 @@
1
+ import type { MermaidConfig } from 'mermaid'
2
+ import type { ExpandOptions } from '../runtime/types/expand'
3
+ import type { MermaidToolbarOptions } from './mermaid'
4
+
5
+ export type JsonPrimitive = string | number | boolean | null
6
+ export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]
7
+ export type JsonObject = { [key: string]: JsonValue }
8
+
9
+ type IsAny<T> = 0 extends (1 & T) ? true : false
10
+
11
+ type RuntimeDataMember<T> = IsAny<T> extends true
12
+ ? never
13
+ : T extends JsonPrimitive
14
+ ? T
15
+ : T extends (...args: never[]) => unknown
16
+ ? never
17
+ : T extends readonly (infer Item)[]
18
+ ? RuntimeDataMember<Item>[]
19
+ : T extends object
20
+ ? { [Key in keyof T]: RuntimeDataMember<T[Key]> }
21
+ : never
22
+
23
+ export type RuntimeMermaidConfig = RuntimeDataMember<MermaidConfig> & JsonObject
24
+ export type PageMermaidConfig = RuntimeMermaidConfig
25
+
26
+ export interface RuntimeOptions {
27
+ /**
28
+ * Enable debug mode for detailed logging and error reporting
29
+ * @default false
30
+ */
31
+ debug?: boolean
32
+ /**
33
+ * Options related to loading mermaid
34
+ */
35
+ loader?: {
36
+ /**
37
+ * Pure-data configuration passed through Nuxt public runtime config.
38
+ */
39
+ init?: RuntimeMermaidConfig
40
+ /**
41
+ * Whether to lazy load the diagram when it enters the viewport.
42
+ * Can be a boolean or an object with IntersectionObserver options.
43
+ * The threshold must be between 0 and 1, inclusive.
44
+ * @default true
45
+ */
46
+ lazy?: boolean | { threshold?: number }
47
+ }
48
+ /**
49
+ * Options related to theme handling
50
+ */
51
+ theme?: {
52
+ /** @deprecated No effect; retained for 3.0 compatibility. */
53
+ useColorModeTheme?: boolean
54
+ light?: MermaidConfig['theme']
55
+ dark?: MermaidConfig['theme']
56
+ }
57
+ /**
58
+ * Names for custom implementation components
59
+ */
60
+ components?: {
61
+ renderer?: string
62
+ spinner?: string
63
+ error?: string
64
+ }
65
+ /**
66
+ * Expand configuration. `false` disables expand, `true` uses defaults.
67
+ */
68
+ expand?: ExpandOptions | boolean
69
+ /**
70
+ * Default toolbar settings for Mermaid component
71
+ */
72
+ toolbar?: MermaidToolbarOptions
73
+ }
74
+
75
+ export interface ModuleOptions extends RuntimeOptions {
76
+ /**
77
+ * Whether to enable the entire Mermaid process at build time.
78
+ * @default true
79
+ */
80
+ enabled?: boolean
81
+ }
82
+
83
+ interface MermaidComponentBaseProps {
84
+ toolbar?: MermaidToolbarOptions
85
+ code?: string
86
+ }
87
+
88
+ export type MermaidComponentProps = MermaidComponentBaseProps & (
89
+ | { pageConfig?: PageMermaidConfig, config?: never }
90
+ | { pageConfig?: never, config?: MermaidConfig }
91
+ )
@@ -0,0 +1,78 @@
1
+ import type { DefineComponent } from 'vue'
2
+ import type { Mermaid } from 'mermaid'
3
+ import type { MermaidComponentProps, ModuleOptions, RuntimeOptions } from './config'
4
+ import type { MermaidThemeMode, SimpleMermaidTheme } from '../runtime/composables/useMermaidTheme'
5
+
6
+ export type MermaidToolbarButtons = {
7
+ copy?: boolean
8
+ fullscreen?: boolean
9
+ expand?: boolean
10
+ }
11
+
12
+ export type MermaidToolbarLabels = {
13
+ copy?: string
14
+ copied?: string
15
+ copyFailed?: string
16
+ expand?: string
17
+ collapse?: string
18
+ minimize?: string
19
+ enterFullscreen?: string
20
+ exitFullscreen?: string
21
+ zoomIn?: string
22
+ zoomOut?: string
23
+ resetZoom?: string
24
+ download?: string
25
+ downloadSvg?: string
26
+ downloadPng?: string
27
+ }
28
+
29
+ export type MermaidToolbarOptions = {
30
+ title?: string
31
+ fontSize?: string | number
32
+ fullscreenToolbarScale?: number
33
+ buttons?: MermaidToolbarButtons
34
+ labels?: MermaidToolbarLabels
35
+ }
36
+
37
+ declare module '#app' {
38
+ interface NuxtApp {
39
+ $mermaid: () => Promise<Mermaid>
40
+ }
41
+ }
42
+ declare module 'vue' {
43
+ interface ComponentCustomProperties {
44
+ $mermaid: () => Promise<Mermaid>
45
+ }
46
+ interface GlobalComponents {
47
+ Mermaid: DefineComponent<MermaidComponentProps>
48
+ }
49
+ }
50
+
51
+ declare module '@nuxt/schema' {
52
+ interface NuxtConfig {
53
+ contentMermaid?: ModuleOptions
54
+ }
55
+
56
+ interface NuxtOptions {
57
+ contentMermaid?: ModuleOptions
58
+ }
59
+
60
+ interface PublicRuntimeConfig {
61
+ contentMermaid?: RuntimeOptions
62
+ }
63
+ }
64
+
65
+ declare global {
66
+ interface ImportMeta {
67
+ glob<T = unknown>(
68
+ pattern: string,
69
+ options?: {
70
+ eager?: boolean
71
+ import?: string
72
+ },
73
+ ): Record<string, () => Promise<T>>
74
+ }
75
+ }
76
+
77
+ export type { MermaidThemeMode, SimpleMermaidTheme }
78
+ export {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barzhsieh/nuxt-content-mermaid",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
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
  "keywords": [
6
6
  "nuxt",
@@ -52,7 +52,7 @@
52
52
  "defu": "^6.1.7",
53
53
  "destr": "^2.0.5",
54
54
  "html-to-image": "1.11.11",
55
- "mermaid": "~11.16.1",
55
+ "mermaid": "~11.17.0",
56
56
  "yaml": "^2.9.0"
57
57
  },
58
58
  "peerDependencies": {
@@ -74,8 +74,8 @@
74
74
  "nuxt": "4.5.2",
75
75
  "playwright": "^1.62.1",
76
76
  "typescript": "~5.9.3",
77
- "vitest": "^4.1.10",
78
- "vue-tsc": "^3.3.9",
77
+ "vitest": "^4.1.11",
78
+ "vue-tsc": "^3.3.10",
79
79
  "yaml": "^2.9.0"
80
80
  },
81
81
  "volta": {
@@ -89,7 +89,7 @@
89
89
  "release:prepare:major": "changelogen --release --major --no-commit --no-tag --no-github && pnpm install --lockfile-only --ignore-scripts",
90
90
  "release:prepare:minor": "changelogen --release --minor --no-commit --no-tag --no-github && pnpm install --lockfile-only --ignore-scripts",
91
91
  "release:prepare:patch": "changelogen --release --patch --no-commit --no-tag --no-github && pnpm install --lockfile-only --ignore-scripts",
92
- "test": "vitest run",
92
+ "test": "vitest run --exclude test/packagePrepack.test.ts && vitest run test/packagePrepack.test.ts",
93
93
  "test:package-artifact": "node scripts/release-verification/package-artifact.mjs --profile v3-known-latest",
94
94
  "test:compatibility-profile": "node scripts/release-verification/package-artifact.mjs",
95
95
  "test:package-contract": "pnpm prepack && vue-tsc -p test/release-verification/consumer-template/type-contracts/tsconfig.json --noEmit",