svelte-streamdown 3.0.0 → 3.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/README.md +260 -139
- package/dist/Elements/FootnoteRef.svelte +1 -1
- package/dist/Elements/Mermaid.svelte +1 -1
- package/dist/Streamdown.svelte +17 -4
- package/dist/context.svelte.d.ts +5 -1
- package/dist/marked/index.d.ts +15 -1
- package/dist/marked/index.js +123 -10
- package/dist/marked/marked-alert.js +12 -3
- package/dist/marked/marked-align.js +9 -5
- package/dist/marked/marked-citations.js +4 -3
- package/dist/marked/marked-dl.js +8 -3
- package/dist/marked/marked-footnotes.js +7 -6
- package/dist/marked/marked-hr.js +4 -3
- package/dist/marked/marked-list.js +55 -18
- package/dist/marked/marked-math.js +12 -14
- package/dist/marked/marked-table.js +14 -34
- package/dist/utils/parse-incomplete-markdown.js +127 -66
- package/dist/utils/url.js +6 -0
- package/package.json +11 -1
|
@@ -90,12 +90,9 @@ const processSpans = (cells, count, prevRow = [], maxColspan = null) => {
|
|
|
90
90
|
const processedCells = [];
|
|
91
91
|
// Track colspan cells that need rowspan
|
|
92
92
|
const colspanCells = new Map();
|
|
93
|
-
// First pass: Process each cell's colspan
|
|
93
|
+
// First pass: Process each cell's colspan
|
|
94
94
|
let cellIndex = 0;
|
|
95
|
-
const mergedIndices = new Set();
|
|
96
95
|
for (i = 0; i < cells.length; i++) {
|
|
97
|
-
if (mergedIndices.has(i))
|
|
98
|
-
continue;
|
|
99
96
|
trimmedCell = cells[i];
|
|
100
97
|
let colspan = 1;
|
|
101
98
|
// Check for colspan marker from consecutive pipes
|
|
@@ -104,24 +101,6 @@ const processSpans = (cells, count, prevRow = [], maxColspan = null) => {
|
|
|
104
101
|
trimmedCell = parts[0];
|
|
105
102
|
colspan = parseInt(parts[1], 10);
|
|
106
103
|
}
|
|
107
|
-
else if (!trimmedCell.trim()) {
|
|
108
|
-
// Fallback: merge empty run into previous cell (backward compatibility)
|
|
109
|
-
let run = 1, k = i + 1;
|
|
110
|
-
while (k < cells.length && !cells[k].trim()) {
|
|
111
|
-
run++;
|
|
112
|
-
mergedIndices.add(k++);
|
|
113
|
-
}
|
|
114
|
-
if (processedCells.length) {
|
|
115
|
-
const target = processedCells[processedCells.length - 1];
|
|
116
|
-
const allowed = maxColspan != null ? Math.min(run, Math.max(0, maxColspan - target.colspan)) : run;
|
|
117
|
-
target.colspan += allowed;
|
|
118
|
-
numCols += allowed;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
colspan = maxColspan != null ? Math.min(run, maxColspan) : run;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
104
|
if (maxColspan !== null && colspan > maxColspan)
|
|
126
105
|
colspan = maxColspan;
|
|
127
106
|
processedCells[cellIndex] = {
|
|
@@ -141,8 +120,11 @@ const processSpans = (cells, count, prevRow = [], maxColspan = null) => {
|
|
|
141
120
|
// Check if it's a rowspan indicator (single ^ at end) vs superscript (^text^)
|
|
142
121
|
const isRowspanIndicator = cellText.slice(-1) === '^' && !cellText.match(/\^[^^\n\r]+\^$/); // Not a superscript pattern ^text^
|
|
143
122
|
if (isRowspanIndicator && prevRow.length > 0) {
|
|
144
|
-
// Clean the ^ indicator from the cell text
|
|
123
|
+
// Clean the ^ indicator from the cell text. A cell that is nothing but
|
|
124
|
+
// carets (the usual `^^` continuation marker) carries no content.
|
|
145
125
|
cell.text = cellText.slice(0, -1).trim();
|
|
126
|
+
if (/^\^*$/.test(cell.text))
|
|
127
|
+
cell.text = '';
|
|
146
128
|
cellText = cell.text;
|
|
147
129
|
let targetFound = false;
|
|
148
130
|
const startPosition = cell.position || 0;
|
|
@@ -161,8 +143,10 @@ const processSpans = (cells, count, prevRow = [], maxColspan = null) => {
|
|
|
161
143
|
// If the cell spans exactly match, simple case
|
|
162
144
|
if (cell.colspan === prevCell.colspan && cell.position === prevCell.position) {
|
|
163
145
|
cell.rowSpanTarget = prevCell.rowSpanTarget ?? prevCell;
|
|
164
|
-
// Only append text if it's different from the target cell
|
|
165
|
-
|
|
146
|
+
// Only append text if it's different from the target cell.
|
|
147
|
+
// cell.text was already cleaned of its ^ indicator above —
|
|
148
|
+
// slicing again here used to drop the last real character.
|
|
149
|
+
const textToAppend = cell.text.trim();
|
|
166
150
|
const targetText = cell.rowSpanTarget.text.trim();
|
|
167
151
|
// Don't append if the text is the same or already contained (common case for rowspan indicators)
|
|
168
152
|
if (textToAppend &&
|
|
@@ -188,8 +172,9 @@ const processSpans = (cells, count, prevRow = [], maxColspan = null) => {
|
|
|
188
172
|
else {
|
|
189
173
|
// Standard case of single column cell with rowspan
|
|
190
174
|
cell.rowSpanTarget = prevCell.rowSpanTarget ?? prevCell;
|
|
191
|
-
// Only append text if it's different from the target cell
|
|
192
|
-
|
|
175
|
+
// Only append text if it's different from the target cell.
|
|
176
|
+
// cell.text was already cleaned of its ^ indicator above.
|
|
177
|
+
const textToAppend = cell.text.trim();
|
|
193
178
|
const targetText = cell.rowSpanTarget.text.trim();
|
|
194
179
|
// Don't append if the text is the same or already contained (common case for rowspan indicators)
|
|
195
180
|
if (textToAppend && textToAppend !== targetText && !targetText.includes(textToAppend)) {
|
|
@@ -202,13 +187,8 @@ const processSpans = (cells, count, prevRow = [], maxColspan = null) => {
|
|
|
202
187
|
}
|
|
203
188
|
}
|
|
204
189
|
}
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
// Only clean if it was actually a rowspan indicator, not superscript
|
|
208
|
-
if (isRowspanIndicator) {
|
|
209
|
-
cell.text = cell.text.slice(0, -1);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
190
|
+
// No target found: cell.text was already cleaned of its ^ indicator
|
|
191
|
+
// above, so the cell simply renders as a normal cell.
|
|
212
192
|
}
|
|
213
193
|
}
|
|
214
194
|
// Process any complex colspan+rowspan combinations we tracked
|
|
@@ -108,12 +108,15 @@ export class IncompleteMarkdownParser {
|
|
|
108
108
|
let inMathBlock = false;
|
|
109
109
|
let inCenterBlock = false;
|
|
110
110
|
let inRightBlock = false;
|
|
111
|
+
let centerOpenLine = -1;
|
|
112
|
+
let rightOpenLine = -1;
|
|
111
113
|
// Track which lines are in which contexts for state management
|
|
112
114
|
const lineContexts = [];
|
|
113
115
|
for (let i = 0; i < lines.length; i++) {
|
|
114
116
|
const line = lines[i];
|
|
115
|
-
// Check for block boundaries
|
|
116
|
-
|
|
117
|
+
// Check for block boundaries (fences may be quoted inside blockquotes/alerts: "> ```")
|
|
118
|
+
const fenceLine = line.replace(/^[ \t]*(?:>[ \t]*)*/, '');
|
|
119
|
+
if (fenceLine.startsWith('```') || fenceLine.startsWith('~~~')) {
|
|
117
120
|
inCodeBlock = !inCodeBlock;
|
|
118
121
|
}
|
|
119
122
|
if (line.trim().startsWith('$$') && !line.trim().includes('$$', 2)) {
|
|
@@ -121,12 +124,14 @@ export class IncompleteMarkdownParser {
|
|
|
121
124
|
}
|
|
122
125
|
if (line.trim() === '[center]') {
|
|
123
126
|
inCenterBlock = true;
|
|
127
|
+
centerOpenLine = i;
|
|
124
128
|
}
|
|
125
129
|
if (line.trim() === '[/center]') {
|
|
126
130
|
inCenterBlock = false;
|
|
127
131
|
}
|
|
128
132
|
if (line.trim() === '[right]') {
|
|
129
133
|
inRightBlock = true;
|
|
134
|
+
rightOpenLine = i;
|
|
130
135
|
}
|
|
131
136
|
if (line.trim() === '[/right]') {
|
|
132
137
|
inRightBlock = false;
|
|
@@ -144,9 +149,11 @@ export class IncompleteMarkdownParser {
|
|
|
144
149
|
finalContexts.add('code');
|
|
145
150
|
if (inMathBlock)
|
|
146
151
|
finalContexts.add('math');
|
|
147
|
-
|
|
152
|
+
// Only auto-close center/right when content follows the opening tag;
|
|
153
|
+
// a bare trailing '[center]'/'[right]' line is left untouched.
|
|
154
|
+
if (inCenterBlock && centerOpenLine < lines.length - 1)
|
|
148
155
|
finalContexts.add('center');
|
|
149
|
-
if (inRightBlock)
|
|
156
|
+
if (inRightBlock && rightOpenLine < lines.length - 1)
|
|
150
157
|
finalContexts.add('right');
|
|
151
158
|
// Return both the text and the updated state
|
|
152
159
|
return {
|
|
@@ -158,20 +165,22 @@ export class IncompleteMarkdownParser {
|
|
|
158
165
|
};
|
|
159
166
|
},
|
|
160
167
|
postprocess: ({ text, state }) => {
|
|
161
|
-
// Complete incomplete blocks at end of input
|
|
168
|
+
// Complete incomplete blocks at end of input.
|
|
169
|
+
// Close inner blocks (code/math) before alignment wrappers.
|
|
170
|
+
let result = text;
|
|
162
171
|
if (state.blockingContexts.has('code')) {
|
|
163
|
-
|
|
172
|
+
result += '\n```';
|
|
164
173
|
}
|
|
165
174
|
if (state.blockingContexts.has('math')) {
|
|
166
|
-
|
|
175
|
+
result += '\n$$';
|
|
167
176
|
}
|
|
168
177
|
if (state.blockingContexts.has('center')) {
|
|
169
|
-
|
|
178
|
+
result += '\n[/center]';
|
|
170
179
|
}
|
|
171
180
|
if (state.blockingContexts.has('right')) {
|
|
172
|
-
|
|
181
|
+
result += '\n[/right]';
|
|
173
182
|
}
|
|
174
|
-
return
|
|
183
|
+
return result;
|
|
175
184
|
}
|
|
176
185
|
},
|
|
177
186
|
{
|
|
@@ -190,7 +199,13 @@ export class IncompleteMarkdownParser {
|
|
|
190
199
|
if (isEndingWithTripleAsterisk) {
|
|
191
200
|
return line.substring(0, lastTripleAsteriskIndex);
|
|
192
201
|
}
|
|
193
|
-
|
|
202
|
+
const before = line.substring(0, endOfCellOrLine);
|
|
203
|
+
// Part of the closing '***' may have already arrived (a trailing '*' or
|
|
204
|
+
// '**'); only add the missing asterisks so we complete to exactly '***'
|
|
205
|
+
// instead of leaving stray asterisks after the text.
|
|
206
|
+
const trailing = before.match(/\*+$/)?.[0].length ?? 0;
|
|
207
|
+
const missing = trailing >= 1 && trailing <= 2 ? 3 - trailing : 3;
|
|
208
|
+
return before + '*'.repeat(missing) + line.substring(endOfCellOrLine);
|
|
194
209
|
}
|
|
195
210
|
return line;
|
|
196
211
|
}
|
|
@@ -211,7 +226,12 @@ export class IncompleteMarkdownParser {
|
|
|
211
226
|
if (isEndingWithDoubleAsterisk) {
|
|
212
227
|
return line.substring(0, lastDoubleAsteriskIndex);
|
|
213
228
|
}
|
|
214
|
-
|
|
229
|
+
const before = line.substring(0, endOfCellOrLine);
|
|
230
|
+
// If the content already ends with a single '*' — the first half of the
|
|
231
|
+
// closing '**' arriving mid-stream — only add one more '*' to complete it.
|
|
232
|
+
// Otherwise we'd emit '***' and leave a stray '*' after the bold text.
|
|
233
|
+
const closing = before.endsWith('*') && !before.endsWith('**') ? '*' : '**';
|
|
234
|
+
return before + closing + line.substring(endOfCellOrLine);
|
|
215
235
|
}
|
|
216
236
|
return line;
|
|
217
237
|
}
|
|
@@ -232,7 +252,11 @@ export class IncompleteMarkdownParser {
|
|
|
232
252
|
if (isEndingWithDoubleUnderscore) {
|
|
233
253
|
return line.substring(0, lastDoubleUnderscoreIndex);
|
|
234
254
|
}
|
|
235
|
-
|
|
255
|
+
const before = line.substring(0, endOfCellOrLine);
|
|
256
|
+
// A half-typed closing '__' (a lone trailing '_') only needs one more '_',
|
|
257
|
+
// not a full '__' that would leave a stray underscore after the text.
|
|
258
|
+
const closing = before.endsWith('_') && !before.endsWith('__') ? '_' : '__';
|
|
259
|
+
return before + closing + line.substring(endOfCellOrLine);
|
|
236
260
|
}
|
|
237
261
|
return line;
|
|
238
262
|
}
|
|
@@ -253,7 +277,10 @@ export class IncompleteMarkdownParser {
|
|
|
253
277
|
if (isEndingWithDoubleTilde) {
|
|
254
278
|
return line.substring(0, lastDoubleTildeIndex);
|
|
255
279
|
}
|
|
256
|
-
|
|
280
|
+
const before = line.substring(0, endOfCellOrLine);
|
|
281
|
+
// A half-typed closing '~~' (a lone trailing '~') only needs one more '~'.
|
|
282
|
+
const closing = before.endsWith('~') && !before.endsWith('~~') ? '~' : '~~';
|
|
283
|
+
return before + closing + line.substring(endOfCellOrLine);
|
|
257
284
|
}
|
|
258
285
|
}
|
|
259
286
|
return line;
|
|
@@ -435,40 +462,73 @@ export class IncompleteMarkdownParser {
|
|
|
435
462
|
return line;
|
|
436
463
|
}
|
|
437
464
|
},
|
|
465
|
+
{
|
|
466
|
+
// Must run before inlineCitation: otherwise a trailing `[^label` gets
|
|
467
|
+
// closed with a plain `]`, defeating the streamdown:footnote marker.
|
|
468
|
+
name: 'footnoteRef',
|
|
469
|
+
pattern: /\[\^[^\]\s,]*/,
|
|
470
|
+
skipInBlockTypes: ['code', 'math'],
|
|
471
|
+
handler: ({ line }) => {
|
|
472
|
+
if (!line.includes(']')) {
|
|
473
|
+
return line.replace(/\[\^[^\]\s,]*/, '[^streamdown:footnote]');
|
|
474
|
+
}
|
|
475
|
+
return line;
|
|
476
|
+
}
|
|
477
|
+
},
|
|
438
478
|
{
|
|
439
479
|
name: 'inlineCitation',
|
|
440
480
|
pattern: /\[/,
|
|
441
481
|
skipInBlockTypes: ['code', 'math'],
|
|
442
482
|
handler: ({ line }) => {
|
|
443
|
-
//
|
|
444
|
-
|
|
483
|
+
// Lines that already contain link/image "](" syntax belong to the
|
|
484
|
+
// linksAndImages plugin: completing or preserving them is its job.
|
|
485
|
+
if (line.includes('](')) {
|
|
486
|
+
return line;
|
|
487
|
+
}
|
|
488
|
+
// Collect unescaped opening brackets without a matching closing bracket
|
|
489
|
+
const unclosedPositions = [];
|
|
445
490
|
for (let i = 0; i < line.length; i++) {
|
|
446
491
|
if (line[i] === '[' && (i === 0 || line[i - 1] !== '\\')) {
|
|
447
492
|
// Check if this bracket has a matching closing bracket later in the line
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (closingIndex === -1) {
|
|
451
|
-
unclosedBrackets++;
|
|
493
|
+
if (line.indexOf(']', i + 1) === -1) {
|
|
494
|
+
unclosedPositions.push(i);
|
|
452
495
|
}
|
|
453
496
|
}
|
|
454
497
|
}
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
498
|
+
// Close every unclosed citation bracket (right to left so indices stay
|
|
499
|
+
// valid). Brackets that look like incomplete images (`![`), footnotes
|
|
500
|
+
// (`[^`), link text containing markdown formatting, table-cell content,
|
|
501
|
+
// or a trailing bracket preceded by a completed `[...]` pair (evidence
|
|
502
|
+
// of an in-progress link) are left for the dedicated plugins
|
|
503
|
+
// (footnoteRef, linksAndImages).
|
|
504
|
+
let result = line;
|
|
505
|
+
for (let k = unclosedPositions.length - 1; k >= 0; k--) {
|
|
506
|
+
const pos = unclosedPositions[k];
|
|
507
|
+
const endOfCellOrLine = findEndOfCellOrLineContaining(result, pos);
|
|
508
|
+
const content = result.substring(pos + 1, endOfCellOrLine);
|
|
509
|
+
const isImage = pos > 0 && result[pos - 1] === '!';
|
|
510
|
+
const isFootnote = content.startsWith('^');
|
|
511
|
+
const hasFormatting = /[*~`_]/.test(content);
|
|
512
|
+
const isTableCell = endOfCellOrLine < result.length && result[endOfCellOrLine] === '|';
|
|
513
|
+
const hasPriorCompletedPair = /\[[^\]]*\]/.test(line.substring(0, pos));
|
|
514
|
+
if (isImage || isFootnote || hasFormatting || isTableCell || hasPriorCompletedPair) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (k === unclosedPositions.length - 1) {
|
|
518
|
+
// Last bracket: close at end of cell/line (keeps multi-key citations together)
|
|
519
|
+
result =
|
|
520
|
+
result.substring(0, endOfCellOrLine) + ']' + result.substring(endOfCellOrLine);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
// Earlier brackets: close right after the citation key (first word)
|
|
524
|
+
const keyMatch = content.match(/^\s*\S+/);
|
|
525
|
+
if (keyMatch) {
|
|
526
|
+
const insertAt = pos + 1 + keyMatch[0].length;
|
|
527
|
+
result = result.substring(0, insertAt) + ']' + result.substring(insertAt);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
470
530
|
}
|
|
471
|
-
return
|
|
531
|
+
return result;
|
|
472
532
|
}
|
|
473
533
|
},
|
|
474
534
|
{
|
|
@@ -638,8 +698,11 @@ export class IncompleteMarkdownParser {
|
|
|
638
698
|
const linkMatch = line.match(/(!?\[)([^\]]*?)$/);
|
|
639
699
|
if (linkMatch && !line.includes('](')) {
|
|
640
700
|
const [, openBracket, linkTextWithPossibleBoundary] = linkMatch;
|
|
641
|
-
//
|
|
642
|
-
|
|
701
|
+
// Position of the matched opening bracket (the regex matches the first
|
|
702
|
+
// bracket that stays unclosed through the end of the line). Using the
|
|
703
|
+
// match index keeps the replacement aligned with the captured link text;
|
|
704
|
+
// `lastIndexOf` could point at a different bracket and duplicate text.
|
|
705
|
+
const bracketIndex = linkMatch.index ?? 0;
|
|
643
706
|
const endOfCellOrLine = findEndOfCellOrLineContaining(line, bracketIndex);
|
|
644
707
|
// Extract the clean link text (remove any trailing | or whitespace)
|
|
645
708
|
const linkText = linkTextWithPossibleBoundary.replace(/[\s|]+$/, '');
|
|
@@ -649,24 +712,8 @@ export class IncompleteMarkdownParser {
|
|
|
649
712
|
// Replace from bracket to end of cell/line, including boundary if it's |
|
|
650
713
|
const includeBoundary = endOfCellOrLine < line.length && line[endOfCellOrLine] === '|';
|
|
651
714
|
const incompleteEnd = includeBoundary ? endOfCellOrLine + 1 : endOfCellOrLine;
|
|
652
|
-
const incompletePart = line.substring(bracketIndex, incompleteEnd);
|
|
653
715
|
const completedPart = openBracket + linkText + '](' + marker + ')' + (includeBoundary ? '|' : '');
|
|
654
|
-
return line.
|
|
655
|
-
}
|
|
656
|
-
return line;
|
|
657
|
-
}
|
|
658
|
-
},
|
|
659
|
-
{
|
|
660
|
-
name: 'alignmentBlocks',
|
|
661
|
-
pattern: /^(\s*\[(center|right)\])$/,
|
|
662
|
-
skipInBlockTypes: ['code', 'math'],
|
|
663
|
-
handler: ({ line, state }) => {
|
|
664
|
-
// Check if this is an opening alignment tag without content or closing tag
|
|
665
|
-
const alignMatch = line.match(/^(\s*\[(center|right)\])$/);
|
|
666
|
-
if (alignMatch) {
|
|
667
|
-
const indent = alignMatch[1].length - alignMatch[1].trim().length;
|
|
668
|
-
const alignType = alignMatch[2];
|
|
669
|
-
return line + '\n' + ' '.repeat(indent) + '[/' + alignType + ']';
|
|
716
|
+
return line.substring(0, bracketIndex) + completedPart + line.substring(incompleteEnd);
|
|
670
717
|
}
|
|
671
718
|
return line;
|
|
672
719
|
}
|
|
@@ -674,12 +721,19 @@ export class IncompleteMarkdownParser {
|
|
|
674
721
|
{
|
|
675
722
|
name: 'mdx',
|
|
676
723
|
skipInBlockTypes: ['code', 'math', 'center', 'right'],
|
|
677
|
-
preprocess: ({ text }) => {
|
|
724
|
+
preprocess: ({ text, state }) => {
|
|
678
725
|
// Track MDX component states across the entire text
|
|
679
726
|
const lines = text.split('\n');
|
|
680
727
|
const openTags = [];
|
|
681
728
|
let mdxLineStates = [];
|
|
682
729
|
for (let i = 0; i < lines.length; i++) {
|
|
730
|
+
// Lines inside code fences or math blocks are opaque content: MDX-looking
|
|
731
|
+
// tags there must not open/close/track components.
|
|
732
|
+
const lineCtx = state.lineContexts?.[i];
|
|
733
|
+
if (lineCtx?.code || lineCtx?.math) {
|
|
734
|
+
mdxLineStates[i] = { inMdx: false, incompletePositions: [] };
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
683
737
|
const line = lines[i];
|
|
684
738
|
let inMdx = false;
|
|
685
739
|
let incompletePositions = [];
|
|
@@ -691,6 +745,23 @@ export class IncompleteMarkdownParser {
|
|
|
691
745
|
if (tagStart === -1 || tagStart >= line.length - 1)
|
|
692
746
|
break;
|
|
693
747
|
const nextChar = line[tagStart + 1];
|
|
748
|
+
// Closing tag for a component opened on an earlier line. Handled
|
|
749
|
+
// inside the scan so a close that is part of a same-line complete
|
|
750
|
+
// pair (consumed below) is never double-counted against the stack.
|
|
751
|
+
const closeTagMatch = line.substring(tagStart).match(/^<\/([A-Z][a-zA-Z0-9]*)>/);
|
|
752
|
+
if (closeTagMatch) {
|
|
753
|
+
const tagName = closeTagMatch[1];
|
|
754
|
+
// Pop the innermost same-name open (LIFO) so the auto-appended
|
|
755
|
+
// closers keep the right nesting order.
|
|
756
|
+
for (let openIndex = openTags.length - 1; openIndex >= 0; openIndex--) {
|
|
757
|
+
if (openTags[openIndex].tagName === tagName) {
|
|
758
|
+
openTags.splice(openIndex, 1);
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
searchPos = tagStart + closeTagMatch[0].length;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
694
765
|
// Only match if starts with capital letter (MDX component)
|
|
695
766
|
if (!/[A-Z]/.test(nextChar)) {
|
|
696
767
|
searchPos = tagStart + 1;
|
|
@@ -741,16 +812,6 @@ export class IncompleteMarkdownParser {
|
|
|
741
812
|
}
|
|
742
813
|
searchPos = tagStart + 1;
|
|
743
814
|
}
|
|
744
|
-
// Check for closing tags
|
|
745
|
-
const closeTagMatches = line.matchAll(/<\/([A-Z][a-zA-Z0-9]*)>/g);
|
|
746
|
-
for (const closeMatch of closeTagMatches) {
|
|
747
|
-
const tagName = closeMatch[1];
|
|
748
|
-
// Find and remove the matching open tag
|
|
749
|
-
const openIndex = openTags.findIndex((t) => t.tagName === tagName);
|
|
750
|
-
if (openIndex !== -1) {
|
|
751
|
-
openTags.splice(openIndex, 1);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
815
|
mdxLineStates[i] = { inMdx, incompletePositions };
|
|
755
816
|
}
|
|
756
817
|
return {
|
package/dist/utils/url.js
CHANGED
|
@@ -39,6 +39,12 @@ export const transformUrl = (url, allowedPrefixes, defaultOrigin) => {
|
|
|
39
39
|
const urlString = parseUrl(url);
|
|
40
40
|
if (urlString &&
|
|
41
41
|
allowedPrefixes.some((prefix) => {
|
|
42
|
+
// Protocol-only prefixes (e.g. 'https://', 'http://', 'mailto:') allow any
|
|
43
|
+
// URL using that protocol. They are not valid absolute URLs on their own
|
|
44
|
+
// (new URL('https://') throws), so we match them with a simple prefix check.
|
|
45
|
+
if (prefix.endsWith('://') || (prefix.endsWith(':') && !prefix.includes('//'))) {
|
|
46
|
+
return urlString.href.startsWith(prefix);
|
|
47
|
+
}
|
|
42
48
|
const parsedPrefix = parseUrl(prefix);
|
|
43
49
|
if (!parsedPrefix) {
|
|
44
50
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-streamdown",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"packageManager": "pnpm@10.32.1",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/beynar/svelte-streamdown.git"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public",
|
|
11
|
+
"provenance": true
|
|
12
|
+
},
|
|
4
13
|
"scripts": {
|
|
5
14
|
"dev": "vite dev",
|
|
6
15
|
"build": "vite build && npm run prepack",
|
|
@@ -54,6 +63,7 @@
|
|
|
54
63
|
"@sveltejs/package": "^2.0.0",
|
|
55
64
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
|
56
65
|
"@tailwindcss/vite": "^4.0.0",
|
|
66
|
+
"@types/node": "^25.9.2",
|
|
57
67
|
"@vitest/browser": "^3.2.3",
|
|
58
68
|
"@vitest/ui": "^3.2.4",
|
|
59
69
|
"playwright": "^1.53.0",
|