css.bend

gralha-css-style@0.1.0.1 · 462 lines · 71 definitions

1# CSS: stylesheets, selectors, specificity, media queries and the cascade onto an XML tree.
2import Base
3import gralha-text-scan@0.1.0.1/scan.bend as U
4import gralha-xml-doc@0.1.0.1/xml.bend as XML
5
6def css.lower(s: String) -> String:
7  match s:
8    case SNil{}: ""
9    case SCon{Chr{c}, t}:
10      +ch = c
11      SCon{Chr{U.choose(U32, U32.is_ge(ch, 65) && U32.is_le(ch, 90), (ch + 32 : U32), ch)}, css.lower(t)}
12
13def css.pair(s: U.Scan, attrs: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
14  U.Scan{k, v} = s
15  Con{XML.Attr{U.trim.both(k), U.trim.both(v)}, attrs}
16
17def css.more(s: U.Scan, attrs: List<&2, XML.Attr>, next: String -> List<&2, XML.Attr> -> List<&2, XML.Attr>) -> List<&2, XML.Attr>:
18  U.Scan{h, t} = s
19  next(t)(css.pair(U.until(h, 58, ""), attrs))
20
21@unsafe
22def css(s: String, attrs: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
23  match s:
24    case SNil{}: attrs
25    case SCon{c, t}: css.more(U.until(SCon{c, t}, 59, ""), attrs, rest => as => css(rest, as))
26
27def properties(+attrs: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
28  U.branch(List<&2, XML.Attr>, U.string.equal(XML.attr.get(attrs, "#css-ready", ""), "1"), _ => attrs, _ => css(XML.attr.get(attrs, "style", ""), attrs))
29
30# Stylesheets are applied to a rendering copy; authored XML stays unchanged.
31def css.stop(+ch: U32, +mode: U32) -> Bool:
32  U.choose(Bool, U32.is_eq(mode, 0), U.space(ch) || U32.is_eq(ch, 62) || U32.is_eq(ch, 43) || U32.is_eq(ch, 126), U.choose(Bool, U32.is_eq(mode, 1), U32.is_eq(ch, 123) || U32.is_eq(ch, 59), U32.is_eq(ch, mode)))
33
34# Delimiters within strings, brackets or functions do not split CSS tokens.
35@unsafe
36def css.cut(+s: String, +end: U32, +depth: U32, +quote: U32, +escaped: Bool, +acc: String) -> U.Scan:
37  match s:
38    case SNil{}: U.Scan{U.rev(acc, ""), ""}
39    case SCon{Chr{ch}, tail}:
40      +c = ch
41      +t = tail
42      +quoted = Bool.not(U32.is_eq(quote, 0))
43      +opening = U32.is_eq(c, 40) || U32.is_eq(c, 91) || U32.is_eq(c, 123)
44      +closing = U32.is_eq(c, 41) || U32.is_eq(c, 93) || U32.is_eq(c, 125)
45      U.branch(U.Scan, Bool.not(escaped) && Bool.not(quoted) && U32.is_eq(depth, 0) && css.stop(c, end),
46        _ => U.Scan{U.rev(acc, ""), s},
47        _ => U.branch(U.Scan, Bool.not(escaped) && Bool.not(quoted) && U.prefix(s, "/*"),
48          _ => css.cut(U.through(t, "*/"), end, depth, quote, False{}, acc),
49          _ => css.cut(t, end,
50            U.choose(U32, quoted || escaped, depth, U.choose(U32, opening, U32.inc(depth), U.choose(U32, closing && U32.is_gt(depth, 0), (depth - 1 : U32), depth))),
51            U.choose(U32, escaped, quote, U.choose(U32, quoted, U.choose(U32, U32.is_eq(c, quote), 0, quote), U.choose(U32, U32.is_eq(c, 34) || U32.is_eq(c, 39), c, 0))),
52            Bool.not(escaped) && U32.is_eq(c, 92), SCon{Chr{c}, acc})))
53
54def css.scan(s: String, end: U32) -> U.Scan:
55  css.cut(s, end, 0, 0, False{}, "")
56
57@unsafe
58def css.skip(+s: String) -> String:
59  U.branch(String, U.prefix(U.trim(s), "/*"), _ => css.skip(U.through(U.trim(s), "*/")), _ => U.trim(s))
60
61def css.ident.char(+c: U32) -> Bool:
62  U.digit(c) || (U32.is_ge(c, 65) && U32.is_le(c, 90)) || (U32.is_ge(c, 97) && U32.is_le(c, 122))
63    || U32.is_eq(c, 45) || U32.is_eq(c, 95) || U32.is_ge(c, 128)
64
65def css.ident(+s: String, +acc: String) -> U.Scan:
66  match s:
67    case SNil{}: U.Scan{U.rev(acc, ""), ""}
68    case SCon{Chr{c}, t}:
69      U.branch(U.Scan, css.ident.char(c), _ => css.ident(t, SCon{Chr{c}, acc}), _ => U.Scan{U.rev(acc, ""), s})
70
71def css.word(+s: String, +acc: String) -> U.Scan:
72  match s:
73    case SNil{}: U.Scan{U.rev(acc, ""), ""}
74    case SCon{Chr{c}, t}: U.branch(U.Scan, U.space(c), _ => U.Scan{U.rev(acc, ""), t}, _ => css.word(t, SCon{Chr{c}, acc}))
75
76def css.word.test(scan: U.Scan, +word: String, next: String -> Bool) -> Bool:
77  U.Scan{h, t} = scan
78  U.string.equal(h, word) || next(t)
79
80@unsafe
81def css.words(s: String, +word: String) -> Bool:
82  match s:
83    case SNil{}: False{}
84    case SCon{c, t}: css.word.test(css.word(U.trim(SCon{c, t}), ""), word, rest => css.words(rest, word))
85
86type CSSContext is Data:
87  CSSContext{node: XML.Element, parents: List<&2, CSSContext>, previous: List<&2, CSSContext>, index: U32, count: U32}
88
89type CSSStep is Data:
90  CSSStep{compound: String, relation: U32}
91
92def css.parts.scan(scan: U.Scan, rel: U32, acc: List<&2, CSSStep>, next: String -> List<&2, CSSStep> -> List<&2, CSSStep>) -> List<&2, CSSStep>:
93  U.Scan{h, t} = scan
94  next(t)(Con{CSSStep{h, rel}, acc})
95
96@unsafe
97def css.parts(+s: String, +rel: U32, +acc: List<&2, CSSStep>) -> List<&2, CSSStep>:
98  match s:
99    case SNil{}: acc
100    case SCon{Chr{c}, t}:
101      U.branch(List<&2, CSSStep>, U.space(c), _ => css.parts(U.trim(t), rel, acc),
102        _ => U.branch(List<&2, CSSStep>, U32.is_eq(c, 62) || U32.is_eq(c, 43) || U32.is_eq(c, 126),
103          _ => css.parts(U.trim(t), U.choose(U32, U32.is_eq(c, 62), 2, U.choose(U32, U32.is_eq(c, 43), 3, 4)), acc),
104          _ => css.parts.scan(css.scan(s, 0), rel, acc, rest => steps => css.parts(rest, 1, steps))))
105
106def css.attr.value(+actual: String, +expected: String, +op: String) -> Bool:
107  U.branch(Bool, U.string.equal(op, "="), _ => U.string.equal(actual, expected),
108    _ => U.branch(Bool, U.string.equal(expected, ""), _ => False{},
109      _ => U.choose(Bool, U.string.equal(op, "~="), css.words(U.trim(actual), expected),
110        U.choose(Bool, U.string.equal(op, "|="), U.string.equal(actual, expected) || U.prefix(actual, expected ++ "-"),
111          U.choose(Bool, U.string.equal(op, "^="), U.prefix(actual, expected),
112            U.choose(Bool, U.string.equal(op, "$="), U.prefix(U.rev(actual, ""), U.rev(expected, "")),
113              Bool.not(U.string.equal(U.through(actual, expected), "")) || U.prefix(U.rev(actual, ""), U.rev(expected, ""))))))))
114
115def css.attr.compare(scan: U.Scan, +actual: String, +op: String) -> Bool:
116  U.Scan{expected, tail} = scan
117  +ignoreCase = U.string.equal(css.lower(U.trim.both(tail)), "i")
118  +text = expected
119  css.attr.value(U.choose(String, ignoreCase, css.lower(actual), actual), U.choose(String, ignoreCase, css.lower(text), text), op)
120
121def css.attr.test(scan: U.Scan, +as: List<&2, XML.Attr>) -> Bool:
122  U.Scan{k, tail} = scan
123  +key = k
124  +rest = U.trim(tail)
125  +op = U.choose(String, U.prefix(rest, "="), "=", U.choose(String, U.prefix(rest, "~="), "~=", U.choose(String, U.prefix(rest, "|="), "|=", U.choose(String, U.prefix(rest, "^="), "^=", U.choose(String, U.prefix(rest, "$="), "$=", "*=")))))
126  +v = U.trim(U.drop(rest, op))
127  +actual = XML.attr.get(as, key, "#missing-css-attribute#")
128  U.branch(Bool, U.string.equal(rest, ""), _ => Bool.not(U.string.equal(actual, "#missing-css-attribute#")),
129    _ => Bool.not(U.string.equal(actual, "#missing-css-attribute#")) && css.attr.compare(XML.value(v), actual, op))
130
131def css.empty(xs: List<&2, XML.Element>) -> Bool:
132  match xs:
133    case Nil{}: True{}
134    case Con{XML.Element{n, as, kids, txt}, rest}:
135      U.string.equal(n, "#text") && U.string.equal(txt, "") && css.empty(rest)
136
137def css.compact(+s: String) -> String:
138  match s:
139    case SNil{}: ""
140    case SCon{Chr{c}, t}:
141      +tail = t
142      U.branch(String, U.space(c), _ => css.compact(tail), _ => SCon{Chr{c}, css.compact(tail)})
143
144def css.nth.formula(scan: U.Scan, +index: U32) -> Bool:
145  U.Scan{h, t} = scan
146  +head = h
147  +tail = t
148  +a = U.choose(F32, U.string.equal(head, "") || U.string.equal(head, "+"), 1.0, U.choose(F32, U.string.equal(head, "-"), F32.neg(1.0), U.scalar(head, 0.0)))
149  +b = U.scalar(U.drop(tail, "n"), 0.0)
150  +difference = (U32.to_f32(index) - b : F32)
151  +n = (difference / a : F32)
152  U.branch(Bool, U.string.equal(tail, ""), _ => F32.is_eq(U32.to_f32(index), U.scalar(head, F32.neg(1.0))),
153    _ => U.choose(Bool, F32.is_eq(a, 0.0), F32.is_eq(difference, 0.0), F32.is_ge(n, 0.0) && F32.is_eq(n, F32.floor(n))))
154
155def css.nth(+s: String, +index: U32) -> Bool:
156  +value = css.lower(css.compact(s))
157  U.choose(Bool, U.string.equal(value, "odd"), U32.is_eq(U32.mod(index, 2), 1),
158    U.choose(Bool, U.string.equal(value, "even"), U32.is_eq(U32.mod(index, 2), 0), css.nth.formula(css.scan(value, 110), index)))
159
160def css.is.root(parents: List<&2, CSSContext>) -> Bool:
161  match parents:
162    case Nil{}: True{}
163    case Con{h, t}: False{}
164
165def css.pseudo(+key: String, +arg: String, +context: CSSContext) -> Bool:
166  CSSContext{XML.Element{n, as, kids, txt}, parents, previous, index, count} = context
167  U.choose(Bool, U.string.equal(key, "root"), css.is.root(parents),
168    U.choose(Bool, U.string.equal(key, "first-child"), U32.is_eq(index, 1),
169      U.choose(Bool, U.string.equal(key, "last-child"), U32.is_eq(index, count),
170        U.choose(Bool, U.string.equal(key, "only-child"), U32.is_eq(count, 1),
171          U.choose(Bool, U.string.equal(key, "nth-child"), css.nth(arg, index),
172            U.choose(Bool, U.string.equal(key, "nth-last-child"), css.nth(arg, (count + 1 - index : U32)),
173              U.string.equal(key, "empty") && css.empty(kids)))))))
174
175# Simple selectors compose by conjunction; unsupported pseudo-classes do not match.
176def css.simple.scan(scan: U.Scan, +context: CSSContext, +kind: U32, next: String -> Bool) -> Bool:
177  U.Scan{h, t} = scan
178  CSSContext{XML.Element{n, as, kids, txt}, parents, previous, index, count} = context
179  +name = h
180  Bool.not(U.string.equal(name, "")) && U.choose(Bool, U32.is_eq(kind, 35), U.string.equal(XML.attr.get(as, "id", ""), name),
181    U.choose(Bool, U32.is_eq(kind, 46), css.words(U.trim(XML.attr.get(as, "class", "")), name), U.string.equal(n, name))) && next(t)
182
183def css.simple.attr(+scan: U.Scan, +context: CSSContext, next: String -> Bool) -> Bool:
184  U.Scan{h, t} = scan
185  CSSContext{XML.Element{n, as, kids, txt}, parents, previous, index, count} = context
186  U.prefix(t, "]") && css.attr.test(css.ident(U.trim(h), ""), as) && next(U.drop(t, "]"))
187
188def css.simple.pseudo.arg(+scan: U.Scan, +key: String, +context: CSSContext, next: String -> Bool,
189  nested: String -> Bool) -> Bool:
190  U.Scan{h, t} = scan
191  U.prefix(t, ")") && U.branch(Bool, U.string.equal(key, "not"), _ => Bool.not(nested(h)), _ => css.pseudo(key, h, context)) && next(U.drop(t, ")"))
192
193def css.simple.pseudo(scan: U.Scan, +context: CSSContext, next: String -> Bool, nested: String -> Bool) -> Bool:
194  U.Scan{h, t} = scan
195  +key = css.lower(h)
196  +tail = t
197  css.simple.pseudo.arg(U.choose(U.Scan, U.prefix(tail, "("), css.scan(U.drop(tail, "("), 41), U.Scan{"", ")" ++ tail}), key, context, next, nested)
198
199@unsafe
200def css.simple(+fuel: Nat, +s: String, +context: CSSContext) -> Bool:
201  match fuel s:
202    case 0n _: False{}
203    case 1n+d SNil{}: True{}
204    case 1n+d SCon{Chr{c}, t}:
205      U.branch(Bool, U32.is_eq(c, 42), _ => css.simple(d, t, context),
206        _ => U.branch(Bool, U32.is_eq(c, 35) || U32.is_eq(c, 46),
207          _ => css.simple.scan(css.ident(t, ""), context, c, rest => css.simple(d, rest, context)),
208          _ => U.branch(Bool, U32.is_eq(c, 91),
209            _ => css.simple.attr(css.scan(t, 93), context, rest => css.simple(d, rest, context)),
210            _ => U.branch(Bool, U32.is_eq(c, 58),
211              _ => css.simple.pseudo(css.ident(t, ""), context, rest => css.simple(d, rest, context), arg => css.simple(d, arg, context)),
212              _ => css.simple.scan(css.ident(s, ""), context, 0, rest => css.simple(d, rest, context))))))
213
214@unsafe
215def css.match(+steps: List<&2, CSSStep>, +contexts: List<&2, CSSContext>, +one: Bool) -> Bool:
216  match steps contexts:
217    case Nil{} _: True{}
218    case Con{step, rest} Nil{}: False{}
219    case Con{CSSStep{compound, relation}, rest} Con{CSSContext{node, parents, previous, index, count}, others}:
220      +rel = relation
221      (css.simple(128n, compound, CSSContext{node, parents, previous, index, count}) &&
222        U.branch(Bool, U32.is_eq(rel, 0), _ => True{},
223          _ => css.match(rest, U.choose(List<&2, CSSContext>, U32.is_le(rel, 2), parents, previous), U32.is_eq(rel, 2) || U32.is_eq(rel, 3))))
224        || U.branch(Bool, one, _ => False{}, _ => css.match(steps, others, False{}))
225
226def css.score.scan(scan: U.Scan, weight: U32, next: String -> U32) -> U32:
227  U.Scan{h, t} = scan
228  (weight + next(t) : U32)
229
230def css.score.arg(scan: U.Scan, negation: Bool, next: String -> U32, nested: String -> U32) -> U32:
231  U.Scan{h, t} = scan
232  (U.choose(U32, negation, nested(h), 1000) + next(U.drop(t, ")")) : U32)
233
234def css.score.pseudo(scan: U.Scan, next: String -> U32, nested: String -> U32) -> U32:
235  U.Scan{h, t} = scan
236  +tail = t
237  css.score.arg(U.choose(U.Scan, U.prefix(tail, "("), css.scan(U.drop(tail, "("), 41), U.Scan{"", ")" ++ tail}), U.string.equal(h, "not"), next, nested)
238
239@unsafe
240def css.score(+fuel: Nat, +s: String) -> U32:
241  match fuel s:
242    case 0n _: 0
243    case 1n+d SNil{}: 0
244    case 1n+d SCon{Chr{c}, t}:
245      U.branch(U32, U32.is_eq(c, 42), _ => css.score(d, t),
246        _ => U.branch(U32, U32.is_eq(c, 35) || U32.is_eq(c, 46),
247          _ => css.score.scan(css.ident(t, ""), U.choose(U32, U32.is_eq(c, 35), 1000000, 1000), rest => css.score(d, rest)),
248          _ => U.branch(U32, U32.is_eq(c, 91),
249            _ => css.score.scan(css.scan(t, 93), 1000, rest => css.score(d, U.drop(rest, "]"))),
250            _ => U.branch(U32, U32.is_eq(c, 58),
251              _ => css.score.pseudo(css.ident(t, ""), rest => css.score(d, rest), arg => css.score(d, arg)),
252              _ => css.score.scan(css.ident(s, ""), 1, rest => css.score(d, rest))))))
253
254def css.specificity(steps: List<&2, CSSStep>) -> U32:
255  match steps:
256    case Nil{}: 0
257    case Con{CSSStep{s, relation}, rest}: (css.score(128n, s) + css.specificity(rest) : U32)
258
259type CSSMatch is Data:
260  CSSMatch{matched: Bool, specificity: U32}
261
262def css.select.scan(scan: U.Scan, +context: CSSContext, +best: CSSMatch, next: String -> CSSMatch -> CSSMatch) -> CSSMatch:
263  U.Scan{h, t} = scan
264  CSSMatch{found, score} = best
265  +sel = U.trim.both(h)
266  +parts = css.parts(sel, 0, Nil{})
267  +hit = Bool.not(U.string.equal(sel, "")) && css.match(parts, Con{context, Nil{}}, True{})
268  +weight = css.specificity(parts)
269  next(U.drop(t, ","))(CSSMatch{found || hit, U.choose(U32, hit && U32.is_gt(weight, score), weight, score)})
270
271@unsafe
272def css.select(s: String, +context: CSSContext, best: CSSMatch) -> CSSMatch:
273  match s:
274    case SNil{}: best
275    case SCon{c, t}: css.select.scan(css.scan(SCon{c, t}, 44), context, best, rest => result => css.select(rest, context, result))
276
277type CSSRule is Data:
278  CSSRule{selector: String, declarations: List<&2, XML.Attr>}
279
280def css.declaration.pair(scan: U.Scan) -> XML.Attr:
281  U.Scan{k, t} = scan
282  XML.Attr{css.lower(U.trim.both(k)), U.trim.both(U.drop(t, ":"))}
283
284def css.declaration.expand(a: XML.Attr, rest: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
285  XML.Attr{k, v} = a
286  +key = k
287  +val = v
288  +tail = rest
289  U.choose(List<&2, XML.Attr>, U.string.equal(key, "marker"), Con{XML.Attr{"marker-start", val}, Con{XML.Attr{"marker-mid", val}, Con{XML.Attr{"marker-end", val}, tail}}}, Con{XML.Attr{key, val}, tail})
290
291def css.declarations.scan(scan: U.Scan, next: String -> List<&2, XML.Attr>) -> List<&2, XML.Attr>:
292  U.Scan{h, t} = scan
293  css.declaration.expand(css.declaration.pair(css.scan(h, 58)), next(U.drop(t, ";")))
294
295@unsafe
296def css.declarations(s: String) -> List<&2, XML.Attr>:
297  match s:
298    case SNil{}: Nil{}
299    case SCon{c, t}: css.declarations.scan(css.scan(SCon{c, t}, 59), rest => css.declarations(rest))
300
301def css.media.feature(scan: U.Scan, +w: F32, +h: F32) -> Bool:
302  U.Scan{k, v} = scan
303  +key = U.trim.both(k)
304  +value = css.lower(U.trim.both(v))
305  +x = U.unit(U.number(value), w)
306  U.choose(Bool, U.string.equal(key, "min-width"), F32.is_ge(w, x),
307    U.choose(Bool, U.string.equal(key, "max-width"), F32.is_le(w, x),
308      U.choose(Bool, U.string.equal(key, "width"), F32.is_eq(w, x),
309        U.choose(Bool, U.string.equal(key, "min-height"), F32.is_ge(h, x),
310          U.choose(Bool, U.string.equal(key, "max-height"), F32.is_le(h, x),
311            U.choose(Bool, U.string.equal(key, "height"), F32.is_eq(h, x),
312              U.string.equal(key, "orientation") && U.choose(Bool, U.string.equal(value, "portrait"), F32.is_ge(h, w), U.string.equal(value, "landscape") && F32.is_gt(w, h))))))))
313
314def css.media.parens(scan: U.Scan, w: F32, h: F32, next: String -> Bool) -> Bool:
315  U.Scan{body, tail} = scan
316  css.media.feature(U.until(body, 58, ""), w, h) && next(U.drop(tail, ")"))
317
318def css.media.result(+word: String, +result: Bool) -> Bool:
319  U.choose(Bool, U.string.equal(word, "not"), Bool.not(result),
320    (U.string.equal(word, "screen") || U.string.equal(word, "all") || U.string.equal(word, "and") || U.string.equal(word, "only")) && result)
321
322def css.media.word(scan: U.Scan, next: String -> Bool) -> Bool:
323  U.Scan{head, tail} = scan
324  +word = head
325  U.branch(Bool, U.string.equal(word, ""), _ => False{}, _ => css.media.result(word, next(tail)))
326
327@unsafe
328def css.media.query(+s: String, +w: F32, +h: F32) -> Bool:
329  match s:
330    case SNil{}: True{}
331    case SCon{Chr{c}, t}:
332      U.branch(Bool, U.space(c), _ => css.media.query(t, w, h),
333        _ => U.branch(Bool, U32.is_eq(c, 40), _ => css.media.parens(css.scan(t, 41), w, h, rest => css.media.query(rest, w, h)),
334          _ => css.media.word(css.ident(s, ""), rest => css.media.query(rest, w, h))))
335
336def css.media.list(scan: U.Scan, w: F32, h: F32, next: String -> Bool) -> Bool:
337  U.Scan{head, tail} = scan
338  css.media.query(css.lower(U.trim.both(head)), w, h) || next(U.drop(tail, ","))
339
340@unsafe
341def css.media.more(s: String, +w: F32, +h: F32) -> Bool:
342  match s:
343    case SNil{}: False{}
344    case SCon{c, t}: css.media.list(css.scan(SCon{c, t}, 44), w, h, rest => css.media.more(rest, w, h))
345
346def css.media(+s: String, w: F32, h: F32) -> Bool:
347  U.string.equal(U.trim(s), "") || css.media.more(s, w, h)
348
349def css.rules.append(xs: List<&2, CSSRule>, ys: List<&2, CSSRule>) -> List<&2, CSSRule>:
350  match xs:
351    case Nil{}: ys
352    case Con{h, t}: Con{h, css.rules.append(t, ys)}
353
354def css.rules.body(selector: String, scan: U.Scan, w: F32, h: F32,
355  next: String -> List<&2, CSSRule>, nested: String -> List<&2, CSSRule>) -> List<&2, CSSRule>:
356  U.Scan{body, tail} = scan
357  +sel = U.trim.both(selector)
358  +content = body
359  +following = next(U.drop(tail, "}"))
360  U.branch(List<&2, CSSRule>, U.prefix(sel, "@"),
361    _ => U.branch(List<&2, CSSRule>, U.prefix(css.lower(sel), "@media") && css.media(U.drop(sel, "@media"), w, h),
362      _ => css.rules.append(nested(content), following), _ => following),
363    _ => Con{CSSRule{sel, css.declarations(content)}, following})
364
365def css.rules.head(scan: U.Scan, w: F32, h: F32,
366  next: String -> List<&2, CSSRule>, nested: String -> List<&2, CSSRule>) -> List<&2, CSSRule>:
367  U.Scan{head, tail} = scan
368  +rest = tail
369  css.rules.body(head, U.choose(U.Scan, U.prefix(rest, ";"), U.Scan{"", "}" ++ U.drop(rest, ";")}, css.scan(U.drop(rest, "{"), 125)), w, h, next, nested)
370
371@unsafe
372def css.rules(s: String, +w: F32, +h: F32) -> List<&2, CSSRule>:
373  match s:
374    case SNil{}: Nil{}
375    case SCon{c, t}: css.rules.head(css.scan(SCon{c, t}, 1), w, h, rest => css.rules(rest, w, h), body => css.rules(body, w, h))
376
377def css.text(xs: List<&2, XML.Element>) -> String:
378  match xs:
379    case Nil{}: ""
380    case Con{XML.Element{n, as, kids, txt}, rest}: txt ++ css.text(kids) ++ css.text(rest)
381
382def css.sheets(+xs: List<&2, XML.Element>, +w: F32, +h: F32) -> String:
383  match xs:
384    case Nil{}: ""
385    case Con{XML.Element{n, as, kids, txt}, rest}:
386      +nodes = kids
387      U.branch(String, U.string.equal(n, "style"),
388        _ => U.choose(String, U.string.equal(css.lower(XML.attr.get(as, "type", "text/css")), "text/css") && css.media(XML.attr.get(as, "media", ""), w, h), css.text(nodes), ""),
389        _ => css.sheets(nodes, w, h)) ++ "\n" ++ css.sheets(rest, w, h)
390
391type CSSChoice is Data:
392  CSSChoice{key: String, value: String, important: Bool, specificity: U32}
393
394def css.choice.scan(+k: String, value: U.Scan, score: U32) -> CSSChoice:
395  U.Scan{h, t} = value
396  +strong = U.string.equal(css.lower(U.trim.both(U.drop(t, "!"))), "important")
397  CSSChoice{k, U.trim.both(h), strong, score}
398
399def css.wins(a: CSSChoice, b: CSSChoice) -> Bool:
400  CSSChoice{ak, av, ai, aw} = a
401  CSSChoice{bk, bv, bi, bw} = b
402  +astrong = ai
403  +bstrong = bi
404  (astrong && Bool.not(bstrong)) || (((astrong && bstrong) || (Bool.not(astrong) && Bool.not(bstrong))) && U32.is_ge(aw, bw))
405
406def css.insert(+value: CSSChoice, +xs: List<&2, CSSChoice>) -> List<&2, CSSChoice>:
407  CSSChoice{nk, nv, ni, nw} = value
408  match xs:
409    case Nil{}: Con{value, Nil{}}
410    case Con{CSSChoice{k, v, imp, weight}, rest}:
411      +old = {CSSChoice{k, v, imp, weight} : CSSChoice}
412      U.branch(List<&2, CSSChoice>, U.string.equal(nk, k), _ => Con{U.choose(CSSChoice, css.wins(value, old), value, old), rest},
413        _ => Con{old, css.insert(value, rest)})
414
415def css.cascade(ds: List<&2, XML.Attr>, +score: U32, +choices: List<&2, CSSChoice>) -> List<&2, CSSChoice>:
416  match ds:
417    case Nil{}: choices
418    case Con{XML.Attr{k, v}, rest}:
419      +value = v
420      +key = k
421      css.cascade(rest, score, U.branch(List<&2, CSSChoice>, U.string.equal(key, "") || U.string.equal(value, ""), _ => choices,
422        _ => css.insert(css.choice.scan(key, css.scan(value, 33), score), choices)))
423
424def css.rule.matched(result: CSSMatch, ds: List<&2, XML.Attr>, +choices: List<&2, CSSChoice>) -> List<&2, CSSChoice>:
425  CSSMatch{matched, score} = result
426  U.branch(List<&2, CSSChoice>, matched, _ => css.cascade(ds, score, choices), _ => choices)
427
428def css.apply.rules(rs: List<&2, CSSRule>, +context: CSSContext, choices: List<&2, CSSChoice>) -> List<&2, CSSChoice>:
429  match rs:
430    case Nil{}: choices
431    case Con{CSSRule{sel, ds}, rest}:
432      css.apply.rules(rest, context, css.rule.matched(css.select(sel, context, CSSMatch{False{}, 0}), ds, choices))
433
434def css.chosen(xs: List<&2, CSSChoice>, attrs: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
435  match xs:
436    case Nil{}: attrs
437    case Con{CSSChoice{k, v, important, weight}, rest}: Con{XML.Attr{k, v}, css.chosen(rest, attrs)}
438
439def css.styled(+context: CSSContext, rules: List<&2, CSSRule>) -> List<&2, XML.Attr>:
440  CSSContext{XML.Element{n, as, kids, txt}, parents, previous, index, count} = context
441  +attrs = as
442  Con{XML.Attr{"#css-ready", "1"}, Con{XML.Attr{"style", ""}, css.chosen(css.cascade(css.declarations(XML.attr.get(attrs, "style", "")), 1000000000,
443    css.apply.rules(rules, context, Nil{})), attrs)}}
444
445def css.element.count(xs: List<&2, XML.Element>) -> U32:
446  match xs:
447    case Nil{}: 0
448    case Con{XML.Element{n, as, kids, txt}, rest}: (U.choose(U32, U.string.equal(n, "#text"), 0, 1) + css.element.count(rest) : U32)
449
450@unsafe
451def css.tree(+xs: List<&2, XML.Element>, +rules: List<&2, CSSRule>, +parents: List<&2, CSSContext>, +previous: List<&2, CSSContext>, +index: U32, +count: U32) -> List<&2, XML.Element>:
452  match xs:
453    case Nil{}: Nil{}
454    case Con{XML.Element{n, as, kids, txt}, rest}:
455      +tagname = n
456      +children = kids
457      +node = {XML.Element{tagname, as, children, txt} : XML.Element}
458      +context = {CSSContext{node, parents, previous, index, count} : CSSContext}
459      U.branch(List<&2, XML.Element>, U.string.equal(tagname, "#text"),
460        _ => Con{node, css.tree(rest, rules, parents, previous, index, count)},
461        _ => Con{XML.Element{tagname, css.styled(context, rules), css.tree(children, rules, Con{context, parents}, Nil{}, 1, css.element.count(children)), txt},
462          css.tree(rest, rules, parents, Con{context, previous}, U32.inc(index), count)})