svelte-streamdown 2.2.7 → 2.3.1
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 +47 -51
- package/dist/AnimatedText.svelte +6 -8
- package/dist/Block.svelte +2 -2
- package/dist/Elements/Alert.svelte +19 -18
- package/dist/Elements/Code.svelte +20 -17
- package/dist/Elements/Image.svelte +22 -20
- package/dist/Elements/Link.svelte +1 -1
- package/dist/Elements/Math.svelte +4 -7
- package/dist/Elements/Mermaid.svelte +76 -68
- package/dist/Streamdown.svelte +4 -10
- package/dist/context.svelte.d.ts +17 -4
- package/dist/context.svelte.js +2 -1
- package/dist/marked/index.d.ts +4 -2
- package/dist/marked/index.js +10 -6
- package/dist/marked/marked-alert.d.ts +1 -1
- package/dist/marked/marked-alert.js +14 -5
- package/dist/marked/marked-br.d.ts +12 -0
- package/dist/marked/marked-br.js +21 -0
- package/dist/marked/marked-footnotes.d.ts +1 -1
- package/dist/marked/marked-footnotes.js +1 -1
- package/dist/marked/marked-hr.d.ts +12 -0
- package/dist/marked/marked-hr.js +24 -0
- package/dist/marked/marked-list.d.ts +2 -1
- package/dist/marked/marked-list.js +9 -3
- package/dist/marked/marked-math.d.ts +7 -21
- package/dist/marked/marked-math.js +88 -17
- package/dist/marked/marked-subsup.d.ts +1 -1
- package/dist/marked/marked-subsup.js +1 -1
- package/dist/marked/marked-table.d.ts +1 -0
- package/dist/marked/marked-table.js +11 -4
- package/dist/theme.d.ts +9 -1
- package/dist/theme.js +4 -2
- package/dist/utils/hightlighter.svelte.d.ts +15 -13
- package/dist/utils/hightlighter.svelte.js +101 -57
- package/dist/utils/parse-incomplete-markdown.js +352 -157
- package/package.json +4 -2
- package/dist/Elements/Table.svelte +0 -30
- package/dist/Elements/Table.svelte.d.ts +0 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const linkImagePattern = /(!?\[)([^\]]*?)$/;
|
|
2
|
-
const boldPattern = /(\*\*)([
|
|
2
|
+
const boldPattern = /(\*\*)([^]*?)$/;
|
|
3
3
|
const italicPattern = /(__)([^_]*?)$/;
|
|
4
4
|
const boldItalicPattern = /(\*\*\*)([^*]*?)$/;
|
|
5
5
|
const singleAsteriskPattern = /(\*)([^*]*?)$/;
|
|
@@ -8,6 +8,16 @@ const inlineCodePattern = /(`)([^`]*?)$/;
|
|
|
8
8
|
const strikethroughPattern = /(~~)([^~]*?)$/;
|
|
9
9
|
const subPattern = /(~)([^~]*?)$/;
|
|
10
10
|
const supPattern = /(\^)([^\^]*?)$/;
|
|
11
|
+
const incompleteLinkUrlPattern = /(!?\[[^\]]*\]\()([^)]*?)$/;
|
|
12
|
+
// Helper function to find the end of the line containing a specific position
|
|
13
|
+
const findEndOfLineContaining = (text, position) => {
|
|
14
|
+
let endPos = position;
|
|
15
|
+
// Move forward to find the end of the line
|
|
16
|
+
while (endPos < text.length && text[endPos] !== '\n') {
|
|
17
|
+
endPos++;
|
|
18
|
+
}
|
|
19
|
+
return endPos;
|
|
20
|
+
};
|
|
11
21
|
// Helper function to check if we have a complete code block
|
|
12
22
|
const hasCompleteCodeBlock = (text) => {
|
|
13
23
|
const tripleBackticks = (text.match(/```/g) || []).length;
|
|
@@ -15,26 +25,85 @@ const hasCompleteCodeBlock = (text) => {
|
|
|
15
25
|
};
|
|
16
26
|
// Handles incomplete links and images by preserving them with a special marker
|
|
17
27
|
const handleIncompleteLinksAndImages = (text) => {
|
|
28
|
+
// Check for incomplete link URLs like "[text](incomplete-url"
|
|
29
|
+
const urlMatch = text.match(incompleteLinkUrlPattern);
|
|
30
|
+
if (urlMatch) {
|
|
31
|
+
const isImage = urlMatch[1].startsWith('!');
|
|
32
|
+
const url = urlMatch[2];
|
|
33
|
+
// Only handle if there's actually some URL content
|
|
34
|
+
if (url.length > 0) {
|
|
35
|
+
// Check if the URL is actually incomplete
|
|
36
|
+
const isIncomplete = isUrlIncomplete(url);
|
|
37
|
+
if (isIncomplete) {
|
|
38
|
+
// For incomplete URLs, replace with incomplete marker
|
|
39
|
+
const marker = isImage ? 'streamdown:incomplete-image' : 'streamdown:incomplete-link';
|
|
40
|
+
return text.replace(urlMatch[2], marker) + ')';
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// URL is complete, just add closing parenthesis
|
|
44
|
+
return `${text})`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// If URL is empty (just "[text]("), complete it with incomplete marker
|
|
49
|
+
const marker = isImage ? 'streamdown:incomplete-image' : 'streamdown:incomplete-link';
|
|
50
|
+
return `${text}${marker})`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Check for incomplete link text like "[incomplete-text" (but not "[text](")
|
|
18
54
|
const linkMatch = text.match(linkImagePattern);
|
|
19
|
-
if (linkMatch) {
|
|
55
|
+
if (linkMatch && !text.includes('](')) {
|
|
20
56
|
const isImage = linkMatch[1].startsWith('!');
|
|
21
|
-
// For
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return text.substring(0, startIndex);
|
|
25
|
-
}
|
|
26
|
-
// For links, preserve the text and close the link with a
|
|
27
|
-
// special placeholder URL that indicates it's incomplete
|
|
28
|
-
return `${text}](streamdown:incomplete-link)`;
|
|
57
|
+
// For incomplete link/image text, complete with incomplete marker
|
|
58
|
+
const marker = isImage ? 'streamdown:incomplete-image' : 'streamdown:incomplete-link';
|
|
59
|
+
return `${text}](${marker})`;
|
|
29
60
|
}
|
|
30
61
|
return text;
|
|
31
62
|
};
|
|
63
|
+
// Helper function to determine if a URL is incomplete or lacks proper domain extension
|
|
64
|
+
const isUrlIncomplete = (url) => {
|
|
65
|
+
// If URL is clearly incomplete (too short, no protocol, etc.)
|
|
66
|
+
if (!url || url.length < 4) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// If URL starts with protocol but is very short
|
|
70
|
+
if ((url.startsWith('http://') && url.length < 12) ||
|
|
71
|
+
(url.startsWith('https://') && url.length < 13)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// If URL has protocol, extract the domain part
|
|
75
|
+
let domain = url;
|
|
76
|
+
if (url.startsWith('http://')) {
|
|
77
|
+
domain = url.substring(7);
|
|
78
|
+
}
|
|
79
|
+
else if (url.startsWith('https://')) {
|
|
80
|
+
domain = url.substring(8);
|
|
81
|
+
}
|
|
82
|
+
// Remove path, query, and fragment parts to get just the domain
|
|
83
|
+
domain = domain.split('/')[0].split('?')[0].split('#')[0];
|
|
84
|
+
// Check if domain has a proper extension
|
|
85
|
+
const domainParts = domain.split('.');
|
|
86
|
+
if (domainParts.length < 2) {
|
|
87
|
+
return true; // No extension at all
|
|
88
|
+
}
|
|
89
|
+
const extension = domainParts[domainParts.length - 1];
|
|
90
|
+
// Check if extension looks valid (at least 2 characters, only letters)
|
|
91
|
+
if (extension.length < 2 || !/^[a-zA-Z]+$/.test(extension)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
// If we get here, the URL looks reasonably complete
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
32
97
|
// Completes incomplete bold formatting (**)
|
|
33
98
|
const handleIncompleteBold = (text) => {
|
|
34
99
|
// Don't process if inside a complete code block
|
|
35
100
|
if (hasCompleteCodeBlock(text)) {
|
|
36
101
|
return text;
|
|
37
102
|
}
|
|
103
|
+
// IMPORTANT SAFEGUARD: Don't modify text that contains complete italic patterns
|
|
104
|
+
// This prevents accidentally converting *italic* to something else
|
|
105
|
+
const completeItalicPattern = /\*[^*\n]+\*/g;
|
|
106
|
+
const completeItalicMatches = text.match(completeItalicPattern);
|
|
38
107
|
const boldMatch = text.match(boldPattern);
|
|
39
108
|
if (boldMatch) {
|
|
40
109
|
// Find the position of the last ** marker
|
|
@@ -45,23 +114,26 @@ const handleIncompleteBold = (text) => {
|
|
|
45
114
|
if (!contentAfterMarker || /^[\s_~*`]*$/.test(contentAfterMarker)) {
|
|
46
115
|
return text;
|
|
47
116
|
}
|
|
48
|
-
// Check if
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const hasNewlineInContent = contentAfterMarker.includes('\n');
|
|
58
|
-
if (hasNewlineInContent) {
|
|
59
|
-
// Don't complete if the content spans to another line
|
|
60
|
-
return text;
|
|
61
|
-
}
|
|
117
|
+
// Check if this is a standalone horizontal rule (asterisks on a line by themselves)
|
|
118
|
+
const matchIndex = lastDoubleAsteriskIndex;
|
|
119
|
+
const lineStart = text.lastIndexOf('\n', matchIndex) + 1;
|
|
120
|
+
const lineEnd = text.indexOf('\n', matchIndex);
|
|
121
|
+
const lineContent = text.substring(lineStart, lineEnd === -1 ? text.length : lineEnd);
|
|
122
|
+
const trimmedLine = lineContent.trim();
|
|
123
|
+
// If the line contains only asterisks (2 or more), it's a horizontal rule
|
|
124
|
+
if (/^\*+$/.test(trimmedLine) && trimmedLine.length >= 2) {
|
|
125
|
+
return text;
|
|
62
126
|
}
|
|
63
|
-
//
|
|
127
|
+
// For streaming context, we allow multi-line completion
|
|
128
|
+
// Remove the conservative multi-line restriction for better UX
|
|
129
|
+
// ADDITIONAL SAFEGUARD: Check if the content after ** would interfere with existing italic
|
|
64
130
|
if (contentAfterMarker.endsWith('*') && !contentAfterMarker.endsWith('**')) {
|
|
131
|
+
// Before treating as incomplete closing marker, check if this would break italic formatting
|
|
132
|
+
const potentiallyAffectedText = contentAfterMarker.slice(0, -1);
|
|
133
|
+
if (completeItalicMatches && potentiallyAffectedText.includes('*')) {
|
|
134
|
+
// Don't modify if it would interfere with existing italic patterns
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
65
137
|
// The content ends with a single *, treat it as an incomplete closing marker
|
|
66
138
|
// Remove the trailing * and add complete closing **
|
|
67
139
|
const contentWithoutTrailingAsterisk = contentAfterMarker.slice(0, -1);
|
|
@@ -71,7 +143,9 @@ const handleIncompleteBold = (text) => {
|
|
|
71
143
|
const doubleAsteriskMatches = text.match(/\*\*/g) || [];
|
|
72
144
|
const doubleAsteriskCount = doubleAsteriskMatches.length;
|
|
73
145
|
if (doubleAsteriskCount % 2 === 1) {
|
|
74
|
-
|
|
146
|
+
// Find the end of the line containing the incomplete bold marker
|
|
147
|
+
const endOfLine = findEndOfLineContaining(text, lastDoubleAsteriskIndex);
|
|
148
|
+
return text.substring(0, endOfLine) + '**' + text.substring(endOfLine);
|
|
75
149
|
}
|
|
76
150
|
}
|
|
77
151
|
return text;
|
|
@@ -87,13 +161,23 @@ const handleIncompleteDoubleUnderscoreItalic = (text) => {
|
|
|
87
161
|
if (!contentAfterMarker || /^[\s_~*`]*$/.test(contentAfterMarker)) {
|
|
88
162
|
return text;
|
|
89
163
|
}
|
|
164
|
+
// Check if this is a standalone horizontal rule (underscores on a line by themselves)
|
|
165
|
+
const matchIndex = text.lastIndexOf('__');
|
|
166
|
+
const lineStart = text.lastIndexOf('\n', matchIndex) + 1;
|
|
167
|
+
const lineEnd = text.indexOf('\n', matchIndex);
|
|
168
|
+
const lineContent = text.substring(lineStart, lineEnd === -1 ? text.length : lineEnd);
|
|
169
|
+
const trimmedLine = lineContent.trim();
|
|
170
|
+
// If the line contains only underscores (3 or more), it's a horizontal rule
|
|
171
|
+
if (/^_+$/.test(trimmedLine) && trimmedLine.length >= 3) {
|
|
172
|
+
return text;
|
|
173
|
+
}
|
|
90
174
|
// Check if the underscore marker is in a list item context
|
|
91
175
|
// Find the position of the matched underscore marker
|
|
92
176
|
const markerIndex = text.lastIndexOf(italicMatch[1]);
|
|
93
177
|
const beforeMarker = text.substring(0, markerIndex);
|
|
94
178
|
const lastNewlineBeforeMarker = beforeMarker.lastIndexOf('\n');
|
|
95
|
-
const
|
|
96
|
-
const lineBeforeMarker = text.substring(
|
|
179
|
+
const lineStart2 = lastNewlineBeforeMarker === -1 ? 0 : lastNewlineBeforeMarker + 1;
|
|
180
|
+
const lineBeforeMarker = text.substring(lineStart2, markerIndex);
|
|
97
181
|
// Check if this line is a list item with just the underscore marker
|
|
98
182
|
if (/^[\s]*[-*+][\s]+$/.test(lineBeforeMarker)) {
|
|
99
183
|
// This is a list item with just emphasis markers
|
|
@@ -106,46 +190,51 @@ const handleIncompleteDoubleUnderscoreItalic = (text) => {
|
|
|
106
190
|
}
|
|
107
191
|
const underscorePairs = (text.match(/__/g) || []).length;
|
|
108
192
|
if (underscorePairs % 2 === 1) {
|
|
109
|
-
|
|
193
|
+
// Find the position of the last __ marker
|
|
194
|
+
const lastDoubleUnderscoreIndex = text.lastIndexOf('__');
|
|
195
|
+
// Find the end of the line containing the incomplete italic marker
|
|
196
|
+
const endOfLine = findEndOfLineContaining(text, lastDoubleUnderscoreIndex);
|
|
197
|
+
return text.substring(0, endOfLine) + '__' + text.substring(endOfLine);
|
|
110
198
|
}
|
|
111
199
|
}
|
|
112
200
|
return text;
|
|
113
201
|
};
|
|
114
202
|
// Counts single asterisks that are not part of double asterisks, not escaped, and not list markers
|
|
115
203
|
const countSingleAsterisks = (text) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
204
|
+
let count = 0;
|
|
205
|
+
for (let i = 0; i < text.length; i++) {
|
|
206
|
+
if (text[i] === '*') {
|
|
207
|
+
const prevChar = i > 0 ? text[i - 1] : '';
|
|
208
|
+
const nextChar = i < text.length - 1 ? text[i + 1] : '';
|
|
120
209
|
// Skip if escaped with backslash
|
|
121
210
|
if (prevChar === '\\') {
|
|
122
|
-
|
|
211
|
+
continue;
|
|
123
212
|
}
|
|
124
213
|
// Check if this is a list marker (asterisk at start of line followed by space)
|
|
125
214
|
// Look backwards to find the start of the current line
|
|
126
|
-
let lineStartIndex =
|
|
127
|
-
for (let
|
|
128
|
-
if (text[
|
|
129
|
-
lineStartIndex =
|
|
215
|
+
let lineStartIndex = i;
|
|
216
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
217
|
+
if (text[j] === '\n') {
|
|
218
|
+
lineStartIndex = j + 1;
|
|
130
219
|
break;
|
|
131
220
|
}
|
|
132
|
-
if (
|
|
221
|
+
if (j === 0) {
|
|
133
222
|
lineStartIndex = 0;
|
|
134
223
|
break;
|
|
135
224
|
}
|
|
136
225
|
}
|
|
137
226
|
// Check if this asterisk is at the beginning of a line (with optional whitespace)
|
|
138
|
-
const beforeAsterisk = text.substring(lineStartIndex,
|
|
227
|
+
const beforeAsterisk = text.substring(lineStartIndex, i);
|
|
139
228
|
if (beforeAsterisk.trim() === '' && (nextChar === ' ' || nextChar === '\t')) {
|
|
140
229
|
// This is likely a list marker, don't count it
|
|
141
|
-
|
|
230
|
+
continue;
|
|
142
231
|
}
|
|
143
232
|
if (prevChar !== '*' && nextChar !== '*') {
|
|
144
|
-
|
|
233
|
+
count++;
|
|
145
234
|
}
|
|
146
235
|
}
|
|
147
|
-
|
|
148
|
-
|
|
236
|
+
}
|
|
237
|
+
return count;
|
|
149
238
|
};
|
|
150
239
|
// Completes incomplete italic formatting with single asterisks (*)
|
|
151
240
|
const handleIncompleteSingleAsteriskItalic = (text) => {
|
|
@@ -153,6 +242,19 @@ const handleIncompleteSingleAsteriskItalic = (text) => {
|
|
|
153
242
|
if (hasCompleteCodeBlock(text)) {
|
|
154
243
|
return text;
|
|
155
244
|
}
|
|
245
|
+
// IMPORTANT SAFEGUARD: Check if we already have complete italic formatting patterns
|
|
246
|
+
// If text contains complete *word* patterns, don't modify them
|
|
247
|
+
const completeItalicPattern = /\*[^*\n]+\*/g;
|
|
248
|
+
const completeMatches = text.match(completeItalicPattern);
|
|
249
|
+
if (completeMatches) {
|
|
250
|
+
// Count asterisks in complete matches
|
|
251
|
+
const asterisksInCompleteMatches = completeMatches.join('').split('*').length - 1;
|
|
252
|
+
const totalAsterisks = (text.match(/\*/g) || []).length;
|
|
253
|
+
// If most asterisks are already in complete italic patterns, don't process
|
|
254
|
+
if (asterisksInCompleteMatches >= totalAsterisks) {
|
|
255
|
+
return text;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
156
258
|
const singleAsteriskMatch = text.match(singleAsteriskPattern);
|
|
157
259
|
if (singleAsteriskMatch) {
|
|
158
260
|
// Find the first single asterisk position (not part of **)
|
|
@@ -188,7 +290,9 @@ const handleIncompleteSingleAsteriskItalic = (text) => {
|
|
|
188
290
|
}
|
|
189
291
|
const singleAsterisks = countSingleAsterisks(text);
|
|
190
292
|
if (singleAsterisks % 2 === 1) {
|
|
191
|
-
|
|
293
|
+
// Find the end of the line containing the incomplete italic marker
|
|
294
|
+
const endOfLine = findEndOfLineContaining(text, firstSingleAsteriskIndex);
|
|
295
|
+
return text.substring(0, endOfLine) + '*' + text.substring(endOfLine);
|
|
192
296
|
}
|
|
193
297
|
}
|
|
194
298
|
return text;
|
|
@@ -253,31 +357,32 @@ const isWithinFootnoteRef = (text, position) => {
|
|
|
253
357
|
};
|
|
254
358
|
// Counts single underscores that are not part of double underscores, not escaped, and not in math blocks
|
|
255
359
|
const countSingleUnderscores = (text) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
360
|
+
let count = 0;
|
|
361
|
+
for (let i = 0; i < text.length; i++) {
|
|
362
|
+
if (text[i] === '_') {
|
|
363
|
+
const prevChar = i > 0 ? text[i - 1] : '';
|
|
364
|
+
const nextChar = i < text.length - 1 ? text[i + 1] : '';
|
|
260
365
|
// Skip if escaped with backslash
|
|
261
366
|
if (prevChar === '\\') {
|
|
262
|
-
|
|
367
|
+
continue;
|
|
263
368
|
}
|
|
264
369
|
// Skip if within math block
|
|
265
|
-
if (isWithinMathBlock(text,
|
|
266
|
-
|
|
370
|
+
if (isWithinMathBlock(text, i)) {
|
|
371
|
+
continue;
|
|
267
372
|
}
|
|
268
373
|
// Skip if underscore is word-internal (between word characters)
|
|
269
374
|
if (prevChar &&
|
|
270
375
|
nextChar &&
|
|
271
376
|
/[\p{L}\p{N}_]/u.test(prevChar) &&
|
|
272
377
|
/[\p{L}\p{N}_]/u.test(nextChar)) {
|
|
273
|
-
|
|
378
|
+
continue;
|
|
274
379
|
}
|
|
275
380
|
if (prevChar !== '_' && nextChar !== '_') {
|
|
276
|
-
|
|
381
|
+
count++;
|
|
277
382
|
}
|
|
278
383
|
}
|
|
279
|
-
|
|
280
|
-
|
|
384
|
+
}
|
|
385
|
+
return count;
|
|
281
386
|
};
|
|
282
387
|
// Completes incomplete italic formatting with single underscores (_)
|
|
283
388
|
const handleIncompleteSingleUnderscoreItalic = (text) => {
|
|
@@ -320,13 +425,9 @@ const handleIncompleteSingleUnderscoreItalic = (text) => {
|
|
|
320
425
|
}
|
|
321
426
|
const singleUnderscores = countSingleUnderscores(text);
|
|
322
427
|
if (singleUnderscores % 2 === 1) {
|
|
323
|
-
//
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
const textBeforeNewlines = text.slice(0, -trailingNewlineMatch[0].length);
|
|
327
|
-
return `${textBeforeNewlines}_${trailingNewlineMatch[0]}`;
|
|
328
|
-
}
|
|
329
|
-
return `${text}_`;
|
|
428
|
+
// Find the end of the line containing the incomplete underscore italic marker
|
|
429
|
+
const endOfLine = findEndOfLineContaining(text, firstSingleUnderscoreIndex);
|
|
430
|
+
return text.substring(0, endOfLine) + '_' + text.substring(endOfLine);
|
|
330
431
|
}
|
|
331
432
|
}
|
|
332
433
|
return text;
|
|
@@ -390,7 +491,11 @@ const handleIncompleteInlineCode = (text) => {
|
|
|
390
491
|
}
|
|
391
492
|
const singleBacktickCount = countSingleBackticks(text);
|
|
392
493
|
if (singleBacktickCount % 2 === 1) {
|
|
393
|
-
|
|
494
|
+
// Find the position of the last backtick
|
|
495
|
+
const lastBacktickIndex = text.lastIndexOf('`');
|
|
496
|
+
// Find the end of the line containing the incomplete code marker
|
|
497
|
+
const endOfLine = findEndOfLineContaining(text, lastBacktickIndex);
|
|
498
|
+
return text.substring(0, endOfLine) + '`' + text.substring(endOfLine);
|
|
394
499
|
}
|
|
395
500
|
}
|
|
396
501
|
return text;
|
|
@@ -408,27 +513,33 @@ const handleIncompleteStrikethrough = (text) => {
|
|
|
408
513
|
}
|
|
409
514
|
const tildePairs = (text.match(/~~/g) || []).length;
|
|
410
515
|
if (tildePairs % 2 === 1) {
|
|
411
|
-
|
|
516
|
+
// Find the position of the last ~~ marker
|
|
517
|
+
const lastDoubleTildeIndex = text.lastIndexOf('~~');
|
|
518
|
+
// Find the end of the line containing the incomplete strikethrough marker
|
|
519
|
+
const endOfLine = findEndOfLineContaining(text, lastDoubleTildeIndex);
|
|
520
|
+
return text.substring(0, endOfLine) + '~~' + text.substring(endOfLine);
|
|
412
521
|
}
|
|
413
522
|
}
|
|
414
523
|
return text;
|
|
415
524
|
};
|
|
416
525
|
// Counts single tildes that are not part of double tildes and not escaped
|
|
417
526
|
const countSingleTildes = (text) => {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const
|
|
527
|
+
let count = 0;
|
|
528
|
+
for (let i = 0; i < text.length; i++) {
|
|
529
|
+
if (text[i] === '~') {
|
|
530
|
+
const prevChar = i > 0 ? text[i - 1] : '';
|
|
531
|
+
const nextChar = i < text.length - 1 ? text[i + 1] : '';
|
|
422
532
|
// Skip if escaped with backslash
|
|
423
533
|
if (prevChar === '\\') {
|
|
424
|
-
|
|
534
|
+
continue;
|
|
425
535
|
}
|
|
536
|
+
// Skip if part of double tilde
|
|
426
537
|
if (prevChar !== '~' && nextChar !== '~') {
|
|
427
|
-
|
|
538
|
+
count++;
|
|
428
539
|
}
|
|
429
540
|
}
|
|
430
|
-
|
|
431
|
-
|
|
541
|
+
}
|
|
542
|
+
return count;
|
|
432
543
|
};
|
|
433
544
|
// Completes incomplete subscript formatting (~)
|
|
434
545
|
const handleIncompleteSub = (text) => {
|
|
@@ -438,7 +549,7 @@ const handleIncompleteSub = (text) => {
|
|
|
438
549
|
}
|
|
439
550
|
const singleTildes = countSingleTildes(text);
|
|
440
551
|
if (singleTildes % 2 === 1) {
|
|
441
|
-
// Find the last unmatched tilde
|
|
552
|
+
// Find the last unmatched tilde
|
|
442
553
|
const lastTildeIndex = text.lastIndexOf('~');
|
|
443
554
|
if (lastTildeIndex !== -1) {
|
|
444
555
|
// Check if the tilde is within a math block - if so, don't process
|
|
@@ -450,44 +561,31 @@ const handleIncompleteSub = (text) => {
|
|
|
450
561
|
if (!afterTilde || /^[\s_~*`^]*$/.test(afterTilde)) {
|
|
451
562
|
return text;
|
|
452
563
|
}
|
|
453
|
-
// Find the end of the subscript
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (endMatch) {
|
|
457
|
-
const contentLength = endMatch[1].length;
|
|
458
|
-
const insertPosition = lastTildeIndex + 1 + contentLength;
|
|
459
|
-
return text.substring(0, insertPosition) + '~' + text.substring(insertPosition);
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
// Fallback: if no clear boundary, treat everything until space as content
|
|
463
|
-
const spaceMatch = afterTilde.match(/^([^\s]+)/);
|
|
464
|
-
if (spaceMatch) {
|
|
465
|
-
const contentLength = spaceMatch[1].length;
|
|
466
|
-
const insertPosition = lastTildeIndex + 1 + contentLength;
|
|
467
|
-
return text.substring(0, insertPosition) + '~' + text.substring(insertPosition);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
564
|
+
// Find the end of the line containing the incomplete subscript marker
|
|
565
|
+
const endOfLine = findEndOfLineContaining(text, lastTildeIndex);
|
|
566
|
+
return text.substring(0, endOfLine) + '~' + text.substring(endOfLine);
|
|
470
567
|
}
|
|
471
568
|
}
|
|
472
569
|
return text;
|
|
473
570
|
};
|
|
474
571
|
// Counts single carets that are not escaped and not in footnote references
|
|
475
572
|
const countSingleCarets = (text) => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
573
|
+
let count = 0;
|
|
574
|
+
for (let i = 0; i < text.length; i++) {
|
|
575
|
+
if (text[i] === '^') {
|
|
576
|
+
const prevChar = i > 0 ? text[i - 1] : '';
|
|
479
577
|
// Skip if escaped with backslash
|
|
480
578
|
if (prevChar === '\\') {
|
|
481
|
-
|
|
579
|
+
continue;
|
|
482
580
|
}
|
|
483
581
|
// Skip if within footnote reference
|
|
484
|
-
if (isWithinFootnoteRef(text,
|
|
485
|
-
|
|
582
|
+
if (isWithinFootnoteRef(text, i)) {
|
|
583
|
+
continue;
|
|
486
584
|
}
|
|
487
|
-
|
|
585
|
+
count++;
|
|
488
586
|
}
|
|
489
|
-
|
|
490
|
-
|
|
587
|
+
}
|
|
588
|
+
return count;
|
|
491
589
|
};
|
|
492
590
|
// Completes incomplete superscript formatting (^)
|
|
493
591
|
const handleIncompleteSup = (text) => {
|
|
@@ -497,7 +595,7 @@ const handleIncompleteSup = (text) => {
|
|
|
497
595
|
}
|
|
498
596
|
const singleCarets = countSingleCarets(text);
|
|
499
597
|
if (singleCarets % 2 === 1) {
|
|
500
|
-
// Find the last unmatched caret
|
|
598
|
+
// Find the last unmatched caret
|
|
501
599
|
const lastCaretIndex = text.lastIndexOf('^');
|
|
502
600
|
if (lastCaretIndex !== -1) {
|
|
503
601
|
// Check if the caret is within a math block - if so, don't process
|
|
@@ -513,43 +611,83 @@ const handleIncompleteSup = (text) => {
|
|
|
513
611
|
if (!afterCaret || /^[\s_~*`^]*$/.test(afterCaret)) {
|
|
514
612
|
return text;
|
|
515
613
|
}
|
|
516
|
-
// Find the end of the superscript
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (endMatch) {
|
|
520
|
-
const contentLength = endMatch[1].length;
|
|
521
|
-
const insertPosition = lastCaretIndex + 1 + contentLength;
|
|
522
|
-
return text.substring(0, insertPosition) + '^' + text.substring(insertPosition);
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
// Fallback: if no clear boundary, treat everything until space as content
|
|
526
|
-
const spaceMatch = afterCaret.match(/^([^\s]+)/);
|
|
527
|
-
if (spaceMatch) {
|
|
528
|
-
const contentLength = spaceMatch[1].length;
|
|
529
|
-
const insertPosition = lastCaretIndex + 1 + contentLength;
|
|
530
|
-
return text.substring(0, insertPosition) + '^' + text.substring(insertPosition);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
614
|
+
// Find the end of the line containing the incomplete superscript marker
|
|
615
|
+
const endOfLine = findEndOfLineContaining(text, lastCaretIndex);
|
|
616
|
+
return text.substring(0, endOfLine) + '^' + text.substring(endOfLine);
|
|
533
617
|
}
|
|
534
618
|
}
|
|
535
619
|
return text;
|
|
536
620
|
};
|
|
537
|
-
// Counts single dollar signs that are not part of double dollar signs and not
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const
|
|
621
|
+
// Counts single dollar signs that are not part of double dollar signs, not escaped, and not currency
|
|
622
|
+
const countSingleDollarSigns = (text) => {
|
|
623
|
+
let count = 0;
|
|
624
|
+
for (let i = 0; i < text.length; i++) {
|
|
625
|
+
if (text[i] === '$') {
|
|
626
|
+
const prevChar = i > 0 ? text[i - 1] : '';
|
|
627
|
+
const nextChar = i < text.length - 1 ? text[i + 1] : '';
|
|
543
628
|
// Skip if escaped with backslash
|
|
544
629
|
if (prevChar === '\\') {
|
|
545
|
-
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
// Skip if part of double dollar
|
|
633
|
+
if (prevChar === '$' || nextChar === '$') {
|
|
634
|
+
continue;
|
|
546
635
|
}
|
|
547
|
-
if
|
|
548
|
-
|
|
636
|
+
// Skip if this looks like currency (digit immediately after $)
|
|
637
|
+
if (nextChar && /\d/.test(nextChar)) {
|
|
638
|
+
continue;
|
|
549
639
|
}
|
|
640
|
+
count++;
|
|
550
641
|
}
|
|
551
|
-
|
|
552
|
-
|
|
642
|
+
}
|
|
643
|
+
return count;
|
|
644
|
+
};
|
|
645
|
+
// Completes incomplete inline math formatting ($)
|
|
646
|
+
const handleIncompleteInlineMath = (text) => {
|
|
647
|
+
// Don't process if inside a complete code block
|
|
648
|
+
if (hasCompleteCodeBlock(text)) {
|
|
649
|
+
return text;
|
|
650
|
+
}
|
|
651
|
+
// Count single dollar signs (excluding currency patterns)
|
|
652
|
+
const singleDollars = countSingleDollarSigns(text);
|
|
653
|
+
// If we have an odd number of single dollars, we need to complete
|
|
654
|
+
if (singleDollars % 2 === 1) {
|
|
655
|
+
// Find the last unmatched dollar sign
|
|
656
|
+
let lastDollarIndex = -1;
|
|
657
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
658
|
+
if (text[i] === '$') {
|
|
659
|
+
const prevChar = i > 0 ? text[i - 1] : '';
|
|
660
|
+
const nextChar = i < text.length - 1 ? text[i + 1] : '';
|
|
661
|
+
// Skip if escaped or part of double dollar
|
|
662
|
+
if (prevChar === '\\' || prevChar === '$' || nextChar === '$') {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
// Skip if this looks like currency
|
|
666
|
+
if (nextChar && /\d/.test(nextChar)) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
lastDollarIndex = i;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (lastDollarIndex !== -1) {
|
|
674
|
+
const afterDollar = text.substring(lastDollarIndex + 1);
|
|
675
|
+
// Don't complete if there's no meaningful content after the dollar
|
|
676
|
+
if (!afterDollar || /^[\s$]*$/.test(afterDollar)) {
|
|
677
|
+
return text;
|
|
678
|
+
}
|
|
679
|
+
// Check if content after dollar looks like math (contains letters, spaces, symbols)
|
|
680
|
+
// but not pure numbers (which would be currency)
|
|
681
|
+
if (/^\d+(\.\d{2})?\s*$/.test(afterDollar.trim())) {
|
|
682
|
+
// This looks like currency, don't complete
|
|
683
|
+
return text;
|
|
684
|
+
}
|
|
685
|
+
// Find the end of the line containing the incomplete inline math marker
|
|
686
|
+
const endOfLine = findEndOfLineContaining(text, lastDollarIndex);
|
|
687
|
+
return text.substring(0, endOfLine) + '$' + text.substring(endOfLine);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return text;
|
|
553
691
|
};
|
|
554
692
|
// Completes incomplete block KaTeX formatting ($$)
|
|
555
693
|
const handleIncompleteBlockKatex = (text) => {
|
|
@@ -573,13 +711,19 @@ const handleIncompleteBlockKatex = (text) => {
|
|
|
573
711
|
// Counts triple asterisks that are not part of quadruple or more asterisks
|
|
574
712
|
const countTripleAsterisks = (text) => {
|
|
575
713
|
let count = 0;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
714
|
+
for (let i = 0; i < text.length; i++) {
|
|
715
|
+
if (text[i] === '*') {
|
|
716
|
+
// Count consecutive asterisks
|
|
717
|
+
let asteriskCount = 0;
|
|
718
|
+
while (i < text.length && text[i] === '*') {
|
|
719
|
+
asteriskCount++;
|
|
720
|
+
i++;
|
|
721
|
+
}
|
|
722
|
+
i--; // Adjust back one position since the outer loop will increment
|
|
581
723
|
// Each group of exactly 3 asterisks counts as one triple asterisk marker
|
|
582
|
-
|
|
724
|
+
if (asteriskCount >= 3) {
|
|
725
|
+
count += Math.floor(asteriskCount / 3);
|
|
726
|
+
}
|
|
583
727
|
}
|
|
584
728
|
}
|
|
585
729
|
return count;
|
|
@@ -604,40 +748,91 @@ const handleIncompleteBoldItalic = (text) => {
|
|
|
604
748
|
if (!contentAfterMarker || /^[\s_~*`]*$/.test(contentAfterMarker)) {
|
|
605
749
|
return text;
|
|
606
750
|
}
|
|
751
|
+
// Check if this is a standalone horizontal rule (*** on a line by itself)
|
|
752
|
+
const matchIndex = text.lastIndexOf('***');
|
|
753
|
+
const lineStart = text.lastIndexOf('\n', matchIndex) + 1;
|
|
754
|
+
const lineEnd = text.indexOf('\n', matchIndex);
|
|
755
|
+
const lineContent = text.substring(lineStart, lineEnd === -1 ? text.length : lineEnd);
|
|
756
|
+
const trimmedLine = lineContent.trim();
|
|
757
|
+
// If the line contains only *** (possibly with whitespace), it's a horizontal rule
|
|
758
|
+
if (trimmedLine === '***') {
|
|
759
|
+
return text;
|
|
760
|
+
}
|
|
607
761
|
const tripleAsteriskCount = countTripleAsterisks(text);
|
|
608
762
|
if (tripleAsteriskCount % 2 === 1) {
|
|
609
|
-
|
|
763
|
+
// Find the position of the last *** marker
|
|
764
|
+
const lastTripleAsteriskIndex = text.lastIndexOf('***');
|
|
765
|
+
// Find the end of the line containing the incomplete bold-italic marker
|
|
766
|
+
const endOfLine = findEndOfLineContaining(text, lastTripleAsteriskIndex);
|
|
767
|
+
return text.substring(0, endOfLine) + '***' + text.substring(endOfLine);
|
|
610
768
|
}
|
|
611
769
|
}
|
|
612
770
|
return text;
|
|
613
771
|
};
|
|
772
|
+
// Handles incomplete code blocks by leaving them unchanged
|
|
773
|
+
const handleIncompleteCodeBlock = (text) => {
|
|
774
|
+
// Check if we have an incomplete code block (odd number of triple backticks with newlines)
|
|
775
|
+
const tripleBackticks = (text.match(/```/g) || []).length;
|
|
776
|
+
// If we have an odd number of triple backticks and the text contains newlines,
|
|
777
|
+
// this is likely an incomplete code block - leave it unchanged
|
|
778
|
+
if (tripleBackticks % 2 === 1 && text.includes('\n')) {
|
|
779
|
+
return text;
|
|
780
|
+
}
|
|
781
|
+
return text;
|
|
782
|
+
};
|
|
783
|
+
// Handles incomplete footnote references
|
|
784
|
+
const handleIncompleteFootnotes = (text) => {
|
|
785
|
+
// Check for incomplete footnote references like [^ or [^label
|
|
786
|
+
// Match [^ followed by optional simple label, but ensure no ] follows immediately
|
|
787
|
+
// and no complex characters that would indicate this isn't a footnote
|
|
788
|
+
const footnotePattern = /\[\^([a-zA-Z0-9_-]*)(?![^\]\s,])/g;
|
|
789
|
+
let result = text;
|
|
790
|
+
let match;
|
|
791
|
+
while ((match = footnotePattern.exec(text)) !== null) {
|
|
792
|
+
const fullMatch = match[0];
|
|
793
|
+
const label = match[1];
|
|
794
|
+
// Check if this is at the end of a line or followed by simple separators
|
|
795
|
+
const matchIndex = match.index;
|
|
796
|
+
const afterMatch = text.substring(matchIndex + fullMatch.length);
|
|
797
|
+
// If followed by end of string, newline, space, or comma, it's likely a footnote
|
|
798
|
+
if (afterMatch === '' ||
|
|
799
|
+
/^\s/.test(afterMatch) ||
|
|
800
|
+
afterMatch.startsWith('\n') ||
|
|
801
|
+
afterMatch.startsWith(',')) {
|
|
802
|
+
const marker = 'streamdown-footnote';
|
|
803
|
+
const replacement = `[^${marker}]`;
|
|
804
|
+
result = result.replace(fullMatch, replacement);
|
|
805
|
+
break; // Only process the first match to avoid conflicts
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
};
|
|
614
810
|
// Parses markdown text and removes incomplete tokens to prevent partial rendering
|
|
615
811
|
export const parseIncompleteMarkdown = (text) => {
|
|
616
812
|
if (!text || typeof text !== 'string') {
|
|
617
813
|
return text;
|
|
618
814
|
}
|
|
619
815
|
let result = text;
|
|
620
|
-
// Handle incomplete
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
result =
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
result =
|
|
631
|
-
result =
|
|
632
|
-
result =
|
|
633
|
-
result =
|
|
634
|
-
result =
|
|
635
|
-
result =
|
|
636
|
-
|
|
637
|
-
result = handleIncompleteSub(result);
|
|
638
|
-
result = handleIncompleteSup(result);
|
|
639
|
-
// Handle KaTeX formatting (only block math with $$)
|
|
816
|
+
// Handle incomplete code blocks FIRST - this prevents other formatters
|
|
817
|
+
// from processing content that should be treated as literal code
|
|
818
|
+
result = handleIncompleteCodeBlock(result);
|
|
819
|
+
// Handle incomplete footnotes FIRST (before any other processing to avoid conflicts)
|
|
820
|
+
result = handleIncompleteFootnotes(result);
|
|
821
|
+
// Handle various formatting completions ONLY if not inside a code block
|
|
822
|
+
// Handle double patterns before single patterns for proper priority
|
|
823
|
+
result = handleIncompleteBoldItalic(result); // ***
|
|
824
|
+
result = handleIncompleteBold(result); // **
|
|
825
|
+
result = handleIncompleteDoubleUnderscoreItalic(result); // __
|
|
826
|
+
result = handleIncompleteStrikethrough(result); // ~~
|
|
827
|
+
result = handleIncompleteInlineCode(result); // `
|
|
828
|
+
result = handleIncompleteSingleAsteriskItalic(result); // *
|
|
829
|
+
result = handleIncompleteSingleUnderscoreItalic(result); // _
|
|
830
|
+
result = handleIncompleteSub(result); // ~
|
|
831
|
+
result = handleIncompleteSup(result); // ^
|
|
832
|
+
// Handle KaTeX formatting
|
|
640
833
|
result = handleIncompleteBlockKatex(result);
|
|
641
|
-
|
|
834
|
+
result = handleIncompleteInlineMath(result);
|
|
835
|
+
// Handle incomplete links and images LAST
|
|
836
|
+
result = handleIncompleteLinksAndImages(result);
|
|
642
837
|
return result;
|
|
643
838
|
};
|