···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 *)