~alcinnz/argonaut-constellation.org

ref: 4ff2af48968fece03862a777be93cbe9e5e7f4ad argonaut-constellation.org/_posts/2020-11-12-css.md -rw-r--r-- 13.7 KiB
4ff2af48 — Adrian Cochrane Announce NLnet funding! 1 year, 10 months ago
                                                                                
da1ec90f Adrian Cochrane
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
---
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 [`<ol>`](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!