@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 +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +9 -5
- package/dist/runtime/built-in-renderer/BuiltInRenderer.vue +6 -6
- package/dist/runtime/components/ContentMermaidTransport.d.vue.ts +3 -1
- package/dist/runtime/components/ContentMermaidTransport.vue +5 -4
- package/dist/runtime/components/ContentMermaidTransport.vue.d.ts +3 -1
- package/dist/runtime/configuration/module.js +4 -0
- package/dist/runtime/direct-mermaid-config.js +1 -1
- package/dist/runtime/png-font-embedding.d.ts +7 -0
- package/dist/runtime/png-font-embedding.js +305 -0
- package/dist/runtime/png-rasterizer-loader.d.ts +3 -0
- package/dist/runtime/png-rasterizer-loader.js +7 -0
- package/dist/runtime/png-rasterizer.js +8 -2
- package/dist/types/config.ts +91 -0
- package/dist/types/mermaid.d.ts +78 -0
- package/package.json +5 -5
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
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 ===
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
441
|
+
const PAGE_RENDER_DATA_EXPRESSION = "runtimeData";
|
|
438
442
|
function serializeInlineAttributeRecord(value) {
|
|
439
443
|
return JSON.stringify(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'");
|
|
440
444
|
}
|
|
@@ -484,7 +488,7 @@ function transformMarkdownDiagrams(body) {
|
|
|
484
488
|
};
|
|
485
489
|
const buildComponentTag = (encoded, toolbar) => {
|
|
486
490
|
const attrs = [
|
|
487
|
-
`:page-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
"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.
|
|
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.
|
|
78
|
-
"vue-tsc": "^3.3.
|
|
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",
|