fix(site): keep scanning past long lines that are not headings

The heading scan stopped at any setext pair too long to parse, so a long
list item or quote followed by a thematic break hid a later heading. The
pair is now classified from the start of its first line, cut to fit the
parse bound, and the scan stops only when the pair is a heading.
This commit is contained in:
Michael Suchacz
2026-10-02 21:41:12 +00:00
parent b933c553a7
commit 87530695f3
2 changed files with 32 additions and 14 deletions
@@ -287,6 +287,16 @@ describe("getThinkingDisclosureDisplay", () => {
});
});
it.each(["- ", "> "])(
"scans past a long %j line that is not a heading",
(marker) => {
const body = `${marker}${"step ".repeat(100)}\n---\n\n`;
expect(
getThinkingDisclosureDisplay(`${body}## Next`, { isStreaming: false }),
).toEqual({ title: "Thinking about next", body });
},
);
it("ignores headings inside fenced code blocks", () => {
expect(
getThinkingDisclosureDisplay(
@@ -190,6 +190,8 @@ const getParagraphHeadingText = (
return heading;
};
// A first heading too long to parse ends the scan, so a later heading cannot
// stand in for it.
const getFirstHeading = (text: string): HeadingMatch | undefined => {
let activeFence: { character: "`" | "~"; length: number } | undefined;
let setextCandidate: LineRange | undefined;
@@ -213,8 +215,6 @@ const getFirstHeading = (text: string): HeadingMatch | undefined => {
if (/^ {0,3}#{1,6}(?:[ \t]|$)/.test(line)) {
const root = parseHeadingCandidate(line);
// A first heading too long to parse ends the scan, so a later
// heading cannot stand in for it.
if (!root) {
return undefined;
}
@@ -238,18 +238,26 @@ const getFirstHeading = (text: string): HeadingMatch | undefined => {
}
if (setextCandidate && /^ {0,3}(=+|-+)[ \t]*$/.test(line)) {
const root = parseHeadingCandidate(`${setextCandidate.line}\n${line}`);
if (!root) {
return undefined;
}
const [block] = root.children;
const heading = block?.type === "heading" ? getPlainText(block) : "";
if (heading) {
return {
text: heading,
start: setextCandidate.start,
end: lineRange.nextStart,
};
// A line's start decides whether an underline makes it a heading, so
// a line too long to parse is cut to classify the pair.
const candidate = sliceAtGraphemeBoundary(
setextCandidate.line,
PARSED_SOURCE_MAX_LENGTH - line.length - 1,
);
const [block] =
parseHeadingCandidate(`${candidate}\n${line}`)?.children ?? [];
if (block?.type === "heading") {
if (candidate !== setextCandidate.line) {
return undefined;
}
const heading = getPlainText(block);
if (heading) {
return {
text: heading,
start: setextCandidate.start,
end: lineRange.nextStart,
};
}
}
}