svelte-streamdown 2.3.1 → 2.3.2

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.
@@ -15,86 +15,82 @@ const currencyPatterns = {
15
15
  // Common currency words nearby (check surrounding context)
16
16
  currencyContext: /(?:price|cost|dollar|euro|pound|yen|currency|pay|buy|sell|expensive|cheap)/i
17
17
  };
18
- export function markedMath() {
19
- return {
20
- extensions: [
21
- {
22
- name: 'math',
23
- level: 'block',
24
- tokenizer(src) {
25
- const match = src.match(blockRule);
26
- if (match) {
27
- // match[2] is multiline format, match[3] is single-line format
28
- const content = (match[2] || match[3]).trim();
29
- return {
30
- type: 'math',
31
- isInline: false,
32
- displayMode: true,
33
- raw: match[0],
34
- text: content
35
- };
36
- }
18
+ export const markedMath = [
19
+ {
20
+ name: 'math',
21
+ level: 'block',
22
+ tokenizer(src) {
23
+ const match = src.match(blockRule);
24
+ if (match) {
25
+ // match[2] is multiline format, match[3] is single-line format
26
+ const content = (match[2] || match[3]).trim();
27
+ return {
28
+ type: 'math',
29
+ isInline: false,
30
+ displayMode: true,
31
+ raw: match[0],
32
+ text: content
33
+ };
34
+ }
35
+ }
36
+ },
37
+ {
38
+ name: 'math',
39
+ level: 'inline',
40
+ start(src) {
41
+ let index = 0;
42
+ let searchSrc = src;
43
+ while (searchSrc) {
44
+ const dollarIndex = searchSrc.indexOf('$');
45
+ if (dollarIndex === -1) {
46
+ return;
37
47
  }
38
- },
39
- {
40
- name: 'math',
41
- level: 'inline',
42
- start(src) {
43
- let index = 0;
44
- let searchSrc = src;
45
- while (searchSrc) {
46
- const dollarIndex = searchSrc.indexOf('$');
47
- if (dollarIndex === -1) {
48
- return;
49
- }
50
- const currentIndex = index + dollarIndex;
51
- const possibleMath = src.substring(currentIndex);
52
- // Check if this could be math (not currency)
53
- if (possibleMath.match(inlineRule)) {
54
- const match = possibleMath.match(inlineRule);
55
- if (match) {
56
- const content = match[2];
57
- const dollarCount = match[1]; // '$' or '$$'
58
- // Only apply currency detection to single dollars
59
- // Double dollars ($$) indicate explicit math intent
60
- if (dollarCount === '$' && isCurrencyPattern(content, src, currentIndex)) {
61
- // This looks like currency with single dollars, skip it
62
- index += dollarIndex + 1;
63
- searchSrc = src.substring(index);
64
- continue;
65
- }
66
- return currentIndex;
67
- }
68
- }
69
- index += dollarIndex + 1;
70
- searchSrc = src.substring(index);
71
- }
72
- },
73
- tokenizer(src) {
74
- const match = src.match(inlineRule);
48
+ const currentIndex = index + dollarIndex;
49
+ const possibleMath = src.substring(currentIndex);
50
+ // Check if this could be math (not currency)
51
+ if (possibleMath.match(inlineRule)) {
52
+ const match = possibleMath.match(inlineRule);
75
53
  if (match) {
76
54
  const content = match[2];
77
55
  const dollarCount = match[1]; // '$' or '$$'
78
- const isDisplayMode = dollarCount === '$$';
79
56
  // Only apply currency detection to single dollars
80
57
  // Double dollars ($$) indicate explicit math intent
81
- if (dollarCount === '$' && isCurrencyPattern(content, src, 0)) {
58
+ if (dollarCount === '$' && isCurrencyPattern(content, src, currentIndex)) {
82
59
  // This looks like currency with single dollars, skip it
83
- return;
60
+ index += dollarIndex + 1;
61
+ searchSrc = src.substring(index);
62
+ continue;
84
63
  }
85
- return {
86
- type: 'math',
87
- isInline: true, // Inline tokenizer always produces inline math
88
- displayMode: isDisplayMode, // $$ = display mode styling, $ = inline styling
89
- raw: match[0],
90
- text: content.trim()
91
- };
64
+ return currentIndex;
92
65
  }
93
66
  }
67
+ index += dollarIndex + 1;
68
+ searchSrc = src.substring(index);
94
69
  }
95
- ]
96
- };
97
- }
70
+ },
71
+ tokenizer(src) {
72
+ const match = src.match(inlineRule);
73
+ if (match) {
74
+ const content = match[2];
75
+ const dollarCount = match[1]; // '$' or '$$'
76
+ const isDisplayMode = dollarCount === '$$';
77
+ // Only apply currency detection to single dollars
78
+ // Double dollars ($$) indicate explicit math intent
79
+ if (dollarCount === '$' && isCurrencyPattern(content, src, 0)) {
80
+ // This looks like currency with single dollars, skip it
81
+ return;
82
+ }
83
+ return {
84
+ type: 'math',
85
+ isInline: true, // Inline tokenizer always produces inline math
86
+ displayMode: isDisplayMode, // $$ = display mode styling, $ = inline styling
87
+ raw: match[0],
88
+ text: content.trim()
89
+ };
90
+ }
91
+ }
92
+ }
93
+ ];
98
94
  // Helper function to detect currency patterns
99
95
  function isCurrencyPattern(content, fullSrc, dollarIndex) {
100
96
  const trimmedContent = content.trim();
@@ -1,12 +1,6 @@
1
- import type { TokenizerExtensionFunction, TokenizerStartFunction } from 'marked';
2
- export declare function markedSubSup(): {
3
- extensions: {
4
- name: string;
5
- level: 'inline';
6
- tokenizer: TokenizerExtensionFunction;
7
- start?: TokenizerStartFunction;
8
- }[];
9
- };
1
+ import type { Extension } from './index.js';
2
+ export declare const markedSub: Extension;
3
+ export declare const markedSup: Extension;
10
4
  /**
11
5
  * Represents a subscript token.
12
6
  */
@@ -1,45 +1,40 @@
1
1
  const subRule = /^~([^~\s](?:[^~]*[^~\s])?)~/; // ~text~
2
2
  const supRule = /^\^([^\^\s](?:[^\^]*[^\^\s])?)\^/; // ^text^
3
- export function markedSubSup() {
4
- return {
5
- extensions: [
6
- {
7
- name: 'sub',
8
- level: 'inline',
9
- start(src) {
10
- const i = src.indexOf('~');
11
- return i === -1 ? undefined : i;
12
- },
13
- tokenizer(src) {
14
- const match = src.match(subRule);
15
- if (match) {
16
- return {
17
- type: 'sub',
18
- raw: match[0],
19
- text: match[1],
20
- tokens: this.lexer.inlineTokens(match[1])
21
- };
22
- }
23
- }
24
- },
25
- {
26
- name: 'sup',
27
- level: 'inline',
28
- start(src) {
29
- return src.indexOf('^');
30
- },
31
- tokenizer(src) {
32
- const match = src.match(supRule);
33
- if (match) {
34
- return {
35
- type: 'sup',
36
- raw: match[0],
37
- text: match[1],
38
- tokens: this.lexer.inlineTokens(match[1])
39
- };
40
- }
41
- }
42
- }
43
- ]
44
- };
45
- }
3
+ export const markedSub = {
4
+ name: 'sub',
5
+ level: 'inline',
6
+ start(src) {
7
+ const i = src.indexOf('~');
8
+ return i === -1 ? undefined : i;
9
+ },
10
+ tokenizer(src) {
11
+ const match = src.match(subRule);
12
+ if (match) {
13
+ return {
14
+ type: 'sub',
15
+ raw: match[0],
16
+ text: match[1],
17
+ tokens: this.lexer.inlineTokens(match[1])
18
+ };
19
+ }
20
+ }
21
+ };
22
+ export const markedSup = {
23
+ name: 'sup',
24
+ level: 'inline',
25
+ start(src) {
26
+ const i = src.indexOf('^');
27
+ return i === -1 ? undefined : i;
28
+ },
29
+ tokenizer(src) {
30
+ const match = src.match(supRule);
31
+ if (match) {
32
+ return {
33
+ type: 'sup',
34
+ raw: match[0],
35
+ text: match[1],
36
+ tokens: this.lexer.inlineTokens(match[1])
37
+ };
38
+ }
39
+ }
40
+ };
@@ -1,4 +1,4 @@
1
- import type { TokenizerExtensionFunction, TokenizerStartFunction } from 'marked';
1
+ import type { Extension } from './index.js';
2
2
  export interface SpanTableOptions {
3
3
  useTheadTbody?: boolean;
4
4
  useTfoot?: boolean;
@@ -56,12 +56,5 @@ type WorkingRow = WorkingCell[];
56
56
  export declare const DEFAULT_OPTIONS: Required<SpanTableOptions>;
57
57
  export declare const getTableCell: (text: string, cell: BaseCell, type: "th" | "td", align: string | null) => string;
58
58
  export declare const splitCells: (tableRow: string, count: number | null, prevRow?: WorkingRow | null, maxColspan?: number | null) => WorkingRow;
59
- export declare function markedTable(options?: SpanTableOptions): {
60
- extensions: {
61
- name: string;
62
- level: 'block' | 'inline';
63
- start: TokenizerStartFunction;
64
- tokenizer: TokenizerExtensionFunction;
65
- }[];
66
- };
59
+ export declare const markedTable: Extension;
67
60
  export {};
@@ -116,8 +116,7 @@ const processSpans = (cells, count, prevRow = [], maxColspan = null) => {
116
116
  let cellText = cell.text;
117
117
  // Handle Rowspan - cells ending with ^ (but not superscript ^text^)
118
118
  // Check if it's a rowspan indicator (single ^ at end) vs superscript (^text^)
119
- const isRowspanIndicator = cellText.slice(-1) === '^' &&
120
- !cellText.match(/\^[^^\n\r]+\^$/); // Not a superscript pattern ^text^
119
+ const isRowspanIndicator = cellText.slice(-1) === '^' && !cellText.match(/\^[^^\n\r]+\^$/); // Not a superscript pattern ^text^
121
120
  if (isRowspanIndicator && prevRow.length > 0) {
122
121
  // Clean the ^ indicator from the cell text
123
122
  cell.text = cellText.slice(0, -1).trim();
@@ -374,129 +373,121 @@ function processRows(headerRows, bodyRows, alignment, colCount, lexer, maxColspa
374
373
  }
375
374
  return tokens;
376
375
  }
376
+ const { detectFooter, maxColspan } = DEFAULT_OPTIONS;
377
377
  // Adds support for extended tables in marked with row spanning, column spanning,
378
378
  // multi-row headers, and column alignment
379
- export function markedTable(options = {}) {
380
- const config = { ...DEFAULT_OPTIONS, ...options };
381
- const { detectFooter, maxColspan } = config;
382
- return {
383
- extensions: [
384
- {
385
- name: 'table',
386
- level: 'block',
387
- start(src) {
388
- // Check for table with potential header alignment
389
- let match = src.match(/^\n *([^\n ].*\|.*)\n/);
390
- if (match)
391
- return match.index;
392
- // Check for simple table without header alignment
393
- match = src.match(/^\n *(\|.*\|)\n/);
394
- if (match)
395
- return match.index;
379
+ export const markedTable = {
380
+ name: 'table',
381
+ level: 'block',
382
+ start(src) {
383
+ // Check for table with potential header alignment
384
+ let match = src.match(/^\n *([^\n ].*\|.*)\n/);
385
+ if (match)
386
+ return match.index;
387
+ // Check for simple table without header alignment
388
+ match = src.match(/^\n *(\|.*\|)\n/);
389
+ if (match)
390
+ return match.index;
391
+ return undefined;
392
+ },
393
+ tokenizer(src) {
394
+ // Try to match table with header and alignment first
395
+ let regex = new RegExp('^' +
396
+ '([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' + // Header
397
+ ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' + // Header Align
398
+ '(?:\\n((?:(?! *\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' + // Body Cells
399
+ '(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}' +
400
+ '(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |' +
401
+ '<\\/?(?:address|article|aside|base|basefont|blockquote|body|' +
402
+ 'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)');
403
+ let cap = regex.exec(src);
404
+ let hasHeaderAlignment = true;
405
+ // If no match with header alignment, try table without header alignment
406
+ if (!cap) {
407
+ // Simple regex for tables without header alignment
408
+ regex = /^(\|.*\|(?:\n\|.*\|)*)/;
409
+ cap = regex.exec(src);
410
+ hasHeaderAlignment = false;
411
+ }
412
+ if (!cap)
413
+ return undefined;
414
+ // Combine all captured groups to get complete table rows
415
+ let allTableContent = cap[1]; // Headers
416
+ if (cap[2])
417
+ allTableContent += '\n' + cap[2]; // Alignment row
418
+ if (cap[3])
419
+ allTableContent += '\n' + cap[3]; // Body rows
420
+ const allRows = allTableContent.replace(/\n$/, '').split('\n');
421
+ let headerRows = [];
422
+ let bodyRows = [];
423
+ let alignRow = [];
424
+ let alignment = [];
425
+ let colCount = 0;
426
+ if (hasHeaderAlignment) {
427
+ // Traditional table with header and alignment
428
+ // Parse all rows and identify which are headers vs body
429
+ let headerEndIndex = -1;
430
+ // Find the FIRST alignment row (contains dashes/underscores/asterisks)
431
+ for (let i = 0; i < allRows.length; i++) {
432
+ const row = allRows[i].trim();
433
+ const isAlignment = /^ *(\| *)?:?-+:? *(\| *:?-+:? *)*(\| *)?$/.test(row);
434
+ // Check if this row matches alignment pattern (contains only |, spaces, and alignment chars)
435
+ if (isAlignment) {
436
+ headerEndIndex = i;
437
+ alignRow = row.replace(/^ *\| *| *\| *$/g, '').split(/ *\| */);
438
+ break; // Stop at the first alignment row
439
+ }
440
+ }
441
+ if (headerEndIndex === -1) {
442
+ // No alignment row found, treat as simple table
443
+ bodyRows = allRows;
444
+ colCount = bodyRows[0].split('|').filter((cell) => cell.trim() !== '').length;
445
+ alignment = new Array(colCount).fill(null);
446
+ }
447
+ else {
448
+ // Found alignment row, split headers and body
449
+ // Filter out empty rows and the alignment row itself from headers
450
+ headerRows = allRows.slice(0, headerEndIndex).filter((row) => row.trim() !== '');
451
+ bodyRows = headerEndIndex + 1 < allRows.length ? allRows.slice(headerEndIndex + 1) : [];
452
+ // Use alignment row length as the authoritative column count
453
+ colCount = alignRow.length;
454
+ // Validate that we have a reasonable table structure
455
+ if (colCount === 0)
396
456
  return undefined;
397
- },
398
- tokenizer(src) {
399
- // Try to match table with header and alignment first
400
- let regex = new RegExp('^' +
401
- '([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' + // Header
402
- ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' + // Header Align
403
- '(?:\\n((?:(?! *\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' + // Body Cells
404
- '(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}' +
405
- '(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |' +
406
- '<\\/?(?:address|article|aside|base|basefont|blockquote|body|' +
407
- 'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)');
408
- let cap = regex.exec(src);
409
- let hasHeaderAlignment = true;
410
- // If no match with header alignment, try table without header alignment
411
- if (!cap) {
412
- // Simple regex for tables without header alignment
413
- regex = /^(\|.*\|(?:\n\|.*\|)*)/;
414
- cap = regex.exec(src);
415
- hasHeaderAlignment = false;
416
- }
417
- if (!cap)
418
- return null;
419
- // Combine all captured groups to get complete table rows
420
- let allTableContent = cap[1]; // Headers
421
- if (cap[2])
422
- allTableContent += '\n' + cap[2]; // Alignment row
423
- if (cap[3])
424
- allTableContent += '\n' + cap[3]; // Body rows
425
- const allRows = allTableContent.replace(/\n$/, '').split('\n');
426
- let headerRows = [];
427
- let bodyRows = [];
428
- let alignRow = [];
429
- let alignment = [];
430
- let colCount = 0;
431
- if (hasHeaderAlignment) {
432
- // Traditional table with header and alignment
433
- // Parse all rows and identify which are headers vs body
434
- let headerEndIndex = -1;
435
- // Find the FIRST alignment row (contains dashes/underscores/asterisks)
436
- for (let i = 0; i < allRows.length; i++) {
437
- const row = allRows[i].trim();
438
- const isAlignment = /^ *(\| *)?:?-+:? *(\| *:?-+:? *)*(\| *)?$/.test(row);
439
- // Check if this row matches alignment pattern (contains only |, spaces, and alignment chars)
440
- if (isAlignment) {
441
- headerEndIndex = i;
442
- alignRow = row.replace(/^ *\| *| *\| *$/g, '').split(/ *\| */);
443
- break; // Stop at the first alignment row
444
- }
445
- }
446
- if (headerEndIndex === -1) {
447
- // No alignment row found, treat as simple table
448
- bodyRows = allRows;
449
- colCount = bodyRows[0].split('|').filter((cell) => cell.trim() !== '').length;
450
- alignment = new Array(colCount).fill(null);
451
- }
452
- else {
453
- // Found alignment row, split headers and body
454
- // Filter out empty rows and the alignment row itself from headers
455
- headerRows = allRows.slice(0, headerEndIndex).filter((row) => row.trim() !== '');
456
- bodyRows =
457
- headerEndIndex + 1 < allRows.length ? allRows.slice(headerEndIndex + 1) : [];
458
- // Use alignment row length as the authoritative column count
459
- colCount = alignRow.length;
460
- // Validate that we have a reasonable table structure
461
- if (colCount === 0)
462
- return null;
463
- // Process alignment
464
- alignment = processAlignment(alignRow);
465
- }
466
- }
467
- else {
468
- // Table without header alignment - treat all rows as body rows
469
- bodyRows = allRows;
470
- const firstRowCells = bodyRows[0].split('|').filter((cell) => cell.trim() !== '');
471
- colCount = firstRowCells.length;
472
- alignment = new Array(colCount).fill(null); // No alignment for tables without headers
473
- }
474
- // Detect footer alignment row pattern in body rows (only for tables with header alignment)
475
- let shouldDetectFooter = false;
476
- let processedBodyRows = bodyRows;
477
- if (detectFooter && hasHeaderAlignment && bodyRows.length > 0) {
478
- // Check if any row matches the alignment pattern (contains only dashes, pipes, colons, and spaces)
479
- for (let i = bodyRows.length - 1; i >= 0; i--) {
480
- const row = bodyRows[i];
481
- if (/^ *\| *:?-+:? *(\| *:?-+:? *)*\| *$/.test(row)) {
482
- // Found footer alignment row - remove it and enable footer detection
483
- shouldDetectFooter = true;
484
- processedBodyRows = bodyRows.slice(0, i).concat(bodyRows.slice(i + 1));
485
- break;
486
- }
487
- }
488
- }
489
- // Process all rows and create table sections
490
- const tokens = processRows(headerRows, processedBodyRows, alignment, colCount, this.lexer, maxColspan, shouldDetectFooter);
491
- const item = {
492
- type: 'table',
493
- tokens,
494
- raw: cap[0],
495
- align: alignment
496
- };
497
- return item;
457
+ // Process alignment
458
+ alignment = processAlignment(alignRow);
459
+ }
460
+ }
461
+ else {
462
+ // Table without header alignment - treat all rows as body rows
463
+ bodyRows = allRows;
464
+ const firstRowCells = bodyRows[0].split('|').filter((cell) => cell.trim() !== '');
465
+ colCount = firstRowCells.length;
466
+ alignment = new Array(colCount).fill(null); // No alignment for tables without headers
467
+ }
468
+ // Detect footer alignment row pattern in body rows (only for tables with header alignment)
469
+ let shouldDetectFooter = false;
470
+ let processedBodyRows = bodyRows;
471
+ if (detectFooter && hasHeaderAlignment && bodyRows.length > 0) {
472
+ // Check if any row matches the alignment pattern (contains only dashes, pipes, colons, and spaces)
473
+ for (let i = bodyRows.length - 1; i >= 0; i--) {
474
+ const row = bodyRows[i];
475
+ if (/^ *\| *:?-+:? *(\| *:?-+:? *)*\| *$/.test(row)) {
476
+ // Found footer alignment row - remove it and enable footer detection
477
+ shouldDetectFooter = true;
478
+ processedBodyRows = bodyRows.slice(0, i).concat(bodyRows.slice(i + 1));
479
+ break;
498
480
  }
499
481
  }
500
- ]
501
- };
502
- }
482
+ }
483
+ // Process all rows and create table sections
484
+ const tokens = processRows(headerRows, processedBodyRows, alignment, colCount, this.lexer, maxColspan, shouldDetectFooter);
485
+ const item = {
486
+ type: 'table',
487
+ tokens,
488
+ raw: cap[0],
489
+ align: alignment
490
+ };
491
+ return item;
492
+ }
493
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -73,7 +73,6 @@
73
73
  "dependencies": {
74
74
  "@floating-ui/dom": "^1.7.4",
75
75
  "clsx": "^2.1.1",
76
- "html-url-attributes": "^3.0.1",
77
76
  "katex": "^0.16.22",
78
77
  "marked": "^16.2.1",
79
78
  "mermaid": "^11.11.0",