Ranges and boundary points¶
A Range is a pair of boundary points over one tree, and every operation on it is boundary-point
arithmetic. A boundary point is a (container, offset) pair: the offset counts the container’s children, or its code
points when the container is character data (a Text, Comment, or
CData). Using code points, not UTF-16 code units, is the one deliberate departure from the DOM
Living Standard: it makes a text-node offset line up with the same index a Python string slices at, which is the model a
Python caller expects.
Every derived answer – collapsed, common_ancestor_container, the
comparisons, and the content operations – rests on one primitive: given two boundary points that share a root, which
comes first in tree order. That comparison is the spec’s four-case rule (equal container compares offsets; otherwise the
deeper or following point is resolved against its ancestor’s child index), and it runs in C by walking parent chains
rather than materializing paths. Because the setters keep start <= end – a set_start() past
the current end, or into a different tree, drags the end onto the new point – the two boundaries always share a root,
so every later comparison is well defined.
The content operations are the WHATWG extract, clone, and delete algorithms, split into the same three regions: the
partially contained children at each end (a node that is an inclusive ancestor of exactly one boundary), the fully
contained children in the middle, and, when both boundaries sit in one character-data node, the single spliced run.
Extract and clone share their structure and differ only in destructiveness: clone deep-copies the contained children and
leaves the tree alone, while extract moves the same nodes into the returned fragment and splices the boundary text out
of the source, then collapses the range to the point the removed content vacated. delete_contents is extract with
the fragment discarded, so the two never drift apart. The straddling ends recurse into a freshly built sub-range, so a
boundary many levels deep rebuilds only the sliver in range, not the whole subtree. The returned fragment is a
document-fragment node minted in the range’s own tree, which is why an extract is a true move – the nodes never cross
an arena boundary – and why a clone reuses the tested deep-copy primitive within that one arena. Every operation takes
the per-tree critical section, so a content edit is atomic under the free-threaded build the same way the other
structural mutations are.
Two scope choices bound this. First, a Range is only as live as its own operations: its content methods move its
boundaries per the spec, but an edit made through another API – appending a sibling, removing a subtree – does not
shift a range you happen to be holding, because the tree’s mutators do not track live ranges. A range is a cursor you
drive, not an observer that follows edits made elsewhere. Second, StaticRange is the immutable
counterpart the spec defines for exactly that observer case: it stores the four boundary values verbatim with no
ordering, bounds, or same-root guarantee, so it is a cheap snapshot to hand around rather than a handle to operate
through.
Every boundary, comparison, and content operation is validated against jsdom, which passes the WPT dom/ranges suite:
tests/conformance/test_dom_jsdom_differential.py replays the same sequences through both libraries over shared
trees and requires byte- and structure-exact agreement, which holds on every case.