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.

StageInputOutputBuilt in
ParseRaw HTML text, raw CSS textA DOM tree, a stylesheetThis course, Chapters 2-5
StyleThe DOM tree + the stylesheetA style tree — every DOM node paired with its own fully resolved computed styleThis course, Chapters 6-9
LayoutThe style treeA layout tree — every box's own real width, height, and positionCourse 2, Chapters 1-6
PaintThe layout treeAn actual pixel bufferCourse 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.

import re def naive_extract(html, tag): pattern = f"<{tag}>(.*?)</{tag}>" return re.findall(pattern, html, re.DOTALL)
Verified directly — nesting breaks it, exactly as the well-known warning says, and gets worse the deeper it goes
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."
This isn't a fixable regex bug — it's a real, structural limit
Regular expressions match regular languages — patterns with no memory of how deep they've gone. HTML's own nesting is a context-free structure — correctly matching an outer tag's own closing tag requires remembering how many same-named tags have opened since, which a regex engine has no mechanism to track at all. Switching to a greedy .* 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.

def depth_aware_extract_first(html, tag): open_tag, close_tag = f"<{tag}>", f"</{tag}>" start = html.find(open_tag) pos = start + len(open_tag) depth = 1 while depth > 0: next_open = html.find(open_tag, pos) next_close = html.find(close_tag, pos) if next_open != -1 and next_open < next_close: depth += 1; pos = next_open + len(open_tag) # one level deeper else: depth -= 1; content_end = next_close; pos = next_close + len(close_tag) return html[start + len(open_tag): content_end]
Verified directly — tracking depth gets the nested case exactly right
On the identical nested input, 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.

source = "<div>\n <p>Hello</p>\n</div>"
Verified directly — a real, if illustrative, DOM tree for a two-element document
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 parentul — 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.

StageWhat it adds to this example
DOM tree (above)divp"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
Nothing here was computed — that's deliberate
This table is a map, not a result. Every specific number above (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

ChapterBuilds
2A real, hand-written HTML tokenizer — the depth-tracking idea above, generalized properly
3A real HTML parser, building an actual DOM tree from the token stream
4A CSS tokenizer and parser, building a real Stylesheet structure
5Selectors and selector matching — deciding whether one CSS rule applies to one DOM node
6Specificity and the cascade — deciding which rule wins when more than one matches
7Inheritance and computed values
8The style tree itself — the DOM tree, the cascade, and inheritance combined into one structure
9A real default stylesheet — why div and span behave differently even with no CSS written at all
10Capstone — a real document parsed all the way to a verified style tree

Hands-On Exercises

Exercise 1

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.

📄 View solution
Exercise 2

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?

📄 View solution
Exercise 3

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.

📄 View solution

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