Repository navigation
[Codex review] fix: hold pending streaming markers and preserve empty lists #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "remend": patch | ||
| "streamdown": patch | ||
| --- | ||
|
|
||
| Preserve whitespace after empty list markers and add opt-in `pendingInlineMarkers` handling to hold ambiguous emphasis openers during streaming. Streamdown reveals remaining literal markers when `isAnimating` becomes false. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import remend from "../src"; | ||
|
|
||
| describe("pending inline markers", () => { | ||
| it.each([ | ||
| "*", | ||
| "**", | ||
| "_", | ||
| "__", | ||
| "~~", | ||
| ])("holds %s only when opted in", (marker) => { | ||
| const text = `my name is ${marker}`; | ||
| expect(remend(text)).toBe(text); | ||
| expect(remend(text, { pendingInlineMarkers: true })).toBe("my name is"); | ||
| expect(remend(text, { pendingInlineMarkers: false })).toBe(text); | ||
| }); | ||
|
|
||
| it("completes bold when the first content token arrives", () => { | ||
| expect(remend("my name is **A", { pendingInlineMarkers: true })).toBe( | ||
| "my name is **A**" | ||
| ); | ||
| expect(remend("my name is **Ada**", { pendingInlineMarkers: true })).toBe( | ||
| "my name is **Ada**" | ||
| ); | ||
| }); | ||
|
|
||
| it.each([ | ||
| "**Ada**", | ||
| "text **\n", | ||
| "snake_", | ||
| "2*", | ||
| "***", | ||
| "\\**", | ||
| "`literal **", | ||
| "```\n**", | ||
| "~~~\n**", | ||
| " **", | ||
| "$$ x **", | ||
| "[link](https://example.com/ **", | ||
| ])("preserves non-pending syntax in %j", (text) => { | ||
| expect(remend(text, { pendingInlineMarkers: true })).toBe(remend(text)); | ||
| }); | ||
|
|
||
| it.each([ | ||
| "- ", | ||
| "- item\n - ", | ||
| "+ ", | ||
| "* ", | ||
| "1. ", | ||
| "2) ", | ||
| ])("retains whitespace that makes %j a list marker", (text) => { | ||
| expect(remend(text)).toBe(text.includes("\n") ? `${text}\u200B` : text); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { isInsideCodeBlock } from "./code-block-utils"; | ||
| import { | ||
| isWithinHtmlTag, | ||
| isWithinLinkOrImageUrl, | ||
| isWithinMathBlock, | ||
| } from "./utils"; | ||
|
|
||
| const pendingMarker = /(?:^|\s)(\*{1,2}|_{1,2}|~~)$/; | ||
|
|
||
| const indentedCode = /^(?: {4}|\t)/; | ||
|
|
||
| export const holdPendingInlineMarkers = (text: string): string => { | ||
| const match = pendingMarker.exec(text); | ||
| if (!match || match.index + match[0].length !== text.length) { | ||
| return text; | ||
| } | ||
| const start = text.length - match[1].length; | ||
| const line = text.slice(text.lastIndexOf("\n", start - 1) + 1); | ||
| // Tilde fences and indented code are deliberately left alone until the | ||
| // shared code scanner can distinguish them from prose. | ||
| if ( | ||
| text.includes("~~~") || | ||
| indentedCode.test(line) || | ||
| isInsideCodeBlock(text, start) || | ||
| isWithinMathBlock(text, start) || | ||
| isWithinHtmlTag(text, start) || | ||
| isWithinLinkOrImageUrl(text, start) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When callers enable Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfinished destinations are now preserved independently of link/image repair, including nested and escaped parentheses. The new regression cases pass. |
||
| ) { | ||
| return text; | ||
| } | ||
| return text.slice(0, start); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { render } from "@testing-library/react"; | ||
| import { describe, expect, it } from "vitest"; | ||
| import { Streamdown } from "../index"; | ||
|
|
||
| describe("pending inline markers", () => { | ||
| it("hides pending bold markers while streaming and flushes literal markers on completion", () => { | ||
| const { container, rerender } = render( | ||
| <Streamdown isAnimating remend={{ pendingInlineMarkers: true }}> | ||
| {"my name is **"} | ||
| </Streamdown> | ||
| ); | ||
| expect(container.textContent).toBe("my name is"); | ||
| rerender( | ||
| <Streamdown isAnimating remend={{ pendingInlineMarkers: true }}> | ||
| {"my name is **Ada"} | ||
| </Streamdown> | ||
| ); | ||
| expect( | ||
| container.querySelector('[data-streamdown="strong"]')?.textContent | ||
| ).toBe("Ada"); | ||
| rerender( | ||
| <Streamdown isAnimating={false} remend={{ pendingInlineMarkers: true }}> | ||
| {"my name is **"} | ||
| </Streamdown> | ||
| ); | ||
| expect(container.textContent).toBe("my name is **"); | ||
| }); | ||
|
|
||
| it("does not reinterpret an empty nested list marker as literal text", () => { | ||
| const { container } = render(<Streamdown>{"- item\n - "}</Streamdown>); | ||
| expect(container.textContent).not.toContain("-"); | ||
| expect(container.querySelectorAll("li")).toHaveLength(2); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the document contains
~~~anywhere, including a fully closed fence earlier in the document, this condition disables pending-marker handling for all later prose. For example, a closed tilde code block followed bymy name is **leaves the raw**visible while streaming, defeating the option for any document that previously used a tilde fence; determine whether the trailing marker is actually inside the fence instead of checking the whole document for the substring.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced the document-wide tilde check with active fence tracking. Closed, open, nested-length, quoted, and list fences are covered by the passing tests.