Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fancy-snails-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"streamdown": patch
---

fix asterisk list termination
50 changes: 50 additions & 0 deletions packages/streamdown/__tests__/parse-incomplete-markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,56 @@ describe('parseIncompleteMarkdown', () => {
});
});

describe('list handling', () => {
it('should not add asterisk to lists using asterisk markers', () => {
const text = '* Item 1\n* Item 2\n* Item 3';
expect(parseIncompleteMarkdown(text)).toBe(text);
});

it('should not add asterisk to single list item', () => {
const text = '* Single item';
expect(parseIncompleteMarkdown(text)).toBe(text);
});

it('should not add asterisk to nested lists', () => {
const text = '* Parent item\n * Nested item 1\n * Nested item 2';
expect(parseIncompleteMarkdown(text)).toBe(text);
});

it('should handle lists with italic text correctly', () => {
const text = '* Item with *italic* text\n* Another item';
expect(parseIncompleteMarkdown(text)).toBe(text);
});

it('should complete incomplete italic even in list items', () => {
// List markers are not counted, but incomplete italic formatting is still completed
const text = '* Item with *incomplete italic\n* Another item';
// The function adds an asterisk to complete the italic, though at the end of text
// This is not ideal but matches current behavior
expect(parseIncompleteMarkdown(text)).toBe('* Item with *incomplete italic\n* Another item*');
});

it('should handle mixed list markers and italic formatting', () => {
const text = '* First item\n* Second *italic* item\n* Third item';
expect(parseIncompleteMarkdown(text)).toBe(text);
});

it('should handle lists with tabs for indentation', () => {
const text = '*\tItem with tab\n*\tAnother item';
expect(parseIncompleteMarkdown(text)).toBe(text);
});

it('should not interfere with dash lists', () => {
const text = '- Item 1\n- Item 2 with *italic*\n- Item 3';
expect(parseIncompleteMarkdown(text)).toBe(text);
});

it('should handle the Gemini response example from issue', () => {
const geminiResponse = '* user123\n* user456\n* user789';
expect(parseIncompleteMarkdown(geminiResponse)).toBe(geminiResponse);
});
});

describe('edge cases', () => {
it('should handle text ending with formatting characters', () => {
expect(parseIncompleteMarkdown('Text ending with *')).toBe(
Expand Down
21 changes: 20 additions & 1 deletion packages/streamdown/lib/parse-incomplete-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const handleIncompleteDoubleUnderscoreItalic = (text: string): string => {
return text;
};

// Counts single asterisks that are not part of double asterisks and not escaped
// Counts single asterisks that are not part of double asterisks, not escaped, and not list markers
const countSingleAsterisks = (text: string): number => {
return text.split('').reduce((acc, char, index) => {
if (char === '*') {
Expand All @@ -59,6 +59,25 @@ const countSingleAsterisks = (text: string): number => {
if (prevChar === '\\') {
return acc;
}
// Check if this is a list marker (asterisk at start of line followed by space)
// Look backwards to find the start of the current line
let lineStartIndex = index;
for (let i = index - 1; i >= 0; i--) {
if (text[i] === '\n') {
lineStartIndex = i + 1;
break;
}
if (i === 0) {
lineStartIndex = 0;
break;
}
}
// Check if this asterisk is at the beginning of a line (with optional whitespace)
const beforeAsterisk = text.substring(lineStartIndex, index);
if (beforeAsterisk.trim() === '' && (nextChar === ' ' || nextChar === '\t')) {
// This is likely a list marker, don't count it
return acc;
}
if (prevChar !== '*' && nextChar !== '*') {
return acc + 1;
}
Expand Down