Tokenizing and Parsing CSS
Building a Web Browser Engine: Parsing & the DOM
Chapter 4 · Tokenizing and Parsing CSS
CSS's own grammar is a lot simpler than HTML's — no implicit closing rules, no fourteen special-cased element types, no ambiguity about what's a tag versus what's content. One chapter is enough to take CSS source text all the way to a real, structured Stylesheet — the second half of what Chapter 8's own style tree will need, alongside the DOM tree Chapter 3 already built.
Three Small Structures
selectors stays a flat list of raw strings in this chapter — ['h1', 'h2'] for h1, h2 { ... } — not yet parsed into anything structured. Chapter 5 is where a selector string actually becomes something that can be matched against a DOM node.
Comments First, Before Anything Else
CSS comments (/* ... */) can legally appear almost anywhere — between rules, inside a declaration block, in the middle of a selector list. Stripping them out in one pass, before any real parsing starts, means nothing downstream ever has to think about them again.
The Real Quirk: CSS Comments Don't Nest
Writing what looks like a comment inside a comment — to temporarily disable a block that already has its own comment in it, say — doesn't do what it looks like it should.
strip_comments("/* outer /* inner */ still outer */ p { color: red; }") returns " still outer */ p { color: red; }" — not an empty string. The scanner has no concept of comment depth; it opens on the first /* and closes on the very next */ it finds, full stop. Everything after that first closing marker — including the second, now-orphaned */ — is treated as ordinary CSS text.
parse_css produces one rule whose own selectors is ['still outer */ p'] — not the clean ['p'] a developer obviously intended. The leftover comment fragment doesn't vanish; it gets swept straight into the next rule's own selector text as ordinary characters. Once Chapter 5 builds real selector matching, a selector string like that will simply never match anything in any real document — no crash, no warning, just CSS that silently does nothing.
parse_css("/* one */ /* two */ p { color: red; }") parses cleanly to a single rule with selectors == ['p'] — exactly as expected. Two consecutive comments, each properly opened and closed on its own, is nothing like one comment written to look nested. The failure above is specifically about a /* appearing before the matching */ of an already-open comment — a structural mistake, not "too many comments."
The Rest of the Grammar
parse_css("p { color: red; font-size: 16px; }") and parse_css("p { color: red; font-size: 16px }") — the second missing its own final semicolon — produce identical declaration lists. Splitting on ; naturally produces a trailing empty chunk when the semicolon is present, which the blank-chunk check simply skips; when it's absent, there's no empty chunk to skip in the first place. The same code handles both shapes without a special case for either.
parse_css("div { margin: 10px 20px; }") produces exactly one Declaration('margin', '10px 20px'). Splitting only ever happens on ; (between declarations) and the first : within each chunk (between property and value) — nothing inside parse_declarations ever splits on whitespace, so a value that's legitimately several space-separated tokens stays exactly that, one string.
Rule objects to a tightly packed one-liner. Every split point in this chapter's own parser strips whitespace immediately afterward, so formatting is never load-bearing.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
A flat list of raw selector strings on each Rule | Chapter 5's own selector matching — this chapter deliberately stops at "here is the text," leaving "does this text match this DOM node" as a separate, dedicated problem |
| CSS comments stripped once, up front, before any rule parsing begins | Mirrors Chapter 3's own closetag handling being defensive by construction — both chapters choose to make one part of the pipeline robust early, so nothing downstream has to re-solve the same problem |
| A corrupted selector from a non-nested comment produces no error anywhere | A direct parallel to this course's own Chapter 1 finding about regex and HTML — a parser that silently produces a plausible-looking wrong answer is a harder failure mode to catch than one that crashes loudly |
| Property names lowercased during parsing | Chapter 3's own tag-name lowercasing — the same "normalize once, at the source, so every later consumer can assume it's already done" discipline, applied to CSS instead of HTML |
Hands-On Exercises
Parse "h1, /* comment */ h2 { color: red; }" using this chapter's own parse_css — a comment sitting between two comma-separated selectors, not inside a declaration block. Verify the resulting rule's own selectors list, and explain why placing strip_comments before selector-splitting is what makes this work correctly.
Parse "p {}" — a rule with a genuinely empty declaration block — using this chapter's own parse_css. Confirm this doesn't raise an error and determine exactly what the resulting Rule's own declarations list contains, then explain which specific check inside parse_declarations is responsible for handling an empty block correctly.
Parse "/* one */ /* two */ p { color: red; }" — two separate, properly closed comments in a row — and confirm the resulting selector is the clean ['p'], not a corrupted string the way this chapter's own nested-looking example produced. Explain precisely, in terms of how strip_comments's own scan position advances, why two consecutive comments never trigger the same failure a nested-looking one does.
Chapter 4 Quick Reference
- Three structures:
Declaration(property/value),Rule(a list of selector strings + a list of declarations),Stylesheet(a list of rules) - Verified — the real quirk: CSS comments don't nest; the first
*/always ends the comment, no matter how many/*came before it - Verified: a non-nested-looking comment silently corrupts the following selector, with no error raised anywhere in the pipeline
- Verified: a trailing semicolon on the last declaration is genuinely optional — the same code handles both shapes with no special case
- Verified: a multi-token value like
"10px 20px"survives intact as one value, since splitting only ever happens on;and the first: - Verified: arbitrary whitespace and indentation never change the parsed result
- Next chapter: Selectors & Selector Matching — deciding whether one of this chapter's own raw selector strings actually applies to a given DOM node