astro-mermaid 1.1.0 → 1.3.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 CHANGED
@@ -218,7 +218,7 @@ All mermaid diagram types are supported:
218
218
 
219
219
  ## Version
220
220
 
221
- **Current:** `v1.0.4` - Enhanced universal compatibility with dual plugin system
221
+ **Current:** `v1.2.0` - Enhanced universal compatibility with dual plugin system
222
222
 
223
223
  See [changelog](https://github.com/joesaby/astro-mermaid/releases) for version history.
224
224
 
@@ -1,5 +1,20 @@
1
1
  import { resolve } from 'import-meta-resolve';
2
2
 
3
+ /**
4
+ * Helper function to HTML-escape text content
5
+ * This ensures HTML tags in mermaid diagrams are preserved as text
6
+ */
7
+ function escapeHtml(text) {
8
+ const htmlEntities = {
9
+ '&': '&',
10
+ '<': '&lt;',
11
+ '>': '&gt;',
12
+ '"': '&quot;',
13
+ "'": '&#39;'
14
+ };
15
+ return text.replace(/[&<>"']/g, char => htmlEntities[char]);
16
+ }
17
+
3
18
  /**
4
19
  * Remark plugin to transform mermaid code blocks at the markdown level
5
20
  */
@@ -13,10 +28,10 @@ function remarkMermaidPlugin(options = {}) {
13
28
  if (node.lang === 'mermaid') {
14
29
  mermaidCount++;
15
30
 
16
- // Transform to html node with pre.mermaid
31
+ // Transform to html node with pre.mermaid, escaping HTML content
17
32
  const htmlNode = {
18
33
  type: 'html',
19
- value: `<pre class="mermaid">${node.value}</pre>`
34
+ value: `<pre class="mermaid">${escapeHtml(node.value)}</pre>`
20
35
  };
21
36
 
22
37
  // Replace the code node with html node
@@ -36,6 +51,49 @@ function remarkMermaidPlugin(options = {}) {
36
51
  };
37
52
  }
38
53
 
54
+ /**
55
+ * Helper function to serialize HAST nodes back to HTML text
56
+ * This preserves HTML tags within the mermaid content
57
+ */
58
+ function serializeHastChildren(children) {
59
+ let result = '';
60
+
61
+ for (const child of children) {
62
+ if (child.type === 'text') {
63
+ result += child.value;
64
+ } else if (child.type === 'element') {
65
+ // Reconstruct the HTML tag
66
+ const tagName = child.tagName;
67
+ const selfClosing = ['br', 'hr', 'img', 'input', 'meta', 'link'].includes(tagName);
68
+
69
+ result += `<${tagName}`;
70
+
71
+ // Add attributes if any
72
+ if (child.properties) {
73
+ for (const [key, value] of Object.entries(child.properties)) {
74
+ if (key !== 'className') {
75
+ result += ` ${key}="${value}"`;
76
+ } else if (Array.isArray(value)) {
77
+ result += ` class="${value.join(' ')}"`;
78
+ }
79
+ }
80
+ }
81
+
82
+ if (selfClosing) {
83
+ result += '/>';
84
+ } else {
85
+ result += '>';
86
+ if (child.children && child.children.length > 0) {
87
+ result += serializeHastChildren(child.children);
88
+ }
89
+ result += `</${tagName}>`;
90
+ }
91
+ }
92
+ }
93
+
94
+ return result;
95
+ }
96
+
39
97
  /**
40
98
  * Rehype plugin to transform mermaid code blocks
41
99
  * Converts ```mermaid code blocks to <pre class="mermaid">
@@ -43,7 +101,6 @@ function remarkMermaidPlugin(options = {}) {
43
101
  function rehypeMermaidPlugin(options = {}) {
44
102
  return async function transformer(tree, file) {
45
103
  const { visit } = await import('unist-util-visit');
46
- const { toString } = await import('mdast-util-to-string');
47
104
 
48
105
  let mermaidCount = 0;
49
106
 
@@ -59,8 +116,8 @@ function rehypeMermaidPlugin(options = {}) {
59
116
 
60
117
  if (Array.isArray(className) && className.includes('language-mermaid')) {
61
118
  mermaidCount++;
62
- // Get the mermaid diagram content
63
- const diagramContent = toString(codeNode);
119
+ // Get the mermaid diagram content, preserving HTML tags
120
+ const diagramContent = serializeHastChildren(codeNode.children || []);
64
121
 
65
122
  // Transform to <pre class="mermaid">
66
123
  node.properties = {
@@ -68,9 +125,10 @@ function rehypeMermaidPlugin(options = {}) {
68
125
  className: ['mermaid']
69
126
  };
70
127
 
128
+ // Escape HTML to preserve it as text content
71
129
  node.children = [{
72
130
  type: 'text',
73
- value: diagramContent
131
+ value: escapeHtml(diagramContent)
74
132
  }];
75
133
 
76
134
  if (options.logger) {
@@ -166,12 +224,16 @@ const hasMermaidDiagrams = () => {
166
224
  return document.querySelectorAll('pre.mermaid').length > 0;
167
225
  };
168
226
 
169
- // Only proceed if there are mermaid diagrams on the page
170
- if (hasMermaidDiagrams()) {
171
- console.log('[astro-mermaid] Mermaid diagrams detected, loading mermaid.js...');
172
-
173
- // Dynamically import mermaid only when needed
174
- import('mermaid').then(async ({ default: mermaid }) => {
227
+ // Shared mermaid initialization function
228
+ let mermaidPromise = null;
229
+ let mermaidInstance = null;
230
+
231
+ async function loadMermaid() {
232
+ if (mermaidPromise) return mermaidPromise;
233
+
234
+ console.log('[astro-mermaid] Loading mermaid.js...');
235
+
236
+ mermaidPromise = import('mermaid').then(async ({ default: mermaid }) => {
175
237
  // Register icon packs if provided
176
238
  const iconPacks = ${JSON.stringify(iconPacksConfig)};
177
239
  if (iconPacks && iconPacks.length > 0) {
@@ -192,132 +254,147 @@ if (elkModule?.default) {
192
254
  }
193
255
  ` : ``}
194
256
 
195
- // Mermaid configuration
196
- const defaultConfig = ${JSON.stringify({
197
- startOnLoad: false,
198
- theme: theme,
199
- ...mermaidConfig
200
- })};
201
-
202
- // Theme mapping for auto-theme switching
203
- const themeMap = {
204
- 'light': 'default',
205
- 'dark': 'dark'
206
- };
207
-
208
- // Initialize all mermaid diagrams
209
- async function initMermaid() {
210
- console.log('[astro-mermaid] Initializing mermaid diagrams...');
211
- const diagrams = document.querySelectorAll('pre.mermaid');
212
-
213
- console.log('[astro-mermaid] Found', diagrams.length, 'mermaid diagrams');
214
-
215
- if (diagrams.length === 0) {
216
- return;
217
- }
218
-
219
- // Get current theme from multiple sources
220
- let currentTheme = defaultConfig.theme;
221
-
222
- if (${autoTheme}) {
223
- // Check both html and body for data-theme attribute
224
- const htmlTheme = document.documentElement.getAttribute('data-theme');
225
- const bodyTheme = document.body.getAttribute('data-theme');
226
- const dataTheme = htmlTheme || bodyTheme;
227
- currentTheme = themeMap[dataTheme] || defaultConfig.theme;
228
- console.log('[astro-mermaid] Using theme:', currentTheme, 'from', htmlTheme ? 'html' : 'body');
229
- }
230
-
231
- // Configure mermaid with gitGraph support
232
- mermaid.initialize({
233
- ...defaultConfig,
234
- theme: currentTheme,
235
- gitGraph: {
236
- mainBranchName: 'main',
237
- showCommitLabel: true,
238
- showBranches: true,
239
- rotateCommitLabel: true
240
- }
241
- });
242
-
243
- // Render each diagram
244
- for (const diagram of diagrams) {
245
- // Skip if already processed
246
- if (diagram.hasAttribute('data-processed')) continue;
247
-
248
- // Store original content
249
- if (!diagram.hasAttribute('data-diagram')) {
250
- diagram.setAttribute('data-diagram', diagram.textContent || '');
251
- }
252
-
253
- const diagramDefinition = diagram.getAttribute('data-diagram') || '';
254
- const id = 'mermaid-' + Math.random().toString(36).slice(2, 11);
255
-
256
- console.log('[astro-mermaid] Rendering diagram:', id);
257
-
258
- try {
259
- // Clear any existing error state
260
- const existingGraph = document.getElementById(id);
261
- if (existingGraph) {
262
- existingGraph.remove();
263
- }
264
-
265
- const { svg } = await mermaid.render(id, diagramDefinition);
266
- diagram.innerHTML = svg;
267
- diagram.setAttribute('data-processed', 'true');
268
- console.log('[astro-mermaid] Successfully rendered diagram:', id);
269
- } catch (error) {
270
- console.error('[astro-mermaid] Mermaid rendering error for diagram:', id, error);
271
- diagram.innerHTML = \`<div style="color: red; padding: 1rem; border: 1px solid red; border-radius: 0.5rem;">
272
- <strong>Error rendering diagram:</strong><br/>
273
- \${error.message || 'Unknown error'}
274
- </div>\`;
275
- diagram.setAttribute('data-processed', 'true');
276
- }
277
- }
257
+ mermaidInstance = mermaid;
258
+ return mermaid;
259
+ }).catch(error => {
260
+ console.error('[astro-mermaid] Failed to load mermaid:', error);
261
+ mermaidPromise = null;
262
+ throw error;
263
+ });
264
+
265
+ return mermaidPromise;
266
+ }
267
+
268
+ // Mermaid configuration
269
+ const defaultConfig = ${JSON.stringify({
270
+ startOnLoad: false,
271
+ theme: theme,
272
+ ...mermaidConfig
273
+ })};
274
+
275
+ // Theme mapping for auto-theme switching
276
+ const themeMap = {
277
+ 'light': 'default',
278
+ 'dark': 'dark'
279
+ };
280
+
281
+ // Initialize all mermaid diagrams
282
+ async function initMermaid() {
283
+ console.log('[astro-mermaid] Initializing mermaid diagrams...');
284
+ const diagrams = document.querySelectorAll('pre.mermaid');
285
+
286
+ console.log('[astro-mermaid] Found', diagrams.length, 'mermaid diagrams');
287
+
288
+ if (diagrams.length === 0) {
289
+ return;
290
+ }
291
+
292
+ // Load mermaid if not already loaded
293
+ const mermaid = await loadMermaid();
294
+
295
+ // Get current theme from multiple sources
296
+ let currentTheme = defaultConfig.theme;
297
+
298
+ if (${autoTheme}) {
299
+ // Check both html and body for data-theme attribute
300
+ const htmlTheme = document.documentElement.getAttribute('data-theme');
301
+ const bodyTheme = document.body.getAttribute('data-theme');
302
+ const dataTheme = htmlTheme || bodyTheme;
303
+ currentTheme = themeMap[dataTheme] || defaultConfig.theme;
304
+ console.log('[astro-mermaid] Using theme:', currentTheme, 'from', htmlTheme ? 'html' : 'body');
305
+ }
306
+
307
+ // Configure mermaid with gitGraph support
308
+ mermaid.initialize({
309
+ ...defaultConfig,
310
+ theme: currentTheme,
311
+ gitGraph: {
312
+ mainBranchName: 'main',
313
+ showCommitLabel: true,
314
+ showBranches: true,
315
+ rotateCommitLabel: true
278
316
  }
317
+ });
279
318
 
280
- // Initialize immediately since DOM is ready
281
- initMermaid();
319
+ // Render each diagram
320
+ for (const diagram of diagrams) {
321
+ // Skip if already processed
322
+ if (diagram.hasAttribute('data-processed')) continue;
282
323
 
283
- // Re-render on theme change if auto-theme is enabled
284
- if (${autoTheme}) {
285
- const observer = new MutationObserver((mutations) => {
286
- for (const mutation of mutations) {
287
- if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
288
- // Reset processed state and re-render
289
- document.querySelectorAll('pre.mermaid[data-processed]').forEach(diagram => {
290
- diagram.removeAttribute('data-processed');
291
- });
292
- initMermaid();
293
- }
294
- }
295
- });
296
-
297
- // Observe both html and body for data-theme changes
298
- observer.observe(document.documentElement, {
299
- attributes: true,
300
- attributeFilter: ['data-theme']
301
- });
302
- observer.observe(document.body, {
303
- attributes: true,
304
- attributeFilter: ['data-theme']
305
- });
324
+ // Store original content
325
+ if (!diagram.hasAttribute('data-diagram')) {
326
+ diagram.setAttribute('data-diagram', diagram.textContent || '');
306
327
  }
307
328
 
308
- // Handle view transitions (for Astro View Transitions API)
309
- document.addEventListener('astro:after-swap', () => {
310
- // Check again if new page has diagrams
311
- if (hasMermaidDiagrams()) {
329
+ const diagramDefinition = diagram.getAttribute('data-diagram') || '';
330
+ const id = 'mermaid-' + Math.random().toString(36).slice(2, 11);
331
+
332
+ console.log('[astro-mermaid] Rendering diagram:', id);
333
+
334
+ try {
335
+ // Clear any existing error state
336
+ const existingGraph = document.getElementById(id);
337
+ if (existingGraph) {
338
+ existingGraph.remove();
339
+ }
340
+
341
+ const { svg } = await mermaid.render(id, diagramDefinition);
342
+ diagram.innerHTML = svg;
343
+ diagram.setAttribute('data-processed', 'true');
344
+ console.log('[astro-mermaid] Successfully rendered diagram:', id);
345
+ } catch (error) {
346
+ console.error('[astro-mermaid] Mermaid rendering error for diagram:', id, error);
347
+ diagram.innerHTML = \`<div style="color: red; padding: 1rem; border: 1px solid red; border-radius: 0.5rem;">
348
+ <strong>Error rendering diagram:</strong><br/>
349
+ \${error.message || 'Unknown error'}
350
+ </div>\`;
351
+ diagram.setAttribute('data-processed', 'true');
352
+ }
353
+ }
354
+ }
355
+
356
+ // Initialize on first load if there are diagrams
357
+ if (hasMermaidDiagrams()) {
358
+ console.log('[astro-mermaid] Mermaid diagrams detected on initial load');
359
+ initMermaid();
360
+ } else {
361
+ console.log('[astro-mermaid] No mermaid diagrams found on initial load');
362
+ }
363
+
364
+ // Re-render on theme change if auto-theme is enabled
365
+ if (${autoTheme}) {
366
+ const observer = new MutationObserver((mutations) => {
367
+ for (const mutation of mutations) {
368
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
369
+ // Reset processed state and re-render
370
+ document.querySelectorAll('pre.mermaid[data-processed]').forEach(diagram => {
371
+ diagram.removeAttribute('data-processed');
372
+ });
312
373
  initMermaid();
313
374
  }
314
- });
315
- }).catch(error => {
316
- console.error('[astro-mermaid] Failed to load mermaid:', error);
375
+ }
376
+ });
377
+
378
+ // Observe both html and body for data-theme changes
379
+ observer.observe(document.documentElement, {
380
+ attributes: true,
381
+ attributeFilter: ['data-theme']
382
+ });
383
+ observer.observe(document.body, {
384
+ attributes: true,
385
+ attributeFilter: ['data-theme']
317
386
  });
318
- } else {
319
- console.log('[astro-mermaid] No mermaid diagrams found on this page, skipping mermaid.js load');
320
387
  }
388
+
389
+ // Handle view transitions (for Astro View Transitions API)
390
+ // This is registered ALWAYS, not just when initial page has diagrams
391
+ document.addEventListener('astro:after-swap', () => {
392
+ console.log('[astro-mermaid] View transition detected');
393
+ // Check if new page has diagrams
394
+ if (hasMermaidDiagrams()) {
395
+ initMermaid();
396
+ }
397
+ });
321
398
  `;
322
399
 
323
400
  injectScript('page', mermaidScriptContent);
@@ -407,4 +484,4 @@ if (elkModule?.default) {
407
484
  }
408
485
  }
409
486
  };
410
- }
487
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-mermaid",
3
- "version": "1.1.0",
3
+ "version": "1.3.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",
@@ -43,11 +43,26 @@
43
43
  "mdast-util-to-string": "^4.0.0",
44
44
  "unist-util-visit": "^5.0.0"
45
45
  },
46
+ "scripts": {
47
+ "claude": "claude",
48
+ "test": "vitest",
49
+ "test:ui": "vitest --ui",
50
+ "test:coverage": "vitest --coverage"
51
+ },
46
52
  "devDependencies": {
53
+ "@anthropic-ai/claude-code": "^2.0.71",
47
54
  "@types/hast": "^3.0.4",
55
+ "@vitest/ui": "^3.2.4",
48
56
  "astro": "^5.0.0",
57
+ "hast-util-from-html": "^2.0.3",
49
58
  "mermaid": "^11.0.0",
50
- "typescript": "^5.0.0"
59
+ "rehype-parse": "^9.0.1",
60
+ "rehype-stringify": "^10.0.1",
61
+ "remark-mermaid": "^0.2.0",
62
+ "remark-parse": "^11.0.0",
63
+ "typescript": "^5.0.0",
64
+ "unified": "^11.0.5",
65
+ "vitest": "^3.2.4"
51
66
  },
52
67
  "repository": {
53
68
  "type": "git",
@@ -57,4 +72,4 @@
57
72
  "bugs": {
58
73
  "url": "https://github.com/joesaby/astro-mermaid/issues"
59
74
  }
60
- }
75
+ }