package miaou-core

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

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
(******************************************************************************)
(*                                                                            *)
(* SPDX-License-Identifier: MIT                                               *)
(* Copyright (c) 2026 Nomadic Labs <contact@nomadic-labs.com>                 *)
(*                                                                            *)
(******************************************************************************)

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}

(* Parsing helpers *)

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 =
  (* Expects to start after ':' *)
  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" ->
          (* Parse (even), (odd), or (n) *)
          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
            | _ ->
                (* Try parsing a number *)
                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

  (* Parse element name (optional) *)
  let element, i =
    if i < len && s.[i] = '*' then (None, i + 1) (* Universal selector *)
    else
      match parse_ident s i with
      | Some (name, i) -> (Some name, i)
      | None -> (None, i)
    (* No element, will have pseudo-classes *)
  in

  (* Parse pseudo-classes *)
  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

  (* Must have at least element or pseudo-class *)
  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
      (* End of string - finalize last part with no combinator *)
      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
            (* Check for combinator *)
            let comb, i =
              if s.[i] = '>' then (Some Child, skip_whitespace s (i + 1))
              else
                (* Space = descendant combinator *)
                (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)

(* Convert to string *)

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

(* Matching *)

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 =
  (* Element must match (or be None for universal) *)
  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 ->
      (* Direct child: must match current, then parent must match 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 ->
      (* Descendant: must match current, then some ancestor must match rest *)
      if not (matches_simple_selector sel ctx) then false
      else begin
        let rec try_ancestors anc =
          match anc with
          | [] -> List.length rest = 0 (* Only valid if nothing left to match *)
          | 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 =
  (* Reverse parts to match from the target (rightmost) first *)
  let reversed_parts = List.rev t.parts in
  matches_with_ancestors reversed_parts ctx.ancestors ctx

(* Specificity *)

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