Source file Cascade.ml
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
open Css_syntax
type origin = User_agent | Author
type sheet = { origin : origin; rules : Css_syntax.rule list }
type media = { width : float; height : float }
let feature (m : media) (inside : component list) : bool =
let ctx : Css_values.context = { em = 16.; rem = 16.; viewport_width = m.width; viewport_height = m.height } in
let len cs = match Css_values.parts cs with [ c ] -> Option.map (fun (l : Css_values.length) -> l.px) (Css_values.length ctx c) | _ -> None in
match split_on Colon (trim inside) with
| [ name; value ] -> (
let name = String.lowercase_ascii (to_string (trim name)) and v = String.lowercase_ascii (to_string (trim value)) in
match name with
| "min-width" -> ( match len value with Some l -> m.width >= l | None -> false)
| "max-width" -> ( match len value with Some l -> m.width <= l | None -> false)
| "min-height" -> ( match len value with Some l -> m.height >= l | None -> false)
| "max-height" -> ( match len value with Some l -> m.height <= l | None -> false)
| "orientation" -> v = if m.width >= m.height then "landscape" else "portrait"
| "prefers-color-scheme" -> v = "light"
| "prefers-reduced-motion" -> v = "no-preference"
| "hover" | "any-hover" -> v = "hover"
| "pointer" | "any-pointer" -> v = "fine"
| _ -> false)
| [ name ] -> ( match String.lowercase_ascii (to_string (trim name)) with "color" | "hover" | "pointer" -> true | _ -> false)
| _ -> false
let query (m : media) (cs : component list) : bool =
let words = Css_values.parts cs in
let negate, words =
match words with
| Token (Ident n) :: rest when String.lowercase_ascii n = "not" -> (true, rest)
| Token (Ident n) :: rest when String.lowercase_ascii n = "only" -> (false, rest)
| _ -> (false, words)
in
let holds =
List.for_all
(fun (c : component) ->
match c with
| Token (Ident n) -> ( match String.lowercase_ascii n with "and" | "screen" | "all" -> true | _ -> false)
| Block ('(', inside) -> feature m inside
| _ -> false)
words
in
if negate then not holds else holds
let media_matches (m : media) (cs : component list) : bool =
let cs = trim cs in
cs = [] || List.exists (fun q -> query m q) (split_on Comma cs)
let rec flatten (m : media) (origin : origin) (rules : Css_syntax.rule list) : (origin * Selectors.complex * declaration list) list =
List.concat_map
(fun (r : Css_syntax.rule) ->
match r with
| Style_rule { prelude; declarations } -> (
match Selectors.parse prelude with
| Some sels -> List.map (fun s -> (origin, s, declarations)) sels
| None -> [])
| At_rule { name = "media"; prelude; block = Some b } -> if media_matches m prelude then flatten m origin (rules_of_block b) else []
| At_rule { name = "supports"; block = Some b; _ } -> flatten m origin (rules_of_block b)
| At_rule _ -> [])
rules
let rules (m : media) (sheets : sheet list) = List.concat_map (fun s -> flatten m s.origin s.rules) sheets
type entry = {
layer_normal : int;
layer_important : int;
specificity : int * int * int;
order : int;
selector : Selectors.complex;
declarations : declaration list;
needs : string list;
}
let key (sel : Selectors.complex) : string =
match List.rev sel with
| (compound, _) :: _ -> (
match List.find_map (function Selectors.Id i -> Some ("#" ^ i) | _ -> None) compound with
| Some k -> k
| None -> (
match List.find_map (function Selectors.Class c -> Some ("." ^ c) | _ -> None) compound with
| Some k -> k
| None -> ( match List.find_map (function Selectors.Type n -> Some n | _ -> None) compound with Some k -> k | None -> "*")))
| [] -> "*"
let words (s : string) : string list = List.filter (( <> ) "") (String.split_on_char ' ' s)
let keys_of (e : Dom.element) : string list =
e.name
:: (match Dom.attribute "id" e with Some i -> [ "#" ^ i ] | None -> [])
@ List.map (fun c -> "." ^ c) (match Dom.attribute "class" e with Some c -> words c | None -> [])
let needs (sel : Selectors.complex) : string list =
let rec go (before : (Selectors.simple list * Selectors.combinator option) list) : string list =
match before with
| (compound, Some (Selectors.Descendant | Selectors.Child)) :: rest ->
List.filter_map (function Selectors.Id i -> Some ("#" ^ i) | Selectors.Class c -> Some ("." ^ c) | Selectors.Type n -> Some n | _ -> None) compound
@ go rest
| _ -> []
in
match List.rev sel with _ :: before -> go before | [] -> []
let find_element (table : (int, Dom.element * 'a) Hashtbl.t) (e : Dom.element) : 'a option =
List.assq_opt e (Hashtbl.find_all table (Hashtbl.hash e))
let hints (ancestors : Dom.element list) (e : Dom.element) : string =
let attr name (e : Dom.element) = Dom.attribute ~extensions:true name e in
let b = Buffer.create 64 in
let add prop value = Buffer.add_string b (Printf.sprintf "%s: %s;" prop value) in
let length v =
let v = String.trim v in
if String.ends_with ~suffix:"%" v then Option.map (fun _ -> v) (float_of_string_opt (String.sub v 0 (String.length v - 1)))
else Option.map (fun n -> Printf.sprintf "%gpx" n) (float_of_string_opt v)
in
let color v =
let v = String.trim v in
if String.length v = 6 && String.for_all (function '0' .. '9' | 'a' .. 'f' | 'A' .. 'F' -> true | _ -> false) v then "#" ^ v else v
in
let lengths props = List.iter (fun (a, prop) -> match Option.bind (attr a e) length with Some l -> add prop l | None -> ()) props in
let table = List.find_opt (fun (a : Dom.element) -> a.name = "table") ancestors in
let lower name = Option.map String.lowercase_ascii (attr name e) in
(match attr "bgcolor" e with Some c -> add "background-color" (color c) | None -> ());
(match e.name with
| "body" -> ( match attr "text" e with Some c -> add "color" (color c) | None -> ())
| "a" when Dom.attribute "href" e <> None -> (
match Option.bind (List.find_opt (fun (a : Dom.element) -> a.name = "body") ancestors) (attr "link") with
| Some c -> add "color" (color c)
| None -> ())
| "font" -> (
(match attr "color" e with Some c -> add "color" (color c) | None -> ());
match Option.bind (attr "size" e) Looks.font_scale with Some k -> add "font-size" (Printf.sprintf "%gpx" (k *. 16.)) | None -> ())
| "table" -> (
lengths [ ("width", "width"); ("height", "height") ];
(match Option.bind (attr "border" e) (fun v -> if v = "" then Some 1. else float_of_string_opt v) with
| Some n when n > 0. -> add "border" (Printf.sprintf "%gpx outset gray" n)
| _ -> ());
match lower "align" with
| Some "center" -> add "margin-left" "auto"; add "margin-right" "auto"
| Some ("left" | "right" as side) -> add "float" side
| _ -> ())
| "td" | "th" -> (
lengths [ ("width", "width"); ("height", "height") ];
(match Option.bind table (attr "cellpadding") with Some p -> Option.iter (add "padding") (length p) | None -> ());
(match Option.bind table (attr "border") with
| Some v when v = "" || (match float_of_string_opt v with Some n -> n > 0. | None -> false) -> add "border" "1px inset gray"
| _ -> ());
if attr "nowrap" e <> None then add "white-space" "nowrap";
match (match lower "valign" with Some v -> Some v | None -> Option.bind (List.nth_opt ancestors 0) (fun tr -> Option.map String.lowercase_ascii (attr "valign" tr))) with
| Some ("top" | "middle" | "bottom" | "baseline" as v) -> add "vertical-align" v
| _ -> ())
| "tr" -> lengths [ ("height", "height") ]
| "svg" | "video" -> lengths [ ("width", "width"); ("height", "height") ]
| "img" -> (
lengths [ ("width", "width"); ("height", "height"); ("hspace", "margin-left"); ("hspace", "margin-right"); ("vspace", "margin-top"); ("vspace", "margin-bottom") ];
match lower "align" with
| Some ("left" | "right" as side) -> add "float" side
| Some ("middle" | "absmiddle") -> add "vertical-align" "middle"
| Some ("top" | "texttop") -> add "vertical-align" "top"
| _ -> ())
| "hr" -> (
lengths [ ("width", "width") ];
match Option.bind (attr "size" e) float_of_string_opt with Some n -> add "height" (Printf.sprintf "%gpx" (Float.max 0. (n -. 2.))) | None -> ())
| "br" -> ( match lower "clear" with Some ("left" | "right" | "both" as v) -> add "clear" v | Some "all" -> add "clear" "both" | _ -> ())
| _ -> ());
(if e.name <> "table" && e.name <> "img" then
match lower "align" with Some ("left" | "right" | "center" | "justify" as v) -> add "text-align" v | _ -> ());
Buffer.contents b
let cascade ?(visited = fun _ -> false) (m : media) (sheets : sheet list) (root : Dom.element) : Dom.element -> (string * component list) list =
let index : (string, entry) Hashtbl.t = Hashtbl.create 1024 in
List.iteri
(fun order (origin, sel, declarations) ->
if Selectors.pseudo_element sel = None then
let layer_normal, layer_important = match origin with User_agent -> (0, 3) | Author -> (1, 2) in
Hashtbl.add index (key sel)
{ layer_normal; layer_important; specificity = Selectors.specificity sel; order; selector = sel; declarations; needs = needs sel })
(rules m sheets);
let table : (int, Dom.element * (string * component list) list) Hashtbl.t = Hashtbl.create 1024 in
let above : (string, int) Hashtbl.t = Hashtbl.create 256 in
let rec go (ancestors : Dom.element list) (e : Dom.element) =
let keys = keys_of e in
let candidates = List.concat_map (fun k -> Hashtbl.find_all index k) (List.sort_uniq compare ("*" :: keys)) in
let matching =
List.filter (fun en -> List.for_all (Hashtbl.mem above) en.needs && Selectors.matches ~visited en.selector ancestors e) candidates
in
let keyed =
List.concat_map
(fun en ->
List.map
(fun (d : declaration) -> (((if d.important then en.layer_important else en.layer_normal), en.specificity, en.order), d))
en.declarations)
matching
@ (match hints ancestors e with "" -> [] | h -> List.map (fun (d : declaration) -> ((1, (0, 0, 0), -1), d)) (parse_declarations h))
@
match Dom.attribute "style" e with
| Some s -> List.map (fun (d : declaration) -> (((if d.important then 2 else 1), (max_int, 0, 0), max_int), d)) (parse_declarations s)
| None -> []
in
let sorted = List.stable_sort (fun (a, _) (b, _) -> compare a b) keyed in
let winning = List.fold_left (fun acc (_, (d : declaration)) -> (d.name, d.value) :: List.remove_assoc d.name acc) [] sorted in
Hashtbl.add table (Hashtbl.hash e) (e, List.rev winning);
List.iter (fun k -> Hashtbl.replace above k (1 + Option.value (Hashtbl.find_opt above k) ~default:0)) keys;
List.iter (fun (n : Dom.node) -> match n with Element c -> go (e :: ancestors) c | Text _ -> ()) e.children;
List.iter (fun k -> match Hashtbl.find_opt above k with Some 1 -> Hashtbl.remove above k | Some n -> Hashtbl.replace above k (n - 1) | None -> ()) keys
in
go [] root;
fun e -> match find_element table e with Some ds -> ds | None -> []
type source = Rule of { sheet : int; selector : Selectors.complex } | Hint | Style_attribute
let explain ?(visited = fun _ -> false) (m : media) (sheets : sheet list) (root : Dom.element) (e : Dom.element) :
(string * component list * source * bool) list =
let rec path (x : Dom.element) (above : Dom.element list) : Dom.element list option =
if x == e then Some above
else List.find_map (fun (n : Dom.node) -> match n with Element c -> path c (x :: above) | Text _ -> None) x.children
in
match path root [] with
| None -> []
| Some ancestors ->
let order = ref 0 in
let keyed =
List.concat
(List.mapi
(fun i (sh : sheet) ->
let layer_normal, layer_important = match sh.origin with User_agent -> (0, 3) | Author -> (1, 2) in
List.concat_map
(fun (_, sel, (declarations : declaration list)) ->
incr order;
if Selectors.pseudo_element sel = None && Selectors.matches ~visited sel ancestors e then
List.map
(fun (d : declaration) ->
( ((if d.important then layer_important else layer_normal), Selectors.specificity sel, !order),
(d, Rule { sheet = i; selector = sel }) ))
declarations
else [])
(flatten m sh.origin sh.rules))
sheets)
@ (match hints ancestors e with "" -> [] | h -> List.map (fun (d : declaration) -> ((1, (0, 0, 0), -1), (d, Hint))) (parse_declarations h))
@
match Dom.attribute "style" e with
| Some s -> List.map (fun (d : declaration) -> (((if d.important then 2 else 1), (max_int, 0, 0), max_int), (d, Style_attribute))) (parse_declarations s)
| None -> []
in
let sorted = List.stable_sort (fun (a, _) (b, _) -> compare a b) keyed in
List.fold_left
(fun acc (_, ((d : declaration), src)) -> (d.name, d.value, src, d.important) :: List.filter (fun (n, _, _, _) -> n <> d.name) acc)
[] sorted
|> List.rev