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.
- package/dist/Elements/Mermaid.svelte +101 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/marked/marked-alert.js +6 -2
- package/package.json +1 -1
|
@@ -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
|
+
'<': '<',
|
|
86
|
+
'>': '>',
|
|
87
|
+
'&': '&',
|
|
88
|
+
'"': '"',
|
|
89
|
+
''': "'",
|
|
90
|
+
''': "'",
|
|
91
|
+
' ': ' ',
|
|
92
|
+
'…': '...',
|
|
93
|
+
'—': '--',
|
|
94
|
+
'–': '-',
|
|
95
|
+
'‘': "'",
|
|
96
|
+
'’': "'",
|
|
97
|
+
'“': '"',
|
|
98
|
+
'”': '"'
|
|
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
|
-
|
|
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
|
|
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
|
|
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()}\\]
|
|
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
|
-
|
|
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, {
|