Repository navigation
fix(context): keep code blocks whole when splitting long Markdown sections - #208
Conversation
…tions splitAtParagraphs cut a long section at every blank line, including blank lines inside fenced code, so a code block could end up half in one part and half in the next, each with one unpaired fence. Markdown sections are now split only at blank lines outside fences; AsciiDoc and reStructuredText keep the plain blank-line split, and blocks over the hard limit are still cut by splitOversizedContent.
Splitting before MDX cleanup stopped a line holding only a tag such as <Tip> from acting as a paragraph break, so MDX prose without blank lines around its tags came out as one section over the token budget. Treat such a line as blank outside a code block, as before. Expand tabs to the next 4-column stop when measuring fence indent, so a closing fence like " \t```" under "1. ```bash" closes the block.
🦋 Changeset detectedLatest commit: 196f826 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Changes requested — Keeps fenced code blocks together when oversized Markdown sections are split, and adds coverage for Markdown fence and MDX cases.
Changes behavior: no — This corrects the existing section-splitting bug rather than changing the package's intended promise. First review; no prior behavior finding to clear.
Must fix
- The description promises MDX tag bodies stay together, but they can still split (
packages/context/src/build.ts:410) - The fence rules now live in two separate scanners (
packages/context/src/build.ts:332)
1 note, no action needed
- Suggestion: Tilde-fenced sections still get marked as having no code The new scanner now keeps
~~~blocks together, buthasCodeBlockstill only recognizes backtick fences, so a preserved tilde-fenced section is stored withhasCode: false. That value was already false before this change, so it does not block the fix, but aligning the metadata with the newly recognized fence style would make it accurate. (packages/context/src/build.ts:354)
This codebase is run by Human0.
There was a problem hiding this comment.
Changes requested — Keeps recognized Markdown code fences together when long sections are split, preserving complete code blocks for document ingestion.
Changes behavior: no — This is a bug fix toward the existing behavior promised by Markdown parsing: code blocks should remain intact when long sections are divided. It changes outputs that were malformed, not the project's intended contract. This full look confirms the change is a fix toward the existing promise; no new commits have landed since the earlier review.
Must fix
- Use one shared Markdown fence rule (
packages/context/src/build.ts:384)
1 note, no action needed
- Suggestion: Keep code metadata consistent with tilde fences The new splitter preserves tilde-fenced blocks, but
hasCodeBlockstill recognizes only backticks, so those sections are stored withhasCode: false. It would be better to derive the flag from the same fence rules. (packages/context/src/build.ts:377)
This codebase is run by Human0.
The paragraph splitter in build.ts and the heading splitter in package-builder.ts each had their own fence scanner. Both now use fencedBlocks / fencedLines from markdown-fences.ts, and hasCode and the code-block cut in splitOversizedContent use the same rule instead of a backtick-only regex. For the heading splitter this means a fence behind a list marker (`1. ```bash`) or indented past 3 columns now opens a block, a closing fence must sit at most 3 columns right of its opening fence, and a fence that never closes is read as text instead of hiding every later heading in the file. hasCode is true when a line opens a fence, so `~~~` blocks, quoted fences and each part of a block cut at the hard limit count as code, and ```npm i``` inline code does not. AsciiDoc and reStructuredText pass tildes: false, because `~~~` underlines headings there.
|
The two splitters now share one fence rule in |
There was a problem hiding this comment.
Approved — Keeps fenced code intact when long Markdown sections are split, and uses the shared fence logic for heading detection and code flags. The change moves the splitter toward its existing promise rather than changing the intended contract.
Changes behavior: no — This corrects section splitting and hasCode when fenced code is present; it makes the implementation meet the existing intent rather than adding or changing a promised capability. Full review of the current base-to-head range confirms the behavior is a bug fix toward the existing promise; the earlier changes-requested findings are fixed in this head.
1 note, no action needed
- Suggestion: The tag remover still has its own fence check The MDX tag remover still carries a separate fence check, so tag cleanup and section splitting can drift on fence variants. Sharing the parser there would keep those paths aligned; this is a follow-up, not a blocker. (
packages/context/src/build.ts:252)
This codebase is run by Human0.
Problem
A section over 800 tokens is split by
splitAtParagraphs, which cuts at every blank line:Code blocks often have blank lines in them, so when the budget runs out partway through a block, the block is cut in two. One part ends with an opening fence and no close, so everything after it in that part reads as code. The next part starts in the middle of the code with no fence, so its code reads as prose and its closing fence opens a new block.
has_codeis wrong for both parts.Fastify's
Guides/Database.mdis one example. Its Mongo snippet has a blank line between two options, and the cut lands there. TheDatabasesection ends with:and
Database (part 2)starts with the rest of the code and no fence:fastify.register(require('@fastify/mongodb'), { // force to close the mongodb connection when app stopped // the default value is false forceClose: true, url: 'mongodb://mongo/mydb' })I built packages with
context add <dir> --path <docs>onmainand with this change, and counted sections with an odd number of fence lines:No line of text is lost. Every distinct line in each package on
mainis also in the package with the fix. Token totals move by about 0.1% or less, except Drizzle (+0.2%): whitespace, plus a few whole sections that are now identical to another section and are removed by the existing deduplication (Playwright'strace-viewer.mdshows the same C# block for two test frameworks). Drizzle grows because thecustom-types.mdxpages have a long TypeScript block with```examples in its doc comments. Onmainthat block was cut into dozens of small pieces at those fences, and the existing deduplication dropped the pieces that repeated. It is now cut by line only where the hard limit forces it, so the repeated comment text stays.What is left: code blocks over the 1,200-token hard limit, which
splitOversizedContentstill cuts by line, so each leaves two sections with an unpaired fence. That is all 16 in Python (8 blocks), both in Fastify (1 block), and 12 of the 17 in Drizzle (6 blocks). The other 5 in Drizzle are pages whose own fences do not pair up (cache.mdxhas a stray```,data-querying.mdxhas one inside a JSX comment).Fix
Markdown sections are now split at blank lines outside fenced code only (
splitMarkdownParagraphs). The fence rules follow CommonMark:```npm i```is prose.```jsline inside a```mdexample does not close it, and neither does```inside````.1. ```bash), because fences in list items are indented. A closing fence has no list marker and sits at most 3 columns right of the opening fence (tabs stop every 4 columns). A fence shown indented inside a code example therefore does not close the block around it.These rules live in one helper,
markdown-fences.ts. The paragraph splitter, the##heading splitter thatsplitForParsinguses on very large files,hasCodeBlockand the cut at code blocks insplitOversizedContentall use it, in place of the heading splitter's own scanner and a backtick-only regex. For the heading splitter this adds fences behind a list marker and the closing-indent limit, and a fence that never closes no longer hides every later heading in the file.hasCodeis now true when a line opens a fence, so~~~blocks, fences in a blockquote and each part of a block cut at the hard limit count as code, and```npm i``` installs itdoes not. AsciiDoc and reStructuredText turn tilde fences off, because~~~underlines headings there.The section is split first, and MDX tags are removed from each paragraph afterwards. Removing tags first can shift an indented fence to column 0 (
<Tabs>/<Tab>around an indented block), and then its closing fence no longer matches. Cleaning each paragraph is safe for generics in code (#173), because no paragraph holds part of a block. A line holding only MDX tags (<Tip>,</Note>) outside a code block is a break, like a blank line, so MDX prose without blank lines around its tags still splits. Tag bodies are not kept together: a blank line inside<Tip>splits its body like any other paragraph break.The split scans the text itself. Without MDX support, remark reads a fence directly under a JSX line such as
<Section>as part of an HTML block. I tried splitting at remark'scodenodes, and Drizzle still had 266 sections with an unpaired fence, against 17 with this change.AsciiDoc and reStructuredText keep the plain blank-line split. Their code syntax is different, and an RST subheading underlined with
~~~~~~~would otherwise read as a fence.Not changed
HARD_LIMIT_TOKENSare still cut by line insplitOversizedContent, with an unpaired fence in each part. Closing and reopening the fence at each cut would fix that. It is a separate function, so I left it for a follow-up if you want one. (Before that cut, a paragraph over the limit is now split at the edges of the blocks the fence rules find, not at every```.)hasCodecounts them), CRLF files (the old split did not handle them either), and AsciiDoc----/ RST::blocks with blank lines in them.MDX_TAG_OUTSIDE_CODEkeeps its own fence rules. Sharing one fence scanner between tag removal and splitting would be a larger refactor.Test
build.test.ts:<Tip>tags around it are still removed.```jsline, a tilde fence, a fence in a list item, a fence after1., a fence shown indented inside a block, the same tab-indented, a list item fence shown inside a block, and a fence after1.closed by ` \t```` (a tab stop, not 4 extra columns).<Tabs>/<Tab>stays whole.<Tip>/<Note>lines and no blank lines around them still splits within the budget, with the tags removed.<Tip>Run it twice.</Tip>) is removed from a split section, and its text is kept.~subheadings still splits at paragraphs, and its sections, short or split, havehasCode: false.hasCodeis true for a tilde fence, a fence in a blockquote, and each part of a block cut at the hard limit, and false for a line that opens with```npm i```. An AsciiDoc```fence still sets it.```tsblock with```jsexamples in its doc comments, so each block stays whole.package-builder.test.ts:##line inside a fence behind1.is not a heading.##from splitting.The existing test "keeps generics in a code block split across section parts" used a 12-line block that is now kept whole. It now uses 20 lines, which is over the hard limit, so the block is still split and the generics check still applies.
On
main19 of these tests fail, for exampleexpected [] to have a length of 1 but got +0(no section holds the whole block). With the fix they pass. The blockquote, AsciiDoc and RSThasCodetests and the tag-inside-a-paragraph test pass onmaintoo; they guard against this change taking those away.I broke each part of the fix in turn. 24 of 26 changes turned a test red:
```jsline, and the heading splitter's info-string test1., and##in a fence behind1.1.1.~subheadings~subheadingshasCodehasCodehasCodeignores the backtick info-string rule```npm i```hasCodefrom closed blocks onlyhasCode, parts of a block cut at the hard limitThe two that stayed green are the backwards pass that marks fences which cannot close, and remembering a fence that failed to close (below). Both change speed, not output, so no test covers them. The
<Tip>check used to catch a missing paragraph cleanup, but once a line holding only tags became a break, those<Tip>lines never reached the cleanup, so the tag-inside-a-paragraph test now covers it.The backwards pass is there for speed. Timed with
parseMarkdownon one section with an unclosed```json every fourth line:A fence that fails to close is also remembered, so a later fence with the same character, at least as long and no further right, is read as text without scanning again. With 6,000 info-string fences and one closer indented too far at the end,
parseMarkdowntakes 3.1 s, against 2.7 s onmain(most of it is remark).Validation
vitest runinpackages/context: 18 files, 393 tests pass.biome ci --error-on-warnings: clean.tsc --noEmit -p tsconfig.build.json: clean, inpackages/contextandpackages/registry.context addbuilds of nine doc sets onmainand on this branch..changeset/tidy-fences-hold.md(patch).