refactor(parser): TransformContext with explicit lookahead#82
Merged
Conversation
Replaces the implicit `nextNode` parameter and post-hoc `i++` patterns in `processNodeList` with a first-class `TransformContext` that every transform receives. Cross-sibling peek and consume are now explicit: - `ctx.peekNext()` / `ctx.consumeNext()` for the dropdown-options lookahead in `transformParagraph` (the only cross-sibling peek in the parser). - `ctx.transformChild()` for isolated child recursion (no leak of parent siblings into descendants). - `ctx.transformChildren()` for sibling-aware container children. The previous post-hoc shape rule in `processNodeList` (consume the trailing list when the result is `select` or a `container`-with-`select`) is gone — the consume now lives next to the peek inside `transformParagraph`, which is where the read happens. Also adds: - `packages/core/src/parser/_context.ts` — context interface + `makeContext` / `makeIsolatedContext` factories. - `packages/core/tests/lib/transform-test-helpers.ts` — `mdastFor()` and `makeTestContext()` for isolated parse tests. - `packages/core/tests/parser/dropdown-lookahead.test.ts` — first per-node parse test, pinning the dropdown lookahead contract. Pure refactor: 1,180 existing tests stay green, zero snapshot drift, all 17 `KNOWN_FAILURES` continue to fail. See `.github/dev-docs/parser-contract-plan.md` for the full plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
teezeit
added a commit
that referenced
this pull request
May 1, 2026
…espace (#83) ## Summary Fixes 4 bugs in `remark-containers.ts` (2 targeted + 2 incidental) and recovers content in 2 docs fixtures that was previously silently dropped. **Targeted bugs:** - `::: card ` (trailing spaces on opener) — remark emits a hard \`break\` node, splitting the opener paragraph into multi-child. \`collectContainer\` only handled \`children.length === 1\`, so post-opener content was silently dropped → empty container. - `::: ` (trailing space on closer) — CASE 1 recognized the closer line via \`.trim()\` but silently discarded any text after the closer. **Cascade — incidentally fixed:** - \`docs/page-layouts/sidebar-layout\` and \`sidebar-with-sections\` had \`.expected-fail.invariants.ts\` for the same root cause (multi-child opener paragraph). Both invariants flipped to live. **Content recovery (no prior expected-fail invariants):** - \`docs/tabs/basic-tabs\` — \`**Free** — basic plan\` / \`**Pro** — $12/mo\` lines no longer dropped. - \`docs/navigation/sidebar-nav\` — \`**App**\` brand line no longer dropped. ## What changed **`packages/core/src/parser/remark-containers.ts`** 1. \`collectContainer\` now returns \`{ node, trailing?, nextIndex }\`; \`processNodes\` and the two recursive callsites push trailing siblings after the container. 2. CASE 3 has a new \`else if\` branch for multi-child openers — extracts the post-opener children (skipping a leading hard \`break\` if present) into a synthetic content paragraph. **Tests** - New \`tests/parser/remark-containers-whitespace.test.ts\` — 3 isolated parse tests pinning the opener/closer whitespace contract (uses the parser-contract test helpers from PR #82). - 4 fixture invariants flipped from \`.expected-fail\` to live; \`KNOWN_FAILURES\` in \`review-gate.test.ts\` trimmed. - 6 \`REVIEW_LOG.md\` rows ⏳→✅ (HTML output verified by diff inspection — see note below). ## Known caveat The React/Tailwind snapshots for \`basic-tabs\` and \`sidebar-nav\` reveal a **pre-existing** latent bug: \`transformParagraph\` builds inline \`**strong**\`/\`*em*\` as HTML string literals (lines 776-786 of transformer.ts), which render correctly in HTML but escape to \`<strong>\` in React/JSX. This pattern already exists in 10+ other fixture snapshots — not introduced here. Separate PR will land a proper inline-children fix. ## Test plan - [x] \`pnpm typecheck\` — clean - [x] \`pnpm test\` — **1,187 passed** (1,184 baseline + 3 new isolated remark-containers tests) - [x] \`pnpm review:refresh\` — 6 fixtures flipped to ⏳ then verified ✅ (HTML output is correct; React/Tailwind escape is a separate latent bug) - [x] All 4 expected-fail → live invariants pass; no other expected-fail tests changed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
teezeit
added a commit
that referenced
this pull request
May 2, 2026
…tabs parsing (#87) ## Summary After PRs #82-86 closed all the closer/blank-line parser bugs, the trailing blank line that was previously required before every \`:::\` closer is no longer load-bearing. This PR sweeps it out across all docs and example fixtures (~50 files, ~500 lines removed) and fixes one cascaded-tabs parser bug uncovered by the sweep. ## Parser fix — cascaded tabs Tabs with **no blank line** between adjacent \`::: tab\` containers (e.g. the \`settings-pattern\` fixture in the docs) was producing a single tab and dropping the rest. **Root cause:** the \`text-before-nested-opener\` branch in \`remark-containers.ts\` was matching on synthetic paragraphs whose **first line** was itself an opener (\`::: tab Security\\n…\\n:::\\n::: tab Notifications\`) and pushing the inner content as plain text instead of recursing into the nested container. That branch's intent is "paragraph that starts with prose, then has a nested opener inside" — not "paragraph that's already an opener." **Fix:** one-line guard — skip \`text-before-nested-opener\` when the paragraph itself parses as an opener. The next branch (nested container opener) handles that case correctly via recursion. ## Doc sweep Removed the trailing blank line before \`:::\` closers in every doc and example fixture. Pattern: \`/\\n\\n:::(?=\\n|$)/\` → \`\\n:::\`. Net diff: −922 lines, +416 lines (∆ ~−500 useful blank lines + the parser change). ## Tests - [x] 1,206 → 1,207 passing. - [x] \`settings-pattern\` fixture flips ❌ → ✅. \`KNOWN_FAILURES\` stays empty. - [x] All 50 doc/example markdown sweeps verified — no fixture regressions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pure refactor: replaces the implicit
nextNodeparameter and post-hoci++patterns inprocessNodeListwith a first-classTransformContextthat every transform receives. Cross-sibling peek/consume is now explicit, auditable, and unit-testable in isolation.This is the parser-contract step from
.github/dev-docs/parser-contract-plan.md(PR #81). It unblocks the bug-fix and syntax-change PRs that follow.What changed
New:
packages/core/src/parser/_context.tsTransformContextinterface —peekNext(),consumeNext(),transformChild(),transformChildren(), plus shared helpers (parseAttributes,extractTextContent,isHtmlCommentNode).makeContext(siblings, startIndex, options, deps)— sibling-aware factory used byprocessNodeList.makeIsolatedContext(options, deps)— for transforms that walk a single child without cross-sibling lookahead.Modified:
packages/core/src/parser/transformer.ts(node, options [, nextNode])to(node, ctx).processNodeListbuilds aTransformContextper iteration and advances by readinghandle.getCursor().if (transformed.type === 'select' && nextNode...) i++;) is gone — consumption now lives next to the peek insidetransformParagraph, where the read actually happens.ctx.consumeNext()themselves.__transformerInternalsexport (test-only) so isolated parse tests can dispatch throughtransformNodedirectly.New:
packages/core/tests/lib/transform-test-helpers.tsmdastFor(markdown)— runs the same MDAST stage thatparse()runs, stops before the wiremd transform.makeTestContext(siblings, options)— sibling-aware test context.runTransform(mdast, siblings, options)— firestransformNodeand returns{ node, cursor }so tests can assert on lookahead consumption.New:
packages/core/tests/parser/dropdown-lookahead.test.tsWhat this PR is NOT
transformer.tsstays in one file. Each function gets the newctxparameter; that's it.KNOWN_FAILUREScontinue to fail.((pill)),:::columns, etc.) lands in separate PRs.Test plan
pnpm typecheck— cleanpnpm test— 1,184 passed (1,180 baseline + 4 new isolated parse tests)pnpm review:refresh— "Nothing matched — log unchanged" (zero snapshot drift).expected-fail.invariants.tstests continue failing as expected🤖 Generated with Claude Code