--- 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 [`
    `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol) elements. Or to generate marker labels for the arrow keys to jump to. To implement these I added a [`StyleTree`](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/StyleTree.hs) abstraction to hold the relationship between all parsed `PropertyParser` style objects & aid tree traversals. From there I implemented a [second](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Preprocessor/Text.hs#n31) `PropertyParser` decorator with two tree traversals: [one](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Preprocessor/Text.hs#n179) to collapse whitespace & the [other](https://git.adrian.geek.nz/haskell-stylist.git/tree/src/Data/CSS/Preprocessor/Text.hs#n112) to track counter values before substituting them (as strings) in-place of any [`counter()`](https://www.w3.org/TR/CSS2/generate.html#counter-styles) or [`counters()`](https://developer.mozilla.org/en-US/docs/Web/CSS/counters()) functions. ## [`url()`](https://www.w3.org/TR/CSS2/syndata.html#uri) In most browser engines any resource references (via the `url()` function, which incidentally requires special effort to lex correctly & resolve any relative links) is resolved after the page has been fully styled. I opted to do this prior to styling instead, as a privacy measure I found just as easy to implement as it would be not to do so. Granted this does lead to impaired functionality of the [`style`](https://www.w3.org/TR/html401/present/styles.html#h-14.2.2) attribute, but please don't use that anyways! This was implemented as a pair of `StyleSheet` implementations: one to extract relevant URLs from the stylesheet, and the other to substitute in the filepaths where they were downloaded. eSpeak NG will parse these [`.wav`](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html) files when it's ready to play these sound effects. ## [CSS Inheritance](https://www.w3.org/TR/CSS2/cascade.html#inheritance) Future browser engines of mine will handle this differently, but for Rhapsode I simply reformat the style tree into a [SSML document](https://www.w3.org/TR/speech-synthesis/) to hand to straight to [eSpeak NG](https://adrian.geek.nz/docs/espeak.html). [eSpeak NG](http://espeak.sourceforge.net/ssml.html) (running in-process) will then parse this XML with the aid of a stack to convert it into control codes within the text it's later stages will gradually convert to sound. --- While all this *is* useful to webdevs wanting to give a special feel to their webpages (which, within reason, I don't object to), my main incentive to implement CSS was for my own sake in designing Rhapsode's [useragent stylesheet](https://git.adrian.geek.nz/rhapsode.git/tree/useragent.css). And that stylesheet takes advantage of most of the above. Sure there are features (like support for CSS variables or most pseudoclasses) I decided to implement just because they were easy, but the only thing I'd consider extra complexity beyond the needs of an auditory browser engine are media queries. But I'm sure I'll find a use for those in future browser engines. Otherwise all this code would have to be in Rhapsode in some form or other to give a better auditory experience than eSpeak NG can deliver itself!