···256256let tag_of_string ?namespace name =
257257 let name_lower = String.lowercase_ascii name in
258258 match namespace with
259259- | Some ns when is_svg_namespace ns -> Svg name_lower
260260- | Some ns when is_mathml_namespace ns -> MathML name_lower
259259+ | Some ns when is_svg_namespace ns -> Svg name (* Preserve original case for SVG *)
260260+ | Some ns when is_mathml_namespace ns -> MathML name (* Preserve original case for MathML *)
261261 | Some _ -> Unknown name_lower (* Unknown namespace *)
262262 | None ->
263263 match html_tag_of_string_opt name_lower with
+12
lib/htmlrw_check/error_code.ml
···6161 | `Unrecognized_role of [`Token of string]
6262 | `Tab_without_tabpanel
6363 | `Multiple_main
6464+ | `Accessible_name_prohibited of [`Attr of string] * [`Elem of string]
6465]
65666667type li_role_error = [
···257258 | `Aria (`Unrecognized_role _) -> "unrecognized-role"
258259 | `Aria `Tab_without_tabpanel -> "tab-without-tabpanel"
259260 | `Aria `Multiple_main -> "multiple-main"
261261+ | `Aria (`Accessible_name_prohibited _) -> "aria-not-allowed"
260262261263 (* List item role errors *)
262264 | `Li_role `Div_in_dl_bad_role -> "invalid-role"
···491493 | `Aria `Multiple_main ->
492494 Printf.sprintf "A document should not include more than one visible element with %s."
493495 (q "role=main")
496496+ | `Aria (`Accessible_name_prohibited (`Attr attr, `Elem element)) ->
497497+ (* Roles that prohibit accessible names - defined by ARIA spec *)
498498+ let prohibited_roles = [
499499+ "caption"; "code"; "deletion"; "emphasis"; "generic"; "insertion";
500500+ "paragraph"; "presentation"; "strong"; "subscript"; "superscript"
501501+ ] in
502502+ let roles_str = String.concat ", " (List.map q (List.rev (List.tl (List.rev prohibited_roles)))) ^
503503+ ", or " ^ q (List.hd (List.rev prohibited_roles)) in
504504+ Printf.sprintf "The %s attribute must not be specified on any %s element unless the element has a %s value other than %s."
505505+ (q attr) (q element) (q "role") roles_str
494506495507 (* List item role errors *)
496508 | `Li_role `Div_in_dl_bad_role ->
+6
lib/htmlrw_check/error_code.mli
···312312 (** Document has multiple visible main landmarks.
313313 Only one visible [role="main"] or [<main>] should exist
314314 per document for proper landmark navigation. *)
315315+316316+ | `Accessible_name_prohibited of [`Attr of string] * [`Elem of string]
317317+ (** Accessible name attribute not allowed on element with generic role.
318318+ Elements with implicit [role="generic"] (or no role) cannot have
319319+ [aria-label], [aria-labelledby], or [aria-braillelabel] unless
320320+ they have an explicit role that supports accessible names. *)
315321]
316322317323(** List item role constraint errors.
+58-18
lib/htmlrw_check/specialized/aria_checker.ml
···11(** ARIA validation checker implementation. *)
2233+(** Quote helper for consistent message formatting. *)
44+let q = Error_code.q
55+36(** Valid WAI-ARIA 1.2 roles.
4758 These are all the valid role values according to the WAI-ARIA 1.2
···422425let render_role_set roles =
423426 match roles with
424427 | [] -> ""
425425- | [role] -> "\"" ^ role ^ "\""
428428+ | [role] -> q role
426429 | _ ->
427427- let quoted = List.map (fun r -> "\"" ^ r ^ "\"") roles in
430430+ let quoted = List.map q roles in
428431 String.concat " or " quoted
429432430433let start_element state ~element collector =
···505508 (* Generate error if element cannot have accessible name but has one *)
506509 if has_aria_label && not can_have_name then
507510 Message_collector.add_typed collector
508508- (`Aria (`Must_not_specify (`Attr "aria-label", `Elem name,
509509- `Condition "the element has a \xe2\x80\x9crole\xe2\x80\x9d value other than \xe2\x80\x9ccaption\xe2\x80\x9d, \xe2\x80\x9ccode\xe2\x80\x9d, \xe2\x80\x9cdeletion\xe2\x80\x9d, \xe2\x80\x9cemphasis\xe2\x80\x9d, \xe2\x80\x9cgeneric\xe2\x80\x9d, \xe2\x80\x9cinsertion\xe2\x80\x9d, \xe2\x80\x9cparagraph\xe2\x80\x9d, \xe2\x80\x9cpresentation\xe2\x80\x9d, \xe2\x80\x9cstrong\xe2\x80\x9d, \xe2\x80\x9csubscript\xe2\x80\x9d, or \xe2\x80\x9csuperscript\xe2\x80\x9d")));
511511+ (`Aria (`Accessible_name_prohibited (`Attr "aria-label", `Elem name)));
510512511513 if has_aria_labelledby && not can_have_name then
512514 Message_collector.add_typed collector
513513- (`Aria (`Must_not_specify (`Attr "aria-labelledby", `Elem name,
514514- `Condition "the element has a \xe2\x80\x9crole\xe2\x80\x9d value other than \xe2\x80\x9ccaption\xe2\x80\x9d, \xe2\x80\x9ccode\xe2\x80\x9d, \xe2\x80\x9cdeletion\xe2\x80\x9d, \xe2\x80\x9cemphasis\xe2\x80\x9d, \xe2\x80\x9cgeneric\xe2\x80\x9d, \xe2\x80\x9cinsertion\xe2\x80\x9d, \xe2\x80\x9cparagraph\xe2\x80\x9d, \xe2\x80\x9cpresentation\xe2\x80\x9d, \xe2\x80\x9cstrong\xe2\x80\x9d, \xe2\x80\x9csubscript\xe2\x80\x9d, or \xe2\x80\x9csuperscript\xe2\x80\x9d")));
515515+ (`Aria (`Accessible_name_prohibited (`Attr "aria-labelledby", `Elem name)));
515516516517 if has_aria_braillelabel && not can_have_name then
517518 Message_collector.add_typed collector
518518- (`Aria (`Must_not_specify (`Attr "aria-braillelabel", `Elem name,
519519- `Condition "the element has a \xe2\x80\x9crole\xe2\x80\x9d value other than \xe2\x80\x9ccaption\xe2\x80\x9d, \xe2\x80\x9ccode\xe2\x80\x9d, \xe2\x80\x9cdeletion\xe2\x80\x9d, \xe2\x80\x9cemphasis\xe2\x80\x9d, \xe2\x80\x9cgeneric\xe2\x80\x9d, \xe2\x80\x9cinsertion\xe2\x80\x9d, \xe2\x80\x9cparagraph\xe2\x80\x9d, \xe2\x80\x9cpresentation\xe2\x80\x9d, \xe2\x80\x9cstrong\xe2\x80\x9d, \xe2\x80\x9csubscript\xe2\x80\x9d, or \xe2\x80\x9csuperscript\xe2\x80\x9d")));
519519+ (`Aria (`Accessible_name_prohibited (`Attr "aria-braillelabel", `Elem name)));
520520521521 (* Check for img with empty alt having role attribute *)
522522 if name_lower = "img" then begin
···616616 | None -> "text"
617617 in
618618 if not has_list && input_type = "text" then
619619- "for an \xe2\x80\x9cinput\xe2\x80\x9d element that has no \xe2\x80\x9clist\xe2\x80\x9d attribute and whose type is \xe2\x80\x9ctext\xe2\x80\x9d"
619619+ Printf.sprintf "for an %s element that has no %s attribute and whose type is %s"
620620+ (q "input") (q "list") (q "text")
620621 else
621621- Printf.sprintf "for element \xe2\x80\x9c%s\xe2\x80\x9d" name
622622+ Printf.sprintf "for element %s" (q name)
622623 end else
623623- Printf.sprintf "for element \xe2\x80\x9c%s\xe2\x80\x9d" name
624624+ Printf.sprintf "for element %s" (q name)
624625 in
625626 Message_collector.add_typed collector
626627 (`Aria (`Unnecessary_role (`Role first_role, `Elem name, `Reason reason)))
···644645 if Hashtbl.mem roles_which_cannot_be_named role && has_accessible_name then
645646 Message_collector.add_typed collector
646647 (`Generic (Printf.sprintf
647647- "Elements with role=\"%s\" must not have accessible names (via aria-label or aria-labelledby)."
648648- role));
648648+ "Elements with %s must not have accessible names (via aria-label or aria-labelledby)."
649649+ (q ("role=" ^ role))));
649650650651 (* Check for required ancestor roles *)
651652 begin match Hashtbl.find_opt required_role_ancestor_by_descendant role with
···653654 if not (has_required_ancestor_role state required_ancestors) then
654655 Message_collector.add_typed collector
655656 (`Generic (Printf.sprintf
656656- "An element with \"role=%s\" must be contained in, or owned by, an element with the \"role\" value %s."
657657- role
657657+ "An element with %s must be contained in, or owned by, an element with the %s value %s."
658658+ (q ("role=" ^ role))
659659+ (q "role")
658660 (render_role_set required_ancestors)))
659661 | None -> ()
660662 end;
···682684 if value_lower = default_value then
683685 Message_collector.add_typed collector
684686 (`Generic (Printf.sprintf
685685- "The \xe2\x80\x9c%s\xe2\x80\x9d attribute is unnecessary for the value \xe2\x80\x9c%s\xe2\x80\x9d."
686686- attr_name attr_value))
687687+ "The %s attribute is unnecessary for the value %s."
688688+ (q attr_name) (q attr_value)))
687689 | None -> ()
688690 ) attrs;
689691···724726 implicit_role;
725727 } in
726728 state.stack <- node :: state.stack
727727- | _ -> () (* Skip non-HTML elements *)
729729+730730+ | Tag.Custom name ->
731731+ (* Custom elements (autonomous custom elements) have generic role by default
732732+ and cannot have accessible names unless they have an explicit role *)
733733+ let attrs = element.raw_attrs in
734734+ let role_attr = List.assoc_opt "role" attrs in
735735+ let aria_label = List.assoc_opt "aria-label" attrs in
736736+ let aria_labelledby = List.assoc_opt "aria-labelledby" attrs in
737737+ let aria_braillelabel = List.assoc_opt "aria-braillelabel" attrs in
738738+ let has_aria_label = match aria_label with Some v -> String.trim v <> "" | None -> false in
739739+ let has_aria_labelledby = match aria_labelledby with Some v -> String.trim v <> "" | None -> false in
740740+ let has_aria_braillelabel = match aria_braillelabel with Some v -> String.trim v <> "" | None -> false in
741741+742742+ (* Parse explicit roles from role attribute *)
743743+ let explicit_roles = match role_attr with
744744+ | Some role_value -> split_roles role_value
745745+ | None -> []
746746+ in
747747+748748+ (* Custom elements have no implicit role (generic) *)
749749+ let implicit_role = None in
750750+751751+ (* Check if element can have accessible names *)
752752+ let can_have_name = element_can_have_accessible_name name explicit_roles implicit_role in
753753+754754+ (* Generate error if element cannot have accessible name but has one *)
755755+ if has_aria_label && not can_have_name then
756756+ Message_collector.add_typed collector
757757+ (`Aria (`Accessible_name_prohibited (`Attr "aria-label", `Elem name)));
758758+759759+ if has_aria_labelledby && not can_have_name then
760760+ Message_collector.add_typed collector
761761+ (`Aria (`Accessible_name_prohibited (`Attr "aria-labelledby", `Elem name)));
762762+763763+ if has_aria_braillelabel && not can_have_name then
764764+ Message_collector.add_typed collector
765765+ (`Aria (`Accessible_name_prohibited (`Attr "aria-braillelabel", `Elem name)))
766766+767767+ | _ -> () (* Skip SVG, MathML, Unknown elements *)
728768729769let end_element state ~tag _collector =
730770 (* Only process HTML elements *)
+18-6
lib/htmlrw_check/specialized/svg_checker.ml
···8899type fecomponenttransfer_state = {
1010 mutable seen_funcs : string list; (* track feFuncR, feFuncG, etc. *)
1111+ mutable duplicate_error_reported : bool; (* suppress further duplicate errors *)
1112}
12131314type state = {
···366367 | parent :: _ when String.lowercase_ascii parent = "a" ->
367368 if List.mem name_lower a_disallowed_children then
368369 Message_collector.add_typed collector
369369- (`Element (`Not_allowed_as_child (`Child name_lower, `Parent "a")))
370370+ (`Element (`Not_allowed_as_child (`Child name, `Parent "a")))
370371 | _ -> ());
371372372373 (* 2. Track missing-glyph in font *)
···402403 if List.mem name_lower ["fefuncr"; "fefuncg"; "fefuncb"; "fefunca"] then begin
403404 match state.fecomponenttransfer_stack with
404405 | fect :: _ ->
405405- if List.mem name_lower fect.seen_funcs then
406406- Message_collector.add_typed collector
407407- (`Element (`Not_allowed_as_child (`Child name, `Parent "feComponentTransfer")))
408408- else
406406+ if List.mem name_lower fect.seen_funcs then begin
407407+ (* Only report first duplicate error, suppress further *)
408408+ if not fect.duplicate_error_reported then begin
409409+ Message_collector.add_typed collector
410410+ (`Element (`Not_allowed_as_child (`Child name, `Parent "feComponentTransfer")));
411411+ fect.duplicate_error_reported <- true
412412+ end
413413+ end else
409414 fect.seen_funcs <- name_lower :: fect.seen_funcs
410415 | [] -> ()
411416 end
···415420 if name_lower = "font" then
416421 state.font_stack <- { has_missing_glyph = false } :: state.font_stack;
417422 if name_lower = "fecomponenttransfer" then
418418- state.fecomponenttransfer_stack <- { seen_funcs = [] } :: state.fecomponenttransfer_stack;
423423+ state.fecomponenttransfer_stack <- { seen_funcs = []; duplicate_error_reported = false } :: state.fecomponenttransfer_stack;
424424+425425+ (* Check feConvolveMatrix requires order attribute *)
426426+ if name_lower = "feconvolvematrix" then begin
427427+ if not (Attr_utils.has_attr "order" attrs) then
428428+ Message_collector.add_typed collector
429429+ (`Svg (`Missing_attr (`Elem "feConvolveMatrix", `Attr "order")))
430430+ end;
419431420432 state.element_stack <- name :: state.element_stack;
421433