Skip to content
Open
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
16 changes: 15 additions & 1 deletion packages/streamdown/__tests__/preprocess-custom-tags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,25 @@ describe("preprocessCustomTags", () => {
expect(preprocessCustomTags(md, ["custom"])).toBe(md);
});

it("should not modify content without blank lines", () => {
it("should not modify content without blank lines (no newline)", () => {
// Single-line content without any newlines is fine as inline HTML
const md = "<custom>Hello World</custom>";
expect(preprocessCustomTags(md, ["custom"])).toBe(md);
});

it("should restructure inline content with a newline followed by ATX heading", () => {
// A heading inside inline content would prematurely terminate the custom tag
const md = "<ai-thinking> hi\n # yes</ai-thinking>";
const result = preprocessCustomTags(md, ["ai-thinking"]);
expect(result).toBe("<ai-thinking>\n hi\n # yes\n</ai-thinking>\n\n");
});

it("should restructure inline content with any newline (prevents block element splitting)", () => {
const md = "<custom>hello\nworld</custom>";
const result = preprocessCustomTags(md, ["custom"]);
expect(result).toBe("<custom>\nhello\nworld\n</custom>\n\n");
});

it("should not modify tags where content already starts on own line without blank lines", () => {
const md = "<custom>\nHello\n</custom>";
expect(preprocessCustomTags(md, ["custom"])).toBe(md);
Expand Down
14 changes: 10 additions & 4 deletions packages/streamdown/lib/preprocess-custom-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ export const preprocessCustomTags = (
result = result.replace(
pattern,
(_match, open: string, content: string, close: string) => {
// Only restructure when content contains blank lines that would
// split the HTML block. Tags without blank lines work fine as
// inline HTML and don't need restructuring.
if (!content.includes("\n\n")) {
// Restructure when:
// 1. Content has blank lines (\n\n) — these would split the HTML block, or
// 2. Content starts inline (not preceded by \n) and contains any newline —
// a newline inside inline content can introduce block-level elements like
// ATX headings (# title) that the parser would treat as a new block,
// prematurely terminating the custom tag scope.
const hasBlankLines = content.includes("\n\n");
const isInlineContent = !content.startsWith("\n");
const hasNewline = content.includes("\n");
if (!(hasBlankLines || (isInlineContent && hasNewline))) {
return open + content + close;
}

Expand Down
Loading