···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Generic ancestor tracking for DOM traversal. *)
77+88+type 'data context = {
99+ name : string;
1010+ data : 'data;
1111+}
1212+1313+type 'data t = {
1414+ mutable stack : 'data context list;
1515+}
1616+1717+let create () = { stack = [] }
1818+1919+let reset tracker = tracker.stack <- []
2020+2121+let push tracker name data =
2222+ let name_lower = Astring.String.Ascii.lowercase name in
2323+ let context = { name = name_lower; data } in
2424+ tracker.stack <- context :: tracker.stack
2525+2626+let pop tracker =
2727+ match tracker.stack with
2828+ | [] -> () (* Gracefully handle underflow *)
2929+ | _ :: rest -> tracker.stack <- rest
3030+3131+let peek tracker =
3232+ match tracker.stack with
3333+ | [] -> None
3434+ | hd :: _ -> Some hd
3535+3636+let depth tracker = List.length tracker.stack
3737+3838+let has_ancestor tracker name =
3939+ let name_lower = Astring.String.Ascii.lowercase name in
4040+ List.exists (fun ctx -> String.equal ctx.name name_lower) tracker.stack
4141+4242+let has_ancestor_with tracker predicate =
4343+ List.exists (fun ctx -> predicate ctx.name ctx.data) tracker.stack
4444+4545+let find_ancestor tracker name =
4646+ let name_lower = Astring.String.Ascii.lowercase name in
4747+ List.find_opt (fun ctx -> String.equal ctx.name name_lower) tracker.stack
4848+4949+let find_ancestor_with tracker predicate =
5050+ List.find_opt (fun ctx -> predicate ctx.name ctx.data) tracker.stack
5151+5252+let get_all_ancestors tracker = tracker.stack
5353+5454+let filter_ancestors tracker predicate =
5555+ List.filter (fun ctx -> predicate ctx.name ctx.data) tracker.stack
5656+5757+let exists = has_ancestor_with
5858+5959+let for_all tracker predicate =
6060+ List.for_all (fun ctx -> predicate ctx.name ctx.data) tracker.stack
6161+6262+let iter tracker f =
6363+ List.iter (fun ctx -> f ctx.name ctx.data) tracker.stack
6464+6565+let fold tracker f init =
6666+ List.fold_left (fun acc ctx -> f acc ctx.name ctx.data) init tracker.stack
6767+6868+let get_parent = peek
6969+7070+let get_parent_name tracker =
7171+ match peek tracker with
7272+ | Some ctx -> Some ctx.name
7373+ | None -> None
7474+7575+let get_parent_data tracker =
7676+ match peek tracker with
7777+ | Some ctx -> Some ctx.data
7878+ | None -> None
7979+8080+let has_any_ancestor tracker names =
8181+ let names_lower = List.map Astring.String.Ascii.lowercase names in
8282+ List.exists (fun ctx -> List.mem ctx.name names_lower) tracker.stack
8383+8484+let find_first_matching tracker names =
8585+ let names_lower = List.map Astring.String.Ascii.lowercase names in
8686+ List.find_map (fun ctx ->
8787+ if List.mem ctx.name names_lower then Some (ctx.name, ctx)
8888+ else None
8989+ ) tracker.stack
9090+9191+let is_empty tracker = tracker.stack = []
9292+9393+let to_list = get_all_ancestors
9494+9595+let to_name_list tracker =
9696+ List.map (fun ctx -> ctx.name) tracker.stack
+236
lib/check/ancestor_tracker.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Generic ancestor tracking for DOM traversal.
77+88+ This module provides a generic stack-based ancestor tracker that can be
99+ used by various HTML checkers to track element nesting during DOM
1010+ traversal. It is parameterized by the type of data stored per element.
1111+1212+ {2 Design}
1313+1414+ The tracker maintains a stack of element contexts, where each context
1515+ contains:
1616+ - The element name (string)
1717+ - Custom data of type ['data] (checker-specific)
1818+1919+ The stack is automatically managed through [push] and [pop] operations
2020+ that should be called in response to start_element and end_element events.
2121+2222+ {2 Usage Example}
2323+2424+ {[
2525+ (* Define custom data type *)
2626+ type role_data = {
2727+ explicit_roles : string list;
2828+ implicit_role : string option;
2929+ }
3030+3131+ (* Create tracker *)
3232+ let tracker = Ancestor_tracker.create ()
3333+3434+ (* Push element context *)
3535+ let data = { explicit_roles = ["button"]; implicit_role = None } in
3636+ Ancestor_tracker.push tracker "div" data;
3737+3838+ (* Query ancestors *)
3939+ let has_button = Ancestor_tracker.exists tracker
4040+ (fun name data -> List.mem "button" data.explicit_roles)
4141+ in
4242+4343+ (* Pop when element closes *)
4444+ Ancestor_tracker.pop tracker
4545+ ]}
4646+*)
4747+4848+(** {1 Types} *)
4949+5050+(** Ancestor context containing element name and custom data. *)
5151+type 'data context = {
5252+ name : string;
5353+ (** Element name (lowercase). *)
5454+ data : 'data;
5555+ (** Checker-specific data. *)
5656+}
5757+5858+(** Ancestor tracker state. *)
5959+type 'data t
6060+6161+(** {1 Creation} *)
6262+6363+val create : unit -> 'data t
6464+(** [create ()] creates a new empty ancestor tracker. *)
6565+6666+val reset : 'data t -> unit
6767+(** [reset tracker] clears all ancestor contexts from the tracker. *)
6868+6969+(** {1 Stack Operations} *)
7070+7171+val push : 'data t -> string -> 'data -> unit
7272+(** [push tracker name data] pushes a new element context onto the stack.
7373+7474+ This should be called in [start_element] event handlers.
7575+7676+ @param tracker The ancestor tracker
7777+ @param name The element name (will be lowercased)
7878+ @param data Checker-specific data to associate with this element *)
7979+8080+val pop : 'data t -> unit
8181+(** [pop tracker] pops the most recent element context from the stack.
8282+8383+ This should be called in [end_element] event handlers.
8484+8585+ @param tracker The ancestor tracker *)
8686+8787+val peek : 'data t -> 'data context option
8888+(** [peek tracker] returns the most recent element context without removing it.
8989+9090+ @param tracker The ancestor tracker
9191+ @return [Some context] if the stack is non-empty, [None] otherwise *)
9292+9393+val depth : 'data t -> int
9494+(** [depth tracker] returns the current stack depth (number of ancestors).
9595+9696+ @param tracker The ancestor tracker
9797+ @return The number of elements on the stack *)
9898+9999+(** {1 Ancestor Queries} *)
100100+101101+val has_ancestor : 'data t -> string -> bool
102102+(** [has_ancestor tracker name] checks if an element with the given name
103103+ exists anywhere in the ancestor chain.
104104+105105+ @param tracker The ancestor tracker
106106+ @param name The element name to search for (case-insensitive)
107107+ @return [true] if an ancestor with this name exists *)
108108+109109+val has_ancestor_with : 'data t -> (string -> 'data -> bool) -> bool
110110+(** [has_ancestor_with tracker predicate] checks if any ancestor satisfies
111111+ the given predicate.
112112+113113+ @param tracker The ancestor tracker
114114+ @param predicate Function that tests element name and data
115115+ @return [true] if any ancestor satisfies the predicate *)
116116+117117+val find_ancestor : 'data t -> string -> 'data context option
118118+(** [find_ancestor tracker name] finds the nearest ancestor with the given name.
119119+120120+ @param tracker The ancestor tracker
121121+ @param name The element name to search for (case-insensitive)
122122+ @return [Some context] for the nearest matching ancestor, [None] if not found *)
123123+124124+val find_ancestor_with : 'data t -> (string -> 'data -> bool) -> 'data context option
125125+(** [find_ancestor_with tracker predicate] finds the nearest ancestor that
126126+ satisfies the predicate.
127127+128128+ @param tracker The ancestor tracker
129129+ @param predicate Function that tests element name and data
130130+ @return [Some context] for the nearest matching ancestor, [None] if not found *)
131131+132132+val get_all_ancestors : 'data t -> 'data context list
133133+(** [get_all_ancestors tracker] returns all ancestor contexts from nearest to root.
134134+135135+ @param tracker The ancestor tracker
136136+ @return List of contexts, with the most recent first *)
137137+138138+val filter_ancestors : 'data t -> (string -> 'data -> bool) -> 'data context list
139139+(** [filter_ancestors tracker predicate] returns all ancestors that satisfy
140140+ the predicate.
141141+142142+ @param tracker The ancestor tracker
143143+ @param predicate Function that tests element name and data
144144+ @return List of matching contexts, from nearest to root *)
145145+146146+val exists : 'data t -> (string -> 'data -> bool) -> bool
147147+(** [exists tracker predicate] checks if any ancestor satisfies the predicate.
148148+149149+ This is an alias for {!has_ancestor_with}.
150150+151151+ @param tracker The ancestor tracker
152152+ @param predicate Function that tests element name and data
153153+ @return [true] if any ancestor satisfies the predicate *)
154154+155155+val for_all : 'data t -> (string -> 'data -> bool) -> bool
156156+(** [for_all tracker predicate] checks if all ancestors satisfy the predicate.
157157+158158+ @param tracker The ancestor tracker
159159+ @param predicate Function that tests element name and data
160160+ @return [true] if all ancestors satisfy the predicate (vacuously true for empty stack) *)
161161+162162+val iter : 'data t -> (string -> 'data -> unit) -> unit
163163+(** [iter tracker f] applies function [f] to each ancestor from nearest to root.
164164+165165+ @param tracker The ancestor tracker
166166+ @param f Function to apply to each ancestor *)
167167+168168+val fold : 'data t -> ('acc -> string -> 'data -> 'acc) -> 'acc -> 'acc
169169+(** [fold tracker f init] folds over ancestors from nearest to root.
170170+171171+ @param tracker The ancestor tracker
172172+ @param f Folding function
173173+ @param init Initial accumulator value
174174+ @return Final accumulator value *)
175175+176176+(** {1 Parent Access} *)
177177+178178+val get_parent : 'data t -> 'data context option
179179+(** [get_parent tracker] returns the immediate parent element context.
180180+181181+ This is equivalent to {!peek}.
182182+183183+ @param tracker The ancestor tracker
184184+ @return [Some context] for the parent, [None] if at root *)
185185+186186+val get_parent_name : 'data t -> string option
187187+(** [get_parent_name tracker] returns the immediate parent element name.
188188+189189+ @param tracker The ancestor tracker
190190+ @return [Some name] for the parent, [None] if at root *)
191191+192192+val get_parent_data : 'data t -> 'data option
193193+(** [get_parent_data tracker] returns the immediate parent's custom data.
194194+195195+ @param tracker The ancestor tracker
196196+ @return [Some data] for the parent, [None] if at root *)
197197+198198+(** {1 Multiple Ancestor Queries} *)
199199+200200+val has_any_ancestor : 'data t -> string list -> bool
201201+(** [has_any_ancestor tracker names] checks if any of the given element names
202202+ exists in the ancestor chain.
203203+204204+ @param tracker The ancestor tracker
205205+ @param names List of element names to search for
206206+ @return [true] if any name matches an ancestor *)
207207+208208+val find_first_matching : 'data t -> string list -> (string * 'data context) option
209209+(** [find_first_matching tracker names] finds the nearest ancestor that matches
210210+ any of the given names.
211211+212212+ @param tracker The ancestor tracker
213213+ @param names List of element names to search for
214214+ @return [Some (matched_name, context)] for the first match, [None] if no match *)
215215+216216+(** {1 Stack Inspection} *)
217217+218218+val is_empty : 'data t -> bool
219219+(** [is_empty tracker] checks if the stack is empty (at document root).
220220+221221+ @param tracker The ancestor tracker
222222+ @return [true] if no elements are on the stack *)
223223+224224+val to_list : 'data t -> 'data context list
225225+(** [to_list tracker] converts the stack to a list of contexts.
226226+227227+ This is an alias for {!get_all_ancestors}.
228228+229229+ @param tracker The ancestor tracker
230230+ @return List of contexts from nearest to root *)
231231+232232+val to_name_list : 'data t -> string list
233233+(** [to_name_list tracker] returns just the element names in the stack.
234234+235235+ @param tracker The ancestor tracker
236236+ @return List of element names from nearest to root *)
+2-2
lib/check/attr_utils.ml
···33type attrs = (string * string) list
4455let has_attr name attrs =
66- List.exists (fun (n, _) -> String.lowercase_ascii n = name) attrs
66+ List.exists (fun (n, _) -> Astring.String.Ascii.lowercase n = name) attrs
7788let get_attr name attrs =
99 List.find_map (fun (n, v) ->
1010- if String.lowercase_ascii n = name then Some v else None
1010+ if Astring.String.Ascii.lowercase n = name then Some v else None
1111 ) attrs
12121313let get_attr_or name ~default attrs =
···3030 | Some spec ->
3131 List.exists (fun cat -> Element_spec.has_category spec cat) cats)
3232 | Content_model.Elements names ->
3333- List.mem (String.lowercase_ascii element_name)
3434- (List.map String.lowercase_ascii names)
3333+ List.mem (Astring.String.Ascii.lowercase element_name)
3434+ (List.map Astring.String.Ascii.lowercase names)
3535 | Content_model.Mixed cats -> (
3636 match Element_registry.get registry element_name with
3737 | None -> false
···7979 (`Element (`Not_allowed_as_child (`Child name, `Parent prohibited))))
8080 spec.Element_spec.prohibited_ancestors
81818282+(* Check if element is allowed via permitted_parents *)
8383+let is_permitted_parent registry child_name parent_name =
8484+ match Element_registry.get registry child_name with
8585+ | None -> false
8686+ | Some spec ->
8787+ match spec.Element_spec.permitted_parents with
8888+ | None -> false
8989+ | Some parents ->
9090+ List.mem (Astring.String.Ascii.lowercase parent_name)
9191+ (List.map Astring.String.Ascii.lowercase parents)
9292+9393+(* Check if a specific element is in the ancestor stack *)
9494+let has_ancestor state ancestor_name =
9595+ List.exists (fun ctx ->
9696+ String.equal (Astring.String.Ascii.lowercase ctx.name)
9797+ (Astring.String.Ascii.lowercase ancestor_name)
9898+ ) state.ancestor_stack
9999+100100+(* Check if an attribute exists in raw attrs list *)
101101+let has_raw_attr name attrs =
102102+ List.exists (fun (n, _) ->
103103+ Astring.String.Ascii.lowercase n = Astring.String.Ascii.lowercase name
104104+ ) attrs
105105+106106+(* Special cases for content model validation:
107107+ - dt/dd inside div is only valid when dl is an ancestor (div as grouping in dl)
108108+ - meta with property/itemprop/name attribute in body is valid (RDFa/microdata)
109109+ - link with itemprop in body is valid (microdata) *)
110110+let is_special_case_allowed state child_name parent_name raw_attrs =
111111+ let child_lower = Astring.String.Ascii.lowercase child_name in
112112+ let parent_lower = Astring.String.Ascii.lowercase parent_name in
113113+ (* dt/dd inside div is allowed when dl is an ancestor *)
114114+ if (child_lower = "dt" || child_lower = "dd") && parent_lower = "div" then
115115+ has_ancestor state "dl"
116116+ (* meta in body is allowed with property (RDFa), itemprop (microdata), or name+content (meta tags) *)
117117+ else if child_lower = "meta" && parent_lower <> "head" then
118118+ has_raw_attr "property" raw_attrs ||
119119+ has_raw_attr "itemprop" raw_attrs ||
120120+ (has_raw_attr "name" raw_attrs && has_raw_attr "content" raw_attrs)
121121+ (* link in body is allowed with itemprop (microdata) or property (RDFa) *)
122122+ else if child_lower = "link" && parent_lower <> "head" then
123123+ has_raw_attr "itemprop" raw_attrs || has_raw_attr "property" raw_attrs
124124+ (* Custom elements (with hyphen) are valid HTML5 and are flow content *)
125125+ else if String.contains child_lower '-' then
126126+ true
127127+ else
128128+ false
129129+82130(* Validate that a child element is allowed *)
8383-let validate_child_element state child_name collector =
131131+let validate_child_element state child_name raw_attrs collector =
84132 match state.ancestor_stack with
85133 | [] ->
86134 (* Root level - only html allowed *)
8787- if not (String.equal (String.lowercase_ascii child_name) "html") then
135135+ if not (String.equal (Astring.String.Ascii.lowercase child_name) "html") then
88136 Message_collector.add_typed collector
89137 (`Generic (Printf.sprintf "Element '%s' not allowed at document root (only 'html' allowed)" child_name))
90138 | parent :: _ ->
91139 let content_model = parent.spec.Element_spec.content_model in
9292- if not (matches_content_model state.registry child_name content_model) then
140140+ (* Check content model, permitted_parents, or special cases *)
141141+ let allowed_by_content_model = matches_content_model state.registry child_name content_model in
142142+ let allowed_by_permitted_parents = is_permitted_parent state.registry child_name parent.name in
143143+ let allowed_by_special_case = is_special_case_allowed state child_name parent.name raw_attrs in
144144+ if not (allowed_by_content_model || allowed_by_permitted_parents || allowed_by_special_case) then
93145 Message_collector.add_typed collector
94146 (`Element (`Not_allowed_as_child (`Child child_name, `Parent parent.name)))
9514796148let start_element state ~element collector =
97149 let name = Tag.tag_to_string element.Element.tag in
150150+ let raw_attrs = element.Element.raw_attrs in
9815199152 (* Check if we're inside a foreign (SVG/MathML) context *)
100153 let in_foreign_context = match state.ancestor_stack with
···127180 match spec_opt with
128181 | None ->
129182 (* Unknown element - first check if it's allowed in current context *)
130130- validate_child_element state name collector
183183+ validate_child_element state name raw_attrs collector;
184184+ (* Push unknown element onto stack with default flow content model *)
185185+ let spec = Element_spec.make ~name ~content_model:(Content_model.Categories [Content_category.Flow]) () in
186186+ let context = { name; spec; children_count = 0; is_foreign = false } in
187187+ state.ancestor_stack <- context :: state.ancestor_stack
131188 | Some spec ->
132189 (* Check prohibited ancestors *)
133190 check_prohibited_ancestors state name spec collector;
134191135192 (* Validate this element is allowed as child of parent *)
136136- validate_child_element state name collector;
193193+ validate_child_element state name raw_attrs collector;
137194138195 (* Push element context onto stack *)
139196 let context = { name; spec; children_count = 0; is_foreign = false } in
+2-2
lib/check/content_model/element_registry.ml
···33let create () = Hashtbl.create 128
4455let register registry spec =
66- let name = String.lowercase_ascii spec.Element_spec.name in
66+ let name = Astring.String.Ascii.lowercase spec.Element_spec.name in
77 Hashtbl.replace registry name spec
8899let get registry name =
1010- let name = String.lowercase_ascii name in
1010+ let name = Astring.String.Ascii.lowercase name in
1111 Hashtbl.find_opt registry name
12121313let list_names registry =
+3-1
lib/check/content_model/elements_embedded.ml
···3131 ()
32323333let img =
3434+ (* Note: img is only Interactive when it has usemap attribute;
3535+ we omit Interactive from static categories since usemap is rare *)
3436 Element_spec.make ~name:"img" ~void:true
3535- ~categories:[ Flow; Phrasing; Embedded; Palpable; Interactive ]
3737+ ~categories:[ Flow; Phrasing; Embedded; Palpable ]
3638 ~content_model:Nothing
3739 ~attrs:
3840 [
···3232 | '0' .. '9' | 'a' .. 'f' | 'A' .. 'F' -> true
3333 | _ -> false
34343535-(** Case conversion *)
3636-3737-let to_ascii_lowercase c =
3838- match c with 'A' .. 'Z' -> Char.chr (Char.code c + 32) | _ -> c
3535+(** Case conversion - delegated to Astring *)
39364040-let string_to_ascii_lowercase s =
4141- String.map to_ascii_lowercase s
3737+(* Removed to_ascii_lowercase and string_to_ascii_lowercase - use Astring.String.Ascii.lowercase instead *)
42384339(** String predicates *)
4440···7874let make_enum ~name ~values ?(allow_empty = true) () : t =
7975 (* Pre-compute hashtable for O(1) membership *)
8076 let values_tbl = Hashtbl.create (List.length values) in
8181- List.iter (fun v -> Hashtbl.add values_tbl (String.lowercase_ascii v) ()) values;
7777+ List.iter (fun v -> Hashtbl.add values_tbl (Astring.String.Ascii.lowercase v) ()) values;
8278 let values_str = String.concat ", " (List.map (Printf.sprintf "'%s'") values) in
8379 (module struct
8480 let name = name
8581 let validate s =
8686- let s_lower = string_to_ascii_lowercase s in
8282+ let s_lower = Astring.String.Ascii.lowercase s in
8783 if (allow_empty && s = "") || Hashtbl.mem values_tbl s_lower then Ok ()
8884 else Error (Printf.sprintf "The value '%s' is not a valid %s value. Expected %s%s."
8985 s name (if allow_empty then "empty string, " else "") values_str)
+1-5
lib/check/datatype/datatype.mli
···54545555(** {2 Case conversion} *)
56565757-(** Convert an ASCII character to lowercase. *)
5858-val to_ascii_lowercase : char -> char
5959-6060-(** Convert an ASCII string to lowercase. *)
6161-val string_to_ascii_lowercase : string -> string
5757+(** Case conversion functions removed - use Astring.String.Ascii.lowercase instead *)
62586359(** {2 String predicates} *)
6460
+2-1
lib/check/datatype/dt_autocomplete.ml
···2233(* Use shared utilities from Datatype *)
44let is_whitespace = Datatype.is_whitespace
55-let to_ascii_lowercase = Datatype.to_ascii_lowercase
55+let to_ascii_lowercase c =
66+ match c with 'A' .. 'Z' -> Char.chr (Char.code c + 32) | _ -> c
6778(* Use Astring for string operations *)
89let is_prefix = Astring.String.is_prefix
+2-2
lib/check/datatype/dt_boolean.ml
···2222 match s with
2323 | "" | "true" | "false" -> Ok ()
2424 | _ ->
2525- let s_lower = Datatype.string_to_ascii_lowercase s in
2626- let attr_lower = Datatype.string_to_ascii_lowercase attr_name in
2525+ let s_lower = Astring.String.Ascii.lowercase s in
2626+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
2727 if s_lower = attr_lower then Ok ()
2828 else
2929 Error
+1-1
lib/check/datatype/dt_button_type.ml
···77 let name = "button-type"
8899 let validate s =
1010- let s_lower = Datatype.string_to_ascii_lowercase s in
1010+ let s_lower = Astring.String.Ascii.lowercase s in
1111 if List.mem s_lower valid_types then Ok ()
1212 else
1313 Error
+1-1
lib/check/datatype/dt_charset.ml
···66 c = '\'' || c = '+' || c = '_' || c = '`' || c = '{' || c = '}' ||
77 c = '~' || c = '^'
8899-let to_lower = Datatype.string_to_ascii_lowercase
99+let to_lower = Astring.String.Ascii.lowercase
10101111(** Common encoding labels recognized by WHATWG Encoding Standard.
1212 This is a subset of the full list. *)
+1-1
lib/check/datatype/dt_color.ml
···208208 let name = "color"
209209210210 let validate s =
211211- let s = String.trim s |> String.lowercase_ascii in
211211+ let s = String.trim s |> Astring.String.Ascii.lowercase in
212212 if String.length s = 0 then Error "Color value must not be empty"
213213 else if List.mem s named_colors then Ok ()
214214 else if String.length s > 0 && s.[0] = '#' then validate_hex_color s
+1-1
lib/check/datatype/dt_contenteditable.ml
···44 let name = "contenteditable"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "true" | "false" | "plaintext-only" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_crossorigin.ml
···44 let name = "crossorigin"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "anonymous" | "use-credentials" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_decoding.ml
···44 let name = "decoding"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "sync" | "async" | "auto" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_dir.ml
···44 let name = "dir"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "ltr" | "rtl" | "auto" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_draggable.ml
···44 let name = "draggable"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "true" | "false" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_enterkeyhint.ml
···44 let name = "enterkeyhint"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "enter" | "done" | "go" | "next" | "previous" | "search" | "send" ->
1010 Ok ()
+1-1
lib/check/datatype/dt_fetchpriority.ml
···44 let name = "fetchpriority"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "high" | "low" | "auto" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_form_enctype.ml
···1212 let name = "form-enctype"
13131414 let validate s =
1515- let s_lower = Datatype.string_to_ascii_lowercase s in
1515+ let s_lower = Astring.String.Ascii.lowercase s in
1616 if List.mem s_lower valid_enctypes then Ok ()
1717 else
1818 Error
+1-1
lib/check/datatype/dt_form_method.ml
···77 let name = "form-method"
8899 let validate s =
1010- let s_lower = Datatype.string_to_ascii_lowercase s in
1010+ let s_lower = Astring.String.Ascii.lowercase s in
1111 if List.mem s_lower valid_methods then Ok ()
1212 else
1313 Error
+1-1
lib/check/datatype/dt_hidden.ml
···44 let name = "hidden"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "hidden" | "until-found" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_input_type.ml
···3131 let name = "input-type"
32323333 let validate s =
3434- let s_lower = Datatype.string_to_ascii_lowercase s in
3434+ let s_lower = Astring.String.Ascii.lowercase s in
3535 if List.mem s_lower valid_types then Ok ()
3636 else
3737 Error
+1-1
lib/check/datatype/dt_inputmode.ml
···44 let name = "inputmode"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "none" | "text" | "decimal" | "numeric" | "tel" | "search"
1010 | "email" | "url" ->
+1-1
lib/check/datatype/dt_integrity.ml
···4949 "Hash value '%s' must be in format 'algorithm-base64hash'" trimmed)
5050 | Some dash_pos ->
5151 let algorithm = String.sub trimmed 0 dash_pos in
5252- let algorithm_lower = Datatype.string_to_ascii_lowercase algorithm in
5252+ let algorithm_lower = Astring.String.Ascii.lowercase algorithm in
5353 if not (List.mem algorithm_lower valid_algorithms) then
5454 Error
5555 (Printf.sprintf
+1-1
lib/check/datatype/dt_kind.ml
···44 let name = "kind"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "subtitles" | "captions" | "descriptions" | "chapters" | "metadata" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_language.ml
···55(* Use shared character predicates from Datatype *)
66let is_all_alpha = Datatype.is_all_alpha
77let is_all_alphanumeric = Datatype.is_all_alphanumeric
88-let to_lower = Datatype.string_to_ascii_lowercase
88+let to_lower = Astring.String.Ascii.lowercase
991010(** Valid extlang subtags per IANA language-subtag-registry.
1111 Extlangs are 3-letter subtags that follow the primary language.
+1-1
lib/check/datatype/dt_link_type.ml
···3535 let trimmed = Datatype.trim_html_spaces s in
3636 if trimmed = "" then Error "Link type must not be empty"
3737 else
3838- let lower = Datatype.string_to_ascii_lowercase trimmed in
3838+ let lower = Astring.String.Ascii.lowercase trimmed in
3939 if List.mem lower valid_link_types then Ok ()
4040 else
4141 Error
+1-1
lib/check/datatype/dt_list_type.ml
···2626 let name = "ul-type"
27272828 let validate s =
2929- let s_lower = Datatype.string_to_ascii_lowercase s in
2929+ let s_lower = Astring.String.Ascii.lowercase s in
3030 if List.mem s_lower valid_ul_types then Ok ()
3131 else
3232 Error
+1-1
lib/check/datatype/dt_loading.ml
···44 let name = "loading"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "lazy" | "eager" -> Ok ()
1010 | _ ->
+8-8
lib/check/datatype/dt_media_query.ml
···147147 let trimmed = String.trim s in
148148 if String.length trimmed >= 3 then begin
149149 let suffix = String.sub trimmed (String.length trimmed - 3) 3 in
150150- if String.lowercase_ascii suffix = "and" then
150150+ if Astring.String.Ascii.lowercase suffix = "and" then
151151 Error "Parse Error."
152152 else if String.length trimmed >= 4 then begin
153153 let suffix4 = String.sub trimmed (String.length trimmed - 4) 4 in
154154- if String.lowercase_ascii suffix4 = "and(" then
154154+ if Astring.String.Ascii.lowercase suffix4 = "and(" then
155155 Error "Parse Error."
156156 else
157157 validate_media_query_content trimmed
···197197 let has_not = ref false in
198198 (match read_ident () with
199199 | Some w ->
200200- let w_lower = String.lowercase_ascii w in
200200+ let w_lower = Astring.String.Ascii.lowercase w in
201201 if w_lower = "only" then (has_only := true; skip_ws ())
202202 else if w_lower = "not" then (has_not := true; skip_ws ())
203203 else i := !i - String.length w (* put back *)
···234234 match read_ident () with
235235 | None -> Error "Parse Error."
236236 | Some kw ->
237237- let kw_lower = String.lowercase_ascii kw in
237237+ let kw_lower = Astring.String.Ascii.lowercase kw in
238238 if kw_lower <> "and" then Error "Parse Error."
239239 else begin
240240 (* Check that there was whitespace before 'and' *)
···263263 match read_ident () with
264264 | None -> Error "Parse Error."
265265 | Some kw2 ->
266266- let kw2_lower = String.lowercase_ascii kw2 in
266266+ let kw2_lower = Astring.String.Ascii.lowercase kw2 in
267267 if kw2_lower <> "and" then Error "Parse Error."
268268 else begin
269269 skip_ws ();
···291291 match String.index_opt content ':' with
292292 | None ->
293293 (* Just feature name - boolean feature or range syntax *)
294294- let feature_lower = String.lowercase_ascii content in
294294+ let feature_lower = Astring.String.Ascii.lowercase content in
295295 if List.mem feature_lower deprecated_media_features then
296296 Error (Printf.sprintf "Deprecated media feature \"%s\". For guidance, see the Deprecated Media Features section in the current Media Queries specification." feature_lower)
297297 else if List.mem feature_lower valid_media_features then
···301301 | Some colon_pos ->
302302 let feature = String.trim (String.sub content 0 colon_pos) in
303303 let value = String.trim (String.sub content (colon_pos + 1) (String.length content - colon_pos - 1)) in
304304- let feature_lower = String.lowercase_ascii feature in
304304+ let feature_lower = Astring.String.Ascii.lowercase feature in
305305306306 (* Check for deprecated features *)
307307 if List.mem feature_lower deprecated_media_features then
···362362 else if unit_part = "" then
363363 Error "only \"0\" can be a \"unit\". You must put a unit after your number"
364364 else begin
365365- let unit_lower = String.lowercase_ascii unit_part in
365365+ let unit_lower = Astring.String.Ascii.lowercase unit_part in
366366 if List.mem unit_lower valid_length_units then Ok ()
367367 else if List.mem unit_lower valid_resolution_units then
368368 Error (Printf.sprintf "\"%s\" is not a \"%s\" value" value base_feature)
+1-1
lib/check/datatype/dt_mime.ml
···9191 if is_token_char c then parse In_subtype (i + 1)
9292 else if c = ';' then
9393 (* Check if this is a JavaScript MIME type *)
9494- let mime_type = String.sub s 0 i |> String.lowercase_ascii in
9494+ let mime_type = String.sub s 0 i |> Astring.String.Ascii.lowercase in
9595 if List.mem mime_type javascript_mime_types then
9696 Error
9797 "A JavaScript MIME type must not contain any characters after \
+1-1
lib/check/datatype/dt_popover.ml
···44 let name = "popover"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "auto" | "manual" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_preload.ml
···77 let name = "preload"
8899 let validate s =
1010- let s_lower = Datatype.string_to_ascii_lowercase s in
1010+ let s_lower = Astring.String.Ascii.lowercase s in
1111 if List.mem s_lower valid_preloads then Ok ()
1212 else
1313 Error
+1-1
lib/check/datatype/dt_referrer.ml
···44 let name = "referrerpolicy"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | ""
1010 | "no-referrer"
+1-1
lib/check/datatype/dt_scope.ml
···77 let name = "scope"
8899 let validate s =
1010- let s_lower = Datatype.string_to_ascii_lowercase s in
1010+ let s_lower = Astring.String.Ascii.lowercase s in
1111 if List.mem s_lower valid_scopes then Ok ()
1212 else
1313 Error
+1-1
lib/check/datatype/dt_shape.ml
···44 let name = "shape"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "default" | "rect" | "circle" | "poly" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_spellcheck.ml
···44 let name = "spellcheck"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "true" | "false" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_target.ml
···88 if String.length s = 0 then Error "Browsing context name must not be empty"
99 else if s.[0] = '_' then
1010 (* If starts with underscore, must be a special keyword *)
1111- let lower = Datatype.string_to_ascii_lowercase s in
1111+ let lower = Astring.String.Ascii.lowercase s in
1212 if List.mem lower special_keywords then Ok ()
1313 else
1414 Error
+1-1
lib/check/datatype/dt_translate.ml
···44 let name = "translate"
5566 let validate s =
77- let s_lower = Datatype.string_to_ascii_lowercase s in
77+ let s_lower = Astring.String.Ascii.lowercase s in
88 match s_lower with
99 | "" | "yes" | "no" -> Ok ()
1010 | _ ->
+1-1
lib/check/datatype/dt_url.ml
···3030 match s.[i] with
3131 | ':' ->
3232 let scheme =
3333- String.sub s start (i - start) |> Datatype.string_to_ascii_lowercase
3333+ String.sub s start (i - start) |> Astring.String.Ascii.lowercase
3434 in
3535 let rest = String.sub s (i + 1) (len - i - 1) in
3636 Some (scheme, rest)
+1-1
lib/check/datatype/dt_wrap.ml
···77 let name = "wrap"
8899 let validate s =
1010- let s_lower = Datatype.string_to_ascii_lowercase s in
1010+ let s_lower = Astring.String.Ascii.lowercase s in
1111 if List.mem s_lower valid_wraps then Ok ()
1212 else
1313 Error
+3-3
lib/check/element/attr.ml
···571571572572(** Parse a single attribute name-value pair to typed attribute *)
573573let parse_attr name value : t =
574574- let name_lower = String.lowercase_ascii name in
575575- let value_lower = String.lowercase_ascii value in
574574+ let name_lower = Astring.String.Ascii.lowercase name in
575575+ let value_lower = Astring.String.Ascii.lowercase value in
576576 match name_lower with
577577 (* Global attributes *)
578578 | "id" -> `Id value
···875875(** Get rel attribute as list of link types (space-separated, lowercased per HTML5 spec) *)
876876let get_rel_list attrs =
877877 match get_rel attrs with
878878- | Some s -> List.map String.lowercase_ascii (Datatype.split_on_whitespace s)
878878+ | Some s -> List.map Astring.String.Ascii.lowercase (Datatype.split_on_whitespace s)
879879 | None -> []
880880881881(** Get headers attribute as raw string *)
+4-4
lib/check/element/element.ml
···21212222(** Parse element-specific type attribute based on tag *)
2323let parse_type_attr (tag : Tag.html_tag) value : Attr.t =
2424- let value_lower = String.lowercase_ascii value in
2424+ let value_lower = Astring.String.Ascii.lowercase value in
2525 match tag with
2626 | `Input ->
2727 (match Attr.parse_input_type value_lower with
···4242(** Parse attributes with element context for type attribute *)
4343let parse_attrs_for_tag (tag : Tag.element_tag) (raw_attrs : (string * string) list) : Attr.t list =
4444 List.map (fun (name, value) ->
4545- let name_lower = String.lowercase_ascii name in
4545+ let name_lower = Astring.String.Ascii.lowercase name in
4646 if name_lower = "type" then
4747 match tag with
4848 | Tag.Html html_tag -> parse_type_attr html_tag value
···274274(** Get raw attribute value (from original attrs) *)
275275let get_raw_attr name elem =
276276 List.find_map (fun (n, v) ->
277277- if String.lowercase_ascii n = String.lowercase_ascii name then Some v else None
277277+ if Astring.String.Ascii.lowercase n = Astring.String.Ascii.lowercase name then Some v else None
278278 ) elem.raw_attrs
279279280280(** Check if raw attribute exists *)
281281let has_raw_attr name elem =
282282 List.exists (fun (n, _) ->
283283- String.lowercase_ascii n = String.lowercase_ascii name
283283+ Astring.String.Ascii.lowercase n = Astring.String.Ascii.lowercase name
284284 ) elem.raw_attrs
285285286286(** {1 Pattern Matching Helpers} *)
+3-3
lib/check/element/tag.ml
···234234(** Check if a name is a valid custom element name (contains hyphen, not reserved) *)
235235let is_custom_element_name name =
236236 String.contains name '-' &&
237237- not (String.starts_with ~prefix:"xml" (String.lowercase_ascii name)) &&
238238- not (String.equal (String.lowercase_ascii name) "annotation-xml")
237237+ not (String.starts_with ~prefix:"xml" (Astring.String.Ascii.lowercase name)) &&
238238+ not (String.equal (Astring.String.Ascii.lowercase name) "annotation-xml")
239239240240(** SVG namespace URI *)
241241let svg_namespace = "http://www.w3.org/2000/svg"
···255255256256(** Convert tag name and optional namespace to element_tag *)
257257let tag_of_string ?namespace name =
258258- let name_lower = String.lowercase_ascii name in
258258+ let name_lower = Astring.String.Ascii.lowercase name in
259259 match namespace with
260260 | Some ns when is_svg_namespace ns -> Svg name (* Preserve original case for SVG *)
261261 | Some ns when is_mathml_namespace ns -> MathML name (* Preserve original case for MathML *)
+1-1
lib/check/semantic/form_checker.ml
···12121313(** Check if autocomplete value contains webauthn token *)
1414let contains_webauthn value =
1515- let lower = String.lowercase_ascii value in
1515+ let lower = Astring.String.Ascii.lowercase value in
1616 let tokens = String.split_on_char ' ' lower |> List.filter (fun s -> String.length s > 0) in
1717 List.mem "webauthn" tokens
1818
+2-2
lib/check/semantic/lang_detecting_checker.ml
···5454let get_lang_code lang =
5555 (* Extract primary language subtag *)
5656 match String.split_on_char '-' lang with
5757- | code :: _ -> String.lowercase_ascii code
5757+ | code :: _ -> Astring.String.Ascii.lowercase code
5858 | [] -> ""
59596060(* Create detector lazily with deterministic seed *)
···324324 | None ->
325325 Message_collector.add_typed collector
326326 (`I18n (`Missing_dir_rtl (`Language detected_name)))
327327- | Some dir when String.lowercase_ascii dir <> "rtl" ->
327327+ | Some dir when Astring.String.Ascii.lowercase dir <> "rtl" ->
328328 Message_collector.add_typed collector
329329 (`I18n (`Wrong_dir (`Language detected_name, `Declared dir)))
330330 | _ -> ()
+11-13
lib/check/semantic/nesting_checker.ml
···190190 state.ancestor_flags <- empty_flags ()
191191192192(** Get attribute value by name from attribute list. *)
193193-let get_attr attrs name =
194194- List.assoc_opt name attrs
193193+let get_attr = Attr_utils.get_attr
195194196195(** Check if an attribute exists. *)
197197-let has_attr attrs name =
198198- get_attr attrs name <> None
196196+let has_attr = Attr_utils.has_attr
199197200198(** Check if element is interactive based on its attributes. *)
201199let is_interactive_element name attrs =
202200 match name with
203203- | "a" -> has_attr attrs "href"
204204- | "audio" | "video" -> has_attr attrs "controls"
205205- | "img" | "object" -> has_attr attrs "usemap"
201201+ | "a" -> has_attr "href" attrs
202202+ | "audio" | "video" -> has_attr "controls" attrs
203203+ | "img" | "object" -> has_attr "usemap" attrs
206204 | "input" ->
207207- (match get_attr attrs "type" with
205205+ (match get_attr "type" attrs with
208206 | Some "hidden" -> false
209207 | _ -> true)
210208 | "button" | "details" | "embed" | "iframe" | "label" | "select"
···239237 (* Determine attribute to mention in error messages *)
240238 let attr =
241239 match name with
242242- | "a" when has_attr attrs "href" -> Some "href"
243243- | "audio" when has_attr attrs "controls" -> Some "controls"
244244- | "video" when has_attr attrs "controls" -> Some "controls"
245245- | "img" when has_attr attrs "usemap" -> Some "usemap"
246246- | "object" when has_attr attrs "usemap" -> Some "usemap"
240240+ | "a" when has_attr "href" attrs -> Some "href"
241241+ | "audio" when has_attr "controls" attrs -> Some "controls"
242242+ | "video" when has_attr "controls" attrs -> Some "controls"
243243+ | "img" when has_attr "usemap" attrs -> Some "usemap"
244244+ | "object" when has_attr "usemap" attrs -> Some "usemap"
247245 | _ -> None
248246 in
249247
+2-2
lib/check/semantic/obsolete_checker.ml
···260260 match element.Element.tag with
261261 | Tag.Html _ ->
262262 let name = Tag.tag_to_string element.tag in
263263- let name_lower = String.lowercase_ascii name in
263263+ let name_lower = Astring.String.Ascii.lowercase name in
264264 let attrs = element.raw_attrs in
265265266266 (* Track head context *)
···275275276276 (* Check for obsolete attributes *)
277277 List.iter (fun (attr_name, _attr_value) ->
278278- let attr_lower = String.lowercase_ascii attr_name in
278278+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
279279280280 (* Special handling for scoped attribute on style *)
281281 if attr_lower = "scoped" && name_lower = "style" then begin
+1-1
lib/check/semantic/required_attr_checker.ml
···120120 (* popover attribute must have valid value *)
121121 match Attr_utils.get_attr "popover" attrs with
122122 | Some value ->
123123- let value_lower = String.lowercase_ascii value in
123123+ let value_lower = Astring.String.Ascii.lowercase value in
124124 (* Valid values: empty string, auto, manual, hint *)
125125 if value_lower <> "" && value_lower <> "auto" && value_lower <> "manual" && value_lower <> "hint" then
126126 Message_collector.add_typed collector
+34-34
lib/check/specialized/aria_checker.ml
···309309 else
310310 String.split_on_char ' ' trimmed
311311 |> List.filter (fun s -> String.trim s <> "")
312312- |> List.map String.lowercase_ascii
312312+ |> List.map Astring.String.Ascii.lowercase
313313314314(** Get the implicit role for an HTML element. *)
315315let get_implicit_role element_name attrs =
316316 (* Check for input element with type attribute *)
317317 if element_name = "input" then begin
318318- match List.assoc_opt "type" attrs with
318318+ match Attr_utils.get_attr "type" attrs with
319319 | Some input_type ->
320320- let input_type = String.lowercase_ascii input_type in
320320+ let input_type = Astring.String.Ascii.lowercase input_type in
321321 begin match Hashtbl.find_opt input_types_with_implicit_role input_type with
322322 | Some role -> Some role
323323 | None ->
···332332 end
333333 (* Check for area element - implicit role depends on href attribute *)
334334 else if element_name = "area" then begin
335335- match List.assoc_opt "href" attrs with
335335+ match Attr_utils.get_attr "href" attrs with
336336 | Some _ -> Some "link" (* area with href has implicit role "link" *)
337337 | None -> Some "generic" (* area without href has no corresponding role, treated as generic *)
338338 end
339339 (* Check for a element - implicit role depends on href attribute *)
340340 else if element_name = "a" then begin
341341- match List.assoc_opt "href" attrs with
341341+ match Attr_utils.get_attr "href" attrs with
342342 | Some _ -> Some "link" (* a with href has implicit role "link" *)
343343 | None -> Some "generic" (* a without href has no corresponding role, treated as generic *)
344344 end
···430430 match element.Element.tag with
431431 | Tag.Html _ ->
432432 let name = Tag.tag_to_string element.tag in
433433- let name_lower = String.lowercase_ascii name in
433433+ let name_lower = Astring.String.Ascii.lowercase name in
434434 let attrs = element.raw_attrs in
435435- let role_attr = List.assoc_opt "role" attrs in
436436- let aria_label = List.assoc_opt "aria-label" attrs in
437437- let aria_labelledby = List.assoc_opt "aria-labelledby" attrs in
438438- let aria_braillelabel = List.assoc_opt "aria-braillelabel" attrs in
435435+ let role_attr = Attr_utils.get_attr "role" attrs in
436436+ let aria_label = Attr_utils.get_attr "aria-label" attrs in
437437+ let aria_labelledby = Attr_utils.get_attr "aria-labelledby" attrs in
438438+ let aria_braillelabel = Attr_utils.get_attr "aria-braillelabel" attrs in
439439 let has_aria_label = match aria_label with Some v -> String.trim v <> "" | None -> false in
440440 let has_aria_labelledby = match aria_labelledby with Some v -> String.trim v <> "" | None -> false in
441441 let has_aria_braillelabel = match aria_braillelabel with Some v -> String.trim v <> "" | None -> false in
···459459460460 (* Track active tabs and tabpanel roles for end_document validation *)
461461 if List.mem "tab" explicit_roles then begin
462462- let aria_selected = List.assoc_opt "aria-selected" attrs in
462462+ let aria_selected = Attr_utils.get_attr "aria-selected" attrs in
463463 if aria_selected = Some "true" then state.has_active_tab <- true
464464 end;
465465 if List.mem "tabpanel" explicit_roles then state.has_tabpanel <- true;
466466467467 (* Track visible main elements (explicit role=main or implicit main role) *)
468468 let is_hidden =
469469- let aria_hidden = List.assoc_opt "aria-hidden" attrs in
469469+ let aria_hidden = Attr_utils.get_attr "aria-hidden" attrs in
470470 aria_hidden = Some "true"
471471 in
472472 if not is_hidden then begin
···489489 (* Check br/wbr aria-* attribute restrictions - not allowed *)
490490 if name_lower = "br" || name_lower = "wbr" then begin
491491 List.iter (fun (attr_name, _) ->
492492- let attr_lower = String.lowercase_ascii attr_name in
492492+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
493493 if String.starts_with ~prefix:"aria-" attr_lower &&
494494 attr_lower <> "aria-hidden" then
495495 Message_collector.add_typed collector
···515515516516 (* Check for img with empty alt having role attribute *)
517517 if name_lower = "img" then begin
518518- let alt_value = List.assoc_opt "alt" attrs in
518518+ let alt_value = Attr_utils.get_attr "alt" attrs in
519519 match alt_value with
520520 | Some alt when String.trim alt = "" ->
521521 (* img with empty alt must not have role attribute *)
···526526527527 (* Check for input[type=checkbox][role=button] requires aria-pressed *)
528528 if name_lower = "input" then begin
529529- let input_type = match List.assoc_opt "type" attrs with
530530- | Some t -> String.lowercase_ascii t
529529+ let input_type = match Attr_utils.get_attr "type" attrs with
530530+ | Some t -> Astring.String.Ascii.lowercase t
531531 | None -> "text"
532532 in
533533 if input_type = "checkbox" && List.mem "button" explicit_roles then begin
534534- let has_aria_pressed = List.assoc_opt "aria-pressed" attrs <> None in
534534+ let has_aria_pressed = Attr_utils.has_attr "aria-pressed" attrs in
535535 if not has_aria_pressed then
536536 Message_collector.add_typed collector (`Input `Checkbox_needs_aria_pressed)
537537 end
···566566567567 (* Check for aria-hidden="true" on body element *)
568568 if name_lower = "body" then begin
569569- let aria_hidden = List.assoc_opt "aria-hidden" attrs in
569569+ let aria_hidden = Attr_utils.get_attr "aria-hidden" attrs in
570570 match aria_hidden with
571571 | Some "true" ->
572572 Message_collector.add_typed collector (`Aria `Hidden_on_body)
···574574 end;
575575576576 (* Check for aria-checked on input[type=checkbox] *)
577577- let aria_checked = List.assoc_opt "aria-checked" attrs in
577577+ let aria_checked = Attr_utils.get_attr "aria-checked" attrs in
578578 if name_lower = "input" then begin
579579- match List.assoc_opt "type" attrs with
580580- | Some input_type when String.lowercase_ascii input_type = "checkbox" ->
579579+ match Attr_utils.get_attr "type" attrs with
580580+ | Some input_type when Astring.String.Ascii.lowercase input_type = "checkbox" ->
581581 if aria_checked <> None then
582582 Message_collector.add_typed collector
583583 (`Aria (`Must_not_use (`Attr "aria-checked", `Elem "input",
···586586 end;
587587588588 (* Check for aria-expanded on roles that don't support it *)
589589- let aria_expanded = List.assoc_opt "aria-expanded" attrs in
589589+ let aria_expanded = Attr_utils.get_attr "aria-expanded" attrs in
590590 if aria_expanded <> None then begin
591591 let role_to_check = match explicit_roles with
592592 | first :: _ -> Some first
···605605 (* Special message for input[type=text] with role="textbox" *)
606606 let reason =
607607 if name_lower = "input" && first_role = "textbox" then begin
608608- let has_list = List.exists (fun (k, _) -> String.lowercase_ascii k = "list") attrs in
609609- let input_type = match List.assoc_opt "type" attrs with
610610- | Some t -> String.lowercase_ascii t
608608+ let has_list = Attr_utils.has_attr "list" attrs in
609609+ let input_type = match Attr_utils.get_attr "type" attrs with
610610+ | Some t -> Astring.String.Ascii.lowercase t
611611 | None -> "text"
612612 in
613613 if not has_list && input_type = "text" then
···671671672672 (* Check for redundant default ARIA attribute values *)
673673 List.iter (fun (attr_name, attr_value) ->
674674- let attr_lower = String.lowercase_ascii attr_name in
674674+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
675675 if String.starts_with ~prefix:"aria-" attr_lower then
676676 match Hashtbl.find_opt aria_default_values attr_lower with
677677 | Some default_value ->
678678- let value_lower = String.lowercase_ascii (String.trim attr_value) in
678678+ let value_lower = Astring.String.Ascii.lowercase (String.trim attr_value) in
679679 if value_lower = default_value then
680680 Message_collector.add_typed collector
681681 (`Generic (Printf.sprintf
···688688 if name_lower = "summary" then begin
689689 let parent = get_parent_element state in
690690 let is_in_details = parent = Some "details" in
691691- let has_role_attr = List.exists (fun (k, _) -> String.lowercase_ascii k = "role") attrs in
692692- let has_aria_expanded = List.assoc_opt "aria-expanded" attrs <> None in
693693- let has_aria_pressed = List.assoc_opt "aria-pressed" attrs <> None in
691691+ let has_role_attr = Attr_utils.has_attr "role" attrs in
692692+ let has_aria_expanded = Attr_utils.has_attr "aria-expanded" attrs in
693693+ let has_aria_pressed = Attr_utils.has_attr "aria-pressed" attrs in
694694 if is_in_details then begin
695695 (* summary that is the first child of details *)
696696 if has_role_attr then
···726726 (* Custom elements (autonomous custom elements) have generic role by default
727727 and cannot have accessible names unless they have an explicit role *)
728728 let attrs = element.raw_attrs in
729729- let role_attr = List.assoc_opt "role" attrs in
730730- let aria_label = List.assoc_opt "aria-label" attrs in
731731- let aria_labelledby = List.assoc_opt "aria-labelledby" attrs in
732732- let aria_braillelabel = List.assoc_opt "aria-braillelabel" attrs in
729729+ let role_attr = Attr_utils.get_attr "role" attrs in
730730+ let aria_label = Attr_utils.get_attr "aria-label" attrs in
731731+ let aria_labelledby = Attr_utils.get_attr "aria-labelledby" attrs in
732732+ let aria_braillelabel = Attr_utils.get_attr "aria-braillelabel" attrs in
733733 let has_aria_label = match aria_label with Some v -> String.trim v <> "" | None -> false in
734734 let has_aria_labelledby = match aria_labelledby with Some v -> String.trim v <> "" | None -> false in
735735 let has_aria_braillelabel = match aria_braillelabel with Some v -> String.trim v <> "" | None -> false in
···5858 match element.Element.tag with
5959 | Tag.Html _ ->
6060 let name = Tag.tag_to_string element.tag in
6161- let name_lower = String.lowercase_ascii name in
6161+ let name_lower = Astring.String.Ascii.lowercase name in
6262 let attrs = element.raw_attrs in
63636464 (* Detect XHTML mode from xmlns attribute on html element *)
···8686 (* Check for xmlns:* prefixed attributes - not allowed in HTML *)
8787 (* Standard xmlns declarations are allowed but custom prefixes are not *)
8888 List.iter (fun (attr_name, _) ->
8989- let attr_lower = String.lowercase_ascii attr_name in
8989+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
9090 if String.starts_with ~prefix:"xmlns:" attr_lower then begin
9191 let prefix = String.sub attr_lower 6 (String.length attr_lower - 6) in
9292 (* Only xmlns:xlink (with correct value) and xmlns:xml are allowed *)
···113113 (* Validate style type attribute - must be "text/css" or omitted *)
114114 if name_lower = "style" then begin
115115 List.iter (fun (attr_name, attr_value) ->
116116- let attr_lower = String.lowercase_ascii attr_name in
116116+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
117117 if attr_lower = "type" then begin
118118- let value_lower = String.lowercase_ascii (String.trim attr_value) in
118118+ let value_lower = Astring.String.Ascii.lowercase (String.trim attr_value) in
119119 if value_lower <> "text/css" then
120120 Message_collector.add_typed collector (`Misc `Style_type_invalid)
121121 end
···144144 (* imagesrcset requires as="image" *)
145145 if has_imagesrcset then begin
146146 let as_is_image = match as_value with
147147- | Some v -> String.lowercase_ascii (String.trim v) = "image"
147147+ | Some v -> Astring.String.Ascii.lowercase (String.trim v) = "image"
148148 | None -> false
149149 in
150150 if not as_is_image then
···164164 (* Validate img usemap attribute - must be hash-name reference with content *)
165165 if name_lower = "img" then begin
166166 List.iter (fun (attr_name, attr_value) ->
167167- let attr_lower = String.lowercase_ascii attr_name in
167167+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
168168 if attr_lower = "usemap" then begin
169169 if attr_value = "#" then
170170 Message_collector.add_typed collector
···178178 (* Validate embed type attribute - must be valid MIME type *)
179179 if name_lower = "embed" then begin
180180 List.iter (fun (attr_name, attr_value) ->
181181- let attr_lower = String.lowercase_ascii attr_name in
181181+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
182182 if attr_lower = "type" then begin
183183 match Dt_mime.validate_mime_type attr_value with
184184 | Ok () -> ()
···197197 name_lower = "iframe" || name_lower = "source" in
198198 if is_dimension_element then begin
199199 List.iter (fun (attr_name, attr_value) ->
200200- let attr_lower = String.lowercase_ascii attr_name in
200200+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
201201 if attr_lower = "width" || attr_lower = "height" then begin
202202 (* Check for non-negative integer only *)
203203 let is_valid =
···245245 (* Validate area[shape=default] cannot have coords *)
246246 if name_lower = "area" then begin
247247 match Attr_utils.get_attr "shape" attrs with
248248- | Some s when String.lowercase_ascii (String.trim s) = "default" ->
248248+ | Some s when Astring.String.Ascii.lowercase (String.trim s) = "default" ->
249249 if Attr_utils.has_attr "coords" attrs then
250250 Message_collector.add_typed collector
251251 (`Attr (`Not_allowed (`Attr "coords", `Elem "area")))
···257257 match Attr_utils.get_attr "dir" attrs with
258258 | None ->
259259 Message_collector.add_typed collector (`Misc `Bdo_missing_dir)
260260- | Some v when String.lowercase_ascii (String.trim v) = "auto" ->
260260+ | Some v when Astring.String.Ascii.lowercase (String.trim v) = "auto" ->
261261 Message_collector.add_typed collector (`Misc `Bdo_dir_auto)
262262 | _ -> ()
263263 end;
···266266 if name_lower = "input" then begin
267267 if Attr_utils.has_attr "list" attrs then begin
268268 let input_type = Attr_utils.get_attr_or "type" ~default:"text" attrs
269269- |> String.trim |> String.lowercase_ascii in
269269+ |> String.trim |> Astring.String.Ascii.lowercase in
270270 if not (List.mem input_type input_types_allowing_list) then
271271 Message_collector.add_typed collector (`Input `List_not_allowed)
272272 end
···274274275275 (* Validate data-* attributes *)
276276 List.iter (fun (attr_name, _) ->
277277- let attr_lower = String.lowercase_ascii attr_name in
277277+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
278278 (* Check if it starts with "data-" *)
279279 if String.starts_with ~prefix:"data-" attr_lower then begin
280280 let after_prefix = String.sub attr_lower 5 (String.length attr_lower - 5) in
···297297 (match lang_value with
298298 | None ->
299299 Message_collector.add_typed collector (`I18n `Xml_lang_without_lang)
300300- | Some lang when String.lowercase_ascii lang <> String.lowercase_ascii xmllang ->
300300+ | Some lang when Astring.String.Ascii.lowercase lang <> Astring.String.Ascii.lowercase xmllang ->
301301 Message_collector.add_typed collector (`I18n `Xml_lang_without_lang)
302302 | _ -> ())
303303 | None -> ()
···305305306306 (* Validate spellcheck attribute - must be "true" or "false" or empty *)
307307 List.iter (fun (attr_name, attr_value) ->
308308- let attr_lower = String.lowercase_ascii attr_name in
308308+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
309309 if attr_lower = "spellcheck" then begin
310310- let value_lower = String.lowercase_ascii (String.trim attr_value) in
310310+ let value_lower = Astring.String.Ascii.lowercase (String.trim attr_value) in
311311 if value_lower <> "" && value_lower <> "true" && value_lower <> "false" then
312312 Message_collector.add_typed collector
313313 (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason "")))
···317317 (* Validate enterkeyhint attribute - must be one of specific values *)
318318 let valid_enterkeyhint = ["enter"; "done"; "go"; "next"; "previous"; "search"; "send"] in
319319 List.iter (fun (attr_name, attr_value) ->
320320- let attr_lower = String.lowercase_ascii attr_name in
320320+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
321321 if attr_lower = "enterkeyhint" then begin
322322- let value_lower = String.lowercase_ascii (String.trim attr_value) in
322322+ let value_lower = Astring.String.Ascii.lowercase (String.trim attr_value) in
323323 if not (List.mem value_lower valid_enterkeyhint) then
324324 Message_collector.add_typed collector
325325 (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason "")))
···328328329329 (* Validate headingoffset attribute - must be a number between 0 and 8 *)
330330 List.iter (fun (attr_name, attr_value) ->
331331- let attr_lower = String.lowercase_ascii attr_name in
331331+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
332332 if attr_lower = "headingoffset" then begin
333333 let trimmed = String.trim attr_value in
334334 let is_valid =
···346346347347 (* Validate accesskey attribute - each key label must be a single code point *)
348348 List.iter (fun (attr_name, attr_value) ->
349349- let attr_lower = String.lowercase_ascii attr_name in
349349+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
350350 if attr_lower = "accesskey" then begin
351351 (* Split by whitespace to get key labels *)
352352 let keys = String.split_on_char ' ' attr_value |>
···418418 let is_media_element = name_lower = "link" || name_lower = "style" || name_lower = "source" in
419419 if is_media_element then begin
420420 List.iter (fun (attr_name, attr_value) ->
421421- let attr_lower = String.lowercase_ascii attr_name in
421421+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
422422 if attr_lower = "media" then begin
423423 let trimmed = String.trim attr_value in
424424 if trimmed <> "" then begin
···436436437437 (* Validate RDFa prefix attribute - space-separated list of prefix:iri pairs *)
438438 List.iter (fun (attr_name, attr_value) ->
439439- let attr_lower = String.lowercase_ascii attr_name in
439439+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
440440 if attr_lower = "prefix" then begin
441441 (* Parse prefix attribute value - format: "prefix1: iri1 prefix2: iri2 ..." *)
442442 let trimmed = String.trim attr_value in
+1-1
lib/check/specialized/datetime_checker.ml
···451451 if List.mem name datetime_elements then begin
452452 (* Check for datetime attribute *)
453453 let datetime_attr = List.find_map (fun (k, v) ->
454454- if String.lowercase_ascii k = "datetime" then Some v else None
454454+ if Astring.String.Ascii.lowercase k = "datetime" then Some v else None
455455 ) element.raw_attrs in
456456 match datetime_attr with
457457 | None -> ()
+1-1
lib/check/specialized/dl_checker.ml
···106106 (`Element (`Not_allowed_as_child (`Child "div", `Parent "dl")));
107107 (match Attr.get_role element.attrs with
108108 | Some role_value ->
109109- let role_lower = String.lowercase_ascii (String.trim role_value) in
109109+ let role_lower = Astring.String.Ascii.lowercase (String.trim role_value) in
110110 if role_lower <> "presentation" && role_lower <> "none" then
111111 Message_collector.add_typed collector (`Li_role `Div_in_dl_bad_role)
112112 | None -> ());
+2-2
lib/check/specialized/importmap_checker.ml
···270270 | Tag.Html `Script ->
271271 (* Check if type="importmap" *)
272272 let type_attr = List.find_opt (fun (n, _) ->
273273- String.lowercase_ascii n = "type"
273273+ Astring.String.Ascii.lowercase n = "type"
274274 ) element.raw_attrs in
275275 (match type_attr with
276276- | Some (_, v) when String.lowercase_ascii v = "importmap" ->
276276+ | Some (_, v) when Astring.String.Ascii.lowercase v = "importmap" ->
277277 state.in_importmap <- true;
278278 Buffer.clear state.content
279279 | _ -> ())
+1-1
lib/check/specialized/label_checker.ml
···6565 | _ -> ())
66666767 | Tag.Html tag ->
6868- let name_lower = String.lowercase_ascii (Tag.tag_to_string (Tag.Html tag)) in
6868+ let name_lower = Astring.String.Ascii.lowercase (Tag.tag_to_string (Tag.Html tag)) in
69697070 (* Track labelable element IDs *)
7171 (if is_labelable name_lower then
+1-1
lib/check/specialized/language_checker.ml
···27272828(** Check if a language tag contains deprecated subtags. *)
2929let check_deprecated_tag value =
3030- let lower = String.lowercase_ascii value in
3030+ let lower = Astring.String.Ascii.lowercase value in
3131 let subtags = String.split_on_char '-' lower in
3232 match subtags with
3333 | [] -> None
+3-6
lib/check/specialized/mime_type_checker.ml
···153153let create () = ()
154154let reset _state = ()
155155156156-let get_attr_value name attrs =
157157- List.find_map (fun (k, v) ->
158158- if String.lowercase_ascii k = String.lowercase_ascii name then Some v else None
159159- ) attrs
156156+let get_attr_value = Attr_utils.get_attr
160157161158let start_element _state ~element collector =
162159 match element.Element.tag with
163160 | Tag.Html tag ->
164161 let name = Tag.html_tag_to_string tag in
165165- let name_lower = String.lowercase_ascii name in
162162+ let name_lower = Astring.String.Ascii.lowercase name in
166163 (match List.assoc_opt name_lower mime_type_attrs with
167164 | None -> ()
168165 | Some type_attrs ->
···174171 if value = "" then ()
175172 else if name_lower = "script" then
176173 (* script type can be module, importmap, etc. - skip validation for non-MIME types *)
177177- let value_lower = String.lowercase_ascii value in
174174+ let value_lower = Astring.String.Ascii.lowercase value in
178175 if value_lower = "module" || value_lower = "importmap" ||
179176 not (String.contains value '/') then ()
180177 else
+2-2
lib/check/specialized/picture_checker.ml
···133133 let media_value = Attr_utils.get_attr "media" attrs in
134134 let has_type = Attr_utils.has_attr "type" attrs in
135135 let is_media_all = match media_value with
136136- | Some v -> String.lowercase_ascii (String.trim v) = "all"
136136+ | Some v -> Astring.String.Ascii.lowercase (String.trim v) = "all"
137137 | None -> false in
138138 let is_media_empty = match media_value with
139139 | Some v -> String.trim v = ""
···142142 | None -> not has_type
143143 | Some v ->
144144 let trimmed = String.trim v in
145145- trimmed = "" || String.lowercase_ascii trimmed = "all"
145145+ trimmed = "" || Astring.String.Ascii.lowercase trimmed = "all"
146146 in
147147 if is_always_matching then begin
148148 state.has_always_matching_source <- true;
+12-12
lib/check/specialized/srcset_sizes_checker.ml
···153153154154(** Check if scientific notation has invalid exponent (like 1e+1.5 - decimal in exponent) *)
155155let has_invalid_scientific_notation s =
156156- let lower = String.lowercase_ascii s in
156156+ let lower = Astring.String.Ascii.lowercase s in
157157 (* Find 'e' for scientific notation *)
158158 match String.index_opt lower 'e' with
159159 | None -> false
···176176 (* Check for % at the end *)
177177 else if trimmed.[len - 1] = '%' then "%"
178178 else begin
179179- let lower = String.lowercase_ascii trimmed in
179179+ let lower = Astring.String.Ascii.lowercase trimmed in
180180 (* Try to find a unit at the end (letters only) *)
181181 let rec find_unit_length i =
182182 if i < 0 then 0
···205205 if has_invalid_scientific_notation value_no_comments then BadScientificNotation
206206 (* "auto" is only valid with lazy loading, which requires checking the element context.
207207 For general validation, treat "auto" alone as invalid in sizes. *)
208208- else if String.lowercase_ascii value_no_comments = "auto" then
208208+ else if Astring.String.Ascii.lowercase value_no_comments = "auto" then
209209 BadCssNumber (value_no_comments.[0], trimmed)
210210 else if value_no_comments = "" then InvalidUnit ("", trimmed)
211211 else begin
212212- let lower = String.lowercase_ascii value_no_comments in
212212+ let lower = Astring.String.Ascii.lowercase value_no_comments in
213213 (* Check for calc() or other CSS functions first - these are always valid *)
214214 if String.contains value_no_comments '(' then Valid
215215 else begin
···310310 Some "Bad media condition: Parse Error"
311311 end else begin
312312 (* Check for bare "all" which is invalid *)
313313- let lower = String.lowercase_ascii trimmed in
313313+ let lower = Astring.String.Ascii.lowercase trimmed in
314314 let parts = String.split_on_char ' ' lower |> List.filter (fun s -> s <> "") in
315315 match parts with
316316 | keyword :: _ when keyword = "all" ->
···358358 end
359359 else begin
360360 (* Check if remaining starts with "and", "or", "not" followed by space or paren *)
361361- let lower_remaining = String.lowercase_ascii remaining in
361361+ let lower_remaining = Astring.String.Ascii.lowercase remaining in
362362 if remaining_len >= 4 && String.sub lower_remaining 0 4 = "and " then
363363 skip_media_condition (i + (len - i) - remaining_len + 4)
364364 else if remaining_len >= 3 && String.sub lower_remaining 0 3 = "or " then
···577577578578(** Validate srcset descriptor *)
579579let validate_srcset_descriptor desc element_name srcset_value has_sizes collector =
580580- let desc_lower = String.lowercase_ascii (String.trim desc) in
580580+ let desc_lower = Astring.String.Ascii.lowercase (String.trim desc) in
581581 if String.length desc_lower = 0 then true
582582 else begin
583583 let last_char = desc_lower.[String.length desc_lower - 1] in
···723723724724(** Normalize descriptor for duplicate detection (e.g., 1x = 1.0x) *)
725725let normalize_descriptor desc =
726726- let desc_lower = String.lowercase_ascii (String.trim desc) in
726726+ let desc_lower = Astring.String.Ascii.lowercase (String.trim desc) in
727727 if String.length desc_lower = 0 then desc_lower
728728 else
729729 let last_char = desc_lower.[String.length desc_lower - 1] in
···793793 (* Special schemes that require host/content after :// *)
794794 let special_schemes = ["http"; "https"; "ftp"; "ws"; "wss"] in
795795 (* Check for scheme-only URL like "http:" *)
796796- let url_lower = String.lowercase_ascii url in
796796+ let url_lower = Astring.String.Ascii.lowercase url in
797797 List.iter (fun scheme ->
798798 let scheme_colon = scheme ^ ":" in
799799 if url_lower = scheme_colon then
···824824 (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected single descriptor but found extraneous descriptor %s at %s." (q value) (q "srcset") (q element_name) (q extra_desc) (q value)))))
825825 end;
826826827827- let desc_lower = String.lowercase_ascii (String.trim desc) in
827827+ let desc_lower = Astring.String.Ascii.lowercase (String.trim desc) in
828828 if String.length desc_lower > 0 then begin
829829 let last_char = desc_lower.[String.length desc_lower - 1] in
830830 if last_char = 'w' then has_w_descriptor := true
···872872 begin match Hashtbl.find_opt seen_descriptors normalized with
873873 | Some first_url ->
874874 Message_collector.add_typed collector
875875- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: %s for image %s is identical to %s for image %s." (q value) (q "srcset") (q element_name) dup_type (q url) (String.lowercase_ascii dup_type) (q first_url)))))
875875+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: %s for image %s is identical to %s for image %s." (q value) (q "srcset") (q element_name) dup_type (q url) (Astring.String.Ascii.lowercase dup_type) (q first_url)))))
876876 | None ->
877877 begin match (if is_1x then Hashtbl.find_opt seen_descriptors "implicit-1x" else None) with
878878 | Some first_url ->
879879 (* Explicit 1x conflicts with implicit 1x *)
880880 Message_collector.add_typed collector
881881- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: %s for image %s is identical to %s for image %s." (q value) (q "srcset") (q element_name) dup_type (q url) (String.lowercase_ascii dup_type) (q first_url)))))
881881+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: %s for image %s is identical to %s for image %s." (q value) (q "srcset") (q element_name) dup_type (q url) (Astring.String.Ascii.lowercase dup_type) (q first_url)))))
882882 | None ->
883883 Hashtbl.add seen_descriptors normalized url;
884884 if is_1x then Hashtbl.add seen_descriptors "explicit-1x" url
+10-10
lib/check/specialized/svg_checker.ml
···260260261261(* Check if attribute name matches a pattern like "data-*" or "aria-*" - case insensitive *)
262262let matches_pattern attr pattern =
263263- let attr_lower = String.lowercase_ascii attr in
264264- let pattern_lower = String.lowercase_ascii pattern in
263263+ let attr_lower = Astring.String.Ascii.lowercase attr in
264264+ let pattern_lower = Astring.String.Ascii.lowercase pattern in
265265 if String.ends_with ~suffix:"-*" pattern_lower then
266266 let prefix = String.sub pattern_lower 0 (String.length pattern_lower - 1) in
267267 String.starts_with ~prefix attr_lower
···361361 state.in_svg <- true;
362362363363 if is_svg_element || state.in_svg then begin
364364- let name_lower = String.lowercase_ascii name in
364364+ let name_lower = Astring.String.Ascii.lowercase name in
365365366366 (* Check SVG content model rules *)
367367 (* 1. Check if child is allowed in SVG <a> *)
368368 (match state.element_stack with
369369- | parent :: _ when String.lowercase_ascii parent = "a" ->
369369+ | parent :: _ when Astring.String.Ascii.lowercase parent = "a" ->
370370 if List.mem name_lower a_disallowed_children then
371371 Message_collector.add_typed collector
372372 (`Element (`Not_allowed_as_child (`Child name, `Parent "a")))
···382382 (* 2.5 Check stop element is only in linearGradient or radialGradient *)
383383 if name_lower = "stop" then begin
384384 match state.element_stack with
385385- | parent :: _ when (let p = String.lowercase_ascii parent in
385385+ | parent :: _ when (let p = Astring.String.Ascii.lowercase parent in
386386 p = "lineargradient" || p = "radialgradient") -> ()
387387 | parent :: _ ->
388388 Message_collector.add_typed collector
···393393 (* 2.6 Check use element is not nested inside another use element *)
394394 if name_lower = "use" then begin
395395 match state.element_stack with
396396- | parent :: _ when String.lowercase_ascii parent = "use" ->
396396+ | parent :: _ when Astring.String.Ascii.lowercase parent = "use" ->
397397 Message_collector.add_typed collector
398398 (`Element (`Not_allowed_as_child (`Child name, `Parent parent)))
399399 | _ -> ()
···401401402402 (* 3. Check duplicate feFunc* in feComponentTransfer *)
403403 (match state.element_stack with
404404- | parent :: _ when String.lowercase_ascii parent = "fecomponenttransfer" ->
404404+ | parent :: _ when Astring.String.Ascii.lowercase parent = "fecomponenttransfer" ->
405405 if List.mem name_lower ["fefuncr"; "fefuncg"; "fefuncb"; "fefunca"] then begin
406406 match state.fecomponenttransfer_stack with
407407 | fect :: _ ->
···435435436436 (* Check each attribute *)
437437 List.iter (fun (attr, value) ->
438438- let attr_lower = String.lowercase_ascii attr in
438438+ let attr_lower = Astring.String.Ascii.lowercase attr in
439439440440 (* Validate xmlns attributes *)
441441 if String.starts_with ~prefix:"xmlns" attr_lower then
···457457 (match List.assoc_opt name_lower required_attrs with
458458 | Some req_attrs ->
459459 List.iter (fun req_attr ->
460460- if not (List.exists (fun (a, _) -> String.lowercase_ascii a = req_attr) attrs) then
460460+ if not (Attr_utils.has_attr req_attr attrs) then
461461 Message_collector.add_typed collector
462462 (`Svg (`Missing_attr (`Elem name_lower, `Attr req_attr)))
463463 ) req_attrs
···469469 let name = Tag.tag_to_string tag in
470470471471 if is_svg_element || state.in_svg then begin
472472- let name_lower = String.lowercase_ascii name in
472472+ let name_lower = Astring.String.Ascii.lowercase name in
473473474474 (* Check required children when closing font element *)
475475 if name_lower = "font" then begin
+5-5
lib/check/specialized/table_checker.ml
···354354355355(** Parse a non-negative integer attribute, returning 1 if absent or invalid *)
356356let parse_non_negative_int attrs name =
357357- match List.assoc_opt name attrs with
357357+ match Attr_utils.get_attr name attrs with
358358 | None -> 1
359359 | Some v -> (
360360 try
···364364365365(** Parse a positive integer attribute, returning 1 if absent or invalid *)
366366let parse_positive_int attrs name =
367367- match List.assoc_opt name attrs with
367367+ match Attr_utils.get_attr name attrs with
368368 | None -> 1
369369 | Some v -> (
370370 try
···374374375375(** Parse the headers attribute into a list of IDs *)
376376let parse_headers attrs =
377377- match List.assoc_opt "headers" attrs with
377377+ match Attr_utils.get_attr "headers" attrs with
378378 | None -> []
379379 | Some v ->
380380 let parts = String.split_on_char ' ' v in
···523523 table.state <- InCellInRowGroup;
524524 (* Record header ID if present *)
525525 if is_header then (
526526- match List.assoc_opt "id" attrs with
526526+ match Attr_utils.get_attr "id" attrs with
527527 | Some id when String.length id > 0 -> Hashtbl.replace table.header_ids id ()
528528 | _ -> ());
529529 (* Parse cell attributes *)
···541541 table.state <- InCellInImplicitRowGroup;
542542 (* Same logic as above *)
543543 if is_header then (
544544- match List.assoc_opt "id" attrs with
544544+ match Attr_utils.get_attr "id" attrs with
545545 | Some id when String.length id > 0 -> Hashtbl.replace table.header_ids id ()
546546 | _ -> ());
547547 let colspan = abs (parse_positive_int attrs "colspan") in
+1-1
lib/check/specialized/unknown_element_checker.ml
···3131 state.stack <- name :: state.stack
32323333 | Tag.Html tag ->
3434- let name_lower = String.lowercase_ascii (Tag.tag_to_string (Tag.Html tag)) in
3434+ let name_lower = Astring.String.Ascii.lowercase (Tag.tag_to_string (Tag.Html tag)) in
3535 state.stack <- name_lower :: state.stack
36363737 | _ -> () (* SVG, MathML, Custom elements are allowed *)
+24-25
lib/check/specialized/url_checker.ml
···67676868(** Check if pipe is allowed in this host context. *)
6969let is_pipe_allowed_in_host url host =
7070- let scheme = try String.lowercase_ascii (String.sub url 0 (String.index url ':')) with _ -> "" in
7070+ let scheme = try Astring.String.Ascii.lowercase (String.sub url 0 (String.index url ':')) with _ -> "" in
7171 scheme = "file" && is_valid_windows_drive host
72727373(** Special schemes that require double slash (//).
···9595 (c >= '0' && c <= '9') || c = '+' || c = '-' || c = '.'
9696 ) potential_scheme in
9797 if is_valid_scheme then
9898- Some (String.lowercase_ascii potential_scheme)
9898+ Some (Astring.String.Ascii.lowercase potential_scheme)
9999 else
100100 None
101101 with Not_found -> None
···104104let extract_host_and_port url =
105105 try
106106 let double_slash =
107107- try Some (Str.search_forward (Str.regexp "://") url 0 + 3)
108108- with Not_found -> None
107107+ match Astring.String.find_sub ~sub:"://" url with
108108+ | Some pos -> Some (pos + 3)
109109+ | None -> None
109110 in
110111 match double_slash with
111112 | None -> (None, None)
···250251 (* Check for ASCII percent *)
251252 String.contains s '%' ||
252253 (* Check for fullwidth percent (U+FF05 = 0xEF 0xBC 0x85 in UTF-8) *)
253253- try
254254- let _ = Str.search_forward (Str.regexp "\xef\xbc\x85") s 0 in
255255- true
256256- with Not_found -> false
254254+ Astring.String.is_infix ~affix:"\xef\xbc\x85" s
257255258256(** Check if decoded host contains forbidden characters.
259257 Some URLs have percent-encoded fullwidth characters that decode to forbidden chars. *)
···424422let check_path_segment url attr_name element_name =
425423 (* Extract path: everything after authority (or after scheme: for non-authority URLs) *)
426424 let raw_path =
427427- try
428428- let double_slash = Str.search_forward (Str.regexp "://") url 0 in
425425+ match Astring.String.find_sub ~sub:"://" url with
426426+ | Some double_slash ->
429427 let after_auth_start = double_slash + 3 in
430428 let rest = String.sub url after_auth_start (String.length url - after_auth_start) in
431429 (* Find end of authority *)
···437435 String.sub rest path_start (String.length rest - path_start)
438436 else
439437 ""
440440- with Not_found ->
438438+ | None ->
441439 (* No double slash - check for single slash path *)
442442- match extract_scheme url with
440440+ (match extract_scheme url with
443441 | Some _ ->
444444- let colon_pos = String.index url ':' in
445445- let after_colon = String.sub url (colon_pos + 1) (String.length url - colon_pos - 1) in
446446- after_colon
442442+ (try
443443+ let colon_pos = String.index url ':' in
444444+ String.sub url (colon_pos + 1) (String.length url - colon_pos - 1)
445445+ with Not_found -> url)
447446 | None ->
448447 (* Relative URL - the whole thing is the path *)
449449- url
448448+ url)
450449 in
451450 (* Remove query and fragment for path-specific checks *)
452451 let path = remove_query_fragment raw_path in
···546545547546(** Check for illegal characters in userinfo (user:password). *)
548547let check_userinfo url attr_name element_name =
548548+ match Astring.String.find_sub ~sub:"://" url with
549549+ | None -> None
550550+ | Some pos ->
549551 try
550552 (* Look for :// then find the LAST @ before the next / or end *)
551551- let double_slash = Str.search_forward (Str.regexp "://") url 0 + 3 in
553553+ let double_slash = pos + 3 in
552554 let rest = String.sub url double_slash (String.length url - double_slash) in
553555 (* Find first / or ? or # to limit authority section *)
554556 let auth_end =
···633635 let url = String.trim url in
634636 (* Empty URL check for certain attributes *)
635637 if url = "" then begin
636636- let name_lower = String.lowercase_ascii element_name in
637637- let attr_lower = String.lowercase_ascii attr_name in
638638+ let name_lower = Astring.String.Ascii.lowercase element_name in
639639+ let attr_lower = Astring.String.Ascii.lowercase attr_name in
638640 if List.mem attr_lower must_be_non_empty ||
639641 List.mem (name_lower, attr_lower) must_be_non_empty_combinations then
640642 Some (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad URL: Must be non-empty."
···739741let reset _state = ()
740742741743(** Get attribute value by name. *)
742742-let get_attr_value name attrs =
743743- List.find_map (fun (k, v) ->
744744- if String.lowercase_ascii k = String.lowercase_ascii name then Some v else None
745745- ) attrs
744744+let get_attr_value = Attr_utils.get_attr
746745747746let start_element _state ~element collector =
748747 match element.Element.tag with
749748 | Tag.Html _ ->
750749 let name = Tag.tag_to_string element.tag in
751751- let name_lower = String.lowercase_ascii name in
750750+ let name_lower = Astring.String.Ascii.lowercase name in
752751 let attrs = element.raw_attrs in
753752 (* Check URL attributes for elements that have them *)
754753 (match List.assoc_opt name_lower url_attributes with
···794793 match validate_url url name "value" with
795794 | None -> ()
796795 | Some error_msg ->
797797- let error_msg = Str.global_replace (Str.regexp "Bad URL:") "Bad absolute URL:" error_msg in
796796+ let error_msg = Astring.String.concat ~sep:"Bad absolute URL:" (Astring.String.cuts ~sep:"Bad URL:" error_msg) in
798797 Message_collector.add_typed collector (`Attr (`Bad_value_generic (`Message error_msg)))
799798 end
800799 end
+5-5
lib/check/specialized/xhtml_content_checker.ml
···54545555let start_element state ~element collector =
5656 let name = Tag.tag_to_string element.Element.tag in
5757- let name_lower = String.lowercase_ascii name in
5757+ let name_lower = Astring.String.Ascii.lowercase name in
5858 let attrs = element.raw_attrs in
59596060 (* Check data-* attributes for uppercase *)
···6363 (* Check if this element is allowed as child of parent *)
6464 (match state.element_stack with
6565 | parent :: _ ->
6666- let parent_lower = String.lowercase_ascii parent in
6666+ let parent_lower = Astring.String.Ascii.lowercase parent in
6767 if not (is_child_allowed ~parent:parent_lower ~child:name_lower) then
6868 Message_collector.add_typed collector
6969 (`Element (`Not_allowed_as_child (`Child name_lower, `Parent parent_lower)))
···71717272 (* Handle figure content model *)
7373 (match state.element_stack with
7474- | parent :: _ when String.lowercase_ascii parent = "figure" ->
7474+ | parent :: _ when Astring.String.Ascii.lowercase parent = "figure" ->
7575 (* We're inside a figure, check content model *)
7676 (match state.figure_stack with
7777 | fig :: _ ->
···9999 state.element_stack <- name :: state.element_stack
100100101101let end_element state ~tag _collector =
102102- let name_lower = String.lowercase_ascii (Tag.tag_to_string tag) in
102102+ let name_lower = Astring.String.Ascii.lowercase (Tag.tag_to_string tag) in
103103 (* Pop figure state if leaving a figure *)
104104 if name_lower = "figure" then begin
105105 match state.figure_stack with
···115115 match state.element_stack with
116116 | [] -> ()
117117 | parent :: _ ->
118118- let parent_lower = String.lowercase_ascii parent in
118118+ let parent_lower = Astring.String.Ascii.lowercase parent in
119119 let trimmed = String.trim text in
120120 if trimmed <> "" then begin
121121 if parent_lower = "figure" then begin