Skip to content

fix: pair code regions by CommonMark rules in markdown preprocessing - #228

Merged
alecdotdev merged 1 commit into
sftwrdotdev:masterfrom
PathGao:fix/code-region-protection
Jul 27, 2026
Merged

fix: pair code regions by CommonMark rules in markdown preprocessing#228
alecdotdev merged 1 commit into
sftwrdotdev:masterfrom
PathGao:fix/code-region-protection

Conversation

@PathGao

@PathGao PathGao commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Fixes #227.

The five source-level rewrites (![[embeds]], [[#wikilinks]], ^block-ids, ==highlight==, ^[inline footnotes]) protected code regions with the alternation (?s)```.*?```|`.*?`|…, which cannot express CommonMark pairing. One 4-backtick inline sample — or any ~~~ fence, which the pattern did not know at all — desynchronized protection for the entire rest of the document, rewriting later inline code containing ![[x]] into <img> tags with nonlocal collateral damage (details and minimal repro in #227).

Change: a small scanner code_region_ranges() computes protected byte ranges by the CommonMark rules —

  • a fence (``` or ~~~, up to 3 leading spaces) closes only on a line-leading run of the same character at least as long as the opener (unclosed fence protects to EOF; backtick-fence info strings may not contain backticks);
  • an inline span opened by N backticks closes only on a run of exactly N; unpaired runs are literal.

Each rewrite pass then skips matches that start inside a protected range. Ranges are recomputed per pass since replacements shift offsets. No new dependencies; rewrite behavior outside code regions is unchanged.

Tests: 8 new unit tests covering both rupture modes (4-backtick desync, ~~~ fences), pairing rules (longer-run close, exact-N spans, unclosed fence), and unchanged rewrites outside code. cargo test: 10/10.

🤖 Generated with Claude Code

The five source-level rewrites (![[embeds]], [[#wikilinks]], ^block-ids,
==highlight==, ^[inline footnotes]) protected code regions with the regex
alternation ```.*?```|`.*?`, which cannot express CommonMark pairing:
fences close only on a line-leading run of the same character at least as
long as the opener, and a span of N backticks closes only on a run of
exactly N. A single 4-backtick inline sample (or any ~~~ fence, which the
pattern did not know at all) desynchronized protection for the entire rest
of the document — later inline code containing ![[x]] was rewritten into
<img> tags, with nonlocal collateral damage in unrelated blocks.

Replace the alternation with a small scanner (code_region_ranges) that
walks fences and backtick runs by the CommonMark rules, and skip any match
that starts inside a protected range.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alecdotdev
alecdotdev merged commit cc8ec36 into sftwrdotdev:master Jul 27, 2026
1 check passed
@PathGao
PathGao deleted the fix/code-region-protection branch July 29, 2026 07:22
PathGao added a commit that referenced this pull request Aug 7, 2026
…g imports (#506)

* chore: delete the code nothing reaches, and narrow the exports nothing imports

Four functions in MarkdownViewer.svelte have no caller. Three of them are
stale copies of render steps that moved elsewhere and kept evolving there:
processTaskItems and processBlockIds moved into markdown.ts in 09fa88f, and
the ==highlight== rewrite moved into the Rust renderer, where it later grew
code-span protection (#228, #371). Each migration re-pointed the call site
and left the old body behind, so the copies have been sitting there since
b46a283 looking like reusable helpers. getSplitTransition is plain dead.

Also removed: 24 exports nothing outside their own file imports, the
escapeHtmlText alias that only forwards to escapeHtml, an addFrontMatterList-
Item wrapper with no production caller, the toPlainRecord guard that
isFrontMatterMapping already makes unreachable, and FrontMatterField's
editableValue, which computes the same string as displayValue in every
branch with nothing to keep the two from drifting apart.

No behaviour change. The front matter tag test now calls the plural helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(tests): narrow the harness exports nothing imports

Same sweep as the previous commit, finished in scripts/. Twenty-two names in
keymapHarness.ts, renderProtocolDom.ts, renderProtocolFixtures.ts,
windowTagEditor.ts and scrollSyncBlockMapping.test.ts are only used inside
their own file, so the export widened the contract for nothing.
RenderFixtureName had no use anywhere, including its own file, and is gone.

tsconfig.json puts scripts/ inside `npm run check`, so a name another file
still imports would fail the type check rather than pass silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
PathGao added a commit that referenced this pull request Aug 7, 2026
… its offset parent (#515)

* chore: delete the code nothing reaches, and narrow the exports nothing imports

Four functions in MarkdownViewer.svelte have no caller. Three of them are
stale copies of render steps that moved elsewhere and kept evolving there:
processTaskItems and processBlockIds moved into markdown.ts in 09fa88f, and
the ==highlight== rewrite moved into the Rust renderer, where it later grew
code-span protection (#228, #371). Each migration re-pointed the call site
and left the old body behind, so the copies have been sitting there since
b46a283 looking like reusable helpers. getSplitTransition is plain dead.

Also removed: 24 exports nothing outside their own file imports, the
escapeHtmlText alias that only forwards to escapeHtml, an addFrontMatterList-
Item wrapper with no production caller, the toPlainRecord guard that
isFrontMatterMapping already makes unreachable, and FrontMatterField's
editableValue, which computes the same string as displayValue in every
branch with nothing to keep the two from drifting apart.

No behaviour change. The front matter tag test now calls the plural helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(tests): narrow the harness exports nothing imports

Same sweep as the previous commit, finished in scripts/. Twenty-two names in
keymapHarness.ts, renderProtocolDom.ts, renderProtocolFixtures.ts,
windowTagEditor.ts and scrollSyncBlockMapping.test.ts are only used inside
their own file, so the export widened the contract for nothing.
RenderFixtureName had no use anywhere, including its own file, and is gone.

tsconfig.json puts scripts/ inside `npm run check`, so a name another file
still imports would fail the type check rather than pass silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scroll-sync): measure a block's offset from the scroll container, not from its offset parent

The per-block mapping resolves a position by descending to the annotated
element that owns it and reading `offsetTop`. `offsetTop` is measured from
the element's OFFSET PARENT, which CSSOM defines as the nearest positioned
ancestor -- or the nearest `table`, `td` or `th`, positioned or not. The
preview contains both kinds, and comrak stamps a source range on every table
row and cell, so the descent goes inside a table and reads table-relative
offsets as document offsets.

Measured in Chrome over this app's own stylesheet: a row 945px down a
document reports `offsetTop = 1`, and a `<pre>` 1,085px down reports 0
because `renderRichContent` wraps every code block in a `position: relative`
shell for the copy button.

Both halves of #205's surviving report follow. A line inside a table maps to
a couple of hundred pixels, so the pane jumps to the top of the document; and
every offset in the table is far below every row's box, so the reverse
direction sticks on the last row until the reader is past the whole table.

`measureAnchorBox` sums the offset-parent chain and subtracts the
container's own. Subtracting rather than stopping at the container keeps it
exact whether or not the container is itself an offset parent.

A rendered Mermaid diagram now also inherits the source range of the `<pre>`
it replaces. Mermaid's SVG keeps nothing of the code block, so a diagram was
the one block in the preview that mapped to no source line at all -- not even
through a descendant -- and several hundred pixels of preview were attributed
to whatever block was nearest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code-region protection in markdown preprocessing is desynchronized by longer backtick runs (nonlocal corruption)

2 participants