Capstone — Parsing a Real HTML+CSS Document into a Style Tree
Building a Web Browser Engine: Parsing & the DOM
Chapter 10 · Capstone — Parsing a Real HTML+CSS Document into a Style Tree
Every chapter so far has tested one piece in isolation, against hand-built objects standing in for whatever the previous chapter was supposed to hand it. This chapter does something none of them did: run a real HTML document and a real stylesheet through the actual tokenizer, parser, CSS parser, selector matcher, cascade, and style-tree builder — the same functions, wired together, end to end — and check the result against what CSS's own rules predict by hand.
A Real Bug, Found Only by Actually Integrating Everything
Wiring the real parse_css() output directly into the real cascade()-style code surfaced something no single chapter's own tests ever could.
Declaration class stores its field as .property: Declaration(property, value). Every cascade-related chapter since Chapter 6 was written and tested against its own local stand-in for Declaration — and every one of those stand-ins, independently, used .name instead. Handing a real Declaration built by this chapter's own parse_css() into any of those earlier chapters' own final loops — result[decl.name] = decl.value — raises a genuine AttributeError: 'Declaration' object has no attribute 'name'. Nothing in Chapters 6 through 9 was ever wrong in isolation — every one of their own tests, built against their own consistent local stand-ins, passed cleanly. The mismatch was only ever between chapters, and only a real end-to-end run could have caught it.
The fix is a one-word correction, applied everywhere the cascade logic reads a declaration's own property name: decl.property, matching Chapter 4's real, original field — not decl.name.
The Real Document
Run through tokenize() → parse() → assign_parents() → parse_css() → build_style_tree_full() (the Chapter 8 style-tree builder, upgraded to call Chapter 9's cascade_with_origin() against both this author stylesheet and the Chapter 9 UA_STYLESHEET together) — the real pipeline, nothing hand-constructed.
Verifying the Result, Node by Node
<div id="page">: display: block (Chapter 9's UA rule — nothing in the author stylesheet even mentions display on this element) and color: navy (the author's own #page rule).
<h1>: display: block, font-weight: bold, font-size: 2em — all three from the UA stylesheet, since the author stylesheet has no h1 rule at all — plus color: navy, inherited straight from #page (Chapter 7's own mechanism), despite no rule anywhere directly targeting h1's own color.
<p class="intro">: margin: 24px 0 — the author's own p rule — not the UA stylesheet's 16px 0. Both selectors are the identical bare tag p, specificity (0,0,1) on both sides — a genuine tie. Chapter 9's origin tier decides it: author always outranks UA, even without needing higher specificity. font-size: 20px comes from the author's own .intro rule (nothing in the UA sheet touches font-size on p at all), and color: navy is inherited from #page, two levels up.
<b>, nested inside p.intro: display: inline and font-weight: bold, both from the UA stylesheet's own b rule — plus font-size: 20px and color: navy, neither set by any rule matching <b> directly, both inherited transitively from p.intro, which itself only has font-size from its own .intro rule and color inherited yet again from #page.
p.intro gives ['This is ', 'bold', ' and normal text.'] — all three pieces present, correctly ordered, correctly nested. The "bold" text node's own style shows color: navy, font-size: 20px, and font-weight: bold — inheriting the first two from <b>'s own inherited values, and the third from <b>'s own UA-set value — a text node never distinguishes between a value its container inherited versus one its container set directly; it just takes the whole finished computed style as one unit.
<ul>'s style-tree children include real whitespace-only text nodes from the source's own line breaks between tags (a genuine, if invisible, style-tree entry — Course 2's own inline-layout chapters are where whitespace like this actually gets collapsed or preserved for real). Filtering to element children specifically: exactly one survives — the first <li>, containing "One". The second <li class="hidden">, matched by the author's own .hidden { display: none; } rule, is gone completely — itself and its own "Two" text child, removed together as one unit, exactly as Chapter 8 verified. The surviving <li>'s own color is navy — inherited through a real three-level chain, #page → ul → li, with neither intermediate element setting color itself.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A real integration bug, invisible to every individual chapter's own tests | The exact reason a capstone chapter exists at all in this course's own format — Chapters 2 through 9 were each genuinely correct in isolation; only running them together for the first time could ever have surfaced this |
| A fully-resolved style tree, built from real source text start to finish | Course 2, Building a Web Browser Engine: Layout & Rendering — this exact StyledNode tree, produced by this exact pipeline, is the direct input its own first chapter starts from |
| Whitespace-only text nodes surviving as real style-tree entries | Course 2's own inline-layout chapters, which are where a real browser engine actually decides what to do with whitespace between elements — collapsing it, preserving it, or treating it as insignificant depending on context |
What This Course Doesn't Cover
Restating Chapter 1's own honest scope, now that every piece of it has actually been built: no JavaScript, no networking beyond an in-memory string, no images, no Flexbox or Grid, no forms, no real font rendering, and no real GPU/OS rendering surface. This course produces a fully-resolved style tree — the DOM, the cascade, inheritance, and a real (if simplified) user-agent stylesheet, all genuinely working together. It does not yet know how big anything is, where anything sits on a page, or what a single pixel looks like.
Where This Connects: On to Course 2
Course 2, Building a Web Browser Engine: Layout & Rendering, picks up exactly here — turning this StyledNode tree into a real layout tree with actual box dimensions (the CSS box model), resolving block and inline layout, measuring real text, and finally rasterizing the whole thing into an actual pixel buffer. Every chapter in that course assumes this one's own output as its starting input.
Course 1 Complete — Building a Web Browser Engine: Parsing & the DOM
- Chapters 1-2: why a browser engine matters, and a real, forgiving HTML tokenizer (void elements, the ignored-trailing-slash quirk, quoted/unquoted/boolean attributes, comment safety)
- Chapters 3-4: a real stack-based DOM parser (with the implied
<p>-closing rule) and a real CSS tokenizer/parser (with the non-nesting-comment quirk) - Chapters 5-6: real selector parsing and matching (with the added DOM parent-pointer fix), and a real cascade (specificity as a compared tuple, per-property merging, and the comma-selector specificity bug found and fixed)
- Chapters 7-8: real inheritance (with the parent's-raw-cascade bug found and fixed) and the real
StyledNodestyle tree (with the dropped-text-nodes bug found and fixed) - Chapter 9: a real user-agent stylesheet and origin-aware cascading (with the specificity-vs-origin bug found and fixed)
- Chapter 10 (this chapter): every piece wired together for the first time on a real document — surfacing and fixing one final, genuine integration bug (
Declaration.propertyvs..name) that no single chapter's own isolated tests could ever have caught - Next: Building a Web Browser Engine: Layout & Rendering — turning this course's own finished style tree into real, measured, rasterized pixels