Source file selector.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
type pseudo_class =
| Focus
| Selected
| Hover
| Disabled
| First_child
| Last_child
| Nth_child_even
| Nth_child_odd
| Nth_child of int
[@@deriving yojson]
type simple_selector = {
element : string option;
pseudo_classes : pseudo_class list;
}
[@@deriving yojson]
type combinator = Descendant | Child [@@deriving yojson]
type t = {parts : (simple_selector * combinator option) list}
[@@deriving yojson]
type match_context = {
widget_name : string;
focused : bool;
selected : bool;
hover : bool;
disabled : bool;
child_index : int option;
child_count : int option;
ancestors : string list;
}
let empty_context =
{
widget_name = "";
focused = false;
selected = false;
hover = false;
disabled = false;
child_index = None;
child_count = None;
ancestors = [];
}
let context_of_widget name = {empty_context with widget_name = name}
let is_ident_char c =
(c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| c = '_' || c = '-'
let skip_whitespace s i =
let len = String.length s in
let rec loop i =
if i < len && (s.[i] = ' ' || s.[i] = '\t') then loop (i + 1) else i
in
loop i
let parse_ident s i =
let len = String.length s in
let start = i in
let rec loop i = if i < len && is_ident_char s.[i] then loop (i + 1) else i in
let end_i = loop i in
if end_i > start then Some (String.sub s start (end_i - start), end_i)
else None
let parse_pseudo_class s i =
match parse_ident s i with
| None -> None
| Some (name, i) -> (
let name_lower = String.lowercase_ascii name in
match name_lower with
| "focus" -> Some (Focus, i)
| "selected" -> Some (Selected, i)
| "hover" -> Some (Hover, i)
| "disabled" -> Some (Disabled, i)
| "first-child" -> Some (First_child, i)
| "last-child" -> Some (Last_child, i)
| "nth-child" ->
let len = String.length s in
if i < len && s.[i] = '(' then
let i = i + 1 in
let i = skip_whitespace s i in
match parse_ident s i with
| Some ("even", i) ->
let i = skip_whitespace s i in
if i < len && s.[i] = ')' then Some (Nth_child_even, i + 1)
else None
| Some ("odd", i) ->
let i = skip_whitespace s i in
if i < len && s.[i] = ')' then Some (Nth_child_odd, i + 1)
else None
| _ ->
let start = i in
let rec parse_num i =
if i < len && s.[i] >= '0' && s.[i] <= '9' then
parse_num (i + 1)
else i
in
let end_i = parse_num i in
if end_i > start then
let n = int_of_string (String.sub s start (end_i - start)) in
let i = skip_whitespace s end_i in
if i < len && s.[i] = ')' then Some (Nth_child n, i + 1)
else None
else None
else None
| _ -> None)
let parse_simple_selector s i =
let len = String.length s in
let i = skip_whitespace s i in
let element, i =
if i < len && s.[i] = '*' then (None, i + 1)
else
match parse_ident s i with
| Some (name, i) -> (Some name, i)
| None -> (None, i)
in
let rec parse_pseudo_classes i acc =
if i < len && s.[i] = ':' then
match parse_pseudo_class s (i + 1) with
| Some (pc, i) -> parse_pseudo_classes i (pc :: acc)
| None -> (List.rev acc, i)
else (List.rev acc, i)
in
let pseudo_classes, i = parse_pseudo_classes i [] in
if element = None && pseudo_classes = [] then None
else Some ({element; pseudo_classes}, i)
let parse s =
let len = String.length s in
let rec loop i acc =
let i = skip_whitespace s i in
if i >= len then
match acc with
| [] -> None
| (sel, _) :: rest -> Some {parts = List.rev ((sel, None) :: rest)}
else
match parse_simple_selector s i with
| None -> None
| Some (sel, i) ->
let i = skip_whitespace s i in
if i >= len then Some {parts = List.rev ((sel, None) :: acc)}
else
let comb, i =
if s.[i] = '>' then (Some Child, skip_whitespace s (i + 1))
else
(Some Descendant, i)
in
loop i ((sel, comb) :: acc)
in
loop 0 []
let parse_exn s =
match parse s with
| Some sel -> sel
| None -> invalid_arg ("Invalid selector: " ^ s)
let pseudo_class_to_string = function
| Focus -> ":focus"
| Selected -> ":selected"
| Hover -> ":hover"
| Disabled -> ":disabled"
| First_child -> ":first-child"
| Last_child -> ":last-child"
| Nth_child_even -> ":nth-child(even)"
| Nth_child_odd -> ":nth-child(odd)"
| Nth_child n -> Printf.sprintf ":nth-child(%d)" n
let simple_selector_to_string sel =
let elem = match sel.element with Some e -> e | None -> "" in
let pseudos =
String.concat "" (List.map pseudo_class_to_string sel.pseudo_classes)
in
elem ^ pseudos
let to_string t =
let rec loop = function
| [] -> ""
| [(sel, _)] -> simple_selector_to_string sel
| (sel, Some Child) :: rest ->
simple_selector_to_string sel ^ " > " ^ loop rest
| (sel, Some Descendant) :: rest ->
simple_selector_to_string sel ^ " " ^ loop rest
| (sel, None) :: rest -> simple_selector_to_string sel ^ " " ^ loop rest
in
loop t.parts
let matches_pseudo_class pc ctx =
match pc with
| Focus -> ctx.focused
| Selected -> ctx.selected
| Hover -> ctx.hover
| Disabled -> ctx.disabled
| First_child -> ( match ctx.child_index with Some 0 -> true | _ -> false)
| Last_child -> (
match (ctx.child_index, ctx.child_count) with
| Some i, Some c -> i = c - 1
| _ -> false)
| Nth_child_even -> (
match ctx.child_index with Some i -> i mod 2 = 0 | None -> false)
| Nth_child_odd -> (
match ctx.child_index with Some i -> i mod 2 = 1 | None -> false)
| Nth_child n -> (
match ctx.child_index with Some i -> i = n - 1 | None -> false)
let matches_simple_selector sel ctx =
let elem_matches =
match sel.element with
| None -> true
| Some e ->
String.lowercase_ascii e = String.lowercase_ascii ctx.widget_name
in
elem_matches
&& List.for_all (fun pc -> matches_pseudo_class pc ctx) sel.pseudo_classes
let rec matches_with_ancestors parts ancestors ctx =
match parts with
| [] -> true
| [(sel, _)] -> matches_simple_selector sel ctx
| (sel, Some Child) :: rest ->
if not (matches_simple_selector sel ctx) then false
else begin
match ancestors with
| [] -> false
| parent :: grandparents ->
let parent_ctx =
{ctx with widget_name = parent; ancestors = grandparents}
in
matches_with_ancestors rest grandparents parent_ctx
end
| (sel, Some Descendant) :: rest ->
if not (matches_simple_selector sel ctx) then false
else begin
let rec try_ancestors anc =
match anc with
| [] -> List.length rest = 0
| parent :: grandparents ->
let parent_ctx =
{ctx with widget_name = parent; ancestors = grandparents}
in
matches_with_ancestors rest grandparents parent_ctx
|| try_ancestors grandparents
in
try_ancestors ancestors
end
| (sel, None) :: rest ->
matches_simple_selector sel ctx
&& matches_with_ancestors rest ctx.ancestors ctx
let matches t ctx =
let reversed_parts = List.rev t.parts in
matches_with_ancestors reversed_parts ctx.ancestors ctx
type specificity = int * int
let specificity t =
let pseudo_count = ref 0 in
let elem_count = ref 0 in
List.iter
(fun (sel, _) ->
if sel.element <> None then incr elem_count ;
pseudo_count := !pseudo_count + List.length sel.pseudo_classes)
t.parts ;
(!pseudo_count, !elem_count)
let compare_specificity (p1, e1) (p2, e2) =
match compare p1 p2 with 0 -> compare e1 e2 | c -> c