Exercise 3: Reproducing the Origin-vs-Specificity Bug Directly — Possible Solution ==================================================================== THE TEST ------------------------------ elem =
ua_special = Stylesheet([Rule(['p.intro'], [Declaration('margin', '40px')])]) author_reset = Stylesheet([Rule(['p'], [Declaration('margin', '0')])]) cascade_naive_combined(elem, ua_special, author_reset)['margin'] cascade_with_origin(elem, ua_special, author_reset)['margin'] RESULT ------------------------------ cascade_naive_combined() -> '40px' (WRONG) cascade_with_origin() -> '0' (correct) THE EXACT COMPARISON RESPONSIBLE ------------------------------ cascade_naive_combined's own implementation is: combined = Stylesheet(ua_sheet.rules + author_sheet.rules) return cascade(element, combined) It builds ONE flat list of rules with no memory at all of which stylesheet each rule originally came from, then hands it to Chapter 6's own cascade(), whose sort key is `(m[0], m[1])` -- specificity first, source order second, nothing else. specificity('p.intro') is (0,1,1); specificity('p') is (0,0,1). Comparing those two tuples directly, (0,1,1) > (0,0,1) is True -- the class-qualified UA rule is, in raw specificity terms, genuinely "louder" than the author's plain tag rule. cascade()'s own sort has no third dimension to check before specificity that could have caught this -- specificity IS the first and most important thing it looks at, by design, because Chapter 6 was built before origin existed as a concept at all. The single line responsible is the sort key itself: matches.sort(key=lambda m: (m[0], m[1])) # m[0] is specificity here There is nothing wrong with this line in isolation -- it's exactly correct for comparing rules WITHIN one origin, which is all Chapter 6 ever needed it to do. The bug only exists once two genuinely different origins get merged into the same flat rule list before this sort ever runs, at which point the sort has no way to tell "this rule is UA" from "this rule is author" -- that information was already thrown away by the line `combined = Stylesheet(ua_sheet.rules + author_sheet.rules)`, before cascade() is even called. WHY cascade_with_origin FIXES IT ------------------------------ cascade_with_origin never merges the two rule lists into one undifferentiated pool. It iterates `for origin, sheet in ((0, ua_sheet), (1, author_sheet))` and tags every single match with its own origin number as the FIRST element of its sort tuple, `(origin, best_spec, order_counter, ...)`. Sorting on `(m[0], m[1], m[2])` means origin is compared before specificity is ever consulted -- (0, (0,1,1), ...) for the UA rule sorts BEFORE (1, (0,0,1), ...) for the author rule, purely because 0 < 1, regardless of what the second tuple element (specificity) would have said on its own. The author rule ends up applied last, correctly overwriting the UA rule's margin. WHY THIS WORKS AS AN ANSWER ------------------------------ The bug is a genuine design gap, not a coding mistake inside cascade() itself -- cascade() does exactly what it was built to do in Chapter 6, correctly, for a single-origin stylesheet. The moment a SECOND origin enters the picture, the two-part sort key it was designed around (specificity, order) is missing a dimension that real CSS actually has (origin), and no amount of correct specificity math can compensate for that missing dimension once a UA rule's raw specificity happens to outscore a competing author rule's.