--- layout: post title: How Does CSS Work? author: Adrian Cochrane date: 2020-11-12 20:35:06 +1300 --- Rendering a webpage in Rhapsode takes little more than applying a [useragent stylesheet](https://meiert.com/en/blog/user-agent-style-sheets/) to decide how the page's semantics should be communicated. [In addition to](https://www.w3.org/TR/CSS2/cascade.html#cascade) any installed userstyles and *optionally* author styles. Once the [CSS](https://www.w3.org/Style/CSS/Overview.en.html) has been applied Rhapsode sends the styled text to [eSpeak NG](https://github.com/espeak-ng/espeak-ng) to be converted into the sounds you hear. So *how* does Rhapsode apply that CSS? ## Parsing [Parser](http://parsingintro.sourceforge.net/) implementations differ mainly in *what* they implement rather than *how*. They repeatedly look at the next character(s) in the input stream to decide how to represent it in-RAM. Often there'll be a "lexing" step (for which I use [Haskell CSS Syntax](https://hackage.haskell.org/package/css-syntax)) to categorize consecutive characters into "tokens", thereby simplifying the main parser. My choice to use [Haskell](https://www.haskell.org/), however, does change things a little. In Haskell there can be [*no side effects*](https://mmhaskell.com/blog/2017/1/9/immutability-is-awesome); all [outputs **must** be returned](https://mmhaskell.com/blog/2018/1/8/immutability-the-less-things-change-the-more-you-know). So in addition to the parsed tree, each part of the parser must return the rest of text that still needs to be parsed by another sub-parser. Yielding a type signature of [`:: [Token] -> (a, [Token])`](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Syntax/StylishUtil.hs#n11), leading Haskell to allow you to combine these subparsers together in what's called "[parser combinators](https://remusao.github.io/posts/whats-in-a-parser-combinator.html)". Once each style rule is parsed, a method is called on a [`StyleSheet`](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Syntax/StyleSheet.hs#n27) "[typeclass](http://book.realworldhaskell.org/read/using-typeclasses.html)" to return a modified datastructure containing the new rule. And a different method is called to parse any [at-rules](https://www.w3.org/TR/CSS2/syndata.html#at-rules). ## Pseudoclasses Many of my `StyleSheet` implementations handle only certain aspects of CSS, handing off to another implementation to perform the rest. For example most pseudoclasses (ignoring interactive aspects I have no plans to implement) can be re-written into simpler selectors. So I added a configurable `StyleSheet` [decorator](https://refactoring.guru/design-patterns/decorator) just to do that! This pass also resolves any [namespaces](https://www.w3.org/TR/css3-namespace/), and corrects [`:before` & `:after`](https://www.w3.org/TR/CSS2/selector.html#before-and-after) to be parsed as pseudoelements. ## Media Queries & `@import` CSS defines a handful of at-rules which can control whether contained style rules will be applied: * [`@document`](https://developer.mozilla.org/en-US/docs/Web/CSS/@document) allows user & useragent stylesheets to apply style rules only for certain (X)HTML documents & URLs. An interesting Rhapsode-specific feature is [`@document unstyled`](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Preprocessor/Conditions.hs#n84) which applies only if no author styles have already been parsed. * [`@media`](https://drafts.csswg.org/css-conditional-3/#at-media) applies it's style rules only if the given media query evaluates to true. Whilst in Rhapsode only the [`speech`](https://www.w3.org/TR/CSS2/media.html#media-types) or `-rhapsode` mediatypes are supported, I've implemented a full caller-extensible [Shunting Yard](https://en.wikipedia.org/wiki/Shunting-yard_algorithm) interpretor. * [`@import`](https://www.w3.org/TR/css3-cascade/#at-import) fetches & parses the given URL if the given mediatype evaluates to true when you call [`loadImports`](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Preprocessor/Conditions.hs#n138). As a privacy protection for future browsers, callers may avoid hardware details leaking to the webserver by being more vague in this pass. * [`@supports`](https://drafts.csswg.org/css-conditional-3/#at-supports) applies style rules only if the given CSS property or selector syntax parses successfully. Since media queries might need to be rechecked when, say, the window has been resized `@media` (and downloaded `@import`) are resolved to populate a new `StyleSheet` implementation only when the [`resolve`](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Preprocessor/Conditions.hs#n151) function is called. Though again this is overengineered for Rhapsode's uses as instead of window it renders pages to an infinite auditory timeline, media queries are *barely* useful here. ## Indexing Ultimately Rhapsode parses CSS style rules to be stored in a [hashmap](https://en.wikipedia.org/wiki/Hash_table) (or rather a [Hash Array Mapped Trie](https://en.wikipedia.org/wiki/Hash_array_mapped_trie)) [indexed](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Style/Selector/Index.hs#n50) under the right-most selector if any. This dramatically cuts down on how many style rules have to be considered for each element being styled. So that for each element needing styling, it [looks up](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Style/Selector/Index.hs#n68) just those style rules which match it's name, attributes, IDs, and/or classes. However this only considers a single test from each rules' selector, so we need a… ## Interpretor To truly determine whether an element matches a [CSS selector](https://www.w3.org/TR/selectors-3/), we need to actually evaluate that selector! I've implemented this in 3 parts: * [Lowering](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Style/Selector/Interpret.hs#n53) - Reduces how many types of selector tests need to be compiled by e.g. converting `.class` to `[class~=class]`. * [Compilation](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Style/Selector/Interpret.hs#n34) - Converts the parsed selector into a [lambda](https://teraum.writeas.com/anatomy-of-things) function you can call as the style rule is being added to the store. * [Runtime](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Style/Selector/Interpret.hs#n88) - Provides functions that may be called as part of evaluating a CSS selector. Whether there's actually any compilation happening is another question for the [Glasgow Haskell Compiler](https://www.haskell.org/ghc/), but regardless I find it a convenient way to write and think about it. Selectors are interpreted from right-to-left as that tend to shortcircuit sooner, upon an alternate inversely-linked representation of the element tree parsed by [XML Conduit](https://hackage.haskell.org/package/xml-conduit). **NOTE** In webapp-capable browser engines [`querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll) tends to use a *slightly* different selector interpretor because there we know the ancestor element. This makes it more efficient to interpret *those* selectors left-to-right. ## Specificity Style rules should be sorted by a ["selector specificity"](https://www.w3.org/TR/selectors-3/#specificity), which is computed by counting tests on IDs, classes, & tagnames. With ties broken by which come first in the source code and whether the stylesheet came from the [browser, user, or webpage](https://www.w3.org/TR/CSS2/cascade.html#cascade). This is implemented as a decorator around the interpretor & (in turn) indexer. Another decorator strips [`!important`](https://www.w3.org/TR/CSS2/cascade.html#important-rules) off the end of any relevant CSS property values, generating new style rules with higher priority. ## Validation Once `!important` is stripped off, the embedding application is given a chance to validate whether the syntax is valid &, as such, whether it should participate in the CSS cascade. Invalid properties are discarded. At the same time the embedding application can expand CSS [shorthands](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties) into one or more longhand properties. E.g. convert `border-left: thin solid black;` into `border-left-width: thin; border-left-style: solid; border-left-color: black;`. ## CSS [Cascade](https://www.w3.org/TR/css3-cascade/) This was trivial to implement! Once you have a list of style rules listed by specificity, just load all their properties into a [hashmap](http://hackage.haskell.org/package/unordered-containers) & back! Maybe I'll write a little blogpost about how many webdevs seem to be [scared of the cascade](https://mxb.dev/blog/the-css-mindset/#h-the-cascade-is-your-friend)… After cascade, methods are called on a given [`PropertyParser`](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Style/Cascade.hs#n18) to parse each longhand property into an in-memory representation that's easier to process. This typeclass *also* has useful decorators, though few are needed for the small handful of speech-related properties. Haskell's [pattern matching](http://learnyouahaskell.com/syntax-in-functions#pattern-matching) syntax makes the tidious work of parsing the [sheer variety](https://www.w3.org/TR/CSS2/propidx.html#q24.0) of CSS properties absolutely trivial. I didn't have to implement a DSL like other [browser engines do](http://trac.webkit.org/browser/webkit/trunk/Source/WebCore/css/CSSProperties.json)! This is the reason why I chose Haskell! ## CSS Variables [`var()`](https://www.w3.org/TR/css-variables-1/) In CSS3, any property prefixed with [`--`](https://www.w3.org/TR/css-variables-1/#defining-variables) will participate in CSS cascade to specify what tokens the `var()` function should substitute in. If the property no longer parses successfully after this substitution it is ignored. A bit of a [gotcha for webdevs](https://matthiasott.com/notes/css-custom-properties-fail-without-fallback), but makes it quite trivial for me to implement! In fact, beyond prioritizing extraction of `--`-prefixed properties, I needed little more than a [trivial](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Style.hs#n91) `PropertyParser` decorator. ## [Counters](https://www.w3.org/TR/css-counter-styles-3/) There's a [handful of CSS properties](https://www.w3.org/TR/CSS2/text.html#q16.0) which alters the text parsed from the HTML document, predominantly by including counters. Which I use to render [`