svelte-streamdown 2.0.3 → 2.1.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/dist/Elements/Code.svelte +16 -14
- package/dist/Elements/Element.svelte +6 -0
- package/dist/Elements/Mermaid.svelte +196 -103
- package/dist/Streamdown.d.ts +9 -0
- package/dist/Streamdown.svelte +13 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/marked/marked-alert.js +6 -2
- package/dist/theme.js +6 -6
- package/dist/utils/hightlighter.svelte.d.ts +0 -1
- package/dist/utils/hightlighter.svelte.js +7 -5
- package/dist/utils/parse-incomplete-markdown.js +24 -1
- package/package.json +1 -1
|
@@ -47,21 +47,23 @@
|
|
|
47
47
|
<div class={streamdown.theme.code.base} data-language={language}>
|
|
48
48
|
<div class={streamdown.theme.code.header} data-code-block-header data-language={language}>
|
|
49
49
|
<span class={streamdown.theme.code.language}>{language}</span>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
{#if streamdown.controls.code}
|
|
51
|
+
<div class="flex items-center gap-2">
|
|
52
|
+
<!-- Download button snippet -->
|
|
53
|
+
<button
|
|
54
|
+
class={streamdown.theme.code.button}
|
|
55
|
+
onclick={downloadCode}
|
|
56
|
+
title="Download file"
|
|
57
|
+
type="button"
|
|
58
|
+
>
|
|
59
|
+
{@render downloadIcon()}
|
|
60
|
+
</button>
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
<button class={streamdown.theme.code.button} onclick={copy.copy} type="button">
|
|
63
|
+
{@render copyIcon()}
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
65
67
|
</div>
|
|
66
68
|
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
67
69
|
<div>
|
|
@@ -203,6 +203,12 @@
|
|
|
203
203
|
<Alert {token} {children} />
|
|
204
204
|
{:else if token.type === 'footnoteRef'}
|
|
205
205
|
<FootnoteRef {token} />
|
|
206
|
+
{:else if token.type === 'html'}
|
|
207
|
+
{#if streamdown.renderHtml}
|
|
208
|
+
{@const content =
|
|
209
|
+
typeof streamdown.renderHtml === 'function' ? streamdown.renderHtml(token) : token.raw}
|
|
210
|
+
{@html content}
|
|
211
|
+
{/if}
|
|
206
212
|
{:else}
|
|
207
213
|
<!-- For tokens we don't handle specifically, render children or fallback -->
|
|
208
214
|
{@render children?.()}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
2
|
+
import { flushSync, onMount, tick } from 'svelte';
|
|
3
3
|
import { useStreamdown } from '../Streamdown.js';
|
|
4
4
|
import Slot from './Slot.svelte';
|
|
5
5
|
import type { Tokens } from 'marked';
|
|
@@ -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,14 @@
|
|
|
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
|
-
// Use a stable ID based on chart content hash and timestamp to ensure uniqueness
|
|
92
181
|
const chartHash = code.split('').reduce((acc, char) => {
|
|
93
|
-
// biome-ignore lint/suspicious/noBitwiseOperators: "Required for Mermaid"
|
|
94
182
|
return ((acc << 5) - acc + char.charCodeAt(0)) | 0;
|
|
95
183
|
}, 0);
|
|
184
|
+
|
|
96
185
|
const uniqueId = `mermaid-${Math.abs(chartHash)}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
97
186
|
|
|
98
187
|
// Render the diagram
|
|
99
|
-
const { svg: svgString } = await mermaid.render(uniqueId, code);
|
|
188
|
+
const { svg: svgString } = await mermaid.render(uniqueId, sanitizeMermaidCode(code));
|
|
100
189
|
const svg = new DOMParser().parseFromString(svgString, 'image/svg+xml').documentElement;
|
|
101
190
|
|
|
102
191
|
const svgTarget = element.querySelector('[data-mermaid-svg]')!;
|
|
@@ -105,12 +194,14 @@
|
|
|
105
194
|
svgTarget.setAttribute(attribute.name, attribute.value);
|
|
106
195
|
});
|
|
107
196
|
svgTarget.innerHTML = svg.innerHTML;
|
|
108
|
-
// After rendering, fit the SVG within its parent container
|
|
109
197
|
|
|
110
198
|
panzoom.zoomToFit();
|
|
111
199
|
panzoom.zoomToFit();
|
|
112
200
|
} catch (err) {
|
|
113
|
-
|
|
201
|
+
const sanitizedCode = sanitizeMermaidCode(code);
|
|
202
|
+
console.error('Mermaid rendering error:', err);
|
|
203
|
+
console.error('Original code:', code);
|
|
204
|
+
console.error('Sanitized code:', sanitizedCode);
|
|
114
205
|
}
|
|
115
206
|
};
|
|
116
207
|
</script>
|
|
@@ -122,101 +213,103 @@
|
|
|
122
213
|
{@attach insider.attach}
|
|
123
214
|
data-expanded={'false'}
|
|
124
215
|
>
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
d="
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
216
|
+
{#if streamdown.controls.mermaid}
|
|
217
|
+
<div class={streamdown.theme.mermaid.buttons}>
|
|
218
|
+
<button
|
|
219
|
+
class={streamdown.theme.mermaid.button}
|
|
220
|
+
aria-label="Zoom to fit"
|
|
221
|
+
onclick={() => panzoom.zoomToFit()}
|
|
222
|
+
data-panzoom-ignore
|
|
223
|
+
>
|
|
224
|
+
<svg
|
|
225
|
+
class={streamdown.theme.mermaid.icon}
|
|
226
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
227
|
+
viewBox="0 0 24 24"
|
|
228
|
+
fill="none"
|
|
229
|
+
stroke="currentColor"
|
|
230
|
+
stroke-width="2"
|
|
231
|
+
stroke-linecap="round"
|
|
232
|
+
stroke-linejoin="round"
|
|
233
|
+
><path d="M3 7V5a2 2 0 0 1 2-2h2" /><path d="M17 3h2a2 2 0 0 1 2 2v2" /><path
|
|
234
|
+
d="M21 17v2a2 2 0 0 1-2 2h-2"
|
|
235
|
+
/><path d="M7 21H5a2 2 0 0 1-2-2v-2" /><rect
|
|
236
|
+
width="10"
|
|
237
|
+
height="8"
|
|
238
|
+
x="7"
|
|
239
|
+
y="8"
|
|
240
|
+
rx="1"
|
|
241
|
+
/></svg
|
|
242
|
+
>
|
|
243
|
+
</button>
|
|
244
|
+
<button
|
|
245
|
+
class={streamdown.theme.mermaid.button}
|
|
246
|
+
aria-label="Zoom in"
|
|
247
|
+
onclick={() => panzoom.zoomIn()}
|
|
248
|
+
data-panzoom-ignore
|
|
150
249
|
>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
250
|
+
<svg
|
|
251
|
+
class={streamdown.theme.mermaid.icon}
|
|
252
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
253
|
+
viewBox="0 0 24 24"
|
|
254
|
+
fill="none"
|
|
255
|
+
stroke="currentColor"
|
|
256
|
+
stroke-width="2"
|
|
257
|
+
stroke-linecap="round"
|
|
258
|
+
stroke-linejoin="round"
|
|
259
|
+
><circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /><line
|
|
260
|
+
x1="11"
|
|
261
|
+
x2="11"
|
|
262
|
+
y1="8"
|
|
263
|
+
y2="14"
|
|
264
|
+
/><line x1="8" x2="14" y1="11" y2="11" /></svg
|
|
265
|
+
>
|
|
266
|
+
</button>
|
|
267
|
+
<button
|
|
268
|
+
class={streamdown.theme.mermaid.button}
|
|
269
|
+
aria-label="Zoom out"
|
|
270
|
+
onclick={() => panzoom.zoomOut()}
|
|
271
|
+
data-panzoom-ignore
|
|
272
|
+
><svg
|
|
273
|
+
class={streamdown.theme.mermaid.icon}
|
|
274
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
275
|
+
viewBox="0 0 24 24"
|
|
276
|
+
fill="none"
|
|
277
|
+
stroke="currentColor"
|
|
278
|
+
stroke-width="2"
|
|
279
|
+
stroke-linecap="round"
|
|
280
|
+
stroke-linejoin="round"
|
|
281
|
+
><circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /><line
|
|
282
|
+
x1="8"
|
|
283
|
+
x2="14"
|
|
284
|
+
y1="11"
|
|
285
|
+
y2="11"
|
|
286
|
+
/></svg
|
|
287
|
+
></button
|
|
173
288
|
>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
data-panzoom-ignore
|
|
180
|
-
><svg
|
|
181
|
-
class={streamdown.theme.mermaid.icon}
|
|
182
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
183
|
-
viewBox="0 0 24 24"
|
|
184
|
-
fill="none"
|
|
185
|
-
stroke="currentColor"
|
|
186
|
-
stroke-width="2"
|
|
187
|
-
stroke-linecap="round"
|
|
188
|
-
stroke-linejoin="round"
|
|
189
|
-
><circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /><line
|
|
190
|
-
x1="8"
|
|
191
|
-
x2="14"
|
|
192
|
-
y1="11"
|
|
193
|
-
y2="11"
|
|
194
|
-
/></svg
|
|
195
|
-
></button
|
|
196
|
-
>
|
|
197
|
-
<button
|
|
198
|
-
class={streamdown.theme.mermaid.button}
|
|
199
|
-
aria-label="Toggle expand"
|
|
200
|
-
onclick={() => panzoom.toggleExpand()}
|
|
201
|
-
data-panzoom-ignore
|
|
202
|
-
>
|
|
203
|
-
<svg
|
|
204
|
-
class={streamdown.theme.mermaid.icon}
|
|
205
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
206
|
-
viewBox="0 0 24 24"
|
|
207
|
-
fill="none"
|
|
208
|
-
stroke="currentColor"
|
|
209
|
-
stroke-width="2"
|
|
210
|
-
stroke-linecap="round"
|
|
211
|
-
stroke-linejoin="round"
|
|
212
|
-
><path d="m15 15 6 6" /><path d="m15 9 6-6" /><path d="M21 16v5h-5" /><path
|
|
213
|
-
d="M21 8V3h-5"
|
|
214
|
-
/><path d="M3 16v5h5" /><path d="m3 21 6-6" /><path d="M3 8V3h5" /><path
|
|
215
|
-
d="M9 9 3 3"
|
|
216
|
-
/></svg
|
|
289
|
+
<button
|
|
290
|
+
class={streamdown.theme.mermaid.button}
|
|
291
|
+
aria-label="Toggle expand"
|
|
292
|
+
onclick={() => panzoom.toggleExpand()}
|
|
293
|
+
data-panzoom-ignore
|
|
217
294
|
>
|
|
218
|
-
|
|
219
|
-
|
|
295
|
+
<svg
|
|
296
|
+
class={streamdown.theme.mermaid.icon}
|
|
297
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
298
|
+
viewBox="0 0 24 24"
|
|
299
|
+
fill="none"
|
|
300
|
+
stroke="currentColor"
|
|
301
|
+
stroke-width="2"
|
|
302
|
+
stroke-linecap="round"
|
|
303
|
+
stroke-linejoin="round"
|
|
304
|
+
><path d="m15 15 6 6" /><path d="m15 9 6-6" /><path d="M21 16v5h-5" /><path
|
|
305
|
+
d="M21 8V3h-5"
|
|
306
|
+
/><path d="M3 16v5h5" /><path d="m3 21 6-6" /><path d="M3 8V3h5" /><path
|
|
307
|
+
d="M9 9 3 3"
|
|
308
|
+
/></svg
|
|
309
|
+
>
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
{/if}
|
|
220
313
|
<svg {@attach panzoom.attach} data-mermaid-svg></svg>
|
|
221
314
|
</div>
|
|
222
315
|
{:else}
|
package/dist/Streamdown.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export interface StreamdownContext extends Omit<StreamdownProps, keyof Snippets
|
|
|
7
7
|
snippets: Snippets;
|
|
8
8
|
shikiTheme: BundledTheme;
|
|
9
9
|
theme: Theme;
|
|
10
|
+
controls: {
|
|
11
|
+
code: boolean;
|
|
12
|
+
mermaid: boolean;
|
|
13
|
+
};
|
|
10
14
|
}
|
|
11
15
|
export declare class StreamdownContext {
|
|
12
16
|
footnotes: {
|
|
@@ -86,5 +90,10 @@ export type StreamdownProps = {
|
|
|
86
90
|
important?: string;
|
|
87
91
|
};
|
|
88
92
|
};
|
|
93
|
+
controls?: {
|
|
94
|
+
code?: boolean;
|
|
95
|
+
mermaid?: boolean;
|
|
96
|
+
};
|
|
97
|
+
renderHtml?: boolean | ((token: Tokens.HTML | Tokens.Tag) => string);
|
|
89
98
|
} & Partial<Snippets>;
|
|
90
99
|
export {};
|
package/dist/Streamdown.svelte
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
baseTheme,
|
|
22
22
|
mergeTheme: shouldMergeTheme = true,
|
|
23
23
|
streamdown = $bindable(),
|
|
24
|
+
renderHtml,
|
|
25
|
+
controls,
|
|
24
26
|
...snippets
|
|
25
27
|
}: StreamdownProps = $props();
|
|
26
28
|
|
|
@@ -60,11 +62,22 @@
|
|
|
60
62
|
get katexConfig() {
|
|
61
63
|
return katexConfig;
|
|
62
64
|
},
|
|
65
|
+
get renderHtml() {
|
|
66
|
+
return renderHtml;
|
|
67
|
+
},
|
|
63
68
|
get translations() {
|
|
64
69
|
return translations;
|
|
65
70
|
},
|
|
66
71
|
get shikiPreloadThemes() {
|
|
67
72
|
return shikiPreloadThemes;
|
|
73
|
+
},
|
|
74
|
+
get controls() {
|
|
75
|
+
const codeControls = controls?.code ?? true;
|
|
76
|
+
const mermaidControls = controls?.mermaid ?? true;
|
|
77
|
+
return {
|
|
78
|
+
code: codeControls,
|
|
79
|
+
mermaid: mermaidControls
|
|
80
|
+
};
|
|
68
81
|
}
|
|
69
82
|
});
|
|
70
83
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { default as Streamdown } from './Streamdown.svelte';
|
|
2
|
-
export { useStreamdown } from './Streamdown.js';
|
|
3
|
-
export
|
|
4
|
-
export {
|
|
2
|
+
export { useStreamdown, type StreamdownProps } from './Streamdown.js';
|
|
3
|
+
export { theme, shadcnTheme, mergeTheme, type Theme } from './theme.js';
|
|
4
|
+
export { lex, parseBlocks, type StreamdownToken } from './marked/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { default as Streamdown } from './Streamdown.svelte';
|
|
2
2
|
export { useStreamdown } from './Streamdown.js';
|
|
3
|
-
export
|
|
4
|
-
export {
|
|
3
|
+
export { theme, shadcnTheme, mergeTheme } from './theme.js';
|
|
4
|
+
export { lex, parseBlocks } from './marked/index.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, {
|
package/dist/theme.js
CHANGED
|
@@ -3,7 +3,7 @@ import { twMerge } from 'tailwind-merge';
|
|
|
3
3
|
export const cn = (...inputs) => twMerge(clsx(inputs));
|
|
4
4
|
export const theme = {
|
|
5
5
|
link: {
|
|
6
|
-
base: 'text-blue-600 font-medium underline',
|
|
6
|
+
base: 'text-blue-600 font-medium underline wrap-anywhere hover:text-blue-600/80',
|
|
7
7
|
blocked: 'text-gray-500'
|
|
8
8
|
},
|
|
9
9
|
h1: {
|
|
@@ -39,8 +39,8 @@ export const theme = {
|
|
|
39
39
|
},
|
|
40
40
|
code: {
|
|
41
41
|
base: 'my-4 w-full overflow-hidden rounded-xl border border-gray-200 flex flex-col',
|
|
42
|
-
container: ' relative overflow-visible bg-gray-100
|
|
43
|
-
header: 'flex items-center justify-between bg-gray-100/80 p-2
|
|
42
|
+
container: ' relative overflow-visible bg-gray-100 p-2 font-mono text-sm ',
|
|
43
|
+
header: 'flex items-center justify-between bg-gray-100/80 p-2 text-gray-600 text-xs',
|
|
44
44
|
button: 'cursor-pointer size-6 p-1 text-gray-600 transition-all hover:text-gray-900 rounded hover:bg-gray-100',
|
|
45
45
|
language: 'ml-1 font-mono lowercase',
|
|
46
46
|
skeleton: 'rounded-md font-mono text-transparent bg-gray-200 scale-y-90 animate-pulse whitespace-nowrap inline-block',
|
|
@@ -130,7 +130,7 @@ export const theme = {
|
|
|
130
130
|
};
|
|
131
131
|
export const shadcnTheme = {
|
|
132
132
|
link: {
|
|
133
|
-
base: 'text-primary font-medium underline hover:text-primary/80',
|
|
133
|
+
base: 'text-primary wrap-anywhere font-medium underline hover:text-primary/80',
|
|
134
134
|
blocked: 'text-muted-foreground'
|
|
135
135
|
},
|
|
136
136
|
h1: {
|
|
@@ -166,8 +166,8 @@ export const shadcnTheme = {
|
|
|
166
166
|
},
|
|
167
167
|
code: {
|
|
168
168
|
base: 'my-4 w-full overflow-hidden rounded-lg border border-border flex flex-col',
|
|
169
|
-
container: 'relative overflow-visible bg-muted
|
|
170
|
-
header: 'flex items-center justify-between bg-muted/80
|
|
169
|
+
container: 'relative overflow-visible bg-muted p-2 font-mono text-sm',
|
|
170
|
+
header: 'flex items-center justify-between bg-muted/80 px-2 py-1 text-muted-foreground text-xs',
|
|
171
171
|
button: 'cursor-pointer size-6 p-1 text-muted-foreground transition-all hover:text-foreground rounded hover:bg-muted',
|
|
172
172
|
language: 'ml-1 font-mono lowercase',
|
|
173
173
|
skeleton: 'rounded-md font-mono text-transparent bg-border/80 scale-y-90 w-fit animate-pulse whitespace-nowrap inline-block',
|
|
@@ -3,7 +3,6 @@ import { SvelteSet } from 'svelte/reactivity';
|
|
|
3
3
|
export declare const loadShiki: () => Promise<[any, import("shiki").CreateHighlighterFactory<BundledLanguage, BundledTheme>]>;
|
|
4
4
|
declare class HighlighterManager {
|
|
5
5
|
initialized: boolean;
|
|
6
|
-
private highlighter;
|
|
7
6
|
private highlighters;
|
|
8
7
|
private createHighlighter;
|
|
9
8
|
private engine;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {} from 'shiki';
|
|
1
|
+
import { bundledLanguages } from 'shiki';
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
3
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
4
|
+
const isLanguageSupported = (language) => {
|
|
5
|
+
return Object.hasOwn(bundledLanguages, language);
|
|
6
|
+
};
|
|
4
7
|
// Remove background styles from <pre> tags (inline style)
|
|
5
8
|
const removePreBackground = (html) => {
|
|
6
9
|
return html.replace(/<pre[^>]*style="[^"]*background[^";]*;?[^"]*"[^>]*>/g, (match) => match.replace(/style="[^"]*background[^";]*;?[^"]*"/, ''));
|
|
@@ -13,7 +16,6 @@ export const loadShiki = async () => {
|
|
|
13
16
|
};
|
|
14
17
|
class HighlighterManager {
|
|
15
18
|
initialized = $state(false);
|
|
16
|
-
highlighter = null;
|
|
17
19
|
highlighters = new SvelteMap();
|
|
18
20
|
createHighlighter = null;
|
|
19
21
|
engine = null;
|
|
@@ -47,7 +49,7 @@ class HighlighterManager {
|
|
|
47
49
|
}
|
|
48
50
|
const highlighter = await this.createHighlighter?.({
|
|
49
51
|
themes: [theme],
|
|
50
|
-
langs: [language],
|
|
52
|
+
langs: isLanguageSupported(language) ? [language] : ['text'],
|
|
51
53
|
engine: this.engine
|
|
52
54
|
});
|
|
53
55
|
this.highlighters.set(`${theme}:${language}`, highlighter);
|
|
@@ -66,7 +68,7 @@ class HighlighterManager {
|
|
|
66
68
|
if (!this.highlighters.has(`${theme}:${language}`)) {
|
|
67
69
|
const highlighter = await this.createHighlighter({
|
|
68
70
|
themes: [theme],
|
|
69
|
-
langs: [language],
|
|
71
|
+
langs: isLanguageSupported(language) ? [language] : ['text'],
|
|
70
72
|
engine: this.engine
|
|
71
73
|
});
|
|
72
74
|
this.highlighters.set(`${theme}:${language}`, highlighter);
|
|
@@ -83,7 +85,7 @@ class HighlighterManager {
|
|
|
83
85
|
return '';
|
|
84
86
|
}
|
|
85
87
|
let html = highlighter.codeToHtml(code, {
|
|
86
|
-
lang: language,
|
|
88
|
+
lang: isLanguageSupported(language) ? language : 'text',
|
|
87
89
|
theme: theme
|
|
88
90
|
});
|
|
89
91
|
// Remove background and add custom class if needed
|
|
@@ -244,6 +244,13 @@ const countSingleUnderscores = (text) => {
|
|
|
244
244
|
if (isWithinMathBlock(text, index)) {
|
|
245
245
|
return acc;
|
|
246
246
|
}
|
|
247
|
+
// Skip if underscore is word-internal (between word characters)
|
|
248
|
+
if (prevChar &&
|
|
249
|
+
nextChar &&
|
|
250
|
+
/[\p{L}\p{N}_]/u.test(prevChar) &&
|
|
251
|
+
/[\p{L}\p{N}_]/u.test(nextChar)) {
|
|
252
|
+
return acc;
|
|
253
|
+
}
|
|
247
254
|
if (prevChar !== '_' && nextChar !== '_') {
|
|
248
255
|
return acc + 1;
|
|
249
256
|
}
|
|
@@ -259,13 +266,23 @@ const handleIncompleteSingleUnderscoreItalic = (text) => {
|
|
|
259
266
|
}
|
|
260
267
|
const singleUnderscoreMatch = text.match(singleUnderscorePattern);
|
|
261
268
|
if (singleUnderscoreMatch) {
|
|
262
|
-
// Find the first single underscore position (not part of __)
|
|
269
|
+
// Find the first single underscore position (not part of __ and not word-internal)
|
|
263
270
|
let firstSingleUnderscoreIndex = -1;
|
|
264
271
|
for (let i = 0; i < text.length; i++) {
|
|
265
272
|
if (text[i] === '_' &&
|
|
266
273
|
text[i - 1] !== '_' &&
|
|
267
274
|
text[i + 1] !== '_' &&
|
|
275
|
+
text[i - 1] !== '\\' &&
|
|
268
276
|
!isWithinMathBlock(text, i)) {
|
|
277
|
+
// Check if underscore is word-internal (between word characters)
|
|
278
|
+
const prevChar = i > 0 ? text[i - 1] : '';
|
|
279
|
+
const nextChar = i < text.length - 1 ? text[i + 1] : '';
|
|
280
|
+
if (prevChar &&
|
|
281
|
+
nextChar &&
|
|
282
|
+
/[\p{L}\p{N}_]/u.test(prevChar) &&
|
|
283
|
+
/[\p{L}\p{N}_]/u.test(nextChar)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
269
286
|
firstSingleUnderscoreIndex = i;
|
|
270
287
|
break;
|
|
271
288
|
}
|
|
@@ -282,6 +299,12 @@ const handleIncompleteSingleUnderscoreItalic = (text) => {
|
|
|
282
299
|
}
|
|
283
300
|
const singleUnderscores = countSingleUnderscores(text);
|
|
284
301
|
if (singleUnderscores % 2 === 1) {
|
|
302
|
+
// If text ends with newline(s), insert underscore before them
|
|
303
|
+
const trailingNewlineMatch = text.match(/\n+$/);
|
|
304
|
+
if (trailingNewlineMatch) {
|
|
305
|
+
const textBeforeNewlines = text.slice(0, -trailingNewlineMatch[0].length);
|
|
306
|
+
return `${textBeforeNewlines}_${trailingNewlineMatch[0]}`;
|
|
307
|
+
}
|
|
285
308
|
return `${text}_`;
|
|
286
309
|
}
|
|
287
310
|
}
|