Split the selector engine and the CSS value layer into their own modules - #87
Open
brainkim wants to merge 36 commits into
Open
Split the selector engine and the CSS value layer into their own modules#87brainkim wants to merge 36 commits into
brainkim wants to merge 36 commits into
Conversation
It parses and compiles selectors and matches them against the tree. It reads the tree through four link functions dom.ts exports and the state predicates that already existed as functions, so no symbol crosses the module boundary. dom.ts imports the five query functions back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
brainkim
force-pushed
the
split-modules
branch
from
September 7, 2026 17:19
78d8b05 to
15ff386
Compare
Every declaration in cssom.ts that touches no slot, no class and nothing from dom.ts moves: the value grammar, serialization, colors, shorthands, the property tables, and the media query, grid track, easing, calc and counter parsers, with the loops that fill their tables. cssom.ts keeps the CSSOM objects, the cascade, and computed and used values, and imports the value functions and tables it needs. layout and painter import the value types and color helpers from the new module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
dom.ts parsed IntersectionObserver's rootMargin with its own length regex; layout.ts read flex-grow, flex-shrink, order, z-index, alignment qualifiers and grid-auto-flow inline; painter.ts parsed font-weight, text-decoration-line, outline-width and the Canvas and Highlight system colors itself. Each now calls a function in cssvalues.ts: parseCSSNumber, parseCSSInteger, parseEdgeLengths, parseAlignmentKeyword, parseGridAutoFlow, parseFontWeight, parseTextDecorationLine, isCanvasColor and isHighlightColor, next to the parsers they duplicated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
dom.ts, cssom.ts and the value layer each declared their own copy of the HTML namespace. The six namespace URIs live once, beside the tables that name elements in them, and every module imports the ones it reads. Three types nothing imports lose their export. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Six functions that take a node moved with the value layer because they read the tree through the public DOM API alone. They are cascade helpers, not value parsing, and go back beside their callers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The NUL that keeps an unnamed @layer block unreachable by name was a raw byte in the source, which made grep treat the file as binary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The cascade reads 98 names from cssvalues.ts, a wide one-way interface like the one index.ts takes from dom.ts, so it imports the module the same way and each call names the layer it asks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
brainkim
force-pushed
the
split-modules
branch
from
September 7, 2026 18:05
789c5eb to
4e6d626
Compare
The value, stylesheet, selector and at-rule prelude node types are declared on the css-tree module, so cssvalues, cssom and selectors read them as CSSTree.* instead of exporting them to each other. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The CSSOM rule types, the resolved-value property lists, the inset tables, the list gutter, the descriptor names, the scope and layer sentinels and the paint-only list move to cssom. The grid placement and track sentinels move to layout. Computed styles reflect every indexed property, as CSSStyleProperties already does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Each set cssom tested membership in becomes a predicate, the shorthand maps become getLonghands and getShorthands, the initial-value lookup becomes getInitialValue, the keyframes name serializes in cssvalues, and a selector reading says whether it reaches siblings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The legacy pseudo-element set becomes isLegacyPseudoElement. The empty namespaces object goes away: an undefined namespaces means a sheet declared none, so the parameters are optional and cssom no longer compares against the sentinel to decide whether a rule carries namespaces. The attribute-name serializer takes a flag instead of a namespaces object it recognized by identity. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
LayoutNode and the flex, table, grid, block and margin-collapse algorithms over it know nothing about the DOM, and flex.test already drives them without one. layout.ts keeps the mapping from the DOM to boxes and imports the solver. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The selection's two composed points are made once and moved. Its own range is moved too while no script holds it, and replaced only after getRangeAt hands it out. The caret-column probe in modify asks layout for a caret rect at a point instead of measuring a throwaway range. The only internal ranges left to release are the four extraction subranges, which the spec defines as live, so the live-range counter and its test go. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The HTML Standard's IDL now annotates reflection, so scripts/generate-html-tables.ts reads it through @webref/idl and writes src/generated/htmltables.ts: each interface's reflected attributes, each tag's interface from @webref/elements, the ARIA mixin, and the event handler mixins. htmltables.ts keeps what the IDL leaves to prose: enumerated attributes, attributes with their own setter, and the supported token lists, and composes the tables dom.ts installs. The generated reflections add the ones the hand table lacked and correct three: lowsrc is a URL, headers is a string, and the marquee scroll attributes are unsigned longs with defaults. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The property index says which longhands inherit and which take a color, and the grammar index names the colors, line styles, easing keywords and generic families. scripts/generate-css-properties.ts now emits those, with the named colors' values from Bun.color, and cssvalues builds its sets from them. Inheritance now follows the index for every longhand instead of a hand list of 28, with the text decorations kept as this engine's own addition. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The values are the CSS Color 4 table. color-name is that table as a package, so the generator reads it there and runs under Node again. The generated values are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
No index carries the CSS Color 4 values, so the generator holds the table itself and checks it against the grammar's names on every run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
IntersectionObserver's rootMargin takes one to four lengths in px or a percentage. The old parser silently read anything else as 0 and the shared edge parser read it as a length, so the constructor now throws the SyntaxError the spec asks for. selectors.ts reads XML_NAMESPACE from htmltables instead of spelling the URI again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
LayoutNode's setters were Yoga's C API carried over. A restyle now builds a fresh Style with createStyle, fills it in, assigns it and invalidates once, so setStyles and the styling flag that batched the setters go, and a property the element stopped declaring falls back to its initial value instead of the last restyle's. The two DOM-side ties, measureContent and staticPositionFunc, are accessors. freeRecursive, a destructor ported into a garbage-collected language, goes too: every caller already unlinks the node and drops it from the maps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
reflection.ts holds what the IDL leaves to prose and composes the tables dom.ts installs; the namespace URIs and the forwarded body handlers, which are not reflection, move into dom.ts. The generated file is htmlidl.ts, named for its source. The solver lives at layout/solver.ts beside the DOM mapping that drives it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The node carried the pass that last styled it, the measurement key, and the paint extents the painter culls rows by, none of which the solver reads. layout.ts keeps them beside the node instead, and computes the extents after each solve. The flag that tells an algorithm to place its children rather than only size them is named placing, which is what the test already called it, and no longer shares a name with the node's performLayout method. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
dom.ts keeps dom/reflection.ts and dom/inspector.ts; cssom.ts keeps cssom/cssvalues.ts, cssom/selectors.ts and cssom/useragent.ts; layout.ts already kept layout/solver.ts. The peers, exchange, input, screen, painter and text, stay at the top. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
cssselectors.ts, htmlreflection.ts and layoutsolver.ts say what they select, reflect and solve without a directory to say it for them, as cssvalues.ts already did. inspector.ts and useragent.ts return to the top unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
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.
Two cuts along slot ownership, so no symbol crosses a module boundary, and a sweep so CSS text is parsed in one place.
src/internal/selectors.ts(2155 lines out of dom.ts). Selector parsing, compilation and matching. It reads the tree through four link functions dom.ts now exports,getParentNode,getFirstChildNode,getNextSiblingNodeandgetPreviousSiblingNode, and the state predicates that already existed as functions, now exported. Those predicates read slots, so they stay in dom.ts; the export list is the selector engine's state interface. dom.ts imports the five query functions back.src/internal/cssvalues.ts(about 5800 lines out of cssom.ts). Every declaration that touches no slot, no class and nothing from dom.ts: the value grammar, serialization, colors, shorthands, the property tables, and the media query, grid track, easing, calc and counter parsers, with the loops that fill their tables. cssom.ts keeps the CSSOM objects, the cascade, and computed and used values at about 7500 lines, and imports the value functions and tables it needs. The dependency runs one way. Six tree-walking helpers that passed the purity test on a technicality went back to cssom.ts.One parser. dom.ts parsed IntersectionObserver's rootMargin with its own regex; layout.ts read flex numbers, order, z-index, alignment qualifiers and grid-auto-flow inline; painter.ts parsed font-weight, text-decoration-line, outline-width and the Canvas and Highlight system colors itself. Each now calls a named function in cssvalues.ts.
Namespaces. The six namespace URIs live once in htmltables.ts, beside the tables that name elements in them, instead of three copies.
dom.ts goes from 33,501 to 31,416 lines and cssom.ts from 13,200 to about 7500.
Typecheck, lint, suite 1619/1619 on node and bun, 37/37 examples, lib.dom drift 148 exact.
🤖 Generated with Claude Code
https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D