astro-mermaid 1.0.1 → 1.0.3

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
@@ -1,6 +1,6 @@
1
1
  # astro-mermaid
2
2
 
3
- An Astro integration for rendering Mermaid diagrams with automatic theme switching and client-side rendering. Instead of using playright, I used the approach done in https://github.com/cloudflare/cloudflare-docs
3
+ An Astro integration for rendering Mermaid diagrams with automatic theme switching and client-side rendering. This follows the mermaid integration in [cloudflare-docs](https://github.com/cloudflare/cloudflare-docs)
4
4
 
5
5
  ## Features
6
6
 
@@ -10,6 +10,11 @@ An Astro integration for rendering Mermaid diagrams with automatic theme switchi
10
10
  - ⚡ Vite optimization for fast development
11
11
  - 🔧 Customizable mermaid configuration
12
12
  - 🎯 TypeScript support
13
+ - 🔒 Privacy-focused with no external server dependencies
14
+ - 🌐 Offline-capable rendering
15
+ - ⚡ Zero network latency for diagram generation
16
+ - 📦 Conditional loading - mermaid.js only loads on pages with diagrams
17
+ - 🎭 Smooth loading animations to prevent layout shifts
13
18
 
14
19
  ## Installation
15
20
 
@@ -27,7 +32,28 @@ import mermaid from 'astro-mermaid';
27
32
 
28
33
  export default defineConfig({
29
34
  integrations: [
30
- mermaid()
35
+ mermaid({
36
+ theme: 'forest'
37
+ })
38
+ ]
39
+ });
40
+ ```
41
+
42
+ ### Important: Integration Order
43
+
44
+ When using with Starlight or other integrations that process markdown, make sure to place the mermaid integration **before** them:
45
+
46
+ ```js
47
+ import { defineConfig } from 'astro/config';
48
+ import starlight from '@astrojs/starlight';
49
+ import mermaid from 'astro-mermaid';
50
+
51
+ export default defineConfig({
52
+ integrations: [
53
+ mermaid(), // Must come BEFORE starlight
54
+ starlight({
55
+ title: 'My Docs'
56
+ })
31
57
  ]
32
58
  });
33
59
  ```
@@ -59,10 +85,53 @@ mermaid({
59
85
  flowchart: {
60
86
  curve: 'basis'
61
87
  }
62
- }
88
+ },
89
+
90
+ // Register icon packs for use in diagrams
91
+ iconPacks: [
92
+ {
93
+ name: 'logos',
94
+ loader: () => fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then(res => res.json())
95
+ },
96
+ {
97
+ name: 'iconoir',
98
+ loader: () => fetch('https://unpkg.com/@iconify-json/iconoir@1/icons.json').then(res => res.json())
99
+ }
100
+ ]
63
101
  })
64
102
  ```
65
103
 
104
+ ## Icon Packs
105
+
106
+ You can register icon packs to use custom icons in your diagrams. Icon packs are loaded from Iconify JSON sources:
107
+
108
+ ```js
109
+ iconPacks: [
110
+ {
111
+ name: 'logos',
112
+ loader: () => fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then(res => res.json())
113
+ }
114
+ ]
115
+ ```
116
+
117
+ Then use icons in your diagrams:
118
+
119
+ ````markdown
120
+ ```mermaid
121
+ architecture-beta
122
+ group api(logos:aws-lambda)[API]
123
+
124
+ service db(logos:postgresql)[Database] in api
125
+ service disk1(logos:aws-s3)[Storage] in api
126
+ service disk2(logos:cloudflare)[CDN] in api
127
+ service server(logos:docker)[Server] in api
128
+
129
+ db:L -- R:server
130
+ disk1:T -- B:server
131
+ disk2:T -- B:db
132
+ ```
133
+ ````
134
+
66
135
  ## Theme Switching
67
136
 
68
137
  If `autoTheme` is enabled (default), the integration will automatically switch between themes based on your site's `data-theme` attribute:
@@ -70,6 +139,39 @@ If `autoTheme` is enabled (default), the integration will automatically switch b
70
139
  - `data-theme="light"` → uses 'default' mermaid theme
71
140
  - `data-theme="dark"` → uses 'dark' mermaid theme
72
141
 
142
+ ## Client-Side Rendering & Security
143
+
144
+ ### 🔒 Privacy & Security Benefits
145
+
146
+ This integration uses **100% client-side rendering** with zero external dependencies at runtime:
147
+
148
+ - **No Data Transmission**: Your diagram content never leaves your browser
149
+ - **No External Servers**: No calls to mermaid.live or any external services
150
+ - **Offline Capable**: Works completely offline after initial page load
151
+ - **Zero Network Latency**: Instant diagram rendering without network delays
152
+ - **Corporate Firewall Friendly**: No external domains need to be whitelisted
153
+
154
+ ### ⚡ How It Works
155
+
156
+ 1. **Build Time**: Mermaid code blocks are transformed to `<pre class="mermaid">` elements
157
+ 2. **Runtime**: The bundled Mermaid JavaScript library renders diagrams locally
158
+ 3. **Output**: Pure SVG generated entirely in your browser
159
+
160
+ ```javascript
161
+ // All rendering happens locally - no network calls
162
+ import mermaid from 'mermaid';
163
+ const { svg } = await mermaid.render(id, diagramDefinition);
164
+ ```
165
+
166
+ ### 🛡️ Enterprise & Compliance
167
+
168
+ Perfect for:
169
+ - Corporate environments with strict security policies
170
+ - GDPR/privacy-compliant applications
171
+ - Air-gapped or restricted network environments
172
+ - Applications requiring data sovereignty
173
+ - High-security environments where external requests are prohibited
174
+
73
175
  ## Supported Diagrams
74
176
 
75
177
  All mermaid diagram types are supported:
@@ -90,6 +192,14 @@ All mermaid diagram types are supported:
90
192
  - Quadrant charts
91
193
  - And more!
92
194
 
195
+ ## Demo
196
+
197
+ Check out the [live demo](https://starlight-mermaid-demo.netlify.app/) built with Starlight.
198
+
199
+ ## Contributing
200
+
201
+ Contributions are welcome! Please feel free to submit a Pull Request.
202
+
93
203
  ## License
94
204
 
95
205
  MIT
@@ -1,5 +1,17 @@
1
1
  import type { AstroIntegration } from 'astro';
2
2
 
3
+ export interface IconPack {
4
+ /**
5
+ * Name of the icon pack
6
+ */
7
+ name: string;
8
+
9
+ /**
10
+ * Function that returns a promise resolving to the icon pack data
11
+ */
12
+ loader: () => Promise<any>;
13
+ }
14
+
3
15
  export interface AstroMermaidOptions {
4
16
  /**
5
17
  * Default mermaid theme
@@ -18,6 +30,20 @@ export interface AstroMermaidOptions {
18
30
  * @see https://mermaid.js.org/config/setup/modules/mermaidAPI.html#mermaidapi-configuration-defaults
19
31
  */
20
32
  mermaidConfig?: Record<string, any>;
33
+
34
+ /**
35
+ * Icon packs to register with mermaid
36
+ * @example
37
+ * ```js
38
+ * iconPacks: [
39
+ * {
40
+ * name: 'logos',
41
+ * loader: () => fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then(res => res.json())
42
+ * }
43
+ * ]
44
+ * ```
45
+ */
46
+ iconPacks?: IconPack[];
21
47
  }
22
48
 
23
49
  /**
@@ -65,7 +65,8 @@ export default function astroMermaid(options = {}) {
65
65
  const {
66
66
  theme = 'default',
67
67
  autoTheme = true,
68
- mermaidConfig = {}
68
+ mermaidConfig = {},
69
+ iconPacks = []
69
70
  } = options;
70
71
 
71
72
  return {
@@ -92,129 +93,163 @@ export default function astroMermaid(options = {}) {
92
93
  }
93
94
  });
94
95
 
95
- // Inject client-side mermaid script
96
- const mermaidScriptContent = `
97
- import mermaid from 'mermaid';
98
-
99
- // Mermaid configuration
100
- const defaultConfig = ${JSON.stringify({
101
- startOnLoad: false,
102
- theme: theme,
103
- ...mermaidConfig
104
- })};
96
+ // Serialize icon packs for client-side use
97
+ const iconPacksConfig = iconPacks.map(pack => ({
98
+ name: pack.name,
99
+ loader: pack.loader.toString()
100
+ }));
105
101
 
106
- // Theme mapping for auto-theme switching
107
- const themeMap = {
108
- 'light': 'default',
109
- 'dark': 'dark'
102
+ // Inject client-side mermaid script with conditional loading
103
+ const mermaidScriptContent = `
104
+ // Check if page has mermaid diagrams
105
+ const hasMermaidDiagrams = () => {
106
+ return document.querySelectorAll('pre.mermaid').length > 0;
110
107
  };
111
108
 
112
- // Initialize all mermaid diagrams
113
- async function initMermaid() {
114
- console.log('[astro-mermaid] Initializing mermaid diagrams...');
115
- const diagrams = document.querySelectorAll('pre.mermaid');
116
-
117
- console.log('[astro-mermaid] Found', diagrams.length, 'mermaid diagrams');
118
-
119
- if (diagrams.length === 0) {
120
- console.log('[astro-mermaid] No mermaid diagrams found. Looking for code blocks...');
121
- const codeBlocks = document.querySelectorAll('pre code.language-mermaid');
122
- console.log('[astro-mermaid] Found', codeBlocks.length, 'mermaid code blocks');
123
- return;
124
- }
125
-
126
- // Get current theme
127
- let currentTheme = defaultConfig.theme;
128
-
129
- if (${autoTheme}) {
130
- const dataTheme = document.documentElement.getAttribute('data-theme');
131
- currentTheme = themeMap[dataTheme] || defaultConfig.theme;
132
- console.log('[astro-mermaid] Using theme:', currentTheme);
133
- }
134
-
135
- // Configure mermaid
136
- mermaid.initialize({
137
- ...defaultConfig,
138
- theme: currentTheme
139
- });
109
+ // Only proceed if there are mermaid diagrams on the page
110
+ if (hasMermaidDiagrams()) {
111
+ console.log('[astro-mermaid] Mermaid diagrams detected, loading mermaid.js...');
140
112
 
141
- // Render each diagram
142
- for (const diagram of diagrams) {
143
- // Skip if already processed
144
- if (diagram.hasAttribute('data-processed')) continue;
145
-
146
- // Store original content
147
- if (!diagram.hasAttribute('data-diagram')) {
148
- diagram.setAttribute('data-diagram', diagram.textContent || '');
113
+ // Dynamically import mermaid only when needed
114
+ import('mermaid').then(async ({ default: mermaid }) => {
115
+ // Register icon packs if provided
116
+ const iconPacks = ${JSON.stringify(iconPacksConfig)};
117
+ if (iconPacks && iconPacks.length > 0) {
118
+ console.log('[astro-mermaid] Registering', iconPacks.length, 'icon packs');
119
+ const packs = iconPacks.map(pack => ({
120
+ name: pack.name,
121
+ loader: new Function('return ' + pack.loader)()
122
+ }));
123
+ await mermaid.registerIconPacks(packs);
149
124
  }
150
-
151
- const diagramDefinition = diagram.getAttribute('data-diagram') || '';
152
- const id = 'mermaid-' + Math.random().toString(36).slice(2, 11);
153
-
154
- console.log('[astro-mermaid] Rendering diagram:', id);
155
-
156
- try {
157
- const { svg } = await mermaid.render(id, diagramDefinition);
158
- diagram.innerHTML = svg;
159
- diagram.setAttribute('data-processed', 'true');
160
- console.log('[astro-mermaid] Successfully rendered diagram:', id);
161
- } catch (error) {
162
- console.error('[astro-mermaid] Mermaid rendering error:', error);
163
- diagram.innerHTML = '<div style="color: red;">Error rendering diagram</div>';
125
+ // Mermaid configuration
126
+ const defaultConfig = ${JSON.stringify({
127
+ startOnLoad: false,
128
+ theme: theme,
129
+ ...mermaidConfig
130
+ })};
131
+
132
+ // Theme mapping for auto-theme switching
133
+ const themeMap = {
134
+ 'light': 'default',
135
+ 'dark': 'dark'
136
+ };
137
+
138
+ // Initialize all mermaid diagrams
139
+ async function initMermaid() {
140
+ console.log('[astro-mermaid] Initializing mermaid diagrams...');
141
+ const diagrams = document.querySelectorAll('pre.mermaid');
142
+
143
+ console.log('[astro-mermaid] Found', diagrams.length, 'mermaid diagrams');
144
+
145
+ if (diagrams.length === 0) {
146
+ return;
147
+ }
148
+
149
+ // Get current theme
150
+ let currentTheme = defaultConfig.theme;
151
+
152
+ if (${autoTheme}) {
153
+ const dataTheme = document.documentElement.getAttribute('data-theme');
154
+ currentTheme = themeMap[dataTheme] || defaultConfig.theme;
155
+ console.log('[astro-mermaid] Using theme:', currentTheme);
156
+ }
157
+
158
+ // Configure mermaid with gitGraph support
159
+ mermaid.initialize({
160
+ ...defaultConfig,
161
+ theme: currentTheme,
162
+ gitGraph: {
163
+ mainBranchName: 'main',
164
+ showCommitLabel: true,
165
+ showBranches: true,
166
+ rotateCommitLabel: true
167
+ }
168
+ });
169
+
170
+ // Render each diagram
171
+ for (const diagram of diagrams) {
172
+ // Skip if already processed
173
+ if (diagram.hasAttribute('data-processed')) continue;
174
+
175
+ // Store original content
176
+ if (!diagram.hasAttribute('data-diagram')) {
177
+ diagram.setAttribute('data-diagram', diagram.textContent || '');
178
+ }
179
+
180
+ const diagramDefinition = diagram.getAttribute('data-diagram') || '';
181
+ const id = 'mermaid-' + Math.random().toString(36).slice(2, 11);
182
+
183
+ console.log('[astro-mermaid] Rendering diagram:', id);
184
+
185
+ try {
186
+ // Clear any existing error state
187
+ const existingGraph = document.getElementById(id);
188
+ if (existingGraph) {
189
+ existingGraph.remove();
190
+ }
191
+
192
+ const { svg } = await mermaid.render(id, diagramDefinition);
193
+ diagram.innerHTML = svg;
194
+ diagram.setAttribute('data-processed', 'true');
195
+ console.log('[astro-mermaid] Successfully rendered diagram:', id);
196
+ } catch (error) {
197
+ console.error('[astro-mermaid] Mermaid rendering error for diagram:', id, error);
198
+ diagram.innerHTML = \`<div style="color: red; padding: 1rem; border: 1px solid red; border-radius: 0.5rem;">
199
+ <strong>Error rendering diagram:</strong><br/>
200
+ \${error.message || 'Unknown error'}
201
+ </div>\`;
202
+ diagram.setAttribute('data-processed', 'true');
203
+ }
204
+ }
164
205
  }
165
- }
166
- }
167
206
 
168
- // Initialize on DOM ready
169
- if (document.readyState === 'loading') {
170
- document.addEventListener('DOMContentLoaded', initMermaid);
171
- } else {
172
- initMermaid();
173
- }
207
+ // Initialize immediately since DOM is ready
208
+ initMermaid();
174
209
 
175
- // Re-render on theme change if auto-theme is enabled
176
- if (${autoTheme}) {
177
- const observer = new MutationObserver((mutations) => {
178
- for (const mutation of mutations) {
179
- if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
180
- // Reset processed state and re-render
181
- document.querySelectorAll('pre.mermaid[data-processed]').forEach(diagram => {
182
- diagram.removeAttribute('data-processed');
183
- });
210
+ // Re-render on theme change if auto-theme is enabled
211
+ if (${autoTheme}) {
212
+ const observer = new MutationObserver((mutations) => {
213
+ for (const mutation of mutations) {
214
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
215
+ // Reset processed state and re-render
216
+ document.querySelectorAll('pre.mermaid[data-processed]').forEach(diagram => {
217
+ diagram.removeAttribute('data-processed');
218
+ });
219
+ initMermaid();
220
+ }
221
+ }
222
+ });
223
+
224
+ observer.observe(document.documentElement, {
225
+ attributes: true,
226
+ attributeFilter: ['data-theme']
227
+ });
228
+ }
229
+
230
+ // Handle view transitions (for Astro View Transitions API)
231
+ document.addEventListener('astro:after-swap', () => {
232
+ // Check again if new page has diagrams
233
+ if (hasMermaidDiagrams()) {
184
234
  initMermaid();
185
235
  }
186
- }
187
- });
188
-
189
- observer.observe(document.documentElement, {
190
- attributes: true,
191
- attributeFilter: ['data-theme']
236
+ });
237
+ }).catch(error => {
238
+ console.error('[astro-mermaid] Failed to load mermaid:', error);
192
239
  });
240
+ } else {
241
+ console.log('[astro-mermaid] No mermaid diagrams found on this page, skipping mermaid.js load');
193
242
  }
194
-
195
- // Handle view transitions (for Astro View Transitions API)
196
- document.addEventListener('astro:after-swap', initMermaid);
197
243
  `;
198
244
 
199
245
  injectScript('page', mermaidScriptContent);
200
246
 
201
- // Add CSS to the page
247
+ // Add CSS to the page with layout shift prevention
202
248
  injectScript('page', `
203
249
  // Add CSS for mermaid diagrams
204
250
  const style = document.createElement('style');
205
251
  style.textContent = \`
206
- /* Hide diagrams until processed to prevent flash of unstyled content */
207
- pre.mermaid:not([data-processed]) {
208
- opacity: 0;
209
- transition: opacity 0.3s ease-in-out;
210
- }
211
-
212
- /* Show processed diagrams */
213
- pre.mermaid[data-processed] {
214
- opacity: 1;
215
- }
216
-
217
- /* Center mermaid diagrams and add spacing */
252
+ /* Prevent layout shifts by setting minimum height */
218
253
  pre.mermaid {
219
254
  display: flex;
220
255
  justify-content: center;
@@ -224,6 +259,37 @@ document.addEventListener('astro:after-swap', initMermaid);
224
259
  background-color: transparent;
225
260
  border: none;
226
261
  overflow: auto;
262
+ min-height: 200px; /* Prevent layout shift */
263
+ position: relative;
264
+ }
265
+
266
+ /* Loading state with skeleton loader */
267
+ pre.mermaid:not([data-processed]) {
268
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
269
+ background-size: 200% 100%;
270
+ animation: shimmer 1.5s infinite;
271
+ }
272
+
273
+ /* Dark mode skeleton loader */
274
+ [data-theme="dark"] pre.mermaid:not([data-processed]) {
275
+ background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
276
+ background-size: 200% 100%;
277
+ }
278
+
279
+ @keyframes shimmer {
280
+ 0% {
281
+ background-position: -200% 0;
282
+ }
283
+ 100% {
284
+ background-position: 200% 0;
285
+ }
286
+ }
287
+
288
+ /* Show processed diagrams with smooth transition */
289
+ pre.mermaid[data-processed] {
290
+ animation: none;
291
+ background: transparent;
292
+ min-height: auto; /* Allow natural height after render */
227
293
  }
228
294
 
229
295
  /* Ensure responsive sizing for mermaid SVGs */
@@ -234,18 +300,29 @@ document.addEventListener('astro:after-swap', initMermaid);
234
300
 
235
301
  /* Optional: Add subtle background for better visibility */
236
302
  @media (prefers-color-scheme: dark) {
237
- pre.mermaid {
303
+ pre.mermaid[data-processed] {
238
304
  background-color: rgba(255, 255, 255, 0.02);
239
305
  border-radius: 0.5rem;
240
306
  }
241
307
  }
242
308
 
243
309
  @media (prefers-color-scheme: light) {
244
- pre.mermaid {
310
+ pre.mermaid[data-processed] {
245
311
  background-color: rgba(0, 0, 0, 0.02);
246
312
  border-radius: 0.5rem;
247
313
  }
248
314
  }
315
+
316
+ /* Respect user's color scheme preference */
317
+ [data-theme="dark"] pre.mermaid[data-processed] {
318
+ background-color: rgba(255, 255, 255, 0.02);
319
+ border-radius: 0.5rem;
320
+ }
321
+
322
+ [data-theme="light"] pre.mermaid[data-processed] {
323
+ background-color: rgba(0, 0, 0, 0.02);
324
+ border-radius: 0.5rem;
325
+ }
249
326
  \`;
250
327
  document.head.appendChild(style);
251
328
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-mermaid",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",