astro-mermaid 1.3.1 → 2.0.0
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 +7 -5
- package/astro-mermaid-integration.d.ts +18 -4
- package/astro-mermaid-integration.js +132 -32
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -100,17 +100,21 @@ export default defineConfig({
|
|
|
100
100
|
mermaid({
|
|
101
101
|
// Default theme: 'default', 'dark', 'forest', 'neutral', 'base'
|
|
102
102
|
theme: 'forest',
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
// Enable automatic theme switching based on data-theme attribute
|
|
105
105
|
autoTheme: true,
|
|
106
|
-
|
|
106
|
+
|
|
107
|
+
// Enable client-side logging (default: true). Set to false to suppress
|
|
108
|
+
// console.log output in the browser. Errors are always logged.
|
|
109
|
+
enableLog: false,
|
|
110
|
+
|
|
107
111
|
// Additional mermaid configuration
|
|
108
112
|
mermaidConfig: {
|
|
109
113
|
flowchart: {
|
|
110
114
|
curve: 'basis'
|
|
111
115
|
}
|
|
112
116
|
},
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
// Register icon packs for use in diagrams
|
|
115
119
|
iconPacks: [
|
|
116
120
|
{
|
|
@@ -218,8 +222,6 @@ All mermaid diagram types are supported:
|
|
|
218
222
|
|
|
219
223
|
## Version
|
|
220
224
|
|
|
221
|
-
**Current:** `v1.2.0` - Enhanced universal compatibility with dual plugin system
|
|
222
|
-
|
|
223
225
|
See [changelog](https://github.com/joesaby/astro-mermaid/releases) for version history.
|
|
224
226
|
|
|
225
227
|
## Contributing
|
|
@@ -5,11 +5,19 @@ export interface IconPack {
|
|
|
5
5
|
* Name of the icon pack
|
|
6
6
|
*/
|
|
7
7
|
name: string;
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* URL to the icon pack JSON file (preferred, safe serialization).
|
|
11
|
+
* @example 'https://unpkg.com/@iconify-json/logos@1/icons.json'
|
|
12
|
+
*/
|
|
13
|
+
url?: string;
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
|
-
*
|
|
16
|
+
* Legacy: loader function whose source is inspected for a fetch() URL.
|
|
17
|
+
* Prefer using the `url` property instead for safer serialization.
|
|
18
|
+
* @deprecated Use `url` instead.
|
|
11
19
|
*/
|
|
12
|
-
loader
|
|
20
|
+
loader?: () => Promise<any>;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export interface AstroMermaidOptions {
|
|
@@ -24,7 +32,13 @@ export interface AstroMermaidOptions {
|
|
|
24
32
|
* @default true
|
|
25
33
|
*/
|
|
26
34
|
autoTheme?: boolean;
|
|
27
|
-
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enable client-side logging
|
|
38
|
+
* @default true
|
|
39
|
+
*/
|
|
40
|
+
enableLog?: boolean;
|
|
41
|
+
|
|
28
42
|
/**
|
|
29
43
|
* Additional mermaid configuration options
|
|
30
44
|
* @see https://mermaid.js.org/config/setup/modules/mermaidAPI.html#mermaidapi-configuration-defaults
|
|
@@ -15,6 +15,36 @@ function escapeHtml(text) {
|
|
|
15
15
|
return text.replace(/[&<>"']/g, char => htmlEntities[char]);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Sanitize a JSON string for safe embedding inside a <script> tag.
|
|
20
|
+
* Prevents premature script termination via </script> or <!-- sequences.
|
|
21
|
+
*/
|
|
22
|
+
function sanitizeJsonForScript(jsonStr) {
|
|
23
|
+
return jsonStr
|
|
24
|
+
.replace(/<\//g, '<\\/')
|
|
25
|
+
.replace(/<!--/g, '<\\!--');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate that mermaidConfig is a plain object (not an array, null, etc.)
|
|
30
|
+
* and does not contain __proto__ or constructor keys to prevent prototype pollution.
|
|
31
|
+
*/
|
|
32
|
+
function validateConfig(obj, path = 'mermaidConfig') {
|
|
33
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const dangerous = ['__proto__', 'constructor', 'prototype'];
|
|
37
|
+
// Use getOwnPropertyNames to catch __proto__ which Object.keys skips
|
|
38
|
+
for (const key of Object.getOwnPropertyNames(obj)) {
|
|
39
|
+
if (dangerous.includes(key)) {
|
|
40
|
+
throw new Error(`astro-mermaid: "${key}" is not allowed in ${path}`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
43
|
+
validateConfig(obj[key], `${path}.${key}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
18
48
|
/**
|
|
19
49
|
* Remark plugin to transform mermaid code blocks at the markdown level
|
|
20
50
|
*/
|
|
@@ -51,6 +81,27 @@ function remarkMermaidPlugin(options = {}) {
|
|
|
51
81
|
};
|
|
52
82
|
}
|
|
53
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Escape a string for safe use inside an HTML attribute value.
|
|
86
|
+
*/
|
|
87
|
+
function escapeAttribute(value) {
|
|
88
|
+
return String(value).replace(/[&<>"']/g, char => ({
|
|
89
|
+
'&': '&',
|
|
90
|
+
'<': '<',
|
|
91
|
+
'>': '>',
|
|
92
|
+
'"': '"',
|
|
93
|
+
"'": '''
|
|
94
|
+
})[char]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Allowlist of safe HTML tag names that may appear inside mermaid HAST content.
|
|
99
|
+
*/
|
|
100
|
+
const ALLOWED_TAG_NAMES = new Set([
|
|
101
|
+
'b', 'i', 'u', 'em', 'strong', 'br', 'hr', 'sub', 'sup', 'span', 'div',
|
|
102
|
+
'code', 'pre', 'img', 'a', 'p', 'ul', 'ol', 'li'
|
|
103
|
+
]);
|
|
104
|
+
|
|
54
105
|
/**
|
|
55
106
|
* Helper function to serialize HAST nodes back to HTML text
|
|
56
107
|
* This preserves HTML tags within the mermaid content
|
|
@@ -62,19 +113,26 @@ function serializeHastChildren(children) {
|
|
|
62
113
|
if (child.type === 'text') {
|
|
63
114
|
result += child.value;
|
|
64
115
|
} else if (child.type === 'element') {
|
|
65
|
-
// Reconstruct the HTML tag
|
|
116
|
+
// Reconstruct the HTML tag — only allow safe tag names
|
|
66
117
|
const tagName = child.tagName;
|
|
118
|
+
if (!ALLOWED_TAG_NAMES.has(tagName)) {
|
|
119
|
+
// Skip disallowed tags, but still serialize their text children
|
|
120
|
+
if (child.children && child.children.length > 0) {
|
|
121
|
+
result += serializeHastChildren(child.children);
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
67
125
|
const selfClosing = ['br', 'hr', 'img', 'input', 'meta', 'link'].includes(tagName);
|
|
68
126
|
|
|
69
127
|
result += `<${tagName}`;
|
|
70
128
|
|
|
71
|
-
// Add attributes if any
|
|
129
|
+
// Add attributes if any — escape all attribute values
|
|
72
130
|
if (child.properties) {
|
|
73
131
|
for (const [key, value] of Object.entries(child.properties)) {
|
|
74
132
|
if (key !== 'className') {
|
|
75
|
-
result += ` ${key}="${value}"`;
|
|
133
|
+
result += ` ${key}="${escapeAttribute(value)}"`;
|
|
76
134
|
} else if (Array.isArray(value)) {
|
|
77
|
-
result += ` class="${value.join(' ')}"`;
|
|
135
|
+
result += ` class="${escapeAttribute(value.join(' '))}"`;
|
|
78
136
|
}
|
|
79
137
|
}
|
|
80
138
|
}
|
|
@@ -164,6 +222,7 @@ async function isElkInstalled(logger, consumerRoot) {
|
|
|
164
222
|
* @param {string} [options.theme='default'] - Default theme ('default', 'dark', 'forest', 'neutral')
|
|
165
223
|
* @param {boolean} [options.autoTheme=true] - Enable automatic theme switching based on data-theme attribute
|
|
166
224
|
* @param {Object} [options.mermaidConfig={}] - Additional mermaid configuration options
|
|
225
|
+
* @param {boolean} [options.enableLog=true] - Enable client-side logging
|
|
167
226
|
* @returns {import('astro').AstroIntegration}
|
|
168
227
|
*/
|
|
169
228
|
export default function astroMermaid(options = {}) {
|
|
@@ -171,9 +230,13 @@ export default function astroMermaid(options = {}) {
|
|
|
171
230
|
theme = 'default',
|
|
172
231
|
autoTheme = true,
|
|
173
232
|
mermaidConfig = {},
|
|
174
|
-
iconPacks = []
|
|
233
|
+
iconPacks = [],
|
|
234
|
+
enableLog = true
|
|
175
235
|
} = options;
|
|
176
236
|
|
|
237
|
+
// Validate mermaidConfig to prevent prototype pollution
|
|
238
|
+
validateConfig(mermaidConfig);
|
|
239
|
+
|
|
177
240
|
return {
|
|
178
241
|
name: 'astro-mermaid',
|
|
179
242
|
hooks: {
|
|
@@ -211,14 +274,44 @@ export default function astroMermaid(options = {}) {
|
|
|
211
274
|
}
|
|
212
275
|
});
|
|
213
276
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
277
|
+
// Validate and serialize icon packs for client-side use.
|
|
278
|
+
// Only the pack name and a JSON URL string are forwarded to the
|
|
279
|
+
// client — we never serialize arbitrary function bodies.
|
|
280
|
+
const iconPacksConfig = iconPacks.map(pack => {
|
|
281
|
+
if (typeof pack.name !== 'string' || !pack.name) {
|
|
282
|
+
throw new Error('astro-mermaid: each iconPack must have a non-empty "name" string');
|
|
283
|
+
}
|
|
284
|
+
if (typeof pack.url === 'string') {
|
|
285
|
+
// Preferred: explicit URL
|
|
286
|
+
return { name: pack.name, url: pack.url };
|
|
287
|
+
}
|
|
288
|
+
if (typeof pack.loader === 'function') {
|
|
289
|
+
// Legacy: extract URL from loader().toString() if it contains a
|
|
290
|
+
// fetch('...') call, otherwise warn and skip.
|
|
291
|
+
const src = pack.loader.toString();
|
|
292
|
+
const urlMatch = src.match(/fetch\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
293
|
+
if (urlMatch) {
|
|
294
|
+
return { name: pack.name, url: urlMatch[1] };
|
|
295
|
+
}
|
|
296
|
+
logger.warn(
|
|
297
|
+
`astro-mermaid: iconPack "${pack.name}" uses a loader function ` +
|
|
298
|
+
`that could not be safely serialized. Please provide a "url" ` +
|
|
299
|
+
`property instead. This pack will be skipped.`
|
|
300
|
+
);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
throw new Error(
|
|
304
|
+
`astro-mermaid: iconPack "${pack.name}" must have a "url" string ` +
|
|
305
|
+
`or a "loader" function`
|
|
306
|
+
);
|
|
307
|
+
}).filter(Boolean);
|
|
219
308
|
|
|
220
309
|
// Inject client-side mermaid script with conditional loading
|
|
221
310
|
const mermaidScriptContent = `
|
|
311
|
+
// Logging helpers — controlled by enableLog option
|
|
312
|
+
const log = ${enableLog} ? (...args) => console.log('[astro-mermaid]', ...args) : () => {};
|
|
313
|
+
const logError = (...args) => console.error('[astro-mermaid]', ...args);
|
|
314
|
+
|
|
222
315
|
// Check if page has mermaid diagrams
|
|
223
316
|
const hasMermaidDiagrams = () => {
|
|
224
317
|
return document.querySelectorAll('pre.mermaid').length > 0;
|
|
@@ -231,16 +324,16 @@ let mermaidInstance = null;
|
|
|
231
324
|
async function loadMermaid() {
|
|
232
325
|
if (mermaidPromise) return mermaidPromise;
|
|
233
326
|
|
|
234
|
-
|
|
327
|
+
log('Loading mermaid.js...');
|
|
235
328
|
|
|
236
329
|
mermaidPromise = import('mermaid').then(async ({ default: mermaid }) => {
|
|
237
|
-
// Register icon packs if provided
|
|
238
|
-
const iconPacks = ${JSON.stringify(iconPacksConfig)};
|
|
330
|
+
// Register icon packs if provided — uses safe fetch(url) instead of eval
|
|
331
|
+
const iconPacks = ${sanitizeJsonForScript(JSON.stringify(iconPacksConfig))};
|
|
239
332
|
if (iconPacks && iconPacks.length > 0) {
|
|
240
|
-
|
|
333
|
+
log('Registering', iconPacks.length, 'icon packs');
|
|
241
334
|
const packs = iconPacks.map(pack => ({
|
|
242
335
|
name: pack.name,
|
|
243
|
-
loader:
|
|
336
|
+
loader: () => fetch(pack.url).then(res => res.json())
|
|
244
337
|
}));
|
|
245
338
|
await mermaid.registerIconPacks(packs);
|
|
246
339
|
}
|
|
@@ -249,7 +342,7 @@ async function loadMermaid() {
|
|
|
249
342
|
${useElk ? `
|
|
250
343
|
const elkModule = await import("@mermaid-js/layout-elk").catch(() => null);
|
|
251
344
|
if (elkModule?.default) {
|
|
252
|
-
|
|
345
|
+
log('Registering elk layouts');
|
|
253
346
|
mermaid.registerLayoutLoaders(elkModule.default);
|
|
254
347
|
}
|
|
255
348
|
` : ``}
|
|
@@ -257,7 +350,7 @@ if (elkModule?.default) {
|
|
|
257
350
|
mermaidInstance = mermaid;
|
|
258
351
|
return mermaid;
|
|
259
352
|
}).catch(error => {
|
|
260
|
-
|
|
353
|
+
logError('Failed to load mermaid:', error);
|
|
261
354
|
mermaidPromise = null;
|
|
262
355
|
throw error;
|
|
263
356
|
});
|
|
@@ -266,11 +359,11 @@ if (elkModule?.default) {
|
|
|
266
359
|
}
|
|
267
360
|
|
|
268
361
|
// Mermaid configuration
|
|
269
|
-
const defaultConfig = ${JSON.stringify({
|
|
362
|
+
const defaultConfig = ${sanitizeJsonForScript(JSON.stringify({
|
|
270
363
|
startOnLoad: false,
|
|
271
364
|
theme: theme,
|
|
272
365
|
...mermaidConfig
|
|
273
|
-
})};
|
|
366
|
+
}))};
|
|
274
367
|
|
|
275
368
|
// Theme mapping for auto-theme switching
|
|
276
369
|
const themeMap = {
|
|
@@ -280,10 +373,10 @@ const themeMap = {
|
|
|
280
373
|
|
|
281
374
|
// Initialize all mermaid diagrams
|
|
282
375
|
async function initMermaid() {
|
|
283
|
-
|
|
376
|
+
log('Initializing mermaid diagrams...');
|
|
284
377
|
const diagrams = document.querySelectorAll('pre.mermaid');
|
|
285
378
|
|
|
286
|
-
|
|
379
|
+
log('Found', diagrams.length, 'mermaid diagrams');
|
|
287
380
|
|
|
288
381
|
if (diagrams.length === 0) {
|
|
289
382
|
return;
|
|
@@ -301,7 +394,7 @@ async function initMermaid() {
|
|
|
301
394
|
const bodyTheme = document.body.getAttribute('data-theme');
|
|
302
395
|
const dataTheme = htmlTheme || bodyTheme;
|
|
303
396
|
currentTheme = themeMap[dataTheme] || defaultConfig.theme;
|
|
304
|
-
|
|
397
|
+
log('Using theme:', currentTheme, 'from', htmlTheme ? 'html' : 'body');
|
|
305
398
|
}
|
|
306
399
|
|
|
307
400
|
// Configure mermaid with gitGraph support
|
|
@@ -329,7 +422,7 @@ async function initMermaid() {
|
|
|
329
422
|
const diagramDefinition = diagram.getAttribute('data-diagram') || '';
|
|
330
423
|
const id = 'mermaid-' + Math.random().toString(36).slice(2, 11);
|
|
331
424
|
|
|
332
|
-
|
|
425
|
+
log('Rendering diagram:', id);
|
|
333
426
|
|
|
334
427
|
try {
|
|
335
428
|
// Clear any existing error state
|
|
@@ -341,13 +434,20 @@ async function initMermaid() {
|
|
|
341
434
|
const { svg } = await mermaid.render(id, diagramDefinition);
|
|
342
435
|
diagram.innerHTML = svg;
|
|
343
436
|
diagram.setAttribute('data-processed', 'true');
|
|
344
|
-
|
|
437
|
+
log('Successfully rendered diagram:', id);
|
|
345
438
|
} catch (error) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
439
|
+
logError('Mermaid rendering error for diagram:', id, error);
|
|
440
|
+
// Build error UI safely — use textContent to prevent XSS via error messages
|
|
441
|
+
const errorDiv = document.createElement('div');
|
|
442
|
+
errorDiv.style.cssText = 'color: red; padding: 1rem; border: 1px solid red; border-radius: 0.5rem;';
|
|
443
|
+
const strong = document.createElement('strong');
|
|
444
|
+
strong.textContent = 'Error rendering diagram:';
|
|
445
|
+
const msg = document.createElement('span');
|
|
446
|
+
msg.textContent = ' ' + (error.message || 'Unknown error');
|
|
447
|
+
errorDiv.appendChild(strong);
|
|
448
|
+
errorDiv.appendChild(msg);
|
|
449
|
+
diagram.textContent = '';
|
|
450
|
+
diagram.appendChild(errorDiv);
|
|
351
451
|
diagram.setAttribute('data-processed', 'true');
|
|
352
452
|
}
|
|
353
453
|
}
|
|
@@ -355,10 +455,10 @@ async function initMermaid() {
|
|
|
355
455
|
|
|
356
456
|
// Initialize on first load if there are diagrams
|
|
357
457
|
if (hasMermaidDiagrams()) {
|
|
358
|
-
|
|
458
|
+
log('Mermaid diagrams detected on initial load');
|
|
359
459
|
initMermaid();
|
|
360
460
|
} else {
|
|
361
|
-
|
|
461
|
+
log('No mermaid diagrams found on initial load');
|
|
362
462
|
}
|
|
363
463
|
|
|
364
464
|
// Re-render on theme change if auto-theme is enabled
|
|
@@ -389,7 +489,7 @@ if (${autoTheme}) {
|
|
|
389
489
|
// Handle view transitions (for Astro View Transitions API)
|
|
390
490
|
// This is registered ALWAYS, not just when initial page has diagrams
|
|
391
491
|
document.addEventListener('astro:after-swap', () => {
|
|
392
|
-
|
|
492
|
+
log('View transition detected');
|
|
393
493
|
// Check if new page has diagrams
|
|
394
494
|
if (hasMermaidDiagrams()) {
|
|
395
495
|
initMermaid();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-mermaid",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "An Astro integration for rendering Mermaid diagrams with automatic theme switching and client-side rendering",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./astro-mermaid-integration.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@mermaid-js/layout-elk": "^0.2.0",
|
|
33
|
-
"astro": "
|
|
33
|
+
"astro": ">=4",
|
|
34
34
|
"mermaid": "^10.0.0 || ^11.0.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
@@ -46,18 +46,20 @@
|
|
|
46
46
|
"scripts": {
|
|
47
47
|
"test": "vitest",
|
|
48
48
|
"test:ui": "vitest --ui",
|
|
49
|
-
"test:coverage": "vitest --coverage"
|
|
49
|
+
"test:coverage": "vitest --coverage",
|
|
50
|
+
"semantic-release": "semantic-release"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@types/hast": "^3.0.4",
|
|
53
54
|
"@vitest/ui": "^3.2.4",
|
|
54
|
-
"astro": "^
|
|
55
|
+
"astro": "^6.0.0",
|
|
55
56
|
"hast-util-from-html": "^2.0.3",
|
|
56
57
|
"mermaid": "^11.0.0",
|
|
57
58
|
"rehype-parse": "^9.0.1",
|
|
58
59
|
"rehype-stringify": "^10.0.1",
|
|
59
60
|
"remark-mermaid": "^0.2.0",
|
|
60
61
|
"remark-parse": "^11.0.0",
|
|
62
|
+
"semantic-release": "^25.0.3",
|
|
61
63
|
"typescript": "^5.0.0",
|
|
62
64
|
"unified": "^11.0.5",
|
|
63
65
|
"vitest": "^3.2.4"
|
|
@@ -70,4 +72,4 @@
|
|
|
70
72
|
"bugs": {
|
|
71
73
|
"url": "https://github.com/joesaby/astro-mermaid/issues"
|
|
72
74
|
}
|
|
73
|
-
}
|
|
75
|
+
}
|