package miaou-core
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
Miaou core/widgets (no drivers, no SDL)
Install
dune-project
Dependency
Authors
Maintainers
Sources
v0.5.2.tar.gz
md5=60a3b9f181f24572a06a9492532bfdda
sha512=fcc35a275066be2900e6201782faf47503076fa4640f08cf78067835a6f447b74613009e55b2ac799adb7ca46f1bffa261fc5971753f2cc3c6bef327511c7ef6
doc/src/miaou-core.style/selector.ml.html
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
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>