Skip to content

Commit 03e826b

Browse files
fix: handleincompletesingleunderscoreitalic is not accounting for the usage inside math equations (vercel#38)
* Fix underscores in math equations * Create cuddly-goats-warn.md
1 parent 95f0992 commit 03e826b

File tree

3 files changed

+106
-1
lines changed

3 files changed

+106
-1
lines changed

.changeset/cuddly-goats-warn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"streamdown": patch
3+
---
4+
5+
fix: handleIncompleteSingleUnderscoreItalic is not accounting for the usage inside math equations

packages/streamdown/__tests__/parse-incomplete-markdown.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,73 @@ describe('parseIncompleteMarkdown', () => {
626626
});
627627
});
628628

629+
describe('math blocks with underscores', () => {
630+
it('should not complete underscores within inline math blocks', () => {
631+
const text = 'The variable $x_1$ represents the first element';
632+
expect(parseIncompleteMarkdown(text)).toBe(text);
633+
634+
const text2 = 'Formula: $a_b + c_d = e_f$';
635+
expect(parseIncompleteMarkdown(text2)).toBe(text2);
636+
});
637+
638+
it('should not complete underscores within block math', () => {
639+
const text = '$$x_1 + y_2 = z_3$$';
640+
expect(parseIncompleteMarkdown(text)).toBe(text);
641+
642+
const text2 = '$$\na_1 + b_2\nc_3 + d_4\n$$';
643+
expect(parseIncompleteMarkdown(text2)).toBe(text2);
644+
});
645+
646+
it('should not add underscore when math block has incomplete underscore', () => {
647+
// Incomplete math blocks get completed by handleIncompleteInlineKatex
648+
// The underscore inside should not be treated as italic
649+
const text = 'Math expression $x_';
650+
expect(parseIncompleteMarkdown(text)).toBe('Math expression $x_$');
651+
652+
const text2 = '$$formula_';
653+
expect(parseIncompleteMarkdown(text2)).toBe('$$formula_$$');
654+
});
655+
656+
it('should handle underscores outside math blocks normally', () => {
657+
const text = 'Text with _italic_ and math $x_1$';
658+
expect(parseIncompleteMarkdown(text)).toBe(text);
659+
660+
const text2 = '_italic text_ followed by $a_b$';
661+
expect(parseIncompleteMarkdown(text2)).toBe(text2);
662+
});
663+
664+
it('should complete italic underscore outside math but not inside', () => {
665+
const text = 'Start _italic with $x_1$';
666+
expect(parseIncompleteMarkdown(text)).toBe('Start _italic with $x_1$_');
667+
});
668+
669+
it('should handle complex math expressions with multiple underscores', () => {
670+
const text = '$x_1 + x_2 + x_3 = y_1$';
671+
expect(parseIncompleteMarkdown(text)).toBe(text);
672+
673+
const text2 = '$$\\sum_{i=1}^{n} x_i = \\prod_{j=1}^{m} y_j$$';
674+
expect(parseIncompleteMarkdown(text2)).toBe(text2);
675+
});
676+
677+
it('should handle escaped dollar signs correctly', () => {
678+
const text = 'Price is \\$50 and _this is italic_';
679+
expect(parseIncompleteMarkdown(text)).toBe(text);
680+
681+
const text2 = 'Cost \\$100 with _incomplete';
682+
expect(parseIncompleteMarkdown(text2)).toBe('Cost \\$100 with _incomplete_');
683+
});
684+
685+
it('should handle mixed inline and block math', () => {
686+
const text = 'Inline $x_1$ and block $$y_2$$ math';
687+
expect(parseIncompleteMarkdown(text)).toBe(text);
688+
});
689+
690+
it('should not interfere with complete math blocks when adding underscores outside', () => {
691+
const text = '_italic start $x_1$ italic end_';
692+
expect(parseIncompleteMarkdown(text)).toBe(text);
693+
});
694+
});
695+
629696
describe('edge cases', () => {
630697
it('should handle text ending with formatting characters', () => {
631698
expect(parseIncompleteMarkdown('Text ending with *')).toBe(

packages/streamdown/lib/parse-incomplete-markdown.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,36 @@ const handleIncompleteSingleAsteriskItalic = (text: string): string => {
8181
return text;
8282
};
8383

84-
// Counts single underscores that are not part of double underscores and not escaped
84+
// Check if a position is within a math block (between $ or $$)
85+
const isWithinMathBlock = (text: string, position: number): boolean => {
86+
// Count dollar signs before this position
87+
let inInlineMath = false;
88+
let inBlockMath = false;
89+
90+
for (let i = 0; i < text.length && i < position; i++) {
91+
// Skip escaped dollar signs
92+
if (text[i] === '\\' && text[i + 1] === '$') {
93+
i++; // Skip the next character
94+
continue;
95+
}
96+
97+
if (text[i] === '$') {
98+
// Check for block math ($$)
99+
if (text[i + 1] === '$') {
100+
inBlockMath = !inBlockMath;
101+
i++; // Skip the second $
102+
inInlineMath = false; // Block math takes precedence
103+
} else if (!inBlockMath) {
104+
// Only toggle inline math if not in block math
105+
inInlineMath = !inInlineMath;
106+
}
107+
}
108+
}
109+
110+
return inInlineMath || inBlockMath;
111+
};
112+
113+
// Counts single underscores that are not part of double underscores, not escaped, and not in math blocks
85114
const countSingleUnderscores = (text: string): number => {
86115
return text.split('').reduce((acc, char, index) => {
87116
if (char === '_') {
@@ -91,6 +120,10 @@ const countSingleUnderscores = (text: string): number => {
91120
if (prevChar === '\\') {
92121
return acc;
93122
}
123+
// Skip if within math block
124+
if (isWithinMathBlock(text, index)) {
125+
return acc;
126+
}
94127
if (prevChar !== '_' && nextChar !== '_') {
95128
return acc + 1;
96129
}

0 commit comments

Comments
 (0)