Skip to content

MD-3032 Fix replaceRange when range components share elements, and cut nested containers by their own ranges - #48

Draft
benney wants to merge 4 commits into
mainfrom
feature/md-3032-mos-proxy-fix-replacerange-when-range-components-share-elements
Draft

benney wants to merge 4 commits into
mainfrom
feature/md-3032-mos-proxy-fix-replacerange-when-range-components-share-elements

Conversation

@benney

@benney benney commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Two replaceRange changes, plus whole-page tests for range truncation and component content.

Range components sharing elements (bug, since 1.8.1):

  • Only the last onEndTag callback registered on an element runs, so one range component's callback replaced another's. The marker pass then lost count of open containers and skipped every later container of that component, while the replacement pass still counted them. Later containers were cut from the wrong markers.
  • One handler on every range container and descendant now replaces each component's ${container} * handler. It visits each element for the ranges whose container holds it, and the range handlers share one end tag callback per element.
  • It is registered with the first range component, so components ahead of the ranges still see elements before a range removes them. A component registered between two range components now sees the later range's cut first, the same as when the wrapper cuts the page.

Nested containers cut by their own ranges (replaces "outermost wins" from 1.8.1):

  • A container nested in another container of the same range is cut by its own range. The outer container's cut goes around it, keeps the elements holding it, and ignores markers inside it. A marker that is itself a nested container belongs to the next container out. A void element matching the container selector is cut like any other element.
  • The wrapper keeps a stack of open containers per range. A nested container that is a direct child of the outer container's cut closes the cut before it and reopens it after, without a second replacement. A marker directly in a nested container now takes the wrapper path instead of falling back.
  • These still fall back to cutting element by element: a nested container deeper inside the outer container's cut, one in raw text or MathML, one on a page with a nested start marker, or one closed by another element's end tag. Telling the last case apart needs the end tag's name, so onEndTag callbacks now receive it as RewriterEndTag. Native rewriters already pass it.

Tests:

  • test/stages/truncation.spec.ts covers range truncation on whole pages, running each case on both the wrapper and the element-by-element path. test/stages/componentContent.spec.ts covers component content transforms (before, after, prepend, append, remove, MOS elements, ignored selectors).
  • The random-page test now leaves .c elements unclosed too.

Performance (workerd, 1 MB and 5 MB pages):

  • Pages without nested same-selector containers take the same path as before, at the same cost.
  • On the element-by-element path, overlapping ranges are about 14% faster.
  • A nested container deeper inside the outer container's cut now falls back to the element path, about 60x slower than before. Before, the inner container was simply cut.

Patch and minor changesets.

… elements

Only the last end tag callback registered on an element runs. When the
containers or markers of two range components overlapped, one component's
callback replaced the other's, so a container's depth never went back to 0
and every later container of that component was skipped by the marker pass
while the replacement pass still counted it. The two passes then disagreed
on which markers belonged to which range.

One handler on every range container and descendant now starts each element,
visits it for each range whose container it is in (in place of a
`${container} *` handler per component), and lets the range handlers share
one end tag callback for it. It is registered with the first range
component, so components ahead of the ranges keep seeing elements before a
range removes them. A component registered between two range components
now sees the later range's cut first, as it does when the wrapper cuts the
page.
@changeset-bot

changeset-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: caf3838

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@monetizationos/proxy Minor

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

A container nested in another container of the same range is now cut by
its own range. The outer container's cut goes around it, keeps what holds
it, and ignores the markers inside it, which belong to the innermost
container. The Cloudflare proxy's nested `.c2` truncation case is back to
its original output.

Element by element, both passes keep a stack of open containers in place
of the depth counter, each element goes to the innermost container's
range, and a range records where its nested containers start so its cut
keeps them and the elements holding them. A void container has nothing to
cut, so the container it is in still cuts it.

The wrapper keeps the same stack per range. A nested container that is a
direct child of the outer container's cut closes the cut before it and
reopens it after, without a second replacement. A marker is the
innermost container's, so a marker directly in a nested container no
longer sends the page element by element; a marker on a nested container
is the next container out's. A nested container deeper in the cut, in raw
text or MathML, on a page with a nested start marker, or closed by
another element's end tag still goes element by element. The last needs
the end tag's name, so `onEndTag` callbacks now receive it as
`RewriterEndTag`; native rewriters already pass it.
@benney benney changed the title MD-3032 Fix replaceRange when range components share elements MD-3032 Fix replaceRange when range components share elements, and cut nested containers by their own ranges Nov 6, 2026
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.

1 participant