Selectors & Selector Matching
Building a Web Browser Engine: Parsing & the DOM
Chapter 5 · Selectors & Selector Matching
Chapter 4 left every Rule's own selectors as raw strings — "div", ".highlight", "div p" — real text, but not yet anything that can be checked against a DOM node. This chapter builds both halves: a real selector parser, and a matcher that decides, for one selector and one Element, whether they apply to each other at all.
Simple Selectors and the Selector They Combine Into
A single space-separated piece of a selector — div, .card, p#main.highlight — is one SimpleSelector, scanned for a leading tag name followed by any number of .class and #id markers, in whatever order they appear.
div with class="card" matches the selector div and fails to match p. An element with class="foo highlight bar" matches .highlight — the class attribute is space-separated, and .highlight only needs to be one of the classes present, not the whole string. An element with id="main" matches #main and nothing else with a different id.
div.card matches a div that also carries class="card", but a plain div with no class at all does not match. A full three-way compound, p#main.highlight, correctly matches a <p id="main" class="highlight"> and correctly rejects a <span id="main" class="highlight"> — identical id and class, wrong tag, no match.
A Real Gap, Found the Moment Descendant Selectors Are Needed
div p means "a p with a div somewhere above it" — answering that requires walking upward from a node. Chapter 3's own Element/TextNode/CommentNode classes only ever store children. Nothing before this chapter ever needed to go the other direction.
.parent on a freshly-parsed Element — one that never had anything extra done to it — raises AttributeError: 'Element' object has no attribute 'parent'. This isn't a bug in Chapter 3; a tree built purely for top-down parsing genuinely never needed an upward pointer until a selector like div p came along and asked for one.
One walk over the tree, once, before any matching starts, and every node gains a real .parent reference — the same shape a real browser's own Node.parentNode takes.
Matching a Descendant Selector: Search Upward, Part by Part
div p matches a p nested inside a div (through any number of levels), and correctly fails to match a p sitting at the top level with no div anywhere above it.
div section p matches a p nested as div > section > article > p — with a completely unrelated article sitting between section and p, an element the selector never mentions at all. Each part of the selector only has to find some matching ancestor, not the immediate parent — exactly how real CSS descendant combinators work. The same selector correctly fails to match a p sitting directly inside a div with no section anywhere in between.
An Honest Limitation: Nothing Here Enforces id Uniqueness
div and a span — both carrying id="dup" both correctly match #dup, individually. Real HTML documents are supposed to never repeat an id, but nothing in this chapter's own parser or matcher checks for or enforces that — matches_simple only ever asks "does this element's own id attribute equal the one in the selector," with no awareness of any other element in the document at all. A genuinely malformed document with a duplicate id won't raise an error here; it'll just mean an id selector quietly matches more than one thing, which is exactly what a real browser does too when handed the same invalid markup.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Chapter 3's tree had no parent pointer until a descendant selector needed one | A direct parallel to Chapter 9's own origin/base finding in the compiler project — a data structure built correctly for its original purpose still needing an honest, later revision once a new consumer's own requirements arrive |
| Gap-tolerant ancestor search for multi-part descendant selectors | Chapter 6's own specificity and cascade — every rule whose selector matches at all becomes a real candidate for a given element, regardless of how deeply nested the match happened to be |
SimpleSelector/Selector built here, from Chapter 4's own raw strings | Chapter 8's own style tree, which will need to ask "does this rule apply to this element" for every rule in the stylesheet, against every element in the DOM — the exact question this chapter's own matches_selector answers |
| id uniqueness left honestly unenforced | The same kind of stated scope boundary Chapter 3 drew around <li>'s own missing implied-closing rule — a real limitation, named directly rather than silently assumed away |
Hands-On Exercises
Build a tree shaped div > section > article > p (four levels, each a direct child of the one before it) and match it against the selector "div section p" using this chapter's own matches_selector. Confirm it matches despite article never appearing in the selector, then build a second tree — div > p directly, no section anywhere — and confirm the same selector correctly fails to match.
Build two sibling elements — a div and a span — both with id="dup", and match both against the selector "#dup" using this chapter's own matches_selector. Confirm both report a match, and explain specifically why matches_simple has no way to detect or reject this, even in principle, without being handed information about the rest of the document it currently never receives.
Build a <p id="main" class="highlight"> and match it against the compound selector "p#main.highlight" using this chapter's own matches_selector. Then build a <span id="main" class="highlight"> — identical id and class, different tag — and match it against the same selector. Confirm the two results differ, and trace through matches_simple to identify exactly which check is responsible for the rejection.
Chapter 5 Quick Reference
- Two structures:
SimpleSelector(tag/id/classes, one space-separated piece) andSelector(an ordered list of them — ancestors, target last) - Verified: tag, class (against a multi-class attribute), id, and compound (tag+id+class together) selectors all match correctly on their own
- Real gap found and fixed: Chapter 3's own DOM tree had no
.parentreference at all —assign_parents()adds it, once, before any matching begins - Verified: descendant matching is gap-tolerant — an unrelated ancestor sitting between two selector parts doesn't break the match, exactly like real CSS
- Verified — an honest limitation: id uniqueness is never enforced; two elements sharing the same id both "match" an id selector
- Next chapter: Specificity & the Cascade — deciding which rule wins when more than one selector matches the same element