Source file Selectors.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
318
319
320
321
322
323
324
325
326
327
328
329
330
open Css_syntax
type attr_op = Exists | Equals | Includes | Dash | Prefix | Suffix | Substring
type pseudo_class =
| First_child
| Last_child
| Only_child
| Nth_child of int * int
| Not of complex list
| Is of complex list * bool
| Link
| Visited
| Hover
| Active
| Focus
| Root
| Empty
| Checked
| Disabled
| Enabled
and simple =
| Type of string
| Universal
| Id of string
| Class of string
| Attr of string * attr_op * string * bool
| Pseudo of pseudo_class
| Pseudo_element of string
and combinator = Descendant | Child | Next_sibling | Subsequent_sibling
and complex = (simple list * combinator option) list
exception Invalid
let nth (s : string) : int * int =
let s = String.lowercase_ascii (String.concat "" (String.split_on_char ' ' s)) in
let int_of s = match s with "" | "+" -> 1 | "-" -> -1 | s -> ( match int_of_string_opt s with Some n -> n | None -> raise Invalid) in
match s with
| "odd" -> (2, 1)
| "even" -> (2, 0)
| _ -> (
match String.index_opt s 'n' with
| None -> (0, match int_of_string_opt s with Some b -> b | None -> raise Invalid)
| Some i ->
let a = int_of (String.sub s 0 i) in
let rest = String.sub s (i + 1) (String.length s - i - 1) in
let b = if rest = "" then 0 else match int_of_string_opt (if rest.[0] = '+' then String.sub rest 1 (String.length rest - 1) else rest) with Some b -> b | None -> raise Invalid in
(a, b))
let rec parse_list (cs : component list) : complex list =
List.map parse_complex (split_on Comma cs)
and parse_complex (cs : component list) : complex =
let rec go (cs : component list) (current : simple list) (acc : complex) : complex =
match cs with
| [] -> if current = [] then raise Invalid else List.rev ((List.rev current, None) :: acc)
| Token Whitespace :: rest -> (
let rec skip = function Token Whitespace :: r -> skip r | l -> l in
match skip rest with
| [] -> go [] current acc
| Token (Delim (('>' | '+' | '~') as c)) :: rest -> combine c rest current acc
| rest -> if current = [] then go rest current acc else go rest [] ((List.rev current, Some Descendant) :: acc))
| Token (Delim (('>' | '+' | '~') as c)) :: rest -> combine c rest current acc
| _ ->
let s, rest = simple cs in
go rest (s :: current) acc
and combine c rest current acc =
if current = [] then raise Invalid;
let comb = match c with '>' -> Child | '+' -> Next_sibling | _ -> Subsequent_sibling in
let rec skip = function Token Whitespace :: r -> skip r | l -> l in
go (skip rest) [] ((List.rev current, Some comb) :: acc)
in
go cs [] []
and simple (cs : component list) : simple * component list =
match cs with
| Token (Ident n) :: rest -> (Type (String.lowercase_ascii n), rest)
| Token (Delim '*') :: rest -> (Universal, rest)
| Token (Hash h) :: rest -> (Id h, rest)
| Token (Delim '.') :: Token (Ident c) :: rest -> (Class c, rest)
| Block ('[', inside) :: rest -> (attribute (trim inside), rest)
| Token Colon :: Token Colon :: Token (Ident p) :: rest -> (Pseudo_element (String.lowercase_ascii p), rest)
| Token Colon :: Token (Ident (("before" | "after" | "first-line" | "first-letter") as p)) :: rest -> (Pseudo_element p, rest)
| Token Colon :: Token (Ident p) :: rest ->
let pc =
match String.lowercase_ascii p with
| "first-child" -> First_child
| "last-child" -> Last_child
| "only-child" -> Only_child
| "link" | "any-link" -> Link
| "visited" -> Visited
| "hover" -> Hover
| "active" -> Active
| "focus" | "focus-visible" | "focus-within" -> Focus
| "root" -> Root
| "empty" -> Empty
| "checked" -> Checked
| "disabled" -> Disabled
| "enabled" -> Enabled
| "first-of-type" -> First_child
| "last-of-type" -> Last_child
| _ -> raise Invalid
in
(Pseudo pc, rest)
| Token Colon :: Func (f, args) :: rest -> (
match String.lowercase_ascii f with
| "not" -> (Pseudo (Not (parse_list args)), rest)
| "is" | "matches" | "-webkit-any" -> (Pseudo (Is (parse_list args, true)), rest)
| "where" -> (Pseudo (Is (parse_list args, false)), rest)
| "nth-child" | "nth-of-type" -> (
let a, b = nth (to_string args) in
(Pseudo (Nth_child (a, b)), rest))
| _ -> raise Invalid)
| _ -> raise Invalid
and attribute (cs : component list) : simple =
let rec skip = function Token Whitespace :: r -> skip r | l -> l in
match cs with
| Token (Ident name) :: rest -> (
let name = String.lowercase_ascii name in
let value_and_flag (v : component list) : string * bool =
match skip v with
| Token (Ident s) :: r | Token (String s) :: r -> (
match skip r with
| [] -> (s, false)
| Token (Ident ("i" | "I")) :: r when skip r = [] -> (s, true)
| _ -> raise Invalid)
| _ -> raise Invalid
in
let op o v = let s, i = value_and_flag v in Attr (name, o, s, i) in
match skip rest with
| [] -> Attr (name, Exists, "", false)
| Token (Delim '=') :: v -> op Equals v
| Token (Delim '~') :: Token (Delim '=') :: v -> op Includes v
| Token (Delim '|') :: Token (Delim '=') :: v -> op Dash v
| Token (Delim '^') :: Token (Delim '=') :: v -> op Prefix v
| Token (Delim '$') :: Token (Delim '=') :: v -> op Suffix v
| Token (Delim '*') :: Token (Delim '=') :: v -> op Substring v
| _ -> raise Invalid)
| _ -> raise Invalid
let parse (cs : component list) : complex list option = match parse_list cs with l -> Some l | exception Invalid -> None
let parse_string (s : string) : complex list option = parse (components_of s)
let add (a, b, c) (x, y, z) = (a + x, b + y, c + z)
let rec specificity (sel : complex) : int * int * int =
List.fold_left (fun acc (compound, _) -> List.fold_left (fun acc s -> add acc (of_simple s)) acc compound) (0, 0, 0) sel
and of_simple (s : simple) : int * int * int =
match s with
| Id _ -> (1, 0, 0)
| Class _ | Attr _ -> (0, 1, 0)
| Pseudo (Not l) | Pseudo (Is (l, true)) -> List.fold_left (fun m x -> max m (specificity x)) (0, 0, 0) l
| Pseudo (Is (_, false)) -> (0, 0, 0)
| Pseudo _ -> (0, 1, 0)
| Type _ | Pseudo_element _ -> (0, 0, 1)
| Universal -> (0, 0, 0)
let pseudo_element (sel : complex) : string option =
match List.rev sel with
| (compound, _) :: _ -> List.find_map (function Pseudo_element p -> Some p | _ -> None) compound
| [] -> None
let words (s : string) : string list = List.filter (( <> ) "") (String.split_on_char ' ' (String.map (fun c -> if c = '\t' || c = '\n' then ' ' else c) s))
let element_children (e : Dom.element) : Dom.element list =
List.filter_map (fun (n : Dom.node) -> match n with Element c -> Some c | Text _ -> None) e.children
let siblings (ancestors : Dom.element list) (e : Dom.element) : Dom.element list * Dom.element list =
match ancestors with
| [] -> ([], [])
| parent :: _ ->
let rec split before = function [] -> (List.rev before, []) | x :: after -> if x == e then (List.rev before, after) else split (x :: before) after in
split [] (element_children parent)
let attr_matches (e : Dom.element) (name : string) (op : attr_op) (v : string) (ci : bool) : bool =
match Dom.attribute ~extensions:true name e with
| None -> false
| Some a -> (
let a, v = if ci then (String.lowercase_ascii a, String.lowercase_ascii v) else (a, v) in
match op with
| Exists -> true
| Equals -> a = v
| Includes -> List.mem v (words a)
| Dash -> a = v || String.starts_with ~prefix:(v ^ "-") a
| Prefix -> v <> "" && String.starts_with ~prefix:v a
| Suffix -> v <> "" && String.ends_with ~suffix:v a
| Substring ->
v <> ""
&&
let n = String.length a and m = String.length v in
let rec at i = i + m <= n && (String.sub a i m = v || at (i + 1)) in
at 0)
let rec matches_simple ~visited (ancestors : Dom.element list) (e : Dom.element) (s : simple) : bool =
match s with
| Type n -> e.name = n
| Universal | Pseudo_element _ -> true
| Id i -> Dom.attribute "id" e = Some i
| Class c -> ( match Dom.attribute "class" e with Some cs -> List.mem c (words cs) | None -> false)
| Attr (name, op, v, ci) -> attr_matches e name op v ci
| Pseudo p -> (
let before, after = siblings ancestors e in
match p with
| First_child -> ancestors <> [] && before = []
| Last_child -> ancestors <> [] && after = []
| Only_child -> ancestors <> [] && before = [] && after = []
| Nth_child (a, b) ->
let i = List.length before + 1 in
if a = 0 then i = b else (i - b) mod a = 0 && (i - b) / a >= 0
| Not l -> not (List.exists (fun sel -> matches_complex ~visited sel ancestors e) l)
| Is (l, _) -> List.exists (fun sel -> matches_complex ~visited sel ancestors e) l
| Link -> e.name = "a" && Dom.attribute "href" e <> None && not (match Dom.attribute "href" e with Some h -> visited h | None -> false)
| Visited -> e.name = "a" && (match Dom.attribute "href" e with Some h -> visited h | None -> false)
| Hover | Active | Focus -> false
| Root -> ancestors = []
| Empty -> List.for_all (fun (n : Dom.node) -> match n with Text "" -> true | _ -> false) e.children
| Checked -> Dom.attribute "checked" e <> None || Dom.attribute "selected" e <> None
| Disabled -> Dom.attribute "disabled" e <> None
| Enabled -> List.mem e.name [ "input"; "button"; "select"; "textarea"; "option"; "fieldset" ] && Dom.attribute "disabled" e = None)
and matches_compound ~visited ancestors e (compound : simple list) : bool = List.for_all (matches_simple ~visited ancestors e) compound
and matches_complex ~visited (sel : complex) (ancestors : Dom.element list) (e : Dom.element) : bool =
match List.rev sel with
| [] -> false
| (last, _) :: before -> matches_compound ~visited ancestors e last && left ~visited before ancestors e
and left ~visited (before : (simple list * combinator option) list) (ancestors : Dom.element list) (e : Dom.element) : bool =
match before with
| [] -> true
| (compound, comb) :: rest -> (
match comb with
| Some Child | None -> (
match ancestors with p :: up -> matches_compound ~visited up p compound && left ~visited rest up p | [] -> false)
| Some Descendant ->
let rec up = function
| [] -> false
| p :: above -> (matches_compound ~visited above p compound && left ~visited rest above p) || up above
in
up ancestors
| Some Next_sibling -> (
match List.rev (fst (siblings ancestors e)) with
| s :: _ -> matches_compound ~visited ancestors s compound && left ~visited rest ancestors s
| [] -> false)
| Some Subsequent_sibling ->
List.exists (fun s -> matches_compound ~visited ancestors s compound && left ~visited rest ancestors s) (fst (siblings ancestors e)))
let matches ?(visited = fun _ -> false) (sel : complex) (ancestors : Dom.element list) (e : Dom.element) : bool =
matches_complex ~visited sel ancestors e
let rec to_string (sel : complex) : string =
String.concat ""
(List.map
(fun (compound, comb) ->
String.concat "" (List.map simple_to_string compound)
^ match comb with Some Descendant -> " " | Some Child -> " > " | Some Next_sibling -> " + " | Some Subsequent_sibling -> " ~ " | None -> "")
sel)
and simple_to_string (s : simple) : string =
match s with
| Type n -> n
| Universal -> "*"
| Id i -> "#" ^ i
| Class c -> "." ^ c
| Attr (n, op, v, ci) ->
let o = match op with Exists -> "" | Equals -> "=" | Includes -> "~=" | Dash -> "|=" | Prefix -> "^=" | Suffix -> "$=" | Substring -> "*=" in
"[" ^ n ^ (if op = Exists then "" else o ^ "\"" ^ v ^ "\"") ^ (if ci then " i" else "") ^ "]"
| Pseudo_element p -> "::" ^ p
| Pseudo p -> (
":"
^
match p with
| First_child -> "first-child"
| Last_child -> "last-child"
| Only_child -> "only-child"
| Nth_child (a, b) -> Printf.sprintf "nth-child(%dn+%d)" a b
| Not l -> "not(" ^ String.concat ", " (List.map to_string l) ^ ")"
| Is (l, counts) -> (if counts then "is(" else "where(") ^ String.concat ", " (List.map to_string l) ^ ")"
| Link -> "link"
| Visited -> "visited"
| Hover -> "hover"
| Active -> "active"
| Focus -> "focus"
| Root -> "root"
| Empty -> "empty"
| Checked -> "checked"
| Disabled -> "disabled"
| Enabled -> "enabled")