svelte-streamdown 2.0.2 → 2.0.4

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.
@@ -61,8 +61,103 @@
61
61
  }
62
62
  });
63
63
 
64
+ const sanitizeMermaidCode = (code: string): string => {
65
+ try {
66
+ let sanitized = code;
67
+
68
+ // 1. Remove Byte Order Mark (BOM)
69
+ sanitized = sanitized.replace(/^\uFEFF/, '');
70
+
71
+ // 2. Normalize Unicode (NFC form for consistent rendering)
72
+ sanitized = sanitized.normalize('NFC');
73
+
74
+ // 3. Remove invisible/zero-width characters
75
+ sanitized = sanitized.replace(/[\u200B-\u200F\u2028-\u202F\u205F-\u206F]/g, '');
76
+
77
+ // 4. Remove control characters (except tab, line feed, carriage return)
78
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
79
+
80
+ // 5. Normalize line endings to LF
81
+ sanitized = sanitized.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
82
+
83
+ // 6. Decode common HTML entities that might appear in Mermaid code
84
+ const htmlEntities: Record<string, string> = {
85
+ '&lt;': '<',
86
+ '&gt;': '>',
87
+ '&amp;': '&',
88
+ '&quot;': '"',
89
+ '&#39;': "'",
90
+ '&apos;': "'",
91
+ '&nbsp;': ' ',
92
+ '&hellip;': '...',
93
+ '&mdash;': '--',
94
+ '&ndash;': '-',
95
+ '&lsquo;': "'",
96
+ '&rsquo;': "'",
97
+ '&ldquo;': '"',
98
+ '&rdquo;': '"'
99
+ };
100
+
101
+ for (const [entity, replacement] of Object.entries(htmlEntities)) {
102
+ sanitized = sanitized.replace(new RegExp(entity, 'g'), replacement);
103
+ }
104
+
105
+ // 7. Convert smart quotes and other quote variants to standard quotes
106
+ sanitized = sanitized
107
+ .replace(/[\u2018\u2019]/g, "'") // Smart single quotes
108
+ .replace(/[\u201C\u201D]/g, '"') // Smart double quotes
109
+ .replace(/[\u2013\u2014]/g, '-') // Em/en dashes
110
+ .replace(/\u2026/g, '...'); // Horizontal ellipsis
111
+
112
+ // 8. Trim leading/trailing whitespace from each line and remove empty lines
113
+ sanitized = sanitized
114
+ .split('\n')
115
+ .map((line) => line.trim())
116
+ .filter((line) => line.length > 0)
117
+ .join('\n');
118
+
119
+ // 9. Normalize multiple spaces/tabs to single space (but preserve indentation in code blocks)
120
+ sanitized = sanitized.replace(/[ \t]+/g, ' ');
121
+
122
+ // 10. Handle over-escaped characters (common in copied code)
123
+ // Convert double backslashes to single (except in JSON strings)
124
+ sanitized = sanitized.replace(/\\\\(?![\\"])/g, '\\');
125
+
126
+ // 11. Remove non-breaking spaces and other special spaces
127
+ sanitized = sanitized.replace(/[\u00A0\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]/g, ' ');
128
+
129
+ // 12. Ensure proper spacing around operators and keywords
130
+ // Add space after commas if missing (common in CSV-like data)
131
+ sanitized = sanitized.replace(/,([^\s])/g, ', $1');
132
+
133
+ // 13. Clean up Mermaid-specific issues
134
+ // Remove trailing semicolons that might break parsing
135
+ sanitized = sanitized.replace(/;+\s*$/gm, '');
136
+
137
+ // Ensure proper spacing in flowchart syntax
138
+ sanitized = sanitized.replace(
139
+ /([A-Za-z0-9_]+)(\-\-|\-\-\>|\-\.\-|\-\.\-\>|\=\=|\=\=\>|\=\.\=\>|\=\.\-\>)/g,
140
+ '$1 $2'
141
+ );
142
+
143
+ // 14. Final cleanup: trim and ensure single trailing newline
144
+ sanitized = sanitized.trim();
145
+ if (sanitized && !sanitized.endsWith('\n')) {
146
+ sanitized += '\n';
147
+ }
148
+
149
+ return sanitized;
150
+ } catch (error) {
151
+ console.warn('Error during Mermaid code sanitization:', error);
152
+ // Return original code if sanitization fails
153
+ return code;
154
+ }
155
+ };
156
+
64
157
  const renderMermaid = async (code: string, element: HTMLElement) => {
65
158
  try {
159
+ // Sanitize the code first
160
+
66
161
  // Default configuration
67
162
  const defaultConfig: MermaidConfig = {
68
163
  theme: 'base',
@@ -83,20 +178,16 @@
83
178
  const mergedConfig = { ...defaultConfig };
84
179
  mermaid.initialize(mergedConfig);
85
180
 
86
- // Validate and render the diagram
87
- const isValidDiagram = await mermaid.parse(code);
88
- if (!isValidDiagram) {
89
- throw new Error('Invalid mermaid diagram syntax');
90
- }
91
181
  // Use a stable ID based on chart content hash and timestamp to ensure uniqueness
92
182
  const chartHash = code.split('').reduce((acc, char) => {
93
183
  // biome-ignore lint/suspicious/noBitwiseOperators: "Required for Mermaid"
94
184
  return ((acc << 5) - acc + char.charCodeAt(0)) | 0;
95
185
  }, 0);
186
+
96
187
  const uniqueId = `mermaid-${Math.abs(chartHash)}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
97
188
 
98
189
  // Render the diagram
99
- const { svg: svgString } = await mermaid.render(uniqueId, code);
190
+ const { svg: svgString } = await mermaid.render(uniqueId, sanitizeMermaidCode(code));
100
191
  const svg = new DOMParser().parseFromString(svgString, 'image/svg+xml').documentElement;
101
192
 
102
193
  const svgTarget = element.querySelector('[data-mermaid-svg]')!;
@@ -110,7 +201,10 @@
110
201
  panzoom.zoomToFit();
111
202
  panzoom.zoomToFit();
112
203
  } catch (err) {
113
- // Do nothing
204
+ const sanitizedCode = sanitizeMermaidCode(code);
205
+ console.error('Mermaid rendering error:', err);
206
+ console.error('Original code:', code);
207
+ console.error('Sanitized code:', sanitizedCode);
114
208
  }
115
209
  };
116
210
  </script>
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { default as Streamdown, useStreamdown } from './Streamdown.svelte';
1
+ export { default as Streamdown } from './Streamdown.svelte';
2
+ export { useStreamdown } from './Streamdown.js';
2
3
  export * from './Elements/index.js';
3
4
  export { theme, shadcnTheme, mergeTheme, cn, type Theme } from './theme.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
- export { default as Streamdown, useStreamdown } from './Streamdown.svelte';
1
+ export { default as Streamdown } from './Streamdown.svelte';
2
+ export { useStreamdown } from './Streamdown.js';
2
3
  export * from './Elements/index.js';
3
4
  export { theme, shadcnTheme, mergeTheme, cn } from './theme.js';
@@ -1,7 +1,7 @@
1
1
  import { Lexer } from 'marked';
2
2
  const variants = ['note', 'tip', 'important', 'warning', 'caution'];
3
3
  export function createSyntaxPattern(type) {
4
- return `^\\s*\\[!${type.toUpperCase()}\\]\\s+`;
4
+ return `^\\s*[\\*_]*\\[!${type.toUpperCase()}\\][\\*_]*\\s*`;
5
5
  }
6
6
  export default function markedAlert() {
7
7
  const defaultLexer = new Lexer({ gfm: true });
@@ -29,7 +29,11 @@ export function processAlertToken(token, tokenizer) {
29
29
  return;
30
30
  const tokens = token.tokens
31
31
  .map((token) => {
32
- return tokenizer.lexer.blockTokens(token.raw.replaceAll(`[!${matchedVariant.toUpperCase()}]`, '').trim(), [])[0];
32
+ let cleanedRaw = token.raw;
33
+ // Remove alert markers with any markdown formatting (asterisks/underscores)
34
+ const alertPattern = new RegExp(`[\\*_]*\\[!${matchedVariant.toUpperCase()}\\][\\*_]*`, 'g');
35
+ cleanedRaw = cleanedRaw.replaceAll(alertPattern, '').trim();
36
+ return tokenizer.lexer.blockTokens(cleanedRaw, [])[0];
33
37
  })
34
38
  .filter(Boolean);
35
39
  Object.assign(token, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",