Why Build a Browser Engine? HTML, CSS & the Rendering Pipeline
Building a Web Browser Engine: Parsing & the DOM
Chapter 1 · Why Build a Browser Engine? HTML, CSS & the Rendering Pipeline
Type a URL, and somewhere between pressing Enter and seeing a page, raw bytes of HTML and CSS text turn into an arrangement of colored rectangles and text on a screen. This two-course project builds the part of that journey that starts once the HTML and CSS text already exist — no networking, no JavaScript, nothing borrowed from a real browser engine's own source code. Just the actual pipeline, built by hand, in Python, verified at every step the same way every other course on this site has been: by running it and checking the answer, not by asserting it.
The Four-Stage Pipeline: Parse → Style → Layout → Paint
Every real browser engine — however large and however optimized — does the same four things to turn markup into pixels. This project builds each one, split across two courses.
| Stage | Input | Output | Built in |
|---|---|---|---|
| Parse | Raw HTML text, raw CSS text | A DOM tree, a stylesheet | This course, Chapters 2-5 |
| Style | The DOM tree + the stylesheet | A style tree — every DOM node paired with its own fully resolved computed style | This course, Chapters 6-9 |
| Layout | The style tree | A layout tree — every box's own real width, height, and position | Course 2, Chapters 1-6 |
| Paint | The layout tree | An actual pixel buffer | Course 2, Chapters 7-10 |
Each stage's own output is exactly the next stage's own input — a real pipeline, not four unrelated topics loosely grouped under one course title. By the end of Course 2's own capstone, a real static web page will go in as two text files and come out as an actual rendered image.
This Project's Own Honest Scope
Stated up front, the same way every other course in this site's own "ambitious learning projects" tier states its scope before writing a line of code: this is build-to-understand, not build-to-ship.
- No JavaScript. No
<script>execution, no DOM mutation after the initial parse. - No networking. HTML and CSS arrive as plain Python strings — no HTTP, no fetching linked stylesheets or images.
- No images. Layout and paint both work purely with boxes, text, and color.
- No Flexbox, no Grid, no forms. Block and inline layout only — the two layout modes CSS actually started with.
- No real font rendering. Course 2's own text layout uses a simplified, honest glyph-width model, not a real font-rasterization library.
- A software-rasterized pixel buffer, not a real window. The final output is a real image file, not anything drawn to actual screen hardware.
None of these are apologized-for gaps discovered too late — they're the same kind of deliberate, stated-in-advance boundary this site's own compiler1/compiler2 pair drew around JavaScript-style dynamic typing extras, and the same kind every course in this tier draws to stay genuinely finishable while still being genuinely real.
Why Not Just Use Regex?
Before building a real parser, it's worth trying the shortcut everyone tries first — and seeing exactly where it breaks, concretely, rather than taking "you can't parse HTML with regex" on faith.
naive_extract("<div><div>Inner</div></div>", "div") returns ['<div>Inner'] — not the true outer content, "<div>Inner</div>". The regex has no concept of nesting depth at all; it just scans forward from the first <div> and captures raw characters until it hits the first literal </div> it finds — completely oblivious that an inner <div> opened in between. Three levels of nesting makes it worse, not better: "<div><div><div>Deep</div></div></div>" returns ['<div><div>Deep'] — two stray, unclosed opening tags leaked straight into the "content."
.* instead of .*? doesn't fix this — it trades the nesting bug for the opposite failure, matching all the way from the first opening tag to the very last closing tag in the document, swallowing unrelated sibling elements in between.
What a Real Parser Needs Instead: Tracking Depth
The fix a real parser needs is small enough to preview here, one paragraph before Chapter 2 builds the real thing properly: track how many tags deep the scan currently is, and only treat a closing tag as the match once that count returns to zero.
depth_aware_extract_first returns "<div>Inner</div>" — the true, complete content of the outer div. Given a harder input mixing nesting and trailing text before the real close — "<div><div>Inner</div>after inner</div><div>Second</div>" — it correctly returns "<div>Inner</div>after inner", stopping at the right closing tag even with a second, unrelated div sitting right after it in the source.
This is a genuine preview, not a toy — Chapter 2's own real tokenizer and parser are a more complete, more careful version of exactly this idea: never trust a closing tag in isolation, always track what's currently open.
A First Look at a Real DOM Tree
Before building a from-scratch tokenizer, it's worth seeing what the target actually looks like. Python's own standard library includes an HTML parser — used here purely to illustrate the shape of a DOM tree for this one chapter, not reused anywhere else in this course. Chapter 2 builds a real tokenizer and parser completely from scratch.
Element('#document')
Element('div')
Element('p')
Text('Hello')
div contains p, which contains the text "Hello" — a tree, not a flat list, with each element's own children nested directly inside it. A sibling structure, "<ul><li>One</li><li>Two</li></ul>", produces two separate li elements as direct children of the same parent — ul — rather than nested inside one another, confirming the tree correctly distinguishes "nested inside" from "next to."
From DOM to Pixels — Sketching the Rest of the Pipeline
The same tiny div/p/"Hello" example, carried conceptually through every remaining stage this two-course project builds — not yet computed for real, since the cascade, the box model, and the rasterizer don't exist yet, but concrete enough to see where each future chapter's own work actually lands.
| Stage | What it adds to this example |
|---|---|
| DOM tree (above) | div → p → "Hello", with no notion of color, size, or position at all |
| Style tree (Ch.6-9) | Every node paired with its own resolved values — e.g. div might resolve to padding: 10px, p to color: blue, inherited down from whatever the stylesheet and the browser's own defaults specify |
| Layout tree (Course 2, Ch.1-6) | Real numbers — div's own box might resolve to position (0, 0), size 300×40; p's own line box sits inside it, inset by that 10px padding |
| Pixels (Course 2, Ch.7-10) | An actual image — the div's own background color filled into its box's own rectangle, "Hello" rendered as real glyphs at the p's own resolved position |
10px, (0, 0), 300×40) is illustrative, chosen to be plausible, not derived from any real cascade or layout algorithm — those don't exist in this course yet. The point is orientation: knowing what a "style tree" or a "layout tree" is, concretely, before spending several chapters building the machinery that actually produces one correctly.
Where Each Future Chapter Fits
| Chapter | Builds |
|---|---|
| 2 | A real, hand-written HTML tokenizer — the depth-tracking idea above, generalized properly |
| 3 | A real HTML parser, building an actual DOM tree from the token stream |
| 4 | A CSS tokenizer and parser, building a real Stylesheet structure |
| 5 | Selectors and selector matching — deciding whether one CSS rule applies to one DOM node |
| 6 | Specificity and the cascade — deciding which rule wins when more than one matches |
| 7 | Inheritance and computed values |
| 8 | The style tree itself — the DOM tree, the cascade, and inheritance combined into one structure |
| 9 | A real default stylesheet — why div and span behave differently even with no CSS written at all |
| 10 | Capstone — a real document parsed all the way to a verified style tree |
Hands-On Exercises
Using this chapter's own naive_extract, test a four-level-deep nested input — "<div><div><div><div>Deepest</div></div></div></div>" — for the tag "div". Determine exactly what gets captured, and explain the pattern connecting nesting depth to how many stray opening tags leak into the broken result.
Using this chapter's own depth_aware_extract_first, extract the "p" content from "<p>Hello <b>bold</b> world</p>" — a tag containing a genuinely different nested tag, not a repeat of itself. Verify the result, and explain why this case doesn't even trip up the naive regex version — what's specifically different about same-tag nesting versus different-tag nesting?
Using this chapter's own illustrative TinyDOMBuilder, build and print the DOM tree for "<div><p>One</p><p>Two</p></div>" — two sibling paragraphs inside one div. Sketch the expected tree shape by hand first, then verify your sketch against the actual printed output.
Chapter 1 Quick Reference
- The pipeline: parse (text → DOM + stylesheet) → style (+ cascade → style tree) → layout (→ boxes with real positions) → paint (→ pixels)
- Scope: static HTML+CSS only — no JS, no networking, no images, no Flexbox/Grid, no real font rendering, a software-rasterized pixel buffer
- Verified: a naive regex tag-extractor breaks on nested elements exactly as the well-known warning predicts, and gets worse with each additional level of nesting
- Verified: a small depth-aware scan — tracking how many tags are currently open — gets the same nested case exactly right, previewing Chapter 2's own real approach
- Verified: a real, if illustrative, DOM tree correctly distinguishes nested elements from sibling elements
- Next chapter: Tokenizing HTML: A Real, Forgiving Lexer — building the actual, from-scratch tokenizer this chapter only previewed