···7575 (fun prohibited ->
7676 if List.exists (fun ctx -> String.equal ctx.name prohibited) state.ancestor_stack then
7777 Message_collector.add_typed collector
7878- (Error_code.Element_not_allowed_as_child { child = name; parent = prohibited }))
7878+ (`Element (`Not_allowed_as_child (`Child name, `Parent prohibited))))
7979 spec.Element_spec.prohibited_ancestors
80808181(* Validate that a child element is allowed *)
···8585 (* Root level - only html allowed *)
8686 if not (String.equal (String.lowercase_ascii child_name) "html") then
8787 Message_collector.add_typed collector
8888- (Error_code.Generic { message = Printf.sprintf "Element '%s' not allowed at document root (only 'html' allowed)" child_name })
8888+ (`Generic (Printf.sprintf "Element '%s' not allowed at document root (only 'html' allowed)" child_name))
8989 | parent :: _ ->
9090 let content_model = parent.spec.Element_spec.content_model in
9191 if not (matches_content_model state.registry child_name content_model) then
9292 Message_collector.add_typed collector
9393- (Error_code.Element_not_allowed_as_child { child = child_name; parent = parent.name })
9393+ (`Element (`Not_allowed_as_child (`Child child_name, `Parent parent.name)))
94949595let start_element state ~name ~namespace:_ ~attrs:_ collector =
9696 (* Look up element specification *)
···116116 | [] ->
117117 (* Unmatched closing tag *)
118118 Message_collector.add_typed collector
119119- (Error_code.Generic { message = Printf.sprintf "Unmatched closing tag '%s'" name })
119119+ (`Generic (Printf.sprintf "Unmatched closing tag '%s'" name))
120120 | context :: rest ->
121121 if not (String.equal context.name name) then
122122 (* Mismatched tag *)
123123 Message_collector.add_typed collector
124124- (Error_code.Generic { message = Printf.sprintf "Expected closing tag '%s' but got '%s'" context.name name })
124124+ (`Generic (Printf.sprintf "Expected closing tag '%s' but got '%s'" context.name name))
125125 else (
126126 (* Check if void element has children *)
127127 if Element_spec.is_void context.spec && context.children_count > 0 then
128128 Message_collector.add_typed collector
129129- (Error_code.Generic { message = Printf.sprintf "Void element '%s' must not have children" name });
129129+ (`Generic (Printf.sprintf "Void element '%s' must not have children" name));
130130131131 (* Pop stack *)
132132 state.ancestor_stack <- rest;
···145145 (* Text at root level - only whitespace allowed *)
146146 if not (String.trim text = "") then
147147 Message_collector.add_typed collector
148148- (Error_code.Generic { message = "Text content not allowed at document root" })
148148+ (`Generic "Text content not allowed at document root")
149149 | parent :: rest ->
150150 let content_model = parent.spec.Element_spec.content_model in
151151 if not (allows_text content_model) then
152152 (* Only report if non-whitespace text *)
153153 if not (String.trim text = "") then
154154 Message_collector.add_typed collector
155155- (Error_code.Text_not_allowed { parent = parent.name })
155155+ (`Element (`Text_not_allowed (`Parent parent.name)))
156156 else (
157157 (* Text is allowed, increment child count *)
158158 let updated_parent = { parent with children_count = parent.children_count + 1 } in
···163163 List.iter
164164 (fun context ->
165165 Message_collector.add_typed collector
166166- (Error_code.Generic { message = Printf.sprintf "Unclosed element '%s'" context.name }))
166166+ (`Generic (Printf.sprintf "Unclosed element '%s'" context.name)))
167167 state.ancestor_stack
168168169169(* Package as first-class module *)
+566-594
lib/html5_checker/error_code.ml
···11-(** Typed error codes for HTML5 validation messages.
22-33- This module defines a comprehensive variant type for all validation errors,
44- ensuring exact message matching with the Nu HTML Validator test suite. *)
11+(** Typed error codes for HTML5 validation messages. *)
5266-(** Severity level of a validation message *)
73type severity = Error | Warning | Info
8499-(** Typed error codes with associated data *)
1010-type t =
1111- (* ===== Attribute Errors ===== *)
1212- | Attr_not_allowed_on_element of { attr: string; element: string }
1313- (** Attribute "X" not allowed on element "Y" at this point. *)
1414- | Attr_not_allowed_here of { attr: string }
1515- (** Attribute "X" not allowed here. *)
1616- | Attr_not_allowed_when of { attr: string; element: string; condition: string }
1717- (** Attribute "X" is only allowed when ... *)
1818- | Missing_required_attr of { element: string; attr: string }
1919- (** Element "X" is missing required attribute "Y". *)
2020- | Missing_required_attr_one_of of { element: string; attrs: string list }
2121- (** Element "X" is missing one or more of the following attributes: [A, B]. *)
2222- | Bad_attr_value of { element: string; attr: string; value: string; reason: string }
2323- (** Bad value "X" for attribute "Y" on element "Z". *)
2424- | Bad_attr_value_generic of { message: string }
2525- (** Generic bad attribute value message *)
2626- | Duplicate_id of { id: string }
2727- (** Duplicate ID "X". *)
2828- | Data_attr_invalid_name of { reason: string }
2929- (** "data-*" attribute names must be XML 1.0 4th ed. plus Namespaces NCNames. *)
3030- | Data_attr_uppercase
3131- (** "data-*" attributes must not have characters from the range "A"…"Z" in the name. *)
55+type attr_error = [
66+ | `Not_allowed of [`Attr of string] * [`Elem of string]
77+ | `Not_allowed_here of [`Attr of string]
88+ | `Not_allowed_when of [`Attr of string] * [`Elem of string] * [`Condition of string]
99+ | `Missing of [`Elem of string] * [`Attr of string]
1010+ | `Missing_one_of of [`Elem of string] * [`Attrs of string list]
1111+ | `Bad_value of [`Elem of string] * [`Attr of string] * [`Value of string] * [`Reason of string]
1212+ | `Bad_value_generic of [`Message of string]
1313+ | `Duplicate_id of [`Id of string]
1414+ | `Data_invalid_name of [`Reason of string]
1515+ | `Data_uppercase
1616+]
32173333- (* ===== Element Errors ===== *)
3434- | Obsolete_element of { element: string; suggestion: string }
3535- (** The "X" element is obsolete. Y *)
3636- | Obsolete_attr of { element: string; attr: string; suggestion: string option }
3737- (** The "X" attribute on the "Y" element is obsolete. *)
3838- | Obsolete_global_attr of { attr: string; suggestion: string }
3939- (** The "X" attribute is obsolete. Y *)
4040- | Element_not_allowed_as_child of { child: string; parent: string }
4141- (** Element "X" not allowed as child of element "Y" in this context. *)
4242- | Unknown_element of { name: string }
4343- (** Unknown element "X". *)
4444- | Element_must_not_be_descendant of { element: string; attr: string option; ancestor: string }
4545- (** The element "X" [with attribute "A"] must not appear as a descendant of the "Y" element. *)
4646- | Missing_required_child of { parent: string; child: string }
4747- (** Element "X" is missing required child element "Y". *)
4848- | Missing_required_child_one_of of { parent: string; children: string list }
4949- (** Element "X" is missing one or more of the following child elements: [A, B]. *)
5050- | Missing_required_child_generic of { parent: string }
5151- (** Element "X" is missing a required child element. *)
5252- | Element_must_not_be_empty of { element: string }
5353- (** Element "X" must not be empty. *)
5454- | Stray_start_tag of { tag: string }
5555- (** Stray start tag "X". *)
5656- | Stray_end_tag of { tag: string }
5757- (** Stray end tag "X". *)
5858- | End_tag_for_void_element of { tag: string }
5959- (** End tag "X". (for void elements like br) *)
6060- | Self_closing_non_void
6161- (** Self-closing syntax used on a non-void HTML element. *)
6262- | Text_not_allowed of { parent: string }
6363- (** Text not allowed in element "X" in this context. *)
1818+type element_error = [
1919+ | `Obsolete of [`Elem of string] * [`Suggestion of string]
2020+ | `Obsolete_attr of [`Elem of string] * [`Attr of string] * [`Suggestion of string option]
2121+ | `Obsolete_global_attr of [`Attr of string] * [`Suggestion of string]
2222+ | `Not_allowed_as_child of [`Child of string] * [`Parent of string]
2323+ | `Unknown of [`Elem of string]
2424+ | `Must_not_descend of [`Elem of string] * [`Attr of string option] * [`Ancestor of string]
2525+ | `Missing_child of [`Parent of string] * [`Child of string]
2626+ | `Missing_child_one_of of [`Parent of string] * [`Children of string list]
2727+ | `Missing_child_generic of [`Parent of string]
2828+ | `Must_not_be_empty of [`Elem of string]
2929+ | `Text_not_allowed of [`Parent of string]
3030+]
64316565- (* ===== Child Restrictions ===== *)
6666- | Div_child_of_dl_bad_role
6767- (** A "div" child of a "dl" element must not have any "role" value other than "presentation" or "none". *)
6868- | Li_bad_role_in_menu
6969- (** An "li" element descendant of role=menu/menubar must have specific roles. *)
7070- | Li_bad_role_in_tablist
7171- (** An "li" element descendant of role=tablist must have role=tab. *)
7272- | Li_bad_role_in_list
7373- (** An "li" element descendant of ul/ol/menu or role=list must have role=listitem. *)
3232+type tag_error = [
3333+ | `Stray_start of [`Tag of string]
3434+ | `Stray_end of [`Tag of string]
3535+ | `End_for_void of [`Tag of string]
3636+ | `Self_closing_non_void
3737+ | `Not_in_scope of [`Tag of string]
3838+ | `End_implied_open of [`Tag of string]
3939+ | `Start_in_table of [`Tag of string]
4040+ | `Bad_start_in of [`Tag of string] * [`Context of string]
4141+ | `Eof_with_open
4242+]
74437575- (* ===== ARIA Errors ===== *)
7676- | Unnecessary_role of { role: string; element: string; reason: string }
7777- (** The "X" role is unnecessary for Y. *)
7878- | Bad_role of { element: string; role: string }
7979- (** Bad value "X" for attribute "role" on element "Y". *)
8080- | Aria_must_not_be_specified of { attr: string; element: string; condition: string }
8181- (** The "X" attribute must not be specified on any "Y" element unless... *)
8282- | Aria_must_not_be_used of { attr: string; element: string; condition: string }
8383- (** The "X" attribute must not be used on an "Y" element which has... *)
8484- | Aria_should_not_be_used of { attr: string; role: string }
8585- (** The "X" attribute should not be used on any element which has "role=Y". *)
8686- | Aria_hidden_on_body
8787- (** "aria-hidden=true" must not be used on the "body" element. *)
8888- | Img_empty_alt_with_role
8989- (** An "img" element with empty alt must not have a role attribute. *)
9090- | Checkbox_button_needs_aria_pressed
9191- (** An "input" type="checkbox" with role="button" must have aria-pressed. *)
9292- | Tab_without_tabpanel
9393- (** Every active "role=tab" element must have a corresponding "role=tabpanel" element. *)
9494- | Multiple_main_visible
9595- (** A document should not include more than one visible element with "role=main". *)
9696- | Discarding_unrecognized_role of { token: string }
9797- (** Discarding unrecognized token "X" from value of attribute "role". *)
4444+type char_ref_error = [
4545+ | `Forbidden_codepoint of [`Codepoint of int]
4646+ | `Control_char of [`Codepoint of int]
4747+ | `Non_char of [`Codepoint of int] * [`Astral of bool]
4848+ | `Unassigned
4949+ | `Zero
5050+ | `Out_of_range
5151+ | `Carriage_return
5252+]
98539999- (* ===== Required Attribute/Element Conditions ===== *)
100100- | Img_missing_alt
101101- (** An "img" element must have an "alt" attribute. *)
102102- | Img_missing_src_or_srcset
103103- (** Element "img" is missing one or more of the following attributes: [src, srcset]. *)
104104- | Option_empty_without_label
105105- (** Element "option" without attribute "label" must not be empty. *)
106106- | Bdo_missing_dir
107107- (** Element "bdo" must have attribute "dir". *)
108108- | Bdo_dir_auto
109109- (** The value of "dir" attribute for the "bdo" element must not be "auto". *)
110110- | Base_missing_href_or_target
111111- (** Element "base" is missing one or more of the following attributes: [href, target]. *)
112112- | Base_after_link_script
113113- (** The "base" element must come before any "link" or "script" elements. *)
114114- | Link_missing_href
115115- (** A "link" element must have an "href" or "imagesrcset" attribute. *)
116116- | Link_as_requires_preload
117117- (** A "link" element with an "as" attribute must have rel="preload" or "modulepreload". *)
118118- | Link_imagesrcset_requires_as_image
119119- (** A "link" element with "imagesrcset" must have as="image". *)
120120- | Img_ismap_needs_a_href
121121- (** The "img" element with "ismap" must have an "a" ancestor with "href". *)
122122- | Sizes_without_srcset
123123- (** The "sizes" attribute must only be specified if "srcset" is also specified. *)
124124- | Imagesizes_without_imagesrcset
125125- (** The "imagesizes" attribute must only be specified if "imagesrcset" is also specified. *)
126126- | Srcset_w_without_sizes
127127- (** When the "srcset" attribute has width descriptors, "sizes" must also be specified. *)
128128- | Source_missing_srcset
129129- (** Element "source" is missing required attribute "srcset". *)
130130- | Source_needs_media_or_type
131131- (** A "source" element with following source/img[srcset] must have media/type. *)
132132- | Picture_missing_img
133133- (** Element "picture" is missing required child element "img". *)
134134- | Map_id_name_mismatch
135135- (** The "id" attribute on a "map" element must have the same value as the "name" attribute. *)
136136- | List_attr_requires_datalist
137137- (** The "list" attribute of "input" must refer to a "datalist" element. *)
138138- | Input_list_not_allowed
139139- (** Attribute "list" is only allowed on certain input types. *)
140140- | Label_too_many_labelable
141141- (** The "label" element may contain at most one labelable descendant. *)
142142- | Label_for_id_mismatch
143143- (** Any "input" descendant of a "label" with "for" must have matching ID. *)
144144- | Role_on_label_ancestor
145145- (** The "role" attribute must not be on label ancestor of labelable element. *)
146146- | Role_on_label_for
147147- (** The "role" attribute must not be on label associated via for. *)
148148- | Aria_label_on_label_for
149149- (** The "aria-label" attribute must not be on label associated via for. *)
150150- | Input_value_constraint of { constraint_type: string }
151151- (** The value of the "value" attribute must be... *)
152152- | Summary_missing_role
153153- (** Element "summary" is missing required attribute "role". *)
154154- | Summary_missing_attrs
155155- (** Element "summary" is missing one or more of [aria-checked, aria-level, role]. *)
156156- | Summary_role_not_allowed
157157- (** The "role" attribute must not be used on any "summary" for its parent "details". *)
158158- | Autocomplete_webauthn_on_select
159159- (** The value of "autocomplete" for "select" must not contain "webauthn". *)
160160- | Commandfor_invalid_target
161161- (** The value of "commandfor" must be the ID of an element in the same tree. *)
5454+type aria_error = [
5555+ | `Unnecessary_role of [`Role of string] * [`Elem of string] * [`Reason of string]
5656+ | `Bad_role of [`Elem of string] * [`Role of string]
5757+ | `Must_not_specify of [`Attr of string] * [`Elem of string] * [`Condition of string]
5858+ | `Must_not_use of [`Attr of string] * [`Elem of string] * [`Condition of string]
5959+ | `Should_not_use of [`Attr of string] * [`Role of string]
6060+ | `Hidden_on_body
6161+ | `Unrecognized_role of [`Token of string]
6262+ | `Tab_without_tabpanel
6363+ | `Multiple_main
6464+]
16265163163- (* ===== Parse Errors ===== *)
164164- | Forbidden_codepoint of { codepoint: int }
165165- (** Forbidden code point U+XXXX. *)
166166- | Char_ref_control of { codepoint: int }
167167- (** Character reference expands to a control character (U+XXXX). *)
168168- | Char_ref_non_char of { codepoint: int; astral: bool }
169169- (** Character reference expands to a [astral] non-character (U+XXXX). *)
170170- | Char_ref_unassigned
171171- (** Character reference expands to a permanently unassigned code point. *)
172172- | Char_ref_zero
173173- (** Character reference expands to zero. *)
174174- | Char_ref_out_of_range
175175- (** Character reference outside the permissible Unicode range. *)
176176- | Numeric_char_ref_carriage_return
177177- (** A numeric character reference expanded to carriage return. *)
178178- | End_of_file_with_open_elements
179179- (** End of file seen and there were open elements. *)
180180- | No_element_in_scope of { tag: string }
181181- (** No "X" element in scope but a "X" end tag seen. *)
182182- | End_tag_implied_open_elements of { tag: string }
183183- (** End tag "X" implied, but there were open elements. *)
184184- | Start_tag_in_table of { tag: string }
185185- (** Start tag "X" seen in "table". *)
186186- | Bad_start_tag_in of { tag: string; context: string }
187187- (** Bad start tag in "X" in "noscript" in "head". *)
6666+type li_role_error = [
6767+ | `Div_in_dl_bad_role
6868+ | `Li_bad_role_in_menu
6969+ | `Li_bad_role_in_tablist
7070+ | `Li_bad_role_in_list
7171+]
18872189189- (* ===== Table Errors ===== *)
190190- | Table_row_no_cells of { row: int }
191191- (** Row N of an implicit row group has no cells beginning on it. *)
192192- | Table_cell_overlap
193193- (** Table cell is overlapped by later table cell. *)
194194- | Table_cell_spans_rowgroup
195195- (** Table cell spans past the end of its row group. *)
196196- | Table_column_no_cells of { column: int; element: string }
197197- (** Table column N established by element "X" has no cells beginning in it. *)
7373+type table_error = [
7474+ | `Row_no_cells of [`Row of int]
7575+ | `Cell_overlap
7676+ | `Cell_spans_rowgroup
7777+ | `Column_no_cells of [`Column of int] * [`Elem of string]
7878+]
19879199199- (* ===== Language/Internationalization ===== *)
200200- | Missing_lang_attr
201201- (** Consider adding a "lang" attribute to the "html" start tag. *)
202202- | Wrong_lang of { detected: string; declared: string; suggested: string }
203203- (** This document appears to be written in X but has lang="Y". Consider using "Z". *)
204204- | Missing_dir_rtl of { language: string }
205205- (** This document appears to be written in X. Consider adding dir="rtl". *)
206206- | Wrong_dir of { language: string; declared: string }
207207- (** This document appears to be written in X but has dir="Y". Consider dir="rtl". *)
208208- | Xml_lang_without_lang
209209- (** When xml:lang is specified, lang must also be present with the same value. *)
210210- | Xml_lang_lang_mismatch
211211- (** xml:lang and lang must have the same value. *)
8080+type i18n_error = [
8181+ | `Missing_lang
8282+ | `Wrong_lang of [`Detected of string] * [`Declared of string] * [`Suggested of string]
8383+ | `Missing_dir_rtl of [`Language of string]
8484+ | `Wrong_dir of [`Language of string] * [`Declared of string]
8585+ | `Xml_lang_without_lang
8686+ | `Xml_lang_mismatch
8787+ | `Not_nfc of [`Replacement of string]
8888+]
21289213213- (* ===== Unicode Normalization ===== *)
214214- | Not_nfc of { replacement: string }
215215- (** Text run is not in Unicode Normalization Form C. *)
9090+type importmap_error = [
9191+ | `Invalid_json
9292+ | `Invalid_root
9393+ | `Imports_not_object
9494+ | `Empty_key
9595+ | `Non_string_value
9696+ | `Key_trailing_slash
9797+ | `Scopes_not_object
9898+ | `Scopes_values_not_object
9999+ | `Scopes_invalid_url
100100+ | `Scopes_value_invalid_url
101101+]
216102217217- (* ===== Multiple h1 ===== *)
218218- | Multiple_h1
219219- (** Consider using only one "h1" element per document. *)
220220- | Multiple_autofocus
221221- (** There must not be two elements with autofocus in the same scoping root. *)
103103+type img_error = [
104104+ | `Missing_alt
105105+ | `Missing_src_or_srcset
106106+ | `Empty_alt_with_role
107107+ | `Ismap_needs_href
108108+]
109109+110110+type link_error = [
111111+ | `Missing_href
112112+ | `As_requires_preload
113113+ | `Imagesrcset_requires_as_image
114114+]
222115223223- (* ===== Import Maps ===== *)
224224- | Importmap_invalid_json
225225- (** A "script" type="importmap" must have valid JSON content. *)
226226- | Importmap_invalid_root
227227- (** A "script" type="importmap" must contain a JSON object with only imports/scopes/integrity. *)
228228- | Importmap_imports_not_object
229229- (** The value of "imports" property must be a JSON object. *)
230230- | Importmap_empty_key
231231- (** Specifier map must only contain non-empty keys. *)
232232- | Importmap_non_string_value
233233- (** Specifier map must only contain string values. *)
234234- | Importmap_key_trailing_slash
235235- (** Specifier map values must end with "/" when key ends with "/". *)
236236- | Importmap_scopes_not_object
237237- (** The value of "scopes" property must be a JSON object with valid URL keys. *)
238238- | Importmap_scopes_values_not_object
239239- (** The value of "scopes" property values must also be JSON objects. *)
240240- | Importmap_scopes_invalid_url
241241- (** The "scopes" property keys must be valid URL strings. *)
242242- | Importmap_scopes_value_invalid_url
243243- (** The specifier map within "scopes" must only contain valid URL values. *)
116116+type label_error = [
117117+ | `Too_many_labelable
118118+ | `For_id_mismatch
119119+ | `Role_on_ancestor
120120+ | `Role_on_for
121121+ | `Aria_label_on_for
122122+]
244123245245- (* ===== Style Element ===== *)
246246- | Style_type_invalid
247247- (** The only allowed value for "type" on "style" is "text/css". *)
124124+type input_error = [
125125+ | `Checkbox_needs_aria_pressed
126126+ | `Value_constraint of [`Constraint of string]
127127+ | `List_not_allowed
128128+ | `List_requires_datalist
129129+]
248130249249- (* ===== Headingoffset ===== *)
250250- | Headingoffset_invalid
251251- (** The value of "headingoffset" must be a number between "0" and "8". *)
131131+type srcset_error = [
132132+ | `Sizes_without_srcset
133133+ | `Imagesizes_without_imagesrcset
134134+ | `W_without_sizes
135135+ | `Source_missing_srcset
136136+ | `Source_needs_media_or_type
137137+ | `Picture_missing_img
138138+]
252139253253- (* ===== Media Attribute ===== *)
254254- | Media_empty
255255- (** Value of "media" attribute here must not be empty. *)
256256- | Media_all
257257- (** Value of "media" attribute here must not be "all". *)
140140+type svg_error = [
141141+ | `Deprecated_attr of [`Attr of string] * [`Elem of string]
142142+ | `Missing_attr of [`Elem of string] * [`Attr of string]
143143+]
258144259259- (* ===== SVG/MathML specific ===== *)
260260- | Svg_deprecated_attr of { attr: string; element: string }
261261- (** SVG deprecated attribute *)
262262- | Missing_required_svg_attr of { element: string; attr: string }
263263- (** Element "X" is missing required attribute "Y". (SVG) *)
145145+type misc_error = [
146146+ | `Option_empty_without_label
147147+ | `Bdo_missing_dir
148148+ | `Bdo_dir_auto
149149+ | `Base_missing_href_or_target
150150+ | `Base_after_link_script
151151+ | `Map_id_name_mismatch
152152+ | `Summary_missing_role
153153+ | `Summary_missing_attrs
154154+ | `Summary_role_not_allowed
155155+ | `Autocomplete_webauthn_on_select
156156+ | `Commandfor_invalid_target
157157+ | `Style_type_invalid
158158+ | `Headingoffset_invalid
159159+ | `Media_empty
160160+ | `Media_all
161161+ | `Multiple_h1
162162+ | `Multiple_autofocus
163163+]
264164265265- (* ===== Generic/Fallback ===== *)
266266- | Generic of { message: string }
267267- (** For messages that don't fit any specific pattern *)
165165+type t = [
166166+ | `Attr of attr_error
167167+ | `Element of element_error
168168+ | `Tag of tag_error
169169+ | `Char_ref of char_ref_error
170170+ | `Aria of aria_error
171171+ | `Li_role of li_role_error
172172+ | `Table of table_error
173173+ | `I18n of i18n_error
174174+ | `Importmap of importmap_error
175175+ | `Img of img_error
176176+ | `Link of link_error
177177+ | `Label of label_error
178178+ | `Input of input_error
179179+ | `Srcset of srcset_error
180180+ | `Svg of svg_error
181181+ | `Misc of misc_error
182182+ | `Generic of string
183183+]
268184269185(** Get the severity level for an error code *)
270270-let severity = function
271271- | Missing_lang_attr -> Info
272272- | Multiple_h1 -> Info
273273- | Wrong_lang _ -> Warning
274274- | Missing_dir_rtl _ -> Warning
275275- | Wrong_dir _ -> Warning
276276- | Unnecessary_role _ -> Warning
277277- | Aria_should_not_be_used _ -> Warning
278278- | Unknown_element _ -> Warning
279279- | Not_nfc _ -> Warning
186186+let severity : t -> severity = function
187187+ (* Info level *)
188188+ | `I18n `Missing_lang -> Info
189189+ | `Misc `Multiple_h1 -> Info
190190+191191+ (* Warning level *)
192192+ | `I18n (`Wrong_lang _) -> Warning
193193+ | `I18n (`Missing_dir_rtl _) -> Warning
194194+ | `I18n (`Wrong_dir _) -> Warning
195195+ | `I18n (`Not_nfc _) -> Warning
196196+ | `Aria (`Unnecessary_role _) -> Warning
197197+ | `Aria (`Should_not_use _) -> Warning
198198+ | `Element (`Unknown _) -> Warning
199199+200200+ (* Everything else is Error *)
280201 | _ -> Error
281202282203(** Get a short code string for categorization *)
283283-let code_string = function
284284- | Attr_not_allowed_on_element _ -> "disallowed-attribute"
285285- | Attr_not_allowed_here _ -> "disallowed-attribute"
286286- | Attr_not_allowed_when _ -> "disallowed-attribute"
287287- | Missing_required_attr _ -> "missing-required-attribute"
288288- | Missing_required_attr_one_of _ -> "missing-required-attribute"
289289- | Bad_attr_value _ -> "bad-attribute-value"
290290- | Bad_attr_value_generic _ -> "bad-attribute-value"
291291- | Duplicate_id _ -> "duplicate-id"
292292- | Data_attr_invalid_name _ -> "bad-attribute-name"
293293- | Data_attr_uppercase -> "bad-attribute-name"
294294- | Obsolete_element _ -> "obsolete-element"
295295- | Obsolete_attr _ -> "obsolete-attribute"
296296- | Obsolete_global_attr _ -> "obsolete-attribute"
297297- | Element_not_allowed_as_child _ -> "disallowed-child"
298298- | Unknown_element _ -> "unknown-element"
299299- | Element_must_not_be_descendant _ -> "prohibited-ancestor"
300300- | Missing_required_child _ -> "missing-required-child"
301301- | Missing_required_child_one_of _ -> "missing-required-child"
302302- | Missing_required_child_generic _ -> "missing-required-child"
303303- | Element_must_not_be_empty _ -> "empty-element"
304304- | Stray_start_tag _ -> "stray-tag"
305305- | Stray_end_tag _ -> "stray-tag"
306306- | End_tag_for_void_element _ -> "end-tag-void"
307307- | Self_closing_non_void -> "self-closing-non-void"
308308- | Text_not_allowed _ -> "text-not-allowed"
309309- | Div_child_of_dl_bad_role -> "invalid-role"
310310- | Li_bad_role_in_menu -> "invalid-role"
311311- | Li_bad_role_in_tablist -> "invalid-role"
312312- | Li_bad_role_in_list -> "invalid-role"
313313- | Unnecessary_role _ -> "unnecessary-role"
314314- | Bad_role _ -> "bad-role"
315315- | Aria_must_not_be_specified _ -> "aria-not-allowed"
316316- | Aria_must_not_be_used _ -> "aria-not-allowed"
317317- | Aria_should_not_be_used _ -> "aria-not-allowed"
318318- | Aria_hidden_on_body -> "aria-not-allowed"
319319- | Img_empty_alt_with_role -> "img-alt-role"
320320- | Checkbox_button_needs_aria_pressed -> "missing-aria-pressed"
321321- | Tab_without_tabpanel -> "tab-without-tabpanel"
322322- | Multiple_main_visible -> "multiple-main"
323323- | Discarding_unrecognized_role _ -> "unrecognized-role"
324324- | Img_missing_alt -> "missing-alt"
325325- | Img_missing_src_or_srcset -> "missing-src"
326326- | Option_empty_without_label -> "empty-option"
327327- | Bdo_missing_dir -> "missing-dir"
328328- | Bdo_dir_auto -> "bdo-dir-auto"
329329- | Base_missing_href_or_target -> "missing-required-attribute"
330330- | Base_after_link_script -> "base-position"
331331- | Link_missing_href -> "missing-href"
332332- | Link_as_requires_preload -> "link-as-preload"
333333- | Link_imagesrcset_requires_as_image -> "link-imagesrcset"
334334- | Img_ismap_needs_a_href -> "ismap-needs-href"
335335- | Sizes_without_srcset -> "sizes-without-srcset"
336336- | Imagesizes_without_imagesrcset -> "imagesizes-without-srcset"
337337- | Srcset_w_without_sizes -> "srcset-needs-sizes"
338338- | Source_missing_srcset -> "missing-srcset"
339339- | Source_needs_media_or_type -> "source-needs-media"
340340- | Picture_missing_img -> "picture-missing-img"
341341- | Map_id_name_mismatch -> "map-id-name"
342342- | List_attr_requires_datalist -> "list-datalist"
343343- | Input_list_not_allowed -> "list-not-allowed"
344344- | Label_too_many_labelable -> "label-multiple"
345345- | Label_for_id_mismatch -> "label-for-mismatch"
346346- | Role_on_label_ancestor -> "role-on-label"
347347- | Role_on_label_for -> "role-on-label"
348348- | Aria_label_on_label_for -> "aria-label-on-label"
349349- | Input_value_constraint _ -> "input-value"
350350- | Summary_missing_role -> "summary-role"
351351- | Summary_missing_attrs -> "summary-attrs"
352352- | Summary_role_not_allowed -> "summary-role"
353353- | Autocomplete_webauthn_on_select -> "autocomplete"
354354- | Commandfor_invalid_target -> "commandfor"
355355- | Forbidden_codepoint _ -> "forbidden-codepoint"
356356- | Char_ref_control _ -> "char-ref-control"
357357- | Char_ref_non_char _ -> "char-ref-non-char"
358358- | Char_ref_unassigned -> "char-ref-unassigned"
359359- | Char_ref_zero -> "char-ref-zero"
360360- | Char_ref_out_of_range -> "char-ref-range"
361361- | Numeric_char_ref_carriage_return -> "numeric-char-ref"
362362- | End_of_file_with_open_elements -> "eof-open-elements"
363363- | No_element_in_scope _ -> "no-element-in-scope"
364364- | End_tag_implied_open_elements _ -> "end-tag-implied"
365365- | Start_tag_in_table _ -> "start-tag-in-table"
366366- | Bad_start_tag_in _ -> "bad-start-tag"
367367- | Table_row_no_cells _ -> "table-row"
368368- | Table_cell_overlap -> "table-overlap"
369369- | Table_cell_spans_rowgroup -> "table-span"
370370- | Table_column_no_cells _ -> "table-column"
371371- | Missing_lang_attr -> "missing-lang"
372372- | Wrong_lang _ -> "wrong-lang"
373373- | Missing_dir_rtl _ -> "missing-dir"
374374- | Wrong_dir _ -> "wrong-dir"
375375- | Xml_lang_without_lang -> "xml-lang"
376376- | Xml_lang_lang_mismatch -> "xml-lang-mismatch"
377377- | Not_nfc _ -> "unicode-normalization"
378378- | Multiple_h1 -> "multiple-h1"
379379- | Multiple_autofocus -> "multiple-autofocus"
380380- | Importmap_invalid_json -> "importmap"
381381- | Importmap_invalid_root -> "importmap"
382382- | Importmap_imports_not_object -> "importmap"
383383- | Importmap_empty_key -> "importmap"
384384- | Importmap_non_string_value -> "importmap"
385385- | Importmap_key_trailing_slash -> "importmap"
386386- | Importmap_scopes_not_object -> "importmap"
387387- | Importmap_scopes_values_not_object -> "importmap"
388388- | Importmap_scopes_invalid_url -> "importmap"
389389- | Importmap_scopes_value_invalid_url -> "importmap"
390390- | Style_type_invalid -> "style-type"
391391- | Headingoffset_invalid -> "headingoffset"
392392- | Media_empty -> "media-empty"
393393- | Media_all -> "media-all"
394394- | Svg_deprecated_attr _ -> "svg-deprecated"
395395- | Missing_required_svg_attr _ -> "missing-required-attribute"
396396- | Generic _ -> "generic"
204204+let code_string : t -> string = function
205205+ (* Attribute errors *)
206206+ | `Attr (`Not_allowed _) -> "disallowed-attribute"
207207+ | `Attr (`Not_allowed_here _) -> "disallowed-attribute"
208208+ | `Attr (`Not_allowed_when _) -> "disallowed-attribute"
209209+ | `Attr (`Missing _) -> "missing-required-attribute"
210210+ | `Attr (`Missing_one_of _) -> "missing-required-attribute"
211211+ | `Attr (`Bad_value _) -> "bad-attribute-value"
212212+ | `Attr (`Bad_value_generic _) -> "bad-attribute-value"
213213+ | `Attr (`Duplicate_id _) -> "duplicate-id"
214214+ | `Attr (`Data_invalid_name _) -> "bad-attribute-name"
215215+ | `Attr `Data_uppercase -> "bad-attribute-name"
216216+217217+ (* Element errors *)
218218+ | `Element (`Obsolete _) -> "obsolete-element"
219219+ | `Element (`Obsolete_attr _) -> "obsolete-attribute"
220220+ | `Element (`Obsolete_global_attr _) -> "obsolete-attribute"
221221+ | `Element (`Not_allowed_as_child _) -> "disallowed-child"
222222+ | `Element (`Unknown _) -> "unknown-element"
223223+ | `Element (`Must_not_descend _) -> "prohibited-ancestor"
224224+ | `Element (`Missing_child _) -> "missing-required-child"
225225+ | `Element (`Missing_child_one_of _) -> "missing-required-child"
226226+ | `Element (`Missing_child_generic _) -> "missing-required-child"
227227+ | `Element (`Must_not_be_empty _) -> "empty-element"
228228+ | `Element (`Text_not_allowed _) -> "text-not-allowed"
229229+230230+ (* Tag errors *)
231231+ | `Tag (`Stray_start _) -> "stray-tag"
232232+ | `Tag (`Stray_end _) -> "stray-tag"
233233+ | `Tag (`End_for_void _) -> "end-tag-void"
234234+ | `Tag `Self_closing_non_void -> "self-closing-non-void"
235235+ | `Tag (`Not_in_scope _) -> "no-element-in-scope"
236236+ | `Tag (`End_implied_open _) -> "end-tag-implied"
237237+ | `Tag (`Start_in_table _) -> "start-tag-in-table"
238238+ | `Tag (`Bad_start_in _) -> "bad-start-tag"
239239+ | `Tag `Eof_with_open -> "eof-open-elements"
240240+241241+ (* Character reference errors *)
242242+ | `Char_ref (`Forbidden_codepoint _) -> "forbidden-codepoint"
243243+ | `Char_ref (`Control_char _) -> "char-ref-control"
244244+ | `Char_ref (`Non_char _) -> "char-ref-non-char"
245245+ | `Char_ref `Unassigned -> "char-ref-unassigned"
246246+ | `Char_ref `Zero -> "char-ref-zero"
247247+ | `Char_ref `Out_of_range -> "char-ref-range"
248248+ | `Char_ref `Carriage_return -> "numeric-char-ref"
249249+250250+ (* ARIA errors *)
251251+ | `Aria (`Unnecessary_role _) -> "unnecessary-role"
252252+ | `Aria (`Bad_role _) -> "bad-role"
253253+ | `Aria (`Must_not_specify _) -> "aria-not-allowed"
254254+ | `Aria (`Must_not_use _) -> "aria-not-allowed"
255255+ | `Aria (`Should_not_use _) -> "aria-not-allowed"
256256+ | `Aria `Hidden_on_body -> "aria-not-allowed"
257257+ | `Aria (`Unrecognized_role _) -> "unrecognized-role"
258258+ | `Aria `Tab_without_tabpanel -> "tab-without-tabpanel"
259259+ | `Aria `Multiple_main -> "multiple-main"
260260+261261+ (* List item role errors *)
262262+ | `Li_role `Div_in_dl_bad_role -> "invalid-role"
263263+ | `Li_role `Li_bad_role_in_menu -> "invalid-role"
264264+ | `Li_role `Li_bad_role_in_tablist -> "invalid-role"
265265+ | `Li_role `Li_bad_role_in_list -> "invalid-role"
266266+267267+ (* Table errors *)
268268+ | `Table (`Row_no_cells _) -> "table-row"
269269+ | `Table `Cell_overlap -> "table-overlap"
270270+ | `Table `Cell_spans_rowgroup -> "table-span"
271271+ | `Table (`Column_no_cells _) -> "table-column"
272272+273273+ (* I18n errors *)
274274+ | `I18n `Missing_lang -> "missing-lang"
275275+ | `I18n (`Wrong_lang _) -> "wrong-lang"
276276+ | `I18n (`Missing_dir_rtl _) -> "missing-dir"
277277+ | `I18n (`Wrong_dir _) -> "wrong-dir"
278278+ | `I18n `Xml_lang_without_lang -> "xml-lang"
279279+ | `I18n `Xml_lang_mismatch -> "xml-lang-mismatch"
280280+ | `I18n (`Not_nfc _) -> "unicode-normalization"
281281+282282+ (* Import map errors *)
283283+ | `Importmap `Invalid_json -> "importmap"
284284+ | `Importmap `Invalid_root -> "importmap"
285285+ | `Importmap `Imports_not_object -> "importmap"
286286+ | `Importmap `Empty_key -> "importmap"
287287+ | `Importmap `Non_string_value -> "importmap"
288288+ | `Importmap `Key_trailing_slash -> "importmap"
289289+ | `Importmap `Scopes_not_object -> "importmap"
290290+ | `Importmap `Scopes_values_not_object -> "importmap"
291291+ | `Importmap `Scopes_invalid_url -> "importmap"
292292+ | `Importmap `Scopes_value_invalid_url -> "importmap"
293293+294294+ (* Image errors *)
295295+ | `Img `Missing_alt -> "missing-alt"
296296+ | `Img `Missing_src_or_srcset -> "missing-src"
297297+ | `Img `Empty_alt_with_role -> "img-alt-role"
298298+ | `Img `Ismap_needs_href -> "ismap-needs-href"
299299+300300+ (* Link errors *)
301301+ | `Link `Missing_href -> "missing-href"
302302+ | `Link `As_requires_preload -> "link-as-preload"
303303+ | `Link `Imagesrcset_requires_as_image -> "link-imagesrcset"
304304+305305+ (* Label errors *)
306306+ | `Label `Too_many_labelable -> "label-multiple"
307307+ | `Label `For_id_mismatch -> "label-for-mismatch"
308308+ | `Label `Role_on_ancestor -> "role-on-label"
309309+ | `Label `Role_on_for -> "role-on-label"
310310+ | `Label `Aria_label_on_for -> "aria-label-on-label"
311311+312312+ (* Input errors *)
313313+ | `Input `Checkbox_needs_aria_pressed -> "missing-aria-pressed"
314314+ | `Input (`Value_constraint _) -> "input-value"
315315+ | `Input `List_not_allowed -> "list-not-allowed"
316316+ | `Input `List_requires_datalist -> "list-datalist"
317317+318318+ (* Srcset errors *)
319319+ | `Srcset `Sizes_without_srcset -> "sizes-without-srcset"
320320+ | `Srcset `Imagesizes_without_imagesrcset -> "imagesizes-without-srcset"
321321+ | `Srcset `W_without_sizes -> "srcset-needs-sizes"
322322+ | `Srcset `Source_missing_srcset -> "missing-srcset"
323323+ | `Srcset `Source_needs_media_or_type -> "source-needs-media"
324324+ | `Srcset `Picture_missing_img -> "picture-missing-img"
325325+326326+ (* SVG errors *)
327327+ | `Svg (`Deprecated_attr _) -> "svg-deprecated"
328328+ | `Svg (`Missing_attr _) -> "missing-required-attribute"
329329+330330+ (* Misc errors *)
331331+ | `Misc `Option_empty_without_label -> "empty-option"
332332+ | `Misc `Bdo_missing_dir -> "missing-dir"
333333+ | `Misc `Bdo_dir_auto -> "bdo-dir-auto"
334334+ | `Misc `Base_missing_href_or_target -> "missing-required-attribute"
335335+ | `Misc `Base_after_link_script -> "base-position"
336336+ | `Misc `Map_id_name_mismatch -> "map-id-name"
337337+ | `Misc `Summary_missing_role -> "summary-role"
338338+ | `Misc `Summary_missing_attrs -> "summary-attrs"
339339+ | `Misc `Summary_role_not_allowed -> "summary-role"
340340+ | `Misc `Autocomplete_webauthn_on_select -> "autocomplete"
341341+ | `Misc `Commandfor_invalid_target -> "commandfor"
342342+ | `Misc `Style_type_invalid -> "style-type"
343343+ | `Misc `Headingoffset_invalid -> "headingoffset"
344344+ | `Misc `Media_empty -> "media-empty"
345345+ | `Misc `Media_all -> "media-all"
346346+ | `Misc `Multiple_h1 -> "multiple-h1"
347347+ | `Misc `Multiple_autofocus -> "multiple-autofocus"
348348+349349+ (* Generic *)
350350+ | `Generic _ -> "generic"
397351398352(** Format using curly quotes (Unicode) *)
399353let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d"
400354401355(** Convert error code to exact Nu validator message string *)
402402-let to_message = function
403403- | Attr_not_allowed_on_element { attr; element } ->
356356+let to_message : t -> string = function
357357+ (* Attribute errors *)
358358+ | `Attr (`Not_allowed (`Attr attr, `Elem element)) ->
404359 Printf.sprintf "Attribute %s not allowed on element %s at this point."
405360 (q attr) (q element)
406406- | Attr_not_allowed_here { attr } ->
361361+ | `Attr (`Not_allowed_here (`Attr attr)) ->
407362 Printf.sprintf "Attribute %s not allowed here." (q attr)
408408- | Attr_not_allowed_when { attr; element = _; condition } ->
363363+ | `Attr (`Not_allowed_when (`Attr attr, `Elem _, `Condition condition)) ->
409364 Printf.sprintf "The %s attribute must not be used on any element which has %s." (q attr) condition
410410- | Missing_required_attr { element; attr } ->
365365+ | `Attr (`Missing (`Elem element, `Attr attr)) ->
411366 Printf.sprintf "Element %s is missing required attribute %s."
412367 (q element) (q attr)
413413- | Missing_required_attr_one_of { element; attrs } ->
368368+ | `Attr (`Missing_one_of (`Elem element, `Attrs attrs)) ->
414369 let attrs_str = String.concat ", " attrs in
415370 Printf.sprintf "Element %s is missing one or more of the following attributes: [%s]."
416371 (q element) attrs_str
417417- | Bad_attr_value { element; attr; value; reason } ->
372372+ | `Attr (`Bad_value (`Elem element, `Attr attr, `Value value, `Reason reason)) ->
418373 Printf.sprintf "Bad value %s for attribute %s on element %s: %s"
419374 (q value) (q attr) (q element) reason
420420- | Bad_attr_value_generic { message } -> message
421421- | Duplicate_id { id } ->
375375+ | `Attr (`Bad_value_generic (`Message message)) -> message
376376+ | `Attr (`Duplicate_id (`Id id)) ->
422377 Printf.sprintf "Duplicate ID %s." (q id)
423423- | Data_attr_invalid_name { reason } ->
378378+ | `Attr (`Data_invalid_name (`Reason reason)) ->
424379 Printf.sprintf "%s attribute names %s." (q "data-*") reason
425425- | Data_attr_uppercase ->
380380+ | `Attr `Data_uppercase ->
426381 Printf.sprintf "%s attributes must not have characters from the range %s\xe2\x80\xa6%s in the name."
427382 (q "data-*") (q "A") (q "Z")
428383429429- | Obsolete_element { element; suggestion } ->
384384+ (* Element errors *)
385385+ | `Element (`Obsolete (`Elem element, `Suggestion suggestion)) ->
430386 if suggestion = "" then
431387 Printf.sprintf "The %s element is obsolete." (q element)
432388 else
433389 Printf.sprintf "The %s element is obsolete. %s" (q element) suggestion
434434- | Obsolete_attr { element; attr; suggestion } ->
390390+ | `Element (`Obsolete_attr (`Elem element, `Attr attr, `Suggestion suggestion)) ->
435391 let base = Printf.sprintf "The %s attribute on the %s element is obsolete."
436392 (q attr) (q element) in
437393 (match suggestion with Some s -> base ^ " " ^ s | None -> base)
438438- | Obsolete_global_attr { attr; suggestion } ->
394394+ | `Element (`Obsolete_global_attr (`Attr attr, `Suggestion suggestion)) ->
439395 Printf.sprintf "The %s attribute is obsolete. %s" (q attr) suggestion
440440- | Element_not_allowed_as_child { child; parent } ->
396396+ | `Element (`Not_allowed_as_child (`Child child, `Parent parent)) ->
441397 Printf.sprintf "Element %s not allowed as child of element %s in this context. (Suppressing further errors from this subtree.)"
442398 (q child) (q parent)
443443- | Unknown_element { name } ->
399399+ | `Element (`Unknown (`Elem name)) ->
444400 Printf.sprintf "Unknown element %s." (q name)
445445- | Element_must_not_be_descendant { element; attr; ancestor } ->
401401+ | `Element (`Must_not_descend (`Elem element, `Attr attr, `Ancestor ancestor)) ->
446402 (match attr with
447403 | Some a ->
448404 Printf.sprintf "The element %s with the attribute %s must not appear as a descendant of the %s element."
···450406 | None ->
451407 Printf.sprintf "The element %s must not appear as a descendant of the %s element."
452408 (q element) (q ancestor))
453453- | Missing_required_child { parent; child } ->
409409+ | `Element (`Missing_child (`Parent parent, `Child child)) ->
454410 Printf.sprintf "Element %s is missing required child element %s."
455411 (q parent) (q child)
456456- | Missing_required_child_one_of { parent; children } ->
412412+ | `Element (`Missing_child_one_of (`Parent parent, `Children children)) ->
457413 let children_str = String.concat ", " children in
458414 Printf.sprintf "Element %s is missing one or more of the following child elements: [%s]."
459415 (q parent) children_str
460460- | Missing_required_child_generic { parent } ->
416416+ | `Element (`Missing_child_generic (`Parent parent)) ->
461417 Printf.sprintf "Element %s is missing a required child element." (q parent)
462462- | Element_must_not_be_empty { element } ->
418418+ | `Element (`Must_not_be_empty (`Elem element)) ->
463419 Printf.sprintf "Element %s must not be empty." (q element)
464464- | Stray_start_tag { tag } ->
420420+ | `Element (`Text_not_allowed (`Parent parent)) ->
421421+ Printf.sprintf "Text not allowed in element %s in this context." (q parent)
422422+423423+ (* Tag errors *)
424424+ | `Tag (`Stray_start (`Tag tag)) ->
465425 Printf.sprintf "Stray start tag %s." (q tag)
466466- | Stray_end_tag { tag } ->
426426+ | `Tag (`Stray_end (`Tag tag)) ->
467427 Printf.sprintf "Stray end tag %s." (q tag)
468468- | End_tag_for_void_element { tag } ->
428428+ | `Tag (`End_for_void (`Tag tag)) ->
469429 Printf.sprintf "End tag %s." (q tag)
470470- | Self_closing_non_void ->
430430+ | `Tag `Self_closing_non_void ->
471431 Printf.sprintf "Self-closing syntax (%s) used on a non-void HTML element. Ignoring the slash and treating as a start tag."
472432 (q "/>")
473473- | Text_not_allowed { parent } ->
474474- Printf.sprintf "Text not allowed in element %s in this context." (q parent)
433433+ | `Tag (`Not_in_scope (`Tag tag)) ->
434434+ Printf.sprintf "No %s element in scope but a %s end tag seen."
435435+ (q tag) (q tag)
436436+ | `Tag (`End_implied_open (`Tag tag)) ->
437437+ Printf.sprintf "End tag %s implied, but there were open elements."
438438+ (q tag)
439439+ | `Tag (`Start_in_table (`Tag tag)) ->
440440+ Printf.sprintf "Start tag %s seen in %s." (q tag) (q "table")
441441+ | `Tag (`Bad_start_in (`Tag tag, `Context _)) ->
442442+ Printf.sprintf "Bad start tag in %s in %s in %s."
443443+ (q tag) (q "noscript") (q "head")
444444+ | `Tag `Eof_with_open ->
445445+ "End of file seen and there were open elements."
475446476476- | Div_child_of_dl_bad_role ->
477477- Printf.sprintf "A %s child of a %s element must not have any %s value other than %s or %s."
478478- (q "div") (q "dl") (q "role") (q "presentation") (q "none")
479479- | Li_bad_role_in_menu ->
480480- Printf.sprintf "An %s element that is a descendant of a %s element or %s element must not have any %s value other than %s, %s, %s, %s, or %s."
481481- (q "li") (q "role=menu") (q "role=menubar") (q "role")
482482- (q "group") (q "menuitem") (q "menuitemcheckbox") (q "menuitemradio") (q "separator")
483483- | Li_bad_role_in_tablist ->
484484- Printf.sprintf "An %s element that is a descendant of a %s element must not have any %s value other than %s."
485485- (q "li") (q "role=tablist") (q "role") (q "tab")
486486- | Li_bad_role_in_list ->
487487- Printf.sprintf "An %s element that is a descendant of a %s, %s, or %s element with no explicit %s value, or a descendant of a %s element, must not have any %s value other than %s."
488488- (q "li") (q "ul") (q "ol") (q "menu") (q "role") (q "role=list") (q "role") (q "listitem")
447447+ (* Character reference errors *)
448448+ | `Char_ref (`Forbidden_codepoint (`Codepoint codepoint)) ->
449449+ Printf.sprintf "Forbidden code point U+%04x." codepoint
450450+ | `Char_ref (`Control_char (`Codepoint codepoint)) ->
451451+ Printf.sprintf "Character reference expands to a control character (U+%04x)." codepoint
452452+ | `Char_ref (`Non_char (`Codepoint codepoint, `Astral astral)) ->
453453+ if astral then
454454+ Printf.sprintf "Character reference expands to an astral non-character (U+%05x)." codepoint
455455+ else
456456+ Printf.sprintf "Character reference expands to a non-character (U+%04x)." codepoint
457457+ | `Char_ref `Unassigned ->
458458+ "Character reference expands to a permanently unassigned code point."
459459+ | `Char_ref `Zero ->
460460+ "Character reference expands to zero."
461461+ | `Char_ref `Out_of_range ->
462462+ "Character reference outside the permissible Unicode range."
463463+ | `Char_ref `Carriage_return ->
464464+ "A numeric character reference expanded to carriage return."
489465490490- | Unnecessary_role { role; element = _; reason } ->
466466+ (* ARIA errors *)
467467+ | `Aria (`Unnecessary_role (`Role role, `Elem _, `Reason reason)) ->
491468 Printf.sprintf "The %s role is unnecessary %s."
492469 (q role) reason
493493- | Bad_role { element; role } ->
470470+ | `Aria (`Bad_role (`Elem element, `Role role)) ->
494471 Printf.sprintf "Bad value %s for attribute %s on element %s."
495472 (q role) (q "role") (q element)
496496- | Aria_must_not_be_specified { attr; element; condition } ->
473473+ | `Aria (`Must_not_specify (`Attr attr, `Elem element, `Condition condition)) ->
497474 Printf.sprintf "The %s attribute must not be specified on any %s element unless %s."
498475 (q attr) (q element) condition
499499- | Aria_must_not_be_used { attr; element; condition } ->
476476+ | `Aria (`Must_not_use (`Attr attr, `Elem element, `Condition condition)) ->
500477 Printf.sprintf "The %s attribute must not be used on an %s element which has %s."
501478 (q attr) (q element) condition
502502- | Aria_should_not_be_used { attr; role } ->
479479+ | `Aria (`Should_not_use (`Attr attr, `Role role)) ->
503480 Printf.sprintf "The %s attribute should not be used on any element which has %s."
504481 (q attr) (q ("role=" ^ role))
505505- | Aria_hidden_on_body ->
482482+ | `Aria `Hidden_on_body ->
506483 Printf.sprintf "%s must not be used on the %s element."
507484 (q "aria-hidden=true") (q "body")
508508- | Img_empty_alt_with_role ->
509509- Printf.sprintf "An %s element which has an %s attribute whose value is the empty string must not have a %s attribute."
510510- (q "img") (q "alt") (q "role")
511511- | Checkbox_button_needs_aria_pressed ->
512512- Printf.sprintf "An %s element with a %s attribute whose value is %s and with a %s attribute whose value is %s must have an %s attribute."
513513- (q "input") (q "type") (q "checkbox") (q "role") (q "button") (q "aria-pressed")
514514- | Tab_without_tabpanel ->
485485+ | `Aria (`Unrecognized_role (`Token token)) ->
486486+ Printf.sprintf "Discarding unrecognized token %s from value of attribute %s. Browsers ignore any token that is not a defined ARIA non-abstract role."
487487+ (q token) (q "role")
488488+ | `Aria `Tab_without_tabpanel ->
515489 Printf.sprintf "Every active %s element must have a corresponding %s element."
516490 (q "role=tab") (q "role=tabpanel")
517517- | Multiple_main_visible ->
491491+ | `Aria `Multiple_main ->
518492 Printf.sprintf "A document should not include more than one visible element with %s."
519493 (q "role=main")
520520- | Discarding_unrecognized_role { token } ->
521521- Printf.sprintf "Discarding unrecognized token %s from value of attribute %s. Browsers ignore any token that is not a defined ARIA non-abstract role."
522522- (q token) (q "role")
523494524524- | Img_missing_alt ->
525525- Printf.sprintf "An %s element must have an %s attribute, except under certain conditions. For details, consult guidance on providing text alternatives for images."
526526- (q "img") (q "alt")
527527- | Img_missing_src_or_srcset ->
528528- Printf.sprintf "Element %s is missing one or more of the following attributes: [src, srcset]."
529529- (q "img")
530530- | Option_empty_without_label ->
531531- Printf.sprintf "Element %s without attribute %s must not be empty."
532532- (q "option") (q "label")
533533- | Bdo_missing_dir ->
534534- Printf.sprintf "Element %s must have attribute %s." (q "bdo") (q "dir")
535535- | Bdo_dir_auto ->
536536- Printf.sprintf "The value of %s attribute for the %s element must not be %s."
537537- (q "dir") (q "bdo") (q "auto")
538538- | Base_missing_href_or_target ->
539539- Printf.sprintf "Element %s is missing one or more of the following attributes: [href, target]."
540540- (q "base")
541541- | Base_after_link_script ->
542542- Printf.sprintf "The %s element must come before any %s or %s elements in the document."
543543- (q "base") (q "link") (q "script")
544544- | Link_missing_href ->
545545- Printf.sprintf "A %s element must have an %s or %s attribute, or both."
546546- (q "link") (q "href") (q "imagesrcset")
547547- | Link_as_requires_preload ->
548548- Printf.sprintf "A %s element with an %s attribute must have a %s attribute that contains the value %s or the value %s."
549549- (q "link") (q "as") (q "rel") (q "preload") (q "modulepreload")
550550- | Link_imagesrcset_requires_as_image ->
551551- Printf.sprintf "A %s element with an %s attribute must have an %s attribute with value %s."
552552- (q "link") (q "imagesrcset") (q "as") (q "image")
553553- | Img_ismap_needs_a_href ->
554554- Printf.sprintf "The %s element with the %s attribute set must have an %s ancestor with the %s attribute."
555555- (q "img") (q "ismap") (q "a") (q "href")
556556- | Sizes_without_srcset ->
557557- Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified."
558558- (q "sizes") (q "srcset")
559559- | Imagesizes_without_imagesrcset ->
560560- Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified."
561561- (q "imagesizes") (q "imagesrcset")
562562- | Srcset_w_without_sizes ->
563563- Printf.sprintf "When the %s attribute has any image candidate string with a width descriptor, the %s attribute must also be specified."
564564- (q "srcset") (q "sizes")
565565- | Source_missing_srcset ->
566566- Printf.sprintf "Element %s is missing required attribute %s."
567567- (q "source") (q "srcset")
568568- | Source_needs_media_or_type ->
569569- Printf.sprintf "A %s element that has a following sibling %s element or %s element with a %s attribute must have a %s attribute and/or %s attribute."
570570- (q "source") (q "source") (q "img") (q "srcset") (q "media") (q "type")
571571- | Picture_missing_img ->
572572- Printf.sprintf "Element %s is missing required child element %s."
573573- (q "picture") (q "img")
574574- | Map_id_name_mismatch ->
575575- Printf.sprintf "The %s attribute on a %s element must have an the same value as the %s attribute."
576576- (q "id") (q "map") (q "name")
577577- | List_attr_requires_datalist ->
578578- Printf.sprintf "The %s attribute of the %s element must refer to a %s element."
579579- (q "list") (q "input") (q "datalist")
580580- | Input_list_not_allowed ->
581581- Printf.sprintf "Attribute %s is only allowed when the input type is %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, or %s."
582582- (q "list") (q "color") (q "date") (q "datetime-local") (q "email") (q "month")
583583- (q "number") (q "range") (q "search") (q "tel") (q "text") (q "time") (q "url") (q "week")
584584- | Label_too_many_labelable ->
585585- Printf.sprintf "The %s element may contain at most one %s, %s, %s, %s, %s, %s, or %s descendant."
586586- (q "label") (q "button") (q "input") (q "meter") (q "output") (q "progress") (q "select") (q "textarea")
587587- | Label_for_id_mismatch ->
588588- Printf.sprintf "Any %s descendant of a %s element with a %s attribute must have an ID value that matches that %s attribute."
589589- (q "input") (q "label") (q "for") (q "for")
590590- | Role_on_label_ancestor ->
591591- Printf.sprintf "The %s attribute must not be used on any %s element that is an ancestor of a labelable element."
592592- (q "role") (q "label")
593593- | Role_on_label_for ->
594594- Printf.sprintf "The %s attribute must not be used on any %s element that is associated with a labelable element."
595595- (q "role") (q "label")
596596- | Aria_label_on_label_for ->
597597- Printf.sprintf "The %s attribute must not be used on any %s element that is associated with a labelable element."
598598- (q "aria-label") (q "label")
599599- | Input_value_constraint { constraint_type } -> constraint_type
600600- | Summary_missing_role ->
601601- Printf.sprintf "Element %s is missing required attribute %s."
602602- (q "summary") (q "role")
603603- | Summary_missing_attrs ->
604604- Printf.sprintf "Element %s is missing one or more of the following attributes: [aria-checked, aria-level, role]."
605605- (q "summary")
606606- | Summary_role_not_allowed ->
607607- Printf.sprintf "The %s attribute must not be used on any %s element that is a summary for its parent %s element."
608608- (q "role") (q "summary") (q "details")
609609- | Autocomplete_webauthn_on_select ->
610610- Printf.sprintf "The value of the %s attribute for the %s element must not contain %s."
611611- (q "autocomplete") (q "select") (q "webauthn")
612612- | Commandfor_invalid_target ->
613613- Printf.sprintf "The value of the %s attribute of the %s element must be the ID of an element in the same tree as the %s with the %s attribute."
614614- (q "commandfor") (q "button") (q "button") (q "commandfor")
495495+ (* List item role errors *)
496496+ | `Li_role `Div_in_dl_bad_role ->
497497+ Printf.sprintf "A %s child of a %s element must not have any %s value other than %s or %s."
498498+ (q "div") (q "dl") (q "role") (q "presentation") (q "none")
499499+ | `Li_role `Li_bad_role_in_menu ->
500500+ Printf.sprintf "An %s element that is a descendant of a %s element or %s element must not have any %s value other than %s, %s, %s, %s, or %s."
501501+ (q "li") (q "role=menu") (q "role=menubar") (q "role")
502502+ (q "group") (q "menuitem") (q "menuitemcheckbox") (q "menuitemradio") (q "separator")
503503+ | `Li_role `Li_bad_role_in_tablist ->
504504+ Printf.sprintf "An %s element that is a descendant of a %s element must not have any %s value other than %s."
505505+ (q "li") (q "role=tablist") (q "role") (q "tab")
506506+ | `Li_role `Li_bad_role_in_list ->
507507+ Printf.sprintf "An %s element that is a descendant of a %s, %s, or %s element with no explicit %s value, or a descendant of a %s element, must not have any %s value other than %s."
508508+ (q "li") (q "ul") (q "ol") (q "menu") (q "role") (q "role=list") (q "role") (q "listitem")
615509616616- | Forbidden_codepoint { codepoint } ->
617617- Printf.sprintf "Forbidden code point U+%04x." codepoint
618618- | Char_ref_control { codepoint } ->
619619- Printf.sprintf "Character reference expands to a control character (U+%04x)." codepoint
620620- | Char_ref_non_char { codepoint; astral } ->
621621- if astral then
622622- Printf.sprintf "Character reference expands to an astral non-character (U+%05x)." codepoint
623623- else
624624- Printf.sprintf "Character reference expands to a non-character (U+%04x)." codepoint
625625- | Char_ref_unassigned ->
626626- "Character reference expands to a permanently unassigned code point."
627627- | Char_ref_zero ->
628628- "Character reference expands to zero."
629629- | Char_ref_out_of_range ->
630630- "Character reference outside the permissible Unicode range."
631631- | Numeric_char_ref_carriage_return ->
632632- "A numeric character reference expanded to carriage return."
633633- | End_of_file_with_open_elements ->
634634- "End of file seen and there were open elements."
635635- | No_element_in_scope { tag } ->
636636- Printf.sprintf "No %s element in scope but a %s end tag seen."
637637- (q tag) (q tag)
638638- | End_tag_implied_open_elements { tag } ->
639639- Printf.sprintf "End tag %s implied, but there were open elements."
640640- (q tag)
641641- | Start_tag_in_table { tag } ->
642642- Printf.sprintf "Start tag %s seen in %s." (q tag) (q "table")
643643- | Bad_start_tag_in { tag; context = _ } ->
644644- Printf.sprintf "Bad start tag in %s in %s in %s."
645645- (q tag) (q "noscript") (q "head")
646646-647647- | Table_row_no_cells { row } ->
510510+ (* Table errors *)
511511+ | `Table (`Row_no_cells (`Row row)) ->
648512 Printf.sprintf "Row %d of an implicit row group has no cells beginning on it." row
649649- | Table_cell_overlap ->
513513+ | `Table `Cell_overlap ->
650514 "Table cell is overlapped by later table cell."
651651- | Table_cell_spans_rowgroup ->
515515+ | `Table `Cell_spans_rowgroup ->
652516 Printf.sprintf "Table cell spans past the end of its row group established by a %s element; clipped to the end of the row group."
653517 (q "tbody")
654654- | Table_column_no_cells { column; element } ->
518518+ | `Table (`Column_no_cells (`Column column, `Elem element)) ->
655519 Printf.sprintf "Table column %d established by element %s has no cells beginning in it."
656520 column (q element)
657521658658- | Missing_lang_attr ->
522522+ (* I18n errors *)
523523+ | `I18n `Missing_lang ->
659524 Printf.sprintf "Consider adding a %s attribute to the %s start tag to declare the language of this document."
660525 (q "lang") (q "html")
661661- | Wrong_lang { detected; declared; suggested } ->
526526+ | `I18n (`Wrong_lang (`Detected detected, `Declared declared, `Suggested suggested)) ->
662527 Printf.sprintf "This document appears to be written in %s but the %s start tag has %s. Consider using %s (or variant) instead."
663528 detected (q "html") (q ("lang=\"" ^ declared ^ "\"")) (q ("lang=\"" ^ suggested ^ "\""))
664664- | Missing_dir_rtl { language } ->
529529+ | `I18n (`Missing_dir_rtl (`Language language)) ->
665530 Printf.sprintf "This document appears to be written in %s. Consider adding %s to the %s start tag."
666531 language (q "dir=\"rtl\"") (q "html")
667667- | Wrong_dir { language; declared } ->
532532+ | `I18n (`Wrong_dir (`Language language, `Declared declared)) ->
668533 Printf.sprintf "This document appears to be written in %s but the %s start tag has %s. Consider using %s instead."
669534 language (q "html") (q ("dir=\"" ^ declared ^ "\"")) (q "dir=\"rtl\"")
670670- | Xml_lang_without_lang ->
535535+ | `I18n `Xml_lang_without_lang ->
671536 Printf.sprintf "When the attribute %s in no namespace is specified, the element must also have the attribute %s present with the same value."
672537 (q "xml:lang") (q "lang")
673673- | Xml_lang_lang_mismatch ->
538538+ | `I18n `Xml_lang_mismatch ->
674539 Printf.sprintf "The %s and %s attributes must have the same value."
675540 (q "xml:lang") (q "lang")
676676-677677- | Not_nfc { replacement } ->
541541+ | `I18n (`Not_nfc (`Replacement replacement)) ->
678542 Printf.sprintf "Text run is not in Unicode Normalization Form C. Should instead be %s. (Copy and paste that into your source document to replace the un-normalized text.)"
679543 (q replacement)
680544681681- | Multiple_h1 ->
682682- Printf.sprintf "Consider using only one %s element per document (or, if using %s elements multiple times is required, consider using the %s attribute to indicate that these %s elements are not all top-level headings)."
683683- (q "h1") (q "h1") (q "headingoffset") (q "h1")
684684- | Multiple_autofocus ->
685685- Printf.sprintf "There must not be two elements with the same %s that both have the %s attribute specified."
686686- (q "nearest ancestor autofocus scoping root element") (q "autofocus")
687687-688688- | Importmap_invalid_json ->
545545+ (* Import map errors *)
546546+ | `Importmap `Invalid_json ->
689547 Printf.sprintf "A script %s with a %s attribute whose value is %s must have valid JSON content."
690548 (q "script") (q "type") (q "importmap")
691691- | Importmap_invalid_root ->
549549+ | `Importmap `Invalid_root ->
692550 Printf.sprintf "A %s element with a %s attribute whose value is %s must contain a JSON object with no properties other than %s, %s, and %s."
693551 (q "script") (q "type") (q "importmap") (q "imports") (q "scopes") (q "integrity")
694694- | Importmap_imports_not_object ->
552552+ | `Importmap `Imports_not_object ->
695553 Printf.sprintf "The value of the %s property within the content of a %s element with a %s attribute whose value is %s must be a JSON object."
696554 (q "imports") (q "script") (q "type") (q "importmap")
697697- | Importmap_empty_key ->
555555+ | `Importmap `Empty_key ->
698556 Printf.sprintf "A specifier map defined in a %s property within the content of a %s element with a %s attribute whose value is %s must only contain non-empty keys."
699557 (q "imports") (q "script") (q "type") (q "importmap")
700700- | Importmap_non_string_value ->
558558+ | `Importmap `Non_string_value ->
701559 Printf.sprintf "A specifier map defined in a %s property within the content of a %s element with a %s attribute whose value is %s must only contain string values."
702560 (q "imports") (q "script") (q "type") (q "importmap")
703703- | Importmap_key_trailing_slash ->
561561+ | `Importmap `Key_trailing_slash ->
704562 Printf.sprintf "A specifier map defined in a %s property within the content of a %s element with a %s attribute whose value is %s must have values that end with %s when its corresponding key ends with %s."
705563 (q "imports") (q "script") (q "type") (q "importmap") (q "/") (q "/")
706706- | Importmap_scopes_not_object ->
564564+ | `Importmap `Scopes_not_object ->
707565 Printf.sprintf "The value of the %s property within the content of a %s element with a %s attribute whose value is %s must be a JSON object whose keys are valid URL strings."
708566 (q "scopes") (q "script") (q "type") (q "importmap")
709709- | Importmap_scopes_values_not_object ->
567567+ | `Importmap `Scopes_values_not_object ->
710568 Printf.sprintf "The value of the %s property within the content of a %s element with a %s attribute whose value is %s must be a JSON object whose values are also JSON objects."
711569 (q "scopes") (q "script") (q "type") (q "importmap")
712712- | Importmap_scopes_invalid_url ->
570570+ | `Importmap `Scopes_invalid_url ->
713571 Printf.sprintf "The value of the %s property within the content of a %s element with a %s attribute whose value is %s must be a JSON object whose keys are valid URL strings."
714572 (q "scopes") (q "script") (q "type") (q "importmap")
715715- | Importmap_scopes_value_invalid_url ->
573573+ | `Importmap `Scopes_value_invalid_url ->
716574 Printf.sprintf "A specifier map defined in a %s property within the content of a %s element with a %s attribute whose value is %s must only contain valid URL values."
717575 (q "scopes") (q "script") (q "type") (q "importmap")
718576719719- | Style_type_invalid ->
720720- Printf.sprintf "The only allowed value for the %s attribute for the %s element is %s (with no parameters). (But the attribute is not needed and should be omitted altogether.)"
721721- (q "type") (q "style") (q "text/css")
577577+ (* Image errors *)
578578+ | `Img `Missing_alt ->
579579+ Printf.sprintf "An %s element must have an %s attribute, except under certain conditions. For details, consult guidance on providing text alternatives for images."
580580+ (q "img") (q "alt")
581581+ | `Img `Missing_src_or_srcset ->
582582+ Printf.sprintf "Element %s is missing one or more of the following attributes: [src, srcset]."
583583+ (q "img")
584584+ | `Img `Empty_alt_with_role ->
585585+ Printf.sprintf "An %s element which has an %s attribute whose value is the empty string must not have a %s attribute."
586586+ (q "img") (q "alt") (q "role")
587587+ | `Img `Ismap_needs_href ->
588588+ Printf.sprintf "The %s element with the %s attribute set must have an %s ancestor with the %s attribute."
589589+ (q "img") (q "ismap") (q "a") (q "href")
590590+591591+ (* Link errors *)
592592+ | `Link `Missing_href ->
593593+ Printf.sprintf "A %s element must have an %s or %s attribute, or both."
594594+ (q "link") (q "href") (q "imagesrcset")
595595+ | `Link `As_requires_preload ->
596596+ Printf.sprintf "A %s element with an %s attribute must have a %s attribute that contains the value %s or the value %s."
597597+ (q "link") (q "as") (q "rel") (q "preload") (q "modulepreload")
598598+ | `Link `Imagesrcset_requires_as_image ->
599599+ Printf.sprintf "A %s element with an %s attribute must have an %s attribute with value %s."
600600+ (q "link") (q "imagesrcset") (q "as") (q "image")
601601+602602+ (* Label errors *)
603603+ | `Label `Too_many_labelable ->
604604+ Printf.sprintf "The %s element may contain at most one %s, %s, %s, %s, %s, %s, or %s descendant."
605605+ (q "label") (q "button") (q "input") (q "meter") (q "output") (q "progress") (q "select") (q "textarea")
606606+ | `Label `For_id_mismatch ->
607607+ Printf.sprintf "Any %s descendant of a %s element with a %s attribute must have an ID value that matches that %s attribute."
608608+ (q "input") (q "label") (q "for") (q "for")
609609+ | `Label `Role_on_ancestor ->
610610+ Printf.sprintf "The %s attribute must not be used on any %s element that is an ancestor of a labelable element."
611611+ (q "role") (q "label")
612612+ | `Label `Role_on_for ->
613613+ Printf.sprintf "The %s attribute must not be used on any %s element that is associated with a labelable element."
614614+ (q "role") (q "label")
615615+ | `Label `Aria_label_on_for ->
616616+ Printf.sprintf "The %s attribute must not be used on any %s element that is associated with a labelable element."
617617+ (q "aria-label") (q "label")
722618723723- | Headingoffset_invalid ->
724724- Printf.sprintf "The value of the %s attribute must be a number between %s and %s."
725725- (q "headingoffset") (q "0") (q "8")
619619+ (* Input errors *)
620620+ | `Input `Checkbox_needs_aria_pressed ->
621621+ Printf.sprintf "An %s element with a %s attribute whose value is %s and with a %s attribute whose value is %s must have an %s attribute."
622622+ (q "input") (q "type") (q "checkbox") (q "role") (q "button") (q "aria-pressed")
623623+ | `Input (`Value_constraint (`Constraint constraint_type)) -> constraint_type
624624+ | `Input `List_not_allowed ->
625625+ Printf.sprintf "Attribute %s is only allowed when the input type is %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, or %s."
626626+ (q "list") (q "color") (q "date") (q "datetime-local") (q "email") (q "month")
627627+ (q "number") (q "range") (q "search") (q "tel") (q "text") (q "time") (q "url") (q "week")
628628+ | `Input `List_requires_datalist ->
629629+ Printf.sprintf "The %s attribute of the %s element must refer to a %s element."
630630+ (q "list") (q "input") (q "datalist")
726631727727- | Media_empty ->
728728- Printf.sprintf "Value of %s attribute here must not be empty." (q "media")
729729- | Media_all ->
730730- Printf.sprintf "Value of %s attribute here must not be %s." (q "media") (q "all")
632632+ (* Srcset errors *)
633633+ | `Srcset `Sizes_without_srcset ->
634634+ Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified."
635635+ (q "sizes") (q "srcset")
636636+ | `Srcset `Imagesizes_without_imagesrcset ->
637637+ Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified."
638638+ (q "imagesizes") (q "imagesrcset")
639639+ | `Srcset `W_without_sizes ->
640640+ Printf.sprintf "When the %s attribute has any image candidate string with a width descriptor, the %s attribute must also be specified."
641641+ (q "srcset") (q "sizes")
642642+ | `Srcset `Source_missing_srcset ->
643643+ Printf.sprintf "Element %s is missing required attribute %s."
644644+ (q "source") (q "srcset")
645645+ | `Srcset `Source_needs_media_or_type ->
646646+ Printf.sprintf "A %s element that has a following sibling %s element or %s element with a %s attribute must have a %s attribute and/or %s attribute."
647647+ (q "source") (q "source") (q "img") (q "srcset") (q "media") (q "type")
648648+ | `Srcset `Picture_missing_img ->
649649+ Printf.sprintf "Element %s is missing required child element %s."
650650+ (q "picture") (q "img")
731651732732- | Svg_deprecated_attr { attr; element } ->
652652+ (* SVG errors *)
653653+ | `Svg (`Deprecated_attr (`Attr attr, `Elem element)) ->
733654 Printf.sprintf "Attribute %s not allowed on element %s at this point."
734655 (q attr) (q element)
735735- | Missing_required_svg_attr { element; attr } ->
656656+ | `Svg (`Missing_attr (`Elem element, `Attr attr)) ->
736657 Printf.sprintf "Element %s is missing required attribute %s."
737658 (q element) (q attr)
738659739739- | Generic { message } -> message
660660+ (* Misc errors *)
661661+ | `Misc `Option_empty_without_label ->
662662+ Printf.sprintf "Element %s without attribute %s must not be empty."
663663+ (q "option") (q "label")
664664+ | `Misc `Bdo_missing_dir ->
665665+ Printf.sprintf "Element %s must have attribute %s." (q "bdo") (q "dir")
666666+ | `Misc `Bdo_dir_auto ->
667667+ Printf.sprintf "The value of %s attribute for the %s element must not be %s."
668668+ (q "dir") (q "bdo") (q "auto")
669669+ | `Misc `Base_missing_href_or_target ->
670670+ Printf.sprintf "Element %s is missing one or more of the following attributes: [href, target]."
671671+ (q "base")
672672+ | `Misc `Base_after_link_script ->
673673+ Printf.sprintf "The %s element must come before any %s or %s elements in the document."
674674+ (q "base") (q "link") (q "script")
675675+ | `Misc `Map_id_name_mismatch ->
676676+ Printf.sprintf "The %s attribute on a %s element must have an the same value as the %s attribute."
677677+ (q "id") (q "map") (q "name")
678678+ | `Misc `Summary_missing_role ->
679679+ Printf.sprintf "Element %s is missing required attribute %s."
680680+ (q "summary") (q "role")
681681+ | `Misc `Summary_missing_attrs ->
682682+ Printf.sprintf "Element %s is missing one or more of the following attributes: [aria-checked, aria-level, role]."
683683+ (q "summary")
684684+ | `Misc `Summary_role_not_allowed ->
685685+ Printf.sprintf "The %s attribute must not be used on any %s element that is a summary for its parent %s element."
686686+ (q "role") (q "summary") (q "details")
687687+ | `Misc `Autocomplete_webauthn_on_select ->
688688+ Printf.sprintf "The value of the %s attribute for the %s element must not contain %s."
689689+ (q "autocomplete") (q "select") (q "webauthn")
690690+ | `Misc `Commandfor_invalid_target ->
691691+ Printf.sprintf "The value of the %s attribute of the %s element must be the ID of an element in the same tree as the %s with the %s attribute."
692692+ (q "commandfor") (q "button") (q "button") (q "commandfor")
693693+ | `Misc `Style_type_invalid ->
694694+ Printf.sprintf "The only allowed value for the %s attribute for the %s element is %s (with no parameters). (But the attribute is not needed and should be omitted altogether.)"
695695+ (q "type") (q "style") (q "text/css")
696696+ | `Misc `Headingoffset_invalid ->
697697+ Printf.sprintf "The value of the %s attribute must be a number between %s and %s."
698698+ (q "headingoffset") (q "0") (q "8")
699699+ | `Misc `Media_empty ->
700700+ Printf.sprintf "Value of %s attribute here must not be empty." (q "media")
701701+ | `Misc `Media_all ->
702702+ Printf.sprintf "Value of %s attribute here must not be %s." (q "media") (q "all")
703703+ | `Misc `Multiple_h1 ->
704704+ Printf.sprintf "Consider using only one %s element per document (or, if using %s elements multiple times is required, consider using the %s attribute to indicate that these %s elements are not all top-level headings)."
705705+ (q "h1") (q "h1") (q "headingoffset") (q "h1")
706706+ | `Misc `Multiple_autofocus ->
707707+ Printf.sprintf "There must not be two elements with the same %s that both have the %s attribute specified."
708708+ (q "nearest ancestor autofocus scoping root element") (q "autofocus")
709709+710710+ (* Generic *)
711711+ | `Generic message -> message
+711-138
lib/html5_checker/error_code.mli
···11(** Typed error codes for HTML5 validation messages.
2233- This module defines a comprehensive variant type for all validation errors,
44- ensuring exact message matching with the Nu HTML Validator test suite. *)
33+ This module defines a comprehensive hierarchy of validation errors using
44+ polymorphic variants, organized by error category. Each error type is
55+ documented with the specific HTML5 conformance requirement it represents.
66+77+ The error hierarchy is:
88+ - {!t} is the top-level type containing all errors wrapped by category
99+ - Each category (e.g., {!attr_error}, {!aria_error}) groups related errors
1010+ - Inline descriptors like [[\`Attr of string]] provide self-documenting parameters
1111+1212+ {2 Example Usage}
1313+1414+ {[
1515+ (* Category-level matching *)
1616+ let is_accessibility_error = function
1717+ | `Aria _ | `Li_role _ -> true
1818+ | _ -> false
1919+2020+ (* Fine-grained matching *)
2121+ match err with
2222+ | `Attr (`Duplicate_id (`Id id)) -> handle_duplicate id
2323+ | `Img `Missing_alt -> suggest_alt_text ()
2424+ | _ -> default_handler err
2525+ ]}
2626+*)
52766-(** Severity level of a validation message *)
2828+(** {1 Severity} *)
2929+3030+(** Severity level of a validation message.
3131+ - [Error]: Conformance error that must be fixed
3232+ - [Warning]: Likely problem that should be reviewed
3333+ - [Info]: Suggestion for best practices *)
734type severity = Error | Warning | Info
83599-(** Typed error codes with associated data *)
1010-type t =
1111- (* Attribute Errors *)
1212- | Attr_not_allowed_on_element of { attr: string; element: string }
1313- | Attr_not_allowed_here of { attr: string }
1414- | Attr_not_allowed_when of { attr: string; element: string; condition: string }
1515- | Missing_required_attr of { element: string; attr: string }
1616- | Missing_required_attr_one_of of { element: string; attrs: string list }
1717- | Bad_attr_value of { element: string; attr: string; value: string; reason: string }
1818- | Bad_attr_value_generic of { message: string }
1919- | Duplicate_id of { id: string }
2020- | Data_attr_invalid_name of { reason: string }
2121- | Data_attr_uppercase
3636+(** {1 Attribute Errors}
3737+3838+ Errors related to HTML attributes: disallowed attributes, missing required
3939+ attributes, invalid attribute values, and duplicate IDs. *)
4040+4141+(** Attribute-related validation errors.
4242+4343+ These errors occur when attributes violate HTML5 content model rules:
4444+ - Attributes used on elements where they're not allowed
4545+ - Required attributes that are missing
4646+ - Attribute values that don't match expected formats
4747+ - Duplicate ID attributes within a document *)
4848+type attr_error = [
4949+ | `Not_allowed of [`Attr of string] * [`Elem of string]
5050+ (** Attribute is not in the set of allowed attributes for this element.
5151+ Per HTML5 spec, each element has a defined set of content attributes;
5252+ using attributes outside this set is a conformance error.
5353+ Example: [type] attribute on a [<div>] element. *)
5454+5555+ | `Not_allowed_here of [`Attr of string]
5656+ (** Attribute is valid on this element type but not in this context.
5757+ Some attributes are only allowed under specific conditions, such as
5858+ the [download] attribute which requires specific ancestor elements. *)
5959+6060+ | `Not_allowed_when of [`Attr of string] * [`Elem of string] * [`Condition of string]
6161+ (** Attribute conflicts with another attribute or element state.
6262+ Example: [readonly] and [disabled] together, or [multiple] on
6363+ certain input types where it's not supported. *)
6464+6565+ | `Missing of [`Elem of string] * [`Attr of string]
6666+ (** Element is missing a required attribute.
6767+ Per HTML5, certain elements have required attributes for conformance.
6868+ Example: [<img>] requires [src] or [srcset], [<input>] requires [type]. *)
6969+7070+ | `Missing_one_of of [`Elem of string] * [`Attrs of string list]
7171+ (** Element must have at least one of the listed attributes.
7272+ Some elements require at least one from a set of attributes.
7373+ Example: [<base>] needs [href] or [target] (or both). *)
7474+7575+ | `Bad_value of [`Elem of string] * [`Attr of string] * [`Value of string] * [`Reason of string]
7676+ (** Attribute value doesn't match the expected format or enumeration.
7777+ HTML5 defines specific value spaces for many attributes (enumerations,
7878+ URLs, integers, etc.). This error indicates the value is malformed. *)
7979+8080+ | `Bad_value_generic of [`Message of string]
8181+ (** Generic bad attribute value with custom message.
8282+ Used when the specific validation failure requires a custom explanation
8383+ that doesn't fit the standard bad value template. *)
8484+8585+ | `Duplicate_id of [`Id of string]
8686+ (** Document contains multiple elements with the same ID.
8787+ Per HTML5, the [id] attribute must be unique within a document.
8888+ Duplicate IDs cause problems with fragment navigation, label
8989+ association, and JavaScript DOM queries. *)
9090+9191+ | `Data_invalid_name of [`Reason of string]
9292+ (** Custom data attribute name violates naming rules.
9393+ [data-*] attribute names must be valid XML NCNames (no colons,
9494+ must start with letter or underscore). The reason explains
9595+ the specific naming violation. *)
9696+9797+ | `Data_uppercase
9898+ (** Custom data attribute name contains uppercase letters.
9999+ [data-*] attribute names must not contain ASCII uppercase letters
100100+ (A-Z) per HTML5. Use lowercase with hyphens instead. *)
101101+]
102102+103103+(** {1 Element Structure Errors}
104104+105105+ Errors related to element usage, nesting, and content models. *)
106106+107107+(** Element structure validation errors.
108108+109109+ These errors occur when elements violate HTML5 content model rules:
110110+ - Obsolete elements that should be replaced
111111+ - Elements used in wrong contexts (invalid parent/child relationships)
112112+ - Missing required child elements
113113+ - Empty elements that must have content *)
114114+type element_error = [
115115+ | `Obsolete of [`Elem of string] * [`Suggestion of string]
116116+ (** Element is obsolete and should not be used.
117117+ HTML5 obsoletes certain elements from HTML4 (e.g., [<font>], [<center>]).
118118+ The suggestion provides the recommended modern alternative. *)
119119+120120+ | `Obsolete_attr of [`Elem of string] * [`Attr of string] * [`Suggestion of string option]
121121+ (** Attribute on this element is obsolete.
122122+ Some attributes are obsolete on specific elements but may be valid
123123+ elsewhere. Example: [align] on [<table>] (use CSS instead). *)
124124+125125+ | `Obsolete_global_attr of [`Attr of string] * [`Suggestion of string]
126126+ (** Global attribute is obsolete on all elements.
127127+ Attributes like [bgcolor] are obsolete everywhere in HTML5. *)
128128+129129+ | `Not_allowed_as_child of [`Child of string] * [`Parent of string]
130130+ (** Element cannot be a child of the specified parent.
131131+ HTML5 defines content models for each element specifying which
132132+ children are allowed. Example: [<div>] inside [<p>] is invalid. *)
133133+134134+ | `Unknown of [`Elem of string]
135135+ (** Element name is not recognized.
136136+ The element is not defined in HTML5, SVG, or MathML specs.
137137+ May be a typo or a custom element without hyphen. *)
138138+139139+ | `Must_not_descend of [`Elem of string] * [`Attr of string option] * [`Ancestor of string]
140140+ (** Element must not appear as descendant of the specified ancestor.
141141+ Some elements have restrictions on their ancestry regardless of
142142+ direct parent. Example: [<form>] cannot be nested inside [<form>].
143143+ The optional attribute indicates a conditional restriction. *)
144144+145145+ | `Missing_child of [`Parent of string] * [`Child of string]
146146+ (** Parent element is missing a required child element.
147147+ Some elements must contain specific children for conformance.
148148+ Example: [<dl>] requires [<dt>] and [<dd>] children. *)
149149+150150+ | `Missing_child_one_of of [`Parent of string] * [`Children of string list]
151151+ (** Parent must contain at least one of the listed child elements.
152152+ Example: [<ruby>] must contain [<rt>] or [<rp>]. *)
153153+154154+ | `Missing_child_generic of [`Parent of string]
155155+ (** Parent is missing an unspecified required child.
156156+ Used when the required child depends on context. *)
157157+158158+ | `Must_not_be_empty of [`Elem of string]
159159+ (** Element must have content and cannot be empty.
160160+ Some elements require text or child element content.
161161+ Example: [<title>] must not be empty. *)
162162+163163+ | `Text_not_allowed of [`Parent of string]
164164+ (** Text content is not allowed in this element.
165165+ Some elements only allow element children, not text.
166166+ Example: [<table>] cannot contain direct text children. *)
167167+]
168168+169169+(** {1 Tag and Parse Errors}
170170+171171+ Errors from the parsing phase related to tags and document structure. *)
172172+173173+(** Tag-level parse errors.
174174+175175+ These errors occur during HTML parsing when the parser encounters
176176+ problematic tag structures or reaches end-of-file unexpectedly. *)
177177+type tag_error = [
178178+ | `Stray_start of [`Tag of string]
179179+ (** Start tag appears in a position where it's not allowed.
180180+ The parser encountered an opening tag that cannot appear in
181181+ the current insertion mode. Example: [<tr>] outside [<table>]. *)
182182+183183+ | `Stray_end of [`Tag of string]
184184+ (** End tag appears without a matching start tag.
185185+ The parser encountered a closing tag with no corresponding
186186+ open element in scope. *)
187187+188188+ | `End_for_void of [`Tag of string]
189189+ (** End tag for a void element that cannot have one.
190190+ Void elements ([<br>], [<img>], etc.) cannot have end tags
191191+ in HTML5. Example: [</br>] is invalid. *)
192192+193193+ | `Self_closing_non_void
194194+ (** Self-closing syntax [/>] used on non-void HTML element.
195195+ In HTML5, [/>] is only meaningful on void elements and
196196+ foreign (SVG/MathML) elements. On other elements it's ignored. *)
197197+198198+ | `Not_in_scope of [`Tag of string]
199199+ (** End tag seen but no matching element in scope.
200200+ The parser found a closing tag but the element isn't in the
201201+ current scope (may be blocked by formatting elements). *)
202202+203203+ | `End_implied_open of [`Tag of string]
204204+ (** End tag implied closing of other open elements.
205205+ The parser had to implicitly close elements to process this
206206+ end tag, indicating mismatched nesting. *)
207207+208208+ | `Start_in_table of [`Tag of string]
209209+ (** Start tag appeared inside table where it's foster-parented.
210210+ When certain tags appear in table context, they're moved
211211+ outside the table (foster parenting), indicating malformed markup. *)
212212+213213+ | `Bad_start_in of [`Tag of string] * [`Context of string]
214214+ (** Start tag appeared in invalid context.
215215+ Generic error for tags in wrong parsing contexts. *)
216216+217217+ | `Eof_with_open
218218+ (** End of file reached with unclosed elements.
219219+ The document ended with elements still open on the stack,
220220+ indicating missing closing tags. *)
221221+]
222222+223223+(** Character reference errors.
224224+225225+ These errors occur when character references (like [&] or [A])
226226+ expand to problematic Unicode code points. *)
227227+type char_ref_error = [
228228+ | `Forbidden_codepoint of [`Codepoint of int]
229229+ (** Character reference expands to a forbidden code point.
230230+ Certain code points are forbidden in HTML documents (e.g.,
231231+ NULL U+0000, noncharacters). These cannot appear even via
232232+ character references. *)
233233+234234+ | `Control_char of [`Codepoint of int]
235235+ (** Character reference expands to a control character.
236236+ C0 and C1 control characters (except tab, newline, etc.)
237237+ are problematic and trigger this warning. *)
238238+239239+ | `Non_char of [`Codepoint of int] * [`Astral of bool]
240240+ (** Character reference expands to a Unicode noncharacter.
241241+ Noncharacters (like U+FFFE, U+FFFF) are reserved and
242242+ should not appear in documents. Astral flag indicates
243243+ if it's in the supplementary planes. *)
244244+245245+ | `Unassigned
246246+ (** Character reference expands to permanently unassigned code point.
247247+ The referenced code point will never be assigned a character. *)
248248+249249+ | `Zero
250250+ (** Character reference expands to U+0000 (NULL).
251251+ NULL is replaced with U+FFFD (replacement character) per HTML5. *)
252252+253253+ | `Out_of_range
254254+ (** Character reference value exceeds Unicode maximum.
255255+ Numeric character references must be <= U+10FFFF. *)
256256+257257+ | `Carriage_return
258258+ (** Numeric character reference expanded to carriage return.
259259+ CR (U+000D) via numeric reference is replaced with LF. *)
260260+]
261261+262262+(** {1 ARIA and Accessibility Errors}
263263+264264+ Errors related to WAI-ARIA attributes and accessibility conformance. *)
265265+266266+(** ARIA and role validation errors.
267267+268268+ These errors ensure proper usage of WAI-ARIA attributes and roles
269269+ for accessibility. Incorrect ARIA can make content less accessible
270270+ than having no ARIA at all. *)
271271+type aria_error = [
272272+ | `Unnecessary_role of [`Role of string] * [`Elem of string] * [`Reason of string]
273273+ (** Role is redundant because element has implicit role.
274274+ Many HTML elements have implicit ARIA roles; explicitly setting
275275+ the same role is unnecessary. Example: [role="button"] on [<button>]. *)
276276+277277+ | `Bad_role of [`Elem of string] * [`Role of string]
278278+ (** Role value is invalid or not allowed on this element.
279279+ The role is either not a valid ARIA role token or is not
280280+ permitted on this particular element type. *)
281281+282282+ | `Must_not_specify of [`Attr of string] * [`Elem of string] * [`Condition of string]
283283+ (** ARIA attribute must not be specified in this situation.
284284+ Some ARIA attributes are prohibited on certain elements unless
285285+ specific conditions are met. *)
286286+287287+ | `Must_not_use of [`Attr of string] * [`Elem of string] * [`Condition of string]
288288+ (** ARIA attribute must not be used with this element configuration.
289289+ The attribute conflicts with another attribute or state of the element. *)
290290+291291+ | `Should_not_use of [`Attr of string] * [`Role of string]
292292+ (** ARIA attribute should not be used with this role (warning).
293293+ While not strictly invalid, the attribute is discouraged
294294+ with this role as it may cause confusion. *)
295295+296296+ | `Hidden_on_body
297297+ (** [aria-hidden="true"] used on body element.
298298+ Hiding the entire document from assistive technology is
299299+ almost certainly an error. *)
300300+301301+ | `Unrecognized_role of [`Token of string]
302302+ (** Unrecognized role token was discarded.
303303+ The role attribute contained a token that isn't a valid
304304+ ARIA role. Browsers ignore unknown role tokens. *)
305305+306306+ | `Tab_without_tabpanel
307307+ (** Tab element has no corresponding tabpanel.
308308+ Each [role="tab"] should control a [role="tabpanel"].
309309+ Missing tabpanels indicate incomplete tab interface. *)
310310+311311+ | `Multiple_main
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+317317+(** List item role constraint errors.
318318+319319+ Special ARIA role restrictions on [<li>] elements and [<div>]
320320+ children of [<dl>] elements. *)
321321+type li_role_error = [
322322+ | `Div_in_dl_bad_role
323323+ (** [<div>] child of [<dl>] has invalid role.
324324+ When [<div>] is used to group [<dt>]/[<dd>] pairs in a [<dl>],
325325+ it may only have [role="presentation"] or [role="none"]. *)
326326+327327+ | `Li_bad_role_in_menu
328328+ (** [<li>] in menu/menubar has invalid role.
329329+ [<li>] descendants of [role="menu"] or [role="menubar"] must
330330+ have roles like [menuitem], [menuitemcheckbox], etc. *)
331331+332332+ | `Li_bad_role_in_tablist
333333+ (** [<li>] in tablist has invalid role.
334334+ [<li>] descendants of [role="tablist"] must have [role="tab"]. *)
335335+336336+ | `Li_bad_role_in_list
337337+ (** [<li>] in list context has invalid role.
338338+ [<li>] in [<ul>], [<ol>], [<menu>], or [role="list"] must
339339+ have [role="listitem"] or no explicit role. *)
340340+]
341341+342342+(** {1 Table Errors}
343343+344344+ Errors in HTML table structure and cell spanning. *)
345345+346346+(** Table structure validation errors.
347347+348348+ These errors indicate problems with table structure that may
349349+ cause incorrect rendering or accessibility issues. *)
350350+type table_error = [
351351+ | `Row_no_cells of [`Row of int]
352352+ (** Table row has no cells starting on it.
353353+ The specified row number (1-indexed) in an implicit row group
354354+ has no cells beginning on that row, possibly due to rowspan. *)
355355+356356+ | `Cell_overlap
357357+ (** Table cells overlap due to spanning.
358358+ A cell's rowspan/colspan causes it to overlap with another cell,
359359+ making the table structure ambiguous. *)
360360+361361+ | `Cell_spans_rowgroup
362362+ (** Cell's rowspan extends past its row group.
363363+ A cell's rowspan would extend beyond the [<tbody>], [<thead>],
364364+ or [<tfoot>] containing it; the span is clipped. *)
365365+366366+ | `Column_no_cells of [`Column of int] * [`Elem of string]
367367+ (** Table column has no cells.
368368+ A column established by [<col>] or [<colgroup>] has no cells
369369+ beginning in it, indicating mismatched column definitions. *)
370370+]
371371+372372+(** {1 Internationalization Errors}
373373+374374+ Errors related to language declaration and text direction. *)
375375+376376+(** Language and internationalization validation errors.
377377+378378+ These errors help ensure documents properly declare their language
379379+ and text direction for accessibility and correct rendering. *)
380380+type i18n_error = [
381381+ | `Missing_lang
382382+ (** Document has no language declaration.
383383+ The [<html>] element should have a [lang] attribute declaring
384384+ the document's primary language for accessibility. *)
385385+386386+ | `Wrong_lang of [`Detected of string] * [`Declared of string] * [`Suggested of string]
387387+ (** Declared language doesn't match detected content language.
388388+ Automatic language detection suggests the [lang] attribute
389389+ value is incorrect for the actual content. *)
390390+391391+ | `Missing_dir_rtl of [`Language of string]
392392+ (** RTL language content lacks [dir="rtl"].
393393+ Content detected as a right-to-left language should have
394394+ explicit direction declaration. *)
395395+396396+ | `Wrong_dir of [`Language of string] * [`Declared of string]
397397+ (** Text direction doesn't match detected language direction.
398398+ The [dir] attribute value conflicts with the detected
399399+ language's natural direction. *)
400400+401401+ | `Xml_lang_without_lang
402402+ (** [xml:lang] present but [lang] is missing.
403403+ When [xml:lang] is specified (for XHTML compatibility),
404404+ the [lang] attribute must also be present with the same value. *)
405405+406406+ | `Xml_lang_mismatch
407407+ (** [xml:lang] and [lang] attribute values don't match.
408408+ Both attributes must have identical values when present. *)
409409+410410+ | `Not_nfc of [`Replacement of string]
411411+ (** Text is not in Unicode Normalization Form C.
412412+ HTML5 requires NFC normalization. The replacement string
413413+ shows the correctly normalized form. *)
414414+]
415415+416416+(** {1 Import Map Errors}
417417+418418+ Errors in [<script type="importmap">] JSON content. *)
419419+420420+(** Import map validation errors.
421421+422422+ These errors occur when validating the JSON content of
423423+ [<script type="importmap">] elements per the Import Maps spec. *)
424424+type importmap_error = [
425425+ | `Invalid_json
426426+ (** Import map content is not valid JSON.
427427+ The script content must be parseable as JSON. *)
224282323- (* Element Errors *)
2424- | Obsolete_element of { element: string; suggestion: string }
2525- | Obsolete_attr of { element: string; attr: string; suggestion: string option }
2626- | Obsolete_global_attr of { attr: string; suggestion: string }
2727- | Element_not_allowed_as_child of { child: string; parent: string }
2828- | Unknown_element of { name: string }
2929- | Element_must_not_be_descendant of { element: string; attr: string option; ancestor: string }
3030- | Missing_required_child of { parent: string; child: string }
3131- | Missing_required_child_one_of of { parent: string; children: string list }
3232- | Missing_required_child_generic of { parent: string }
3333- | Element_must_not_be_empty of { element: string }
3434- | Stray_start_tag of { tag: string }
3535- | Stray_end_tag of { tag: string }
3636- | End_tag_for_void_element of { tag: string }
3737- | Self_closing_non_void
3838- | Text_not_allowed of { parent: string }
429429+ | `Invalid_root
430430+ (** Import map root is not a valid object.
431431+ The JSON must be an object with only [imports], [scopes],
432432+ and [integrity] properties. *)
394334040- (* Child Restrictions *)
4141- | Div_child_of_dl_bad_role
4242- | Li_bad_role_in_menu
4343- | Li_bad_role_in_tablist
4444- | Li_bad_role_in_list
434434+ | `Imports_not_object
435435+ (** The [imports] property is not a JSON object.
436436+ [imports] must be an object mapping specifiers to URLs. *)
454374646- (* ARIA Errors *)
4747- | Unnecessary_role of { role: string; element: string; reason: string }
4848- | Bad_role of { element: string; role: string }
4949- | Aria_must_not_be_specified of { attr: string; element: string; condition: string }
5050- | Aria_must_not_be_used of { attr: string; element: string; condition: string }
5151- | Aria_should_not_be_used of { attr: string; role: string }
5252- | Aria_hidden_on_body
5353- | Img_empty_alt_with_role
5454- | Checkbox_button_needs_aria_pressed
5555- | Tab_without_tabpanel
5656- | Multiple_main_visible
5757- | Discarding_unrecognized_role of { token: string }
438438+ | `Empty_key
439439+ (** Specifier map contains an empty string key.
440440+ Module specifier keys must be non-empty strings. *)
584415959- (* Required Attribute/Element Conditions *)
6060- | Img_missing_alt
6161- | Img_missing_src_or_srcset
6262- | Option_empty_without_label
6363- | Bdo_missing_dir
6464- | Bdo_dir_auto
6565- | Base_missing_href_or_target
6666- | Base_after_link_script
6767- | Link_missing_href
6868- | Link_as_requires_preload
6969- | Link_imagesrcset_requires_as_image
7070- | Img_ismap_needs_a_href
7171- | Sizes_without_srcset
7272- | Imagesizes_without_imagesrcset
7373- | Srcset_w_without_sizes
7474- | Source_missing_srcset
7575- | Source_needs_media_or_type
7676- | Picture_missing_img
7777- | Map_id_name_mismatch
7878- | List_attr_requires_datalist
7979- | Input_list_not_allowed
8080- | Label_too_many_labelable
8181- | Label_for_id_mismatch
8282- | Role_on_label_ancestor
8383- | Role_on_label_for
8484- | Aria_label_on_label_for
8585- | Input_value_constraint of { constraint_type: string }
8686- | Summary_missing_role
8787- | Summary_missing_attrs
8888- | Summary_role_not_allowed
8989- | Autocomplete_webauthn_on_select
9090- | Commandfor_invalid_target
442442+ | `Non_string_value
443443+ (** Specifier map contains a non-string value.
444444+ All values in the specifier map must be strings (URLs). *)
914459292- (* Parse Errors *)
9393- | Forbidden_codepoint of { codepoint: int }
9494- | Char_ref_control of { codepoint: int }
9595- | Char_ref_non_char of { codepoint: int; astral: bool }
9696- | Char_ref_unassigned
9797- | Char_ref_zero
9898- | Char_ref_out_of_range
9999- | Numeric_char_ref_carriage_return
100100- | End_of_file_with_open_elements
101101- | No_element_in_scope of { tag: string }
102102- | End_tag_implied_open_elements of { tag: string }
103103- | Start_tag_in_table of { tag: string }
104104- | Bad_start_tag_in of { tag: string; context: string }
446446+ | `Key_trailing_slash
447447+ (** Specifier with trailing [/] maps to URL without trailing [/].
448448+ When a specifier key ends with [/], its value must also
449449+ end with [/] for proper prefix matching. *)
105450106106- (* Table Errors *)
107107- | Table_row_no_cells of { row: int }
108108- | Table_cell_overlap
109109- | Table_cell_spans_rowgroup
110110- | Table_column_no_cells of { column: int; element: string }
451451+ | `Scopes_not_object
452452+ (** The [scopes] property is not a JSON object.
453453+ [scopes] must be an object with URL keys. *)
111454112112- (* Language/Internationalization *)
113113- | Missing_lang_attr
114114- | Wrong_lang of { detected: string; declared: string; suggested: string }
115115- | Missing_dir_rtl of { language: string }
116116- | Wrong_dir of { language: string; declared: string }
117117- | Xml_lang_without_lang
118118- | Xml_lang_lang_mismatch
455455+ | `Scopes_values_not_object
456456+ (** A [scopes] entry value is not a JSON object.
457457+ Each scope must map to a specifier map object. *)
119458120120- (* Unicode Normalization *)
121121- | Not_nfc of { replacement: string }
459459+ | `Scopes_invalid_url
460460+ (** A [scopes] key is not a valid URL.
461461+ Scope keys must be valid URL strings. *)
122462123123- (* Multiple h1 *)
124124- | Multiple_h1
125125- | Multiple_autofocus
463463+ | `Scopes_value_invalid_url
464464+ (** A specifier value in [scopes] is not a valid URL.
465465+ URL values in scope specifier maps must be valid. *)
466466+]
126467127127- (* Import Maps *)
128128- | Importmap_invalid_json
129129- | Importmap_invalid_root
130130- | Importmap_imports_not_object
131131- | Importmap_empty_key
132132- | Importmap_non_string_value
133133- | Importmap_key_trailing_slash
134134- | Importmap_scopes_not_object
135135- | Importmap_scopes_values_not_object
136136- | Importmap_scopes_invalid_url
137137- | Importmap_scopes_value_invalid_url
468468+(** {1 Element-Specific Errors}
138469139139- (* Style Element *)
140140- | Style_type_invalid
470470+ Validation errors specific to particular HTML elements. *)
141471142142- (* Headingoffset *)
143143- | Headingoffset_invalid
472472+(** Image element ([<img>]) validation errors. *)
473473+type img_error = [
474474+ | `Missing_alt
475475+ (** Image lacks [alt] attribute for accessibility.
476476+ Per WCAG and HTML5, images must have [alt] text describing
477477+ their content, or [alt=""] for decorative images. *)
144478145145- (* Media Attribute *)
146146- | Media_empty
147147- | Media_all
479479+ | `Missing_src_or_srcset
480480+ (** Image has neither [src] nor [srcset].
481481+ An [<img>] must have at least one image source specified. *)
482482+483483+ | `Empty_alt_with_role
484484+ (** Image with [alt=""] has a [role] attribute.
485485+ Decorative images (empty [alt]) must not have [role] because
486486+ they should be hidden from assistive technology. *)
487487+488488+ | `Ismap_needs_href
489489+ (** Image with [ismap] lacks [<a href>] ancestor.
490490+ Server-side image maps require a link wrapper to function. *)
491491+]
492492+493493+(** Link element ([<link>]) validation errors. *)
494494+type link_error = [
495495+ | `Missing_href
496496+ (** [<link>] has no [href] or [imagesrcset].
497497+ A link element must have a resource to link to. *)
498498+499499+ | `As_requires_preload
500500+ (** [<link as="...">] used without [rel="preload"].
501501+ The [as] attribute is only meaningful for preload/modulepreload. *)
502502+503503+ | `Imagesrcset_requires_as_image
504504+ (** [<link imagesrcset>] used without [as="image"].
505505+ Image srcset preloading requires [as="image"]. *)
506506+]
148507149149- (* SVG/MathML specific *)
150150- | Svg_deprecated_attr of { attr: string; element: string }
151151- | Missing_required_svg_attr of { element: string; attr: string }
508508+(** Label element ([<label>]) validation errors. *)
509509+type label_error = [
510510+ | `Too_many_labelable
511511+ (** Label contains multiple labelable descendants.
512512+ A [<label>] should associate with exactly one form control. *)
152513153153- (* Generic/Fallback *)
154154- | Generic of { message: string }
514514+ | `For_id_mismatch
515515+ (** Label's [for] doesn't match descendant input's [id].
516516+ When a [<label>] has both [for] and a descendant input,
517517+ the input's [id] must match the [for] value. *)
518518+519519+ | `Role_on_ancestor
520520+ (** [<label>] with role is ancestor of labelable element.
521521+ Adding [role] to a label that wraps a form control
522522+ breaks the implicit label association. *)
155523156156-(** Get the severity level for an error code *)
524524+ | `Role_on_for
525525+ (** [<label>] with role uses [for] association.
526526+ Labels with explicit [for] association must not have [role]. *)
527527+528528+ | `Aria_label_on_for
529529+ (** [<label>] with [aria-label] uses [for] association.
530530+ [aria-label] on a label associated via [for] creates
531531+ conflicting accessible names. *)
532532+]
533533+534534+(** Input element ([<input>]) validation errors. *)
535535+type input_error = [
536536+ | `Checkbox_needs_aria_pressed
537537+ (** Checkbox with [role="button"] lacks [aria-pressed].
538538+ When a checkbox is styled as a toggle button, it needs
539539+ [aria-pressed] to convey the toggle state. *)
540540+541541+ | `Value_constraint of [`Constraint of string]
542542+ (** Input [value] doesn't meet type-specific constraints.
543543+ Different input types have different value format requirements
544544+ (dates, numbers, emails, etc.). *)
545545+546546+ | `List_not_allowed
547547+ (** [list] attribute used on incompatible input type.
548548+ The [list] attribute for datalist binding is only valid
549549+ on certain input types (text, search, url, etc.). *)
550550+551551+ | `List_requires_datalist
552552+ (** [list] attribute doesn't reference a [<datalist>].
553553+ The [list] attribute must contain the ID of a datalist element. *)
554554+]
555555+556556+(** Responsive image ([srcset]/[sizes]) validation errors. *)
557557+type srcset_error = [
558558+ | `Sizes_without_srcset
559559+ (** [sizes] used without [srcset].
560560+ The [sizes] attribute is meaningless without [srcset]. *)
561561+562562+ | `Imagesizes_without_imagesrcset
563563+ (** [imagesizes] used without [imagesrcset].
564564+ On [<link>], [imagesizes] requires [imagesrcset]. *)
565565+566566+ | `W_without_sizes
567567+ (** [srcset] with width descriptors lacks [sizes].
568568+ When using width descriptors ([w]) in [srcset], the [sizes]
569569+ attribute must specify the rendered size. *)
570570+571571+ | `Source_missing_srcset
572572+ (** [<source>] in [<picture>] lacks [srcset].
573573+ Picture source elements must have a srcset. *)
574574+575575+ | `Source_needs_media_or_type
576576+ (** [<source>] needs [media] or [type] to differentiate.
577577+ When multiple sources exist, each must have selection criteria. *)
578578+579579+ | `Picture_missing_img
580580+ (** [<picture>] lacks required [<img>] child.
581581+ A picture element must contain an img as the fallback. *)
582582+]
583583+584584+(** SVG element validation errors. *)
585585+type svg_error = [
586586+ | `Deprecated_attr of [`Attr of string] * [`Elem of string]
587587+ (** SVG attribute is deprecated.
588588+ Certain SVG presentation attributes are deprecated in
589589+ favor of CSS properties. *)
590590+591591+ | `Missing_attr of [`Elem of string] * [`Attr of string]
592592+ (** SVG element missing required attribute.
593593+ Some SVG elements have required attributes for valid rendering. *)
594594+]
595595+596596+(** Miscellaneous element-specific errors.
597597+598598+ These errors are specific to individual elements that don't
599599+ warrant their own category. *)
600600+type misc_error = [
601601+ | `Option_empty_without_label
602602+ (** [<option>] without [label] attribute is empty.
603603+ Options need either text content or a label attribute. *)
604604+605605+ | `Bdo_missing_dir
606606+ (** [<bdo>] element lacks required [dir] attribute.
607607+ The bidirectional override element must specify direction. *)
608608+609609+ | `Bdo_dir_auto
610610+ (** [<bdo>] has [dir="auto"] which is invalid.
611611+ BDO requires explicit [ltr] or [rtl], not auto-detection. *)
612612+613613+ | `Base_missing_href_or_target
614614+ (** [<base>] has neither [href] nor [target].
615615+ A base element must specify at least one of these. *)
616616+617617+ | `Base_after_link_script
618618+ (** [<base>] appears after [<link>] or [<script>].
619619+ The base URL must be established before other URL resolution. *)
620620+621621+ | `Map_id_name_mismatch
622622+ (** [<map>] [id] and [name] attributes don't match.
623623+ For image maps, both attributes must have the same value. *)
624624+625625+ | `Summary_missing_role
626626+ (** Non-default [<summary>] lacks [role] attribute.
627627+ Custom summary content outside details needs explicit role. *)
628628+629629+ | `Summary_missing_attrs
630630+ (** Non-default [<summary>] missing required ARIA attributes.
631631+ Custom summary implementations need proper ARIA. *)
632632+633633+ | `Summary_role_not_allowed
634634+ (** [<summary>] for its parent [<details>] has [role].
635635+ Default summary for details must not override its role. *)
636636+637637+ | `Autocomplete_webauthn_on_select
638638+ (** [<select>] has [autocomplete] containing [webauthn].
639639+ WebAuthn autocomplete tokens are not valid for select elements. *)
640640+641641+ | `Commandfor_invalid_target
642642+ (** [commandfor] doesn't reference a valid element ID.
643643+ The invoker must reference an element in the same tree. *)
644644+645645+ | `Style_type_invalid
646646+ (** [<style type>] has value other than [text/css].
647647+ HTML5 only supports CSS in style elements. *)
648648+649649+ | `Headingoffset_invalid
650650+ (** [headingoffset] value is out of range.
651651+ Must be an integer between 0 and 8. *)
652652+653653+ | `Media_empty
654654+ (** [media] attribute is empty string.
655655+ Media queries must be non-empty if the attribute is present. *)
656656+657657+ | `Media_all
658658+ (** [media] attribute is just ["all"].
659659+ Using [media="all"] is pointless; omit the attribute instead. *)
660660+661661+ | `Multiple_h1
662662+ (** Document contains multiple [<h1>] elements.
663663+ Best practice is one [<h1>] per document unless using
664664+ [headingoffset] to indicate sectioning. *)
665665+666666+ | `Multiple_autofocus
667667+ (** Multiple elements have [autofocus] in same scope.
668668+ Only one element should have autofocus per scoping root. *)
669669+]
670670+671671+(** {1 Top-Level Error Type} *)
672672+673673+(** All HTML5 validation errors, organized by category.
674674+675675+ Pattern match on the outer constructor to handle error categories,
676676+ or match through to specific errors as needed.
677677+678678+ {[
679679+ let severity_of_category = function
680680+ | `Aria _ -> may_be_warning
681681+ | `I18n _ -> usually_info_or_warning
682682+ | _ -> usually_error
683683+ ]} *)
684684+type t = [
685685+ | `Attr of attr_error
686686+ (** Attribute validation errors *)
687687+ | `Element of element_error
688688+ (** Element structure errors *)
689689+ | `Tag of tag_error
690690+ (** Tag-level parse errors *)
691691+ | `Char_ref of char_ref_error
692692+ (** Character reference errors *)
693693+ | `Aria of aria_error
694694+ (** ARIA and accessibility errors *)
695695+ | `Li_role of li_role_error
696696+ (** List item role constraints *)
697697+ | `Table of table_error
698698+ (** Table structure errors *)
699699+ | `I18n of i18n_error
700700+ (** Language and direction errors *)
701701+ | `Importmap of importmap_error
702702+ (** Import map JSON errors *)
703703+ | `Img of img_error
704704+ (** Image element errors *)
705705+ | `Link of link_error
706706+ (** Link element errors *)
707707+ | `Label of label_error
708708+ (** Label element errors *)
709709+ | `Input of input_error
710710+ (** Input element errors *)
711711+ | `Srcset of srcset_error
712712+ (** Responsive image errors *)
713713+ | `Svg of svg_error
714714+ (** SVG-specific errors *)
715715+ | `Misc of misc_error
716716+ (** Miscellaneous element errors *)
717717+ | `Generic of string
718718+ (** Fallback for messages without specific error codes *)
719719+]
720720+721721+(** {1 Functions} *)
722722+723723+(** Get the severity level for an error.
724724+ Most errors are [Error]; some ARIA and i18n issues are [Warning] or [Info]. *)
157725val severity : t -> severity
158726159159-(** Get a short code string for categorization *)
727727+(** Get a short categorization code string.
728728+ Useful for filtering, grouping, or machine-readable output.
729729+ Example: ["disallowed-attribute"], ["missing-alt"], ["aria-not-allowed"]. *)
160730val code_string : t -> string
161731162162-(** Convert error code to exact Nu validator message string *)
732732+(** Convert error to human-readable message.
733733+ Produces messages matching the Nu HTML Validator format with
734734+ proper Unicode curly quotes around identifiers. *)
163735val to_message : t -> string
164736165165-(** Format a string with curly quotes *)
737737+(** Format a string with Unicode curly quotes.
738738+ Wraps the string in U+201C and U+201D ("..."). *)
166739val q : string -> string
+2-2
lib/html5_checker/html5_checker.ml
···4242 let dummy_doc = Html5rw.parse (Bytesrw.Bytes.Reader.of_string "") in
4343 { doc = dummy_doc; msgs = Message_collector.messages collector; system_id }
4444 | Error msg ->
4545- Message_collector.add_typed collector (Error_code.Generic { message = msg });
4545+ Message_collector.add_typed collector (`Generic msg);
4646 let dummy_doc = Html5rw.parse (Bytesrw.Bytes.Reader.of_string "") in
4747 { doc = dummy_doc; msgs = Message_collector.messages collector; system_id }
4848 end
···62626363 (* Special case: emit missing-lang warning for specific test file *)
6464 if is_missing_lang_test system_id then
6565- Message_collector.add_typed collector Error_code.Missing_lang_attr;
6565+ Message_collector.add_typed collector (`I18n `Missing_lang);
66666767 { doc; msgs = Message_collector.messages collector; system_id }
6868 end
+1-1
lib/html5_checker/semantic/autofocus_checker.ml
···6969 | ctx :: _ ->
7070 ctx.autofocus_count <- ctx.autofocus_count + 1;
7171 if ctx.autofocus_count > 1 then
7272- Message_collector.add_typed collector Error_code.Multiple_autofocus
7272+ Message_collector.add_typed collector (`Misc `Multiple_autofocus)
7373 | [] -> ()
7474 end
7575 end
+2-7
lib/html5_checker/semantic/form_checker.ml
···2626let check_autocomplete_value value element_name collector =
2727 (* webauthn is not allowed on select, only on input and textarea *)
2828 if element_name = "select" && contains_webauthn value then begin
2929- Message_collector.add_typed collector Error_code.Autocomplete_webauthn_on_select
2929+ Message_collector.add_typed collector (`Misc `Autocomplete_webauthn_on_select)
3030 end else begin
3131 (* Use the proper autocomplete validator from dt_autocomplete *)
3232 match Dt_autocomplete.validate_autocomplete value with
···3535 (* Nu validator prefixes autocomplete errors with "Bad autocomplete detail tokens (any): " for select/textarea, but not for input *)
3636 let reason = if element_name = "input" then msg else "Bad autocomplete detail tokens (any): " ^ msg in
3737 Message_collector.add_typed collector
3838- (Error_code.Bad_attr_value {
3939- element = element_name;
4040- attr = "autocomplete";
4141- value;
4242- reason
4343- })
3838+ (`Attr (`Bad_value (`Elem element_name, `Attr "autocomplete", `Value value, `Reason reason)))
4439 end
45404641let start_element _state ~name ~namespace:_ ~attrs collector =
+17-33
lib/html5_checker/semantic/id_checker.ml
···100100 (* Check for empty ID *)
101101 if String.length id = 0 then
102102 Message_collector.add_typed collector
103103- (Error_code.Bad_attr_value_generic {
104104- message = "Bad value \"\" for attribute \"id\": An ID must not be the empty string."
105105- })
103103+ (`Attr (`Bad_value_generic (`Message "Bad value \"\" for attribute \"id\": An ID must not be the empty string.")))
106104 (* Check for whitespace in ID *)
107105 else if contains_whitespace id then
108106 Message_collector.add_typed collector
109109- (Error_code.Bad_attr_value_generic {
110110- message = Printf.sprintf "Bad value %s for attribute \"id\": An ID must not contain whitespace."
111111- (Error_code.q id)
112112- })
107107+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute \"id\": An ID must not contain whitespace."
108108+ (Error_code.q id)))))
113109 (* Check for duplicate ID *)
114110 else if Hashtbl.mem state.ids id then
115115- Message_collector.add_typed collector (Error_code.Duplicate_id { id })
111111+ Message_collector.add_typed collector (`Attr (`Duplicate_id (`Id id)))
116112 else
117113 (* Store the ID *)
118114 Hashtbl.add state.ids id ()
···148144 else
149145 (* Empty hash name: "#" *)
150146 Message_collector.add_typed collector
151151- (Error_code.Bad_attr_value {
152152- element;
153153- attr = name;
154154- value;
155155- reason = Printf.sprintf "Bad hash-name reference: A hash-name reference must have at least one character after %s."
156156- (Error_code.q "#")
157157- })
147147+ (`Attr (`Bad_value (`Elem element, `Attr name, `Value value,
148148+ `Reason (Printf.sprintf "Bad hash-name reference: A hash-name reference must have at least one character after %s."
149149+ (Error_code.q "#")))))
158150 | None ->
159151 if String.length value > 0 then
160152 (* Missing # prefix *)
161153 Message_collector.add_typed collector
162162- (Error_code.Bad_attr_value {
163163- element;
164164- attr = name;
165165- value;
166166- reason = Printf.sprintf "Bad hash-name reference: A hash-name reference must start with %s."
167167- (Error_code.q "#")
168168- })
154154+ (`Attr (`Bad_value (`Elem element, `Attr name, `Value value,
155155+ `Reason (Printf.sprintf "Bad hash-name reference: A hash-name reference must start with %s."
156156+ (Error_code.q "#")))))
169157 end
170158171159 | "name" when element = "map" ->
···201189 let name_opt = List.find_map (fun (n, v) -> if n = "name" then Some v else None) attrs in
202190 match id_opt, name_opt with
203191 | Some id_val, Some name_val when id_val <> name_val ->
204204- Message_collector.add_typed collector Error_code.Map_id_name_mismatch
192192+ Message_collector.add_typed collector (`Misc `Map_id_name_mismatch)
205193 | _ -> ()
206194 end
207195···217205 if not (Hashtbl.mem state.ids ref.referenced_id) then begin
218206 (* Use specific error for list attribute on input *)
219207 if ref.attribute = "list" && ref.referring_element = "input" then
220220- Message_collector.add_typed collector Error_code.List_attr_requires_datalist
208208+ Message_collector.add_typed collector (`Input `List_requires_datalist)
221209 else if ref.attribute = "commandfor" then
222222- Message_collector.add_typed collector Error_code.Commandfor_invalid_target
210210+ Message_collector.add_typed collector (`Misc `Commandfor_invalid_target)
223211 else
224212 (* Use generic for dangling references - format may vary *)
225213 Message_collector.add_typed collector
226226- (Error_code.Generic {
227227- message = Printf.sprintf "The %s attribute on the %s element refers to ID %s which does not exist in the document."
228228- (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id)
229229- })
214214+ (`Generic (Printf.sprintf "The %s attribute on the %s element refers to ID %s which does not exist in the document."
215215+ (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id)))
230216 end
231217 ) state.references;
232218···234220 List.iter (fun ref ->
235221 if not (Hashtbl.mem state.map_names ref.referenced_id) then
236222 Message_collector.add_typed collector
237237- (Error_code.Generic {
238238- message = Printf.sprintf "The %s attribute on the %s element refers to map name %s which does not exist in the document."
239239- (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id)
240240- })
223223+ (`Generic (Printf.sprintf "The %s attribute on the %s element refers to map name %s which does not exist in the document."
224224+ (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id)))
241225 ) state.usemap_references
242226243227let checker = (module struct
···322322 if original_declared = "" then begin
323323 (* No lang attribute - suggest adding one *)
324324 Message_collector.add_typed collector
325325- (Error_code.Wrong_lang {
326326- detected = detected_name;
327327- declared = "";
328328- suggested = suggested_code
329329- })
325325+ (`I18n (`Wrong_lang (`Detected detected_name, `Declared "", `Suggested suggested_code)))
330326 end
331327 else if base_declared <> base_detected &&
332328 (* Don't warn for zh variants *)
333329 not (base_declared = "zh" && base_detected = "zh") then begin
334330 Message_collector.add_typed collector
335335- (Error_code.Wrong_lang {
336336- detected = detected_name;
337337- declared = original_declared;
338338- suggested = suggested_code
339339- })
331331+ (`I18n (`Wrong_lang (`Detected detected_name, `Declared original_declared, `Suggested suggested_code)))
340332 end;
341333342334 (* Check dir attribute for RTL languages *)
···344336 match state.html_dir with
345337 | None ->
346338 Message_collector.add_typed collector
347347- (Error_code.Missing_dir_rtl { language = detected_name })
339339+ (`I18n (`Missing_dir_rtl (`Language detected_name)))
348340 | Some dir when String.lowercase_ascii dir <> "rtl" ->
349341 Message_collector.add_typed collector
350350- (Error_code.Wrong_dir { language = detected_name; declared = dir })
342342+ (`I18n (`Wrong_dir (`Language detected_name, `Declared dir)))
351343 | _ -> ()
352344 end
353345 | _ -> ()
+5-17
lib/html5_checker/semantic/nesting_checker.ml
···263263 | None -> ancestor
264264 in
265265 Message_collector.add_typed collector
266266- (Error_code.Element_not_allowed_as_child {
267267- child = name;
268268- parent
269269- })
266266+ (`Element (`Not_allowed_as_child (`Child name, `Parent parent)))
270267 end else
271268 (* Nesting violation: use "must not be descendant" format *)
272269 Message_collector.add_typed collector
273273- (Error_code.Element_must_not_be_descendant {
274274- element = name;
275275- attr;
276276- ancestor
277277- })
270270+ (`Element (`Must_not_descend (`Elem name, `Attr attr, `Ancestor ancestor)))
278271 end
279272 ) special_ancestors
280273 end
···286279 | "area" ->
287280 if (state.ancestor_mask land map_mask) = 0 then
288281 Message_collector.add_typed collector
289289- (Error_code.Generic {
290290- message = Printf.sprintf "The %s element must have a %s ancestor."
291291- (Error_code.q "area") (Error_code.q "map")
292292- })
282282+ (`Generic (Printf.sprintf "The %s element must have a %s ancestor."
283283+ (Error_code.q "area") (Error_code.q "map")))
293284 | _ -> ()
294285295286(** Check for metadata-only elements appearing outside valid contexts.
···304295 | parent :: _ ->
305296 (* style inside any other element is not allowed *)
306297 Message_collector.add_typed collector
307307- (Error_code.Element_not_allowed_as_child {
308308- child = "style";
309309- parent = parent.name
310310- })
298298+ (`Element (`Not_allowed_as_child (`Child "style", `Parent parent.name)))
311299 | [] -> () (* at root level, would be caught elsewhere *)
312300 end
313301 | _ -> ()
+5-5
lib/html5_checker/semantic/obsolete_checker.ml
···269269 | None -> ()
270270 | Some suggestion ->
271271 Message_collector.add_typed collector
272272- (Error_code.Obsolete_element { element = name; suggestion }));
272272+ (`Element (`Obsolete (`Elem name, `Suggestion suggestion))));
273273274274 (* Check for obsolete attributes *)
275275 List.iter (fun (attr_name, _attr_value) ->
···281281 error from nesting_checker takes precedence *)
282282 if state.in_head then
283283 Message_collector.add_typed collector
284284- (Error_code.Attr_not_allowed_on_element { attr = attr_name; element = name })
284284+ (`Attr (`Not_allowed (`Attr attr_name, `Elem name)))
285285 end else begin
286286 (* Check specific obsolete attributes for this element *)
287287 (match Hashtbl.find_opt obsolete_attributes attr_lower with
···291291 | None -> ()
292292 | Some suggestion ->
293293 Message_collector.add_typed collector
294294- (Error_code.Obsolete_attr { element = name; attr = attr_name; suggestion = Some suggestion })));
294294+ (`Element (`Obsolete_attr (`Elem name, `Attr attr_name, `Suggestion (Some suggestion))))));
295295296296 (* Check obsolete style attributes *)
297297 (match Hashtbl.find_opt obsolete_style_attrs attr_lower with
···299299 | Some elements ->
300300 if List.mem name_lower elements then
301301 Message_collector.add_typed collector
302302- (Error_code.Obsolete_attr { element = name; attr = attr_name; suggestion = Some "Use CSS instead." }));
302302+ (`Element (`Obsolete_attr (`Elem name, `Attr attr_name, `Suggestion (Some "Use CSS instead.")))));
303303304304 (* Check obsolete global attributes *)
305305 (match Hashtbl.find_opt obsolete_global_attrs attr_lower with
306306 | None -> ()
307307 | Some suggestion ->
308308 Message_collector.add_typed collector
309309- (Error_code.Obsolete_global_attr { attr = attr_name; suggestion }))
309309+ (`Element (`Obsolete_global_attr (`Attr attr_name, `Suggestion suggestion))))
310310 end
311311 ) attrs
312312 end
+2-5
lib/html5_checker/semantic/option_checker.ml
···4545 (* Report error for empty label attribute value *)
4646 if label_empty then
4747 Message_collector.add_typed collector
4848- (Error_code.Bad_attr_value {
4949- element = "option"; attr = "label"; value = "";
5050- reason = "Bad non-empty string: Must not be empty."
5151- });
4848+ (`Attr (`Bad_value (`Elem "option", `Attr "label", `Value "", `Reason "Bad non-empty string: Must not be empty.")));
5249 let ctx = { has_text = false; has_label; label_empty } in
5350 state.option_stack <- ctx :: state.option_stack
5451 end
···6966 (* Note: empty label error is already reported at start_element,
7067 so only report empty option without label when there's no label attribute at all *)
7168 if not ctx.has_text && not ctx.has_label then
7272- Message_collector.add_typed collector Error_code.Option_empty_without_label
6969+ Message_collector.add_typed collector (`Misc `Option_empty_without_label)
7370 | [] -> ()
7471 end
7572 end
···2727let check_img_element state attrs collector =
2828 (* Check for required src OR srcset attribute *)
2929 if not (has_attr "src" attrs) && not (has_attr "srcset" attrs) then
3030- Message_collector.add_typed collector Error_code.Img_missing_src_or_srcset;
3030+ Message_collector.add_typed collector (`Img `Missing_src_or_srcset);
31313232 (* Check for alt attribute - always required *)
3333 if not (has_attr "alt" attrs) then
3434- Message_collector.add_typed collector Error_code.Img_missing_alt;
3434+ Message_collector.add_typed collector (`Img `Missing_alt);
35353636 (* Check ismap requires 'a' ancestor with href *)
3737 if has_attr "ismap" attrs && not state.in_a_with_href then
3838- Message_collector.add_typed collector Error_code.Img_ismap_needs_a_href
3838+ Message_collector.add_typed collector (`Img `Ismap_needs_href)
39394040let check_area_element attrs collector =
4141 (* area with href requires alt *)
4242 if has_attr "href" attrs && not (has_attr "alt" attrs) then
4343 Message_collector.add_typed collector
4444- (Error_code.Missing_required_attr { element = "area"; attr = "alt" })
4444+ (`Attr (`Missing (`Elem "area", `Attr "alt")))
45454646let check_input_element attrs collector =
4747 match get_attr "type" attrs with
···4949 (* input[type=image] requires alt *)
5050 if not (has_attr "alt" attrs) then
5151 Message_collector.add_typed collector
5252- (Error_code.Missing_required_attr { element = "input"; attr = "alt" })
5252+ (`Attr (`Missing (`Elem "input", `Attr "alt")))
5353 | Some "hidden" ->
5454 (* input[type=hidden] should not have required attribute *)
5555 if has_attr "required" attrs then
5656 Message_collector.add_typed collector
5757- (Error_code.Attr_not_allowed_when {
5858- attr = "required";
5959- element = "input";
6060- condition = "the type attribute is hidden"
6161- })
5757+ (`Attr (`Not_allowed_when (`Attr "required", `Elem "input", `Condition "the type attribute is hidden")))
6258 | Some "file" ->
6359 (* input[type=file] should not have value attribute *)
6460 if has_attr "value" attrs then
6561 Message_collector.add_typed collector
6666- (Error_code.Attr_not_allowed_when {
6767- attr = "value";
6868- element = "input";
6969- condition = "the type attribute is file"
7070- })
6262+ (`Attr (`Not_allowed_when (`Attr "value", `Elem "input", `Condition "the type attribute is file")))
7163 | _ -> ()
72647365let check_script_element attrs _collector =
···10092 in
1019310294 if not valid then
9595+ let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" in
10396 Message_collector.add_typed collector
104104- (Error_code.Generic {
105105- message = Printf.sprintf "A %s element must have either a %s attribute, a %s attribute with a %s attribute, or an %s attribute with a %s attribute."
106106- (Error_code.q "meta") (Error_code.q "charset") (Error_code.q "name")
107107- (Error_code.q "content") (Error_code.q "http-equiv") (Error_code.q "content")
108108- })
9797+ (`Generic (Printf.sprintf "A %s element must have either a %s attribute, a %s attribute with a %s attribute, or an %s attribute with a %s attribute."
9898+ (q "meta") (q "charset") (q "name")
9999+ (q "content") (q "http-equiv") (q "content")))
109100110101let check_link_element attrs collector =
111102 (* link[rel="stylesheet"] requires href *)
112103 match get_attr "rel" attrs with
113104 | Some rel when String.equal rel "stylesheet" ->
114105 if not (has_attr "href" attrs) then
115115- Message_collector.add_typed collector Error_code.Link_missing_href
106106+ Message_collector.add_typed collector (`Link `Missing_href)
116107 | _ -> ()
117108118109let check_a_element attrs collector =
119110 (* a[download] requires href *)
120111 if has_attr "download" attrs && not (has_attr "href" attrs) then
121112 Message_collector.add_typed collector
122122- (Error_code.Missing_required_attr { element = "a"; attr = "href" })
113113+ (`Attr (`Missing (`Elem "a", `Attr "href")))
123114124115let check_map_element attrs collector =
125116 (* map requires name *)
126117 if not (has_attr "name" attrs) then
127118 Message_collector.add_typed collector
128128- (Error_code.Missing_required_attr { element = "map"; attr = "name" })
119119+ (`Attr (`Missing (`Elem "map", `Attr "name")))
129120130121let check_object_element attrs collector =
131122 (* object requires data attribute (or type attribute alone is not sufficient) *)
···133124 let has_type = has_attr "type" attrs in
134125 if not has_data && has_type then
135126 Message_collector.add_typed collector
136136- (Error_code.Missing_required_attr { element = "object"; attr = "data" })
127127+ (`Attr (`Missing (`Elem "object", `Attr "data")))
137128138129let check_popover_element element_name attrs collector =
139130 (* popover attribute must have valid value *)
···142133 let value_lower = String.lowercase_ascii value in
143134 (* Valid values: empty string, auto, manual, hint *)
144135 if value_lower <> "" && value_lower <> "auto" && value_lower <> "manual" && value_lower <> "hint" then
136136+ let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" in
145137 Message_collector.add_typed collector
146146- (Error_code.Bad_attr_value_generic {
147147- message = Printf.sprintf "Bad value %s for attribute %s on element %s."
148148- (Error_code.q value) (Error_code.q "popover") (Error_code.q element_name)
149149- })
138138+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s."
139139+ (q value) (q "popover") (q element_name)))))
150140 | None -> ()
151141152142let check_meter_element attrs collector =
153143 (* meter requires value attribute *)
154144 if not (has_attr "value" attrs) then
155145 Message_collector.add_typed collector
156156- (Error_code.Missing_required_attr { element = "meter"; attr = "value" })
146146+ (`Attr (`Missing (`Elem "meter", `Attr "value")))
157147 else begin
158148 (* Validate min <= value constraint *)
159149 match get_attr "value" attrs, get_attr "min" attrs with
···162152 let value = float_of_string value_str in
163153 let min_val = float_of_string min_str in
164154 if min_val > value then
155155+ let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" in
165156 Message_collector.add_typed collector
166166- (Error_code.Generic {
167167- message = Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute."
168168- (Error_code.q "min") (Error_code.q "value")
169169- })
157157+ (`Generic (Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute."
158158+ (q "min") (q "value")))
170159 with _ -> ())
171160 | _ -> ()
172161 end
···183172 | Some max_str -> (try float_of_string max_str with _ -> 1.0)
184173 in
185174 if value > max_val then
175175+ let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" in
186176 (* Check which message to use based on whether max is present *)
187177 if has_attr "max" attrs then
188178 Message_collector.add_typed collector
189189- (Error_code.Generic {
179179+ (`Generic (
190180 (* Note: double space before "value" matches Nu validator quirk *)
191191- message = Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute."
192192- (Error_code.q "value") (Error_code.q "max")
193193- })
181181+ Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute."
182182+ (q "value") (q "max")))
194183 else
195184 Message_collector.add_typed collector
196196- (Error_code.Generic {
185185+ (`Generic (
197186 (* Note: double space before "value" matches Nu validator quirk *)
198198- message = Printf.sprintf "The value of the %s attribute must be less than or equal to one when the %s attribute is absent."
199199- (Error_code.q "value") (Error_code.q "max")
200200- })
187187+ Printf.sprintf "The value of the %s attribute must be less than or equal to one when the %s attribute is absent."
188188+ (q "value") (q "max")))
201189 with _ -> ())
202190203191let start_element state ~name ~namespace:_ ~attrs collector =
+34-34
lib/html5_checker/specialized/aria_checker.ml
···452452 List.iter (fun role ->
453453 if not (Hashtbl.mem valid_aria_roles role) then
454454 Message_collector.add_typed collector
455455- (Error_code.Discarding_unrecognized_role { token = role })
455455+ (`Aria (`Unrecognized_role (`Token role)))
456456 ) explicit_roles;
457457458458 (* Get implicit role for this element *)
···484484 let first_role = List.hd explicit_roles in
485485 if first_role <> "none" && first_role <> "presentation" then
486486 Message_collector.add_typed collector
487487- (Error_code.Bad_role { element = name; role = first_role })
487487+ (`Aria (`Bad_role (`Elem name, `Role first_role)))
488488 end;
489489490490 (* Check br/wbr aria-* attribute restrictions - not allowed *)
···494494 if String.length attr_lower > 5 && String.sub attr_lower 0 5 = "aria-" &&
495495 attr_lower <> "aria-hidden" then
496496 Message_collector.add_typed collector
497497- (Error_code.Attr_not_allowed_on_element { attr = attr_name; element = name })
497497+ (`Attr (`Not_allowed (`Attr attr_name, `Elem name)))
498498 ) attrs
499499 end;
500500···504504 (* Generate error if element cannot have accessible name but has one *)
505505 if has_aria_label && not can_have_name then
506506 Message_collector.add_typed collector
507507- (Error_code.Aria_must_not_be_specified { attr = "aria-label"; element = name;
508508- 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" });
507507+ (`Aria (`Must_not_specify (`Attr "aria-label", `Elem name,
508508+ `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")));
509509510510 if has_aria_labelledby && not can_have_name then
511511 Message_collector.add_typed collector
512512- (Error_code.Aria_must_not_be_specified { attr = "aria-labelledby"; element = name;
513513- 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" });
512512+ (`Aria (`Must_not_specify (`Attr "aria-labelledby", `Elem name,
513513+ `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")));
514514515515 if has_aria_braillelabel && not can_have_name then
516516 Message_collector.add_typed collector
517517- (Error_code.Aria_must_not_be_specified { attr = "aria-braillelabel"; element = name;
518518- 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" });
517517+ (`Aria (`Must_not_specify (`Attr "aria-braillelabel", `Elem name,
518518+ `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")));
519519520520 (* Check for img with empty alt having role attribute *)
521521 if name_lower = "img" then begin
···524524 | Some alt when String.trim alt = "" ->
525525 (* img with empty alt must not have role attribute *)
526526 if role_attr <> None then
527527- Message_collector.add_typed collector Error_code.Img_empty_alt_with_role
527527+ Message_collector.add_typed collector (`Img `Empty_alt_with_role)
528528 | _ -> ()
529529 end;
530530···537537 if input_type = "checkbox" && List.mem "button" explicit_roles then begin
538538 let has_aria_pressed = List.assoc_opt "aria-pressed" attrs <> None in
539539 if not has_aria_pressed then
540540- Message_collector.add_typed collector Error_code.Checkbox_button_needs_aria_pressed
540540+ Message_collector.add_typed collector (`Input `Checkbox_needs_aria_pressed)
541541 end
542542 end;
543543···551551 | Some _ ->
552552 let valid_roles = ["group"; "menuitem"; "menuitemcheckbox"; "menuitemradio"; "separator"] in
553553 if not (List.mem first_role valid_roles) then
554554- Message_collector.add_typed collector Error_code.Li_bad_role_in_menu
554554+ Message_collector.add_typed collector (`Li_role `Li_bad_role_in_menu)
555555 | None ->
556556 (* Check if in tablist context *)
557557 match get_ancestor_role state ["tablist"] with
558558 | Some _ ->
559559 if first_role <> "tab" then
560560- Message_collector.add_typed collector Error_code.Li_bad_role_in_tablist
560560+ Message_collector.add_typed collector (`Li_role `Li_bad_role_in_tablist)
561561 | None ->
562562 (* Check if in list context (ul/ol/menu without explicit role, or role=list) *)
563563 (* Nu validator produces this error for ANY explicit role on li in list context,
564564 even role="listitem" - because having an explicit role is itself the problem.
565565 The message says "other than listitem" but the rule is: don't use explicit roles. *)
566566 if is_in_list_context state then
567567- Message_collector.add_typed collector Error_code.Li_bad_role_in_list)
567567+ Message_collector.add_typed collector (`Li_role `Li_bad_role_in_list))
568568 end
569569 end;
570570···573573 let aria_hidden = List.assoc_opt "aria-hidden" attrs in
574574 match aria_hidden with
575575 | Some "true" ->
576576- Message_collector.add_typed collector Error_code.Aria_hidden_on_body
576576+ Message_collector.add_typed collector (`Aria `Hidden_on_body)
577577 | _ -> ()
578578 end;
579579···584584 | Some input_type when String.lowercase_ascii input_type = "checkbox" ->
585585 if aria_checked <> None then
586586 Message_collector.add_typed collector
587587- (Error_code.Aria_must_not_be_used { attr = "aria-checked"; element = "input";
588588- condition = "a \xe2\x80\x9ctype\xe2\x80\x9d attribute whose value is \xe2\x80\x9ccheckbox\xe2\x80\x9d" })
587587+ (`Aria (`Must_not_use (`Attr "aria-checked", `Elem "input",
588588+ `Condition "a \xe2\x80\x9ctype\xe2\x80\x9d attribute whose value is \xe2\x80\x9ccheckbox\xe2\x80\x9d")))
589589 | _ -> ()
590590 end;
591591···599599 match role_to_check with
600600 | Some _role when List.mem _role roles_without_aria_expanded ->
601601 Message_collector.add_typed collector
602602- (Error_code.Attr_not_allowed_on_element { attr = "aria-expanded"; element = name })
602602+ (`Attr (`Not_allowed (`Attr "aria-expanded", `Elem name)))
603603 | _ -> ()
604604 end;
605605···622622 Printf.sprintf "for element \xe2\x80\x9c%s\xe2\x80\x9d" name
623623 in
624624 Message_collector.add_typed collector
625625- (Error_code.Unnecessary_role { role = first_role; element = name; reason })
625625+ (`Aria (`Unnecessary_role (`Role first_role, `Elem name, `Reason reason)))
626626 | _ -> ()
627627 end;
628628···634634 match role_attr with
635635 | Some role_value ->
636636 Message_collector.add_typed collector
637637- (Error_code.Bad_role { element = name; role = role_value })
637637+ (`Aria (`Bad_role (`Elem name, `Role role_value)))
638638 | None -> ()
639639 end;
640640···642642 (* Check if role cannot be named *)
643643 if Hashtbl.mem roles_which_cannot_be_named role && has_accessible_name then
644644 Message_collector.add_typed collector
645645- (Error_code.Generic { message = Printf.sprintf
645645+ (`Generic (Printf.sprintf
646646 "Elements with role=\"%s\" must not have accessible names (via aria-label or aria-labelledby)."
647647- role });
647647+ role));
648648649649 (* Check for required ancestor roles *)
650650 begin match Hashtbl.find_opt required_role_ancestor_by_descendant role with
651651 | Some required_ancestors ->
652652 if not (has_required_ancestor_role state required_ancestors) then
653653 Message_collector.add_typed collector
654654- (Error_code.Generic { message = Printf.sprintf
654654+ (`Generic (Printf.sprintf
655655 "An element with \"role=%s\" must be contained in, or owned by, an element with the \"role\" value %s."
656656 role
657657- (render_role_set required_ancestors) })
657657+ (render_role_set required_ancestors)))
658658 | None -> ()
659659 end;
660660···666666 (* Check if current role is in the deprecated list *)
667667 if Array.mem role deprecated_for_roles then
668668 Message_collector.add_typed collector
669669- (Error_code.Aria_should_not_be_used { attr = attr_name; role })
669669+ (`Aria (`Should_not_use (`Attr attr_name, `Role role)))
670670 | None -> ()
671671 ) attrs
672672 ) explicit_roles;
···680680 let value_lower = String.lowercase_ascii (String.trim attr_value) in
681681 if value_lower = default_value then
682682 Message_collector.add_typed collector
683683- (Error_code.Generic { message = Printf.sprintf
683683+ (`Generic (Printf.sprintf
684684 "The \xe2\x80\x9c%s\xe2\x80\x9d attribute is unnecessary for the value \xe2\x80\x9c%s\xe2\x80\x9d."
685685- attr_name attr_value })
685685+ attr_name attr_value))
686686 | None -> ()
687687 ) attrs;
688688···697697 (* summary that is the first child of details *)
698698 if has_role_attr then
699699 (* Must not have role attribute *)
700700- Message_collector.add_typed collector Error_code.Summary_role_not_allowed
700700+ Message_collector.add_typed collector (`Misc `Summary_role_not_allowed)
701701 else if has_aria_pressed then
702702 (* aria-pressed without role requires role *)
703703- Message_collector.add_typed collector Error_code.Summary_missing_role
703703+ Message_collector.add_typed collector (`Misc `Summary_missing_role)
704704 else if has_aria_expanded then
705705 (* aria-expanded without role requires role *)
706706- Message_collector.add_typed collector Error_code.Summary_missing_attrs
706706+ Message_collector.add_typed collector (`Misc `Summary_missing_attrs)
707707 end else begin
708708 (* summary NOT in details context - different rules apply *)
709709 (* If has aria-expanded or aria-pressed, must have role *)
710710 if (has_aria_expanded || has_aria_pressed) && explicit_roles = [] then begin
711711 if has_aria_pressed then
712712- Message_collector.add_typed collector Error_code.Summary_missing_role
712712+ Message_collector.add_typed collector (`Misc `Summary_missing_role)
713713 else
714714- Message_collector.add_typed collector Error_code.Summary_missing_attrs
714714+ Message_collector.add_typed collector (`Misc `Summary_missing_attrs)
715715 end
716716 end
717717 end;
···739739let end_document state collector =
740740 (* Check that active tabs have corresponding tabpanels *)
741741 if state.has_active_tab && not state.has_tabpanel then
742742- Message_collector.add_typed collector Error_code.Tab_without_tabpanel;
742742+ Message_collector.add_typed collector (`Aria `Tab_without_tabpanel);
743743744744 (* Check for multiple visible main elements *)
745745 if state.visible_main_count > 1 then
746746- Message_collector.add_typed collector Error_code.Multiple_main_visible
746746+ Message_collector.add_typed collector (`Aria `Multiple_main)
747747748748let checker = (module struct
749749 type nonrec state = state
···6060(** Report disallowed attribute error *)
6161let report_disallowed_attr element attr collector =
6262 Message_collector.add_typed collector
6363- (Error_code.Attr_not_allowed_on_element { attr; element })
6363+ (`Attr (`Not_allowed (`Attr attr, `Elem element)))
64646565let start_element state ~name ~namespace ~attrs collector =
6666 let name_lower = String.lowercase_ascii name in
···100100 (* Only xmlns:xlink (with correct value) and xmlns:xml are allowed *)
101101 if prefix <> "xlink" && prefix <> "xml" then
102102 Message_collector.add_typed collector
103103- (Error_code.Attr_not_allowed_here { attr = attr_name })
103103+ (`Attr (`Not_allowed_here (`Attr attr_name)))
104104 end
105105 ) attrs
106106 end;
···116116 if name_lower = "feconvolvematrix" then begin
117117 if not (has_attr "order" attrs) then
118118 Message_collector.add_typed collector
119119- (Error_code.Missing_required_svg_attr { element = "feConvolveMatrix"; attr = "order" })
119119+ (`Svg (`Missing_attr (`Elem "feConvolveMatrix", `Attr "order")))
120120 end;
121121122122 (* Validate style type attribute - must be "text/css" or omitted *)
···126126 if attr_lower = "type" then begin
127127 let value_lower = String.lowercase_ascii (String.trim attr_value) in
128128 if value_lower <> "text/css" then
129129- Message_collector.add_typed collector Error_code.Style_type_invalid
129129+ Message_collector.add_typed collector (`Misc `Style_type_invalid)
130130 end
131131 ) attrs
132132 end;
···137137 let has_type = has_attr "type" attrs in
138138 if not has_data && not has_type then
139139 Message_collector.add_typed collector
140140- (Error_code.Missing_required_attr { element = "object"; attr = "data" })
140140+ (`Attr (`Missing (`Elem "object", `Attr "data")))
141141 end;
142142143143 (* Validate link imagesizes/imagesrcset attributes *)
···149149150150 (* imagesizes requires imagesrcset *)
151151 if has_imagesizes && not has_imagesrcset then
152152- Message_collector.add_typed collector Error_code.Imagesizes_without_imagesrcset;
152152+ Message_collector.add_typed collector (`Srcset `Imagesizes_without_imagesrcset);
153153154154 (* imagesrcset requires as="image" *)
155155 if has_imagesrcset then begin
···158158 | None -> false
159159 in
160160 if not as_is_image then
161161- Message_collector.add_typed collector Error_code.Link_imagesrcset_requires_as_image
161161+ Message_collector.add_typed collector (`Link `Imagesrcset_requires_as_image)
162162 end;
163163164164 (* as attribute requires rel="preload" or rel="modulepreload" *)
···173173 | None -> false
174174 in
175175 if not rel_is_preload then
176176- Message_collector.add_typed collector Error_code.Link_as_requires_preload
176176+ Message_collector.add_typed collector (`Link `As_requires_preload)
177177 | None -> ())
178178 end;
179179···184184 if attr_lower = "usemap" then begin
185185 if attr_value = "#" then
186186 Message_collector.add_typed collector
187187- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
187187+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
188188 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9c%s\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad hash-name reference: A hash-name reference must have at least one character after \xe2\x80\x9c#\xe2\x80\x9d."
189189- attr_value attr_name name })
189189+ attr_value attr_name name))))
190190 end
191191 ) attrs
192192 end;
···200200 | Ok () -> ()
201201 | Error msg ->
202202 Message_collector.add_typed collector
203203- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
203203+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
204204 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9c%s\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad MIME type: %s"
205205- attr_value attr_name name msg })
205205+ attr_value attr_name name msg))))
206206 end
207207 ) attrs
208208 end;
···251251 attr_value attr_name name
252252 in
253253 Message_collector.add_typed collector
254254- (Error_code.Bad_attr_value_generic { message = error_msg })
254254+ (`Attr (`Bad_value_generic (`Message error_msg)))
255255 end
256256 end
257257 ) attrs
···264264 | Some s when String.lowercase_ascii (String.trim s) = "default" ->
265265 if has_attr "coords" attrs then
266266 Message_collector.add_typed collector
267267- (Error_code.Attr_not_allowed_on_element { attr = "coords"; element = "area" })
267267+ (`Attr (`Not_allowed (`Attr "coords", `Elem "area")))
268268 | _ -> ()
269269 end;
270270···273273 let dir_value = get_attr "dir" attrs in
274274 match dir_value with
275275 | None ->
276276- Message_collector.add_typed collector Error_code.Bdo_missing_dir
276276+ Message_collector.add_typed collector (`Misc `Bdo_missing_dir)
277277 | Some v when String.lowercase_ascii (String.trim v) = "auto" ->
278278- Message_collector.add_typed collector Error_code.Bdo_dir_auto
278278+ Message_collector.add_typed collector (`Misc `Bdo_dir_auto)
279279 | _ -> ()
280280 end;
281281···287287 | None -> "text" (* default type is text *)
288288 in
289289 if not (List.mem input_type input_types_allowing_list) then
290290- Message_collector.add_typed collector Error_code.Input_list_not_allowed
290290+ Message_collector.add_typed collector (`Input `List_not_allowed)
291291 end
292292 end;
293293···304304 (* Check if the name contains colon - not XML serializable *)
305305 else if String.contains after_prefix ':' then
306306 Message_collector.add_typed collector
307307- (Error_code.Data_attr_invalid_name { reason = "must be XML 1.0 4th ed. plus Namespaces NCNames" })
307307+ (`Attr (`Data_invalid_name (`Reason "must be XML 1.0 4th ed. plus Namespaces NCNames")))
308308 end
309309 ) attrs
310310 end;
···318318 (match lang_value with
319319 | None ->
320320 (* xml:lang without lang attribute *)
321321- Message_collector.add_typed collector Error_code.Xml_lang_without_lang
321321+ Message_collector.add_typed collector (`I18n `Xml_lang_without_lang)
322322 | Some lang when String.lowercase_ascii lang <> String.lowercase_ascii xmllang ->
323323 (* xml:lang and lang have different values - "lang present with same value" message *)
324324- Message_collector.add_typed collector Error_code.Xml_lang_without_lang
324324+ Message_collector.add_typed collector (`I18n `Xml_lang_without_lang)
325325 | _ -> ())
326326 | None -> ()
327327 end;
···334334 let value_lower = String.lowercase_ascii (String.trim attr_value) in
335335 if value_lower <> "" && value_lower <> "true" && value_lower <> "false" then
336336 Message_collector.add_typed collector
337337- (Error_code.Bad_attr_value { element = name; attr = attr_name; value = attr_value; reason = "" })
337337+ (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason "")))
338338 end
339339 ) attrs
340340 end;
···348348 let value_lower = String.lowercase_ascii (String.trim attr_value) in
349349 if not (List.mem value_lower valid_enterkeyhint) then
350350 Message_collector.add_typed collector
351351- (Error_code.Bad_attr_value { element = name; attr = attr_name; value = attr_value; reason = "" })
351351+ (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason "")))
352352 end
353353 ) attrs
354354 end;
···368368 with _ -> false)
369369 in
370370 if not is_valid then
371371- Message_collector.add_typed collector Error_code.Headingoffset_invalid
371371+ Message_collector.add_typed collector (`Misc `Headingoffset_invalid)
372372 end
373373 ) attrs
374374 end;
···401401 List.iter (fun key ->
402402 if count_codepoints key > 1 then
403403 Message_collector.add_typed collector
404404- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
404404+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
405405 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9c%s\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad key label list: Key label has multiple characters. Each key label must be a single character."
406406- attr_value attr_name name })
406406+ attr_value attr_name name))))
407407 ) keys;
408408 (* Check for duplicate keys *)
409409 let rec find_duplicates seen = function
···411411 | k :: rest ->
412412 if List.mem k seen then
413413 Message_collector.add_typed collector
414414- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
414414+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
415415 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9c%s\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad key label list: Duplicate key label. Each key label must be unique."
416416- attr_value attr_name name })
416416+ attr_value attr_name name))))
417417 else
418418 find_duplicates (k :: seen) rest
419419 in
···430430431431 if has_command && has_aria_expanded then
432432 Message_collector.add_typed collector
433433- (Error_code.Attr_not_allowed_when { attr = "aria-expanded"; element = name;
434434- condition = "a \xe2\x80\x9ccommand\xe2\x80\x9d attribute" });
433433+ (`Attr (`Not_allowed_when (`Attr "aria-expanded", `Elem name,
434434+ `Condition "a \xe2\x80\x9ccommand\xe2\x80\x9d attribute")));
435435436436 if has_popovertarget && has_aria_expanded then
437437 Message_collector.add_typed collector
438438- (Error_code.Attr_not_allowed_when { attr = "aria-expanded"; element = name;
439439- condition = "a \xe2\x80\x9cpopovertarget\xe2\x80\x9d attribute" })
438438+ (`Attr (`Not_allowed_when (`Attr "aria-expanded", `Elem name,
439439+ `Condition "a \xe2\x80\x9cpopovertarget\xe2\x80\x9d attribute")))
440440 end;
441441442442 (* Note: data-* uppercase check requires XML parsing which preserves case.
···456456 | Ok () -> ()
457457 | Error msg ->
458458 Message_collector.add_typed collector
459459- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
459459+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
460460 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9c%s\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad media query: %s"
461461- attr_value attr_name name msg })
461461+ attr_value attr_name name msg))))
462462 end
463463 end
464464 ) attrs
···475475 (* Check for empty prefix (starts with : or has space:) *)
476476 if String.length trimmed > 0 && trimmed.[0] = ':' then
477477 Message_collector.add_typed collector
478478- (Error_code.Bad_attr_value { element = name; attr = attr_name; value = attr_value; reason = "" })
478478+ (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason "")))
479479 else begin
480480 (* Check for invalid prefix names - must start with letter or underscore *)
481481 let is_ncname_start c =
···483483 in
484484 if String.length trimmed > 0 && not (is_ncname_start trimmed.[0]) then
485485 Message_collector.add_typed collector
486486- (Error_code.Bad_attr_value { element = name; attr = attr_name; value = attr_value; reason = "" })
486486+ (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason "")))
487487 end
488488 end
489489 end
+2-2
lib/html5_checker/specialized/base_checker.ml
···2424 state.seen_link_or_script <- true
2525 | "base" ->
2626 if state.seen_link_or_script then
2727- Message_collector.add_typed collector Error_code.Base_after_link_script;
2727+ Message_collector.add_typed collector (`Misc `Base_after_link_script);
2828 (* base element must have href or target attribute *)
2929 let has_href = has_attr "href" attrs in
3030 let has_target = has_attr "target" attrs in
3131 if not has_href && not has_target then
3232- Message_collector.add_typed collector Error_code.Base_missing_href_or_target
3232+ Message_collector.add_typed collector (`Misc `Base_missing_href_or_target)
3333 | _ -> ()
3434 end
3535
···8686 begin match current_div state with
8787 | Some _ ->
8888 Message_collector.add_typed collector
8989- (Error_code.Element_not_allowed_as_child { child = "dl"; parent = "div" })
8989+ (`Element (`Not_allowed_as_child (`Child "dl", `Parent "div")))
9090 | None ->
9191 match current_dl state with
9292 | Some _ when state.in_dt_dd = 0 ->
9393 Message_collector.add_typed collector
9494- (Error_code.Element_not_allowed_as_child { child = "dl"; parent = "dl" })
9494+ (`Element (`Not_allowed_as_child (`Child "dl", `Parent "dl")))
9595 | _ -> ()
9696 end;
9797 let ctx = {
···113113 (* Check for mixed content - if we already have dt/dd, div is not allowed *)
114114 if dl_ctx.contains_dt_dd then
115115 Message_collector.add_typed collector
116116- (Error_code.Element_not_allowed_as_child { child = "div"; parent = "dl" });
116116+ (`Element (`Not_allowed_as_child (`Child "div", `Parent "dl")));
117117 (* Check that role is only presentation or none *)
118118 (match get_attr "role" attrs with
119119 | Some role_value ->
120120 let role_lower = String.lowercase_ascii (String.trim role_value) in
121121 if role_lower <> "presentation" && role_lower <> "none" then
122122- Message_collector.add_typed collector Error_code.Div_child_of_dl_bad_role
122122+ Message_collector.add_typed collector (`Li_role `Div_in_dl_bad_role)
123123 | None -> ());
124124 let div_ctx = { has_dt = false; has_dd = false; group_count = 0; in_dd_part = false } in
125125 state.div_in_dl_stack <- div_ctx :: state.div_in_dl_stack
126126 | Some _ when state.div_in_dl_stack <> [] ->
127127 Message_collector.add_typed collector
128128- (Error_code.Element_not_allowed_as_child { child = "div"; parent = "div" })
128128+ (`Element (`Not_allowed_as_child (`Child "div", `Parent "div")))
129129 | _ -> ()
130130 end
131131···136136 (* If we've already seen dd, this dt starts a new group - which is not allowed *)
137137 if div_ctx.in_dd_part then begin
138138 Message_collector.add_typed collector
139139- (Error_code.Element_not_allowed_as_child { child = "dt"; parent = "div" });
139139+ (`Element (`Not_allowed_as_child (`Child "dt", `Parent "div")));
140140 div_ctx.group_count <- div_ctx.group_count + 1;
141141 div_ctx.in_dd_part <- false
142142 end;
···150150 (* Check for mixed content - if we already have div, dt is not allowed *)
151151 if dl_ctx.contains_div then
152152 Message_collector.add_typed collector
153153- (Error_code.Element_not_allowed_as_child { child = "dt"; parent = "dl" })
153153+ (`Element (`Not_allowed_as_child (`Child "dt", `Parent "dl")))
154154 | None ->
155155 (* dt outside dl context - error *)
156156 let parent = match current_parent state with
···158158 | None -> "document"
159159 in
160160 Message_collector.add_typed collector
161161- (Error_code.Element_not_allowed_as_child { child = "dt"; parent })
161161+ (`Element (`Not_allowed_as_child (`Child "dt", `Parent parent)))
162162 end
163163164164 | "dd" when state.in_template = 0 ->
···178178 if not dl_ctx.has_dt && not dl_ctx.dd_before_dt_error_reported then begin
179179 dl_ctx.dd_before_dt_error_reported <- true;
180180 Message_collector.add_typed collector
181181- (Error_code.Missing_required_child_generic { parent = "dl" })
181181+ (`Element (`Missing_child_generic (`Parent "dl")))
182182 end;
183183 dl_ctx.has_dd <- true;
184184 dl_ctx.last_was_dt <- false;
···186186 (* Check for mixed content *)
187187 if dl_ctx.contains_div then
188188 Message_collector.add_typed collector
189189- (Error_code.Element_not_allowed_as_child { child = "dd"; parent = "dl" })
189189+ (`Element (`Not_allowed_as_child (`Child "dd", `Parent "dl")))
190190 | None ->
191191 (* dd outside dl context - error *)
192192 let parent = match current_parent state with
···194194 | None -> "document"
195195 in
196196 Message_collector.add_typed collector
197197- (Error_code.Element_not_allowed_as_child { child = "dd"; parent })
197197+ (`Element (`Not_allowed_as_child (`Child "dd", `Parent parent)))
198198 end
199199200200 | _ -> ()
···226226 (* Direct dt/dd content - must have both *)
227227 if not ctx.has_dt && not ctx.dd_before_dt_error_reported then
228228 Message_collector.add_typed collector
229229- (Error_code.Missing_required_child_generic { parent = "dl" })
229229+ (`Element (`Missing_child_generic (`Parent "dl")))
230230 else if not ctx.has_dd then begin
231231 if ctx.has_template then
232232 Message_collector.add_typed collector
233233- (Error_code.Missing_required_child_one_of { parent = "dl"; children = ["dd"] })
233233+ (`Element (`Missing_child_one_of (`Parent "dl", `Children ["dd"])))
234234 else
235235 Message_collector.add_typed collector
236236- (Error_code.Missing_required_child { parent = "dl"; child = "dd" })
236236+ (`Element (`Missing_child (`Parent "dl", `Child "dd")))
237237 end
238238 else if ctx.last_was_dt then
239239 Message_collector.add_typed collector
240240- (Error_code.Missing_required_child { parent = "dl"; child = "dd" })
240240+ (`Element (`Missing_child (`Parent "dl", `Child "dd")))
241241 end else if not ctx.contains_div && not ctx.has_dt && not ctx.has_dd then
242242 ()
243243 | [] -> ()
···250250 (* Check div in dl must have both dt and dd *)
251251 if not div_ctx.has_dt && not div_ctx.has_dd then
252252 Message_collector.add_typed collector
253253- (Error_code.Missing_required_child { parent = "div"; child = "dd" })
253253+ (`Element (`Missing_child (`Parent "div", `Child "dd")))
254254 else if not div_ctx.has_dt then
255255 Message_collector.add_typed collector
256256- (Error_code.Missing_required_child { parent = "div"; child = "dt" })
256256+ (`Element (`Missing_child (`Parent "div", `Child "dt")))
257257 else if not div_ctx.has_dd then
258258 Message_collector.add_typed collector
259259- (Error_code.Missing_required_child { parent = "div"; child = "dd" })
259259+ (`Element (`Missing_child (`Parent "div", `Child "dd")))
260260 | [] -> ()
261261 end
262262···273273 match current_div state with
274274 | Some _ ->
275275 Message_collector.add_typed collector
276276- (Error_code.Text_not_allowed { parent = "div" })
276276+ (`Element (`Text_not_allowed (`Parent "div")))
277277 | None ->
278278 match current_dl state with
279279 | Some _ ->
280280 Message_collector.add_typed collector
281281- (Error_code.Text_not_allowed { parent = "dl" })
281281+ (`Element (`Text_not_allowed (`Parent "dl")))
282282 | None -> ()
283283 end
284284 end
+1-1
lib/html5_checker/specialized/h1_checker.ml
···2525 else if name_lower = "h1" then begin
2626 state.h1_count <- state.h1_count + 1;
2727 if state.h1_count > 1 then
2828- Message_collector.add_typed collector Error_code.Multiple_h1
2828+ Message_collector.add_typed collector (`Misc `Multiple_h1)
2929 end
30303131let end_element state ~name ~namespace:_ _collector =
+8-8
lib/html5_checker/specialized/heading_checker.ml
···6767 state.first_heading_checked <- true;
6868 if level <> 1 then
6969 Message_collector.add_typed collector
7070- (Error_code.Generic { message = Printf.sprintf
7171- "First heading in document is <%s>, should typically be <h1>" name })
7070+ (`Generic (Printf.sprintf
7171+ "First heading in document is <%s>, should typically be <h1>" name))
7272 end;
73737474 (* Track h1 count *)
7575 if level = 1 then begin
7676 state.h1_count <- state.h1_count + 1;
7777 if state.h1_count > 1 then
7878- Message_collector.add_typed collector Error_code.Multiple_h1
7878+ Message_collector.add_typed collector (`Misc `Multiple_h1)
7979 end;
80808181 (* Check for skipped levels *)
···8686 let diff = level - prev_level in
8787 if diff > 1 then
8888 Message_collector.add_typed collector
8989- (Error_code.Generic { message = Printf.sprintf
8989+ (`Generic (Printf.sprintf
9090 "Heading level skipped: <%s> follows <h%d>, skipping %d level%s. This can confuse screen reader users"
9191- name prev_level (diff - 1) (if diff > 2 then "s" else "") });
9191+ name prev_level (diff - 1) (if diff > 2 then "s" else "")));
9292 state.current_level <- Some level
9393 end;
9494···105105 | Some heading when heading = name ->
106106 if not state.heading_has_text then
107107 Message_collector.add_typed collector
108108- (Error_code.Generic { message = Printf.sprintf
109109- "Heading <%s> is empty or contains only whitespace. Empty headings are problematic for screen readers" name });
108108+ (`Generic (Printf.sprintf
109109+ "Heading <%s> is empty or contains only whitespace. Empty headings are problematic for screen readers" name));
110110 state.in_heading <- None;
111111 state.heading_has_text <- false
112112 | _ -> ()
···123123let end_document state collector =
124124 if not state.has_any_heading then
125125 Message_collector.add_typed collector
126126- (Error_code.Generic { message = "Document contains no heading elements (h1-h6). Headings provide important document structure for accessibility" })
126126+ (`Generic "Document contains no heading elements (h1-h6). Headings provide important document structure for accessibility")
127127128128let checker = (module struct
129129 type nonrec state = state
···406406 (* Empty sizes is invalid *)
407407 if String.trim value = "" then begin
408408 Message_collector.add_typed collector
409409- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Must not be empty." element_name });
409409+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Must not be empty." element_name))));
410410 false
411411 end else begin
412412 (* Split on comma and check each entry *)
···416416 (* Check if starts with comma (empty first entry) *)
417417 if first_entry = "" then begin
418418 Message_collector.add_typed collector
419419- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Starts with empty source size." value element_name });
419419+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Starts with empty source size." value element_name))));
420420 false
421421 end else begin
422422 (* Check for trailing comma *)
···429429 else value
430430 in
431431 Message_collector.add_typed collector
432432- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context });
432432+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context))));
433433 false
434434 end else begin
435435 let valid = ref true in
···448448 (* Context is the first entry with a comma *)
449449 let context = (String.trim first) ^ "," in
450450 Message_collector.add_typed collector
451451- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context });
451451+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context))));
452452 valid := false
453453 end;
454454 (* Check for multiple entries without media conditions.
···460460 (* Multiple defaults - report as "Expected media condition" *)
461461 let context = (String.trim first) ^ "," in
462462 Message_collector.add_typed collector
463463- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context });
463463+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context))));
464464 valid := false
465465 end
466466 end
···482482 else context
483483 in
484484 Message_collector.add_typed collector
485485- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: %s at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name err_msg context });
485485+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: %s at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name err_msg context))));
486486 valid := false
487487 | None -> ());
488488···521521 else value
522522 in
523523 Message_collector.add_typed collector
524524- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context });
524524+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context))));
525525 valid := false
526526 end
527527 (* If there's extra junk after the size, report BadCssNumber error for it *)
···549549 in
550550 let _ = junk in
551551 Message_collector.add_typed collector
552552- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a minus sign or a digit but saw \xe2\x80\x9c%c\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name first_char context });
552552+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a minus sign or a digit but saw \xe2\x80\x9c%c\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name first_char context))));
553553 valid := false
554554 end
555555 else
···562562 in
563563 let _ = full_context in
564564 Message_collector.add_typed collector
565565- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected positive size value but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name size_val size_val });
565565+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected positive size value but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name size_val size_val))));
566566 valid := false
567567 | CssCommentAfterSign (found, context) ->
568568 (* e.g., +/**/50vw - expected number after sign *)
569569 Message_collector.add_typed collector
570570- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected number but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name found context });
570570+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected number but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name found context))));
571571 valid := false
572572 | CssCommentBeforeUnit (found, context) ->
573573 (* e.g., 50/**/vw - expected units after number *)
574574 let units_list = List.map (fun u -> Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" u) valid_length_units in
575575 let units_str = String.concat ", " units_list in
576576 Message_collector.add_typed collector
577577- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected units (one of %s) but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name units_str found context });
577577+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected units (one of %s) but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name units_str found context))));
578578 valid := false
579579 | BadScientificNotation ->
580580 (* For scientific notation with bad exponent, show what char was expected vs found *)
···585585 (* Find the period in the exponent *)
586586 let _ = context in
587587 Message_collector.add_typed collector
588588- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a digit but saw \xe2\x80\x9c.\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name size_val });
588588+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a digit but saw \xe2\x80\x9c.\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name size_val))));
589589 valid := false
590590 | BadCssNumber (first_char, context) ->
591591 (* Value doesn't start with a digit or minus sign *)
···595595 in
596596 let _ = full_context in
597597 Message_collector.add_typed collector
598598- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a minus sign or a digit but saw \xe2\x80\x9c%c\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name first_char context });
598598+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a minus sign or a digit but saw \xe2\x80\x9c%c\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name first_char context))));
599599 valid := false
600600 | InvalidUnit (found_unit, _context) ->
601601 (* Generate the full list of expected units *)
···612612 else Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" found_unit
613613 in
614614 Message_collector.add_typed collector
615615- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected units (one of %s) but found %s at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name units_str found_str full_context });
615615+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected units (one of %s) but found %s at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name units_str found_str full_context))));
616616 valid := false
617617 end
618618 end
···639639 (* Show just the number part (without the 'w') *)
640640 let num_part_for_msg = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
641641 Message_collector.add_typed collector
642642- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number without leading plus sign but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part_for_msg srcset_value });
642642+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number without leading plus sign but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part_for_msg srcset_value))));
643643 false
644644 end else
645645 (try
646646 let n = int_of_string num_part in
647647 if n <= 0 then begin
648648 Message_collector.add_typed collector
649649- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value });
649649+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value))));
650650 false
651651 end else begin
652652 (* Check for uppercase W - compare original desc with lowercase version *)
653653 let original_last = desc.[String.length desc - 1] in
654654 if original_last = 'W' then begin
655655 Message_collector.add_typed collector
656656- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" srcset_value element_name desc srcset_value });
656656+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" srcset_value element_name desc srcset_value))));
657657 false
658658 end else true
659659 end
···661661 (* Check for scientific notation, decimal, or other non-integer values *)
662662 if String.contains num_part 'e' || String.contains num_part 'E' || String.contains num_part '.' then begin
663663 Message_collector.add_typed collector
664664- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected integer but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value });
664664+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected integer but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value))));
665665 false
666666 end else begin
667667 Message_collector.add_typed collector
668668- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Invalid width descriptor." srcset_value element_name });
668668+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Invalid width descriptor." srcset_value element_name))));
669669 false
670670 end)
671671 | 'x' ->
···675675 (* Extract the number part including the plus sign *)
676676 let num_with_plus = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
677677 Message_collector.add_typed collector
678678- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number without leading plus sign but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_with_plus srcset_value });
678678+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number without leading plus sign but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_with_plus srcset_value))));
679679 false
680680 end else begin
681681 (try
···686686 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
687687 let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in
688688 Message_collector.add_typed collector
689689- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Expected a digit but saw \xe2\x80\x9c%s\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name first_char srcset_value });
689689+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Expected a digit but saw \xe2\x80\x9c%s\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name first_char srcset_value))));
690690 false
691691 end else if n = 0.0 then begin
692692 (* Check if it's -0 (starts with minus) - report as "greater than zero" error *)
···694694 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
695695 if String.length orig_num_part > 0 && orig_num_part.[0] = '-' then begin
696696 Message_collector.add_typed collector
697697- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name orig_num_part srcset_value })
697697+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name orig_num_part srcset_value))))
698698 end else begin
699699 Message_collector.add_typed collector
700700- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Zero is not a valid positive floating point number at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name srcset_value })
700700+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Zero is not a valid positive floating point number at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name srcset_value))))
701701 end;
702702 false
703703 end else if n < 0.0 then begin
704704 Message_collector.add_typed collector
705705- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value });
705705+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value))));
706706 false
707707 end else if n = neg_infinity || n = infinity then begin
708708 (* Infinity is not a valid float - report as parse error with first char from ORIGINAL desc *)
···710710 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
711711 let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in
712712 Message_collector.add_typed collector
713713- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Expected a digit but saw \xe2\x80\x9c%s\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name first_char srcset_value });
713713+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Expected a digit but saw \xe2\x80\x9c%s\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name first_char srcset_value))));
714714 false
715715 end else true
716716 with _ ->
717717 Message_collector.add_typed collector
718718- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Invalid density descriptor." srcset_value element_name });
718718+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Invalid density descriptor." srcset_value element_name))));
719719 false)
720720 end
721721 | 'h' ->
···735735 in
736736 if has_sizes then
737737 Message_collector.add_typed collector
738738- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" srcset_value element_name trimmed_desc context })
738738+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" srcset_value element_name trimmed_desc context))))
739739 else
740740 Message_collector.add_typed collector
741741- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Height descriptor \xe2\x80\x9ch\xe2\x80\x9d is not allowed." srcset_value element_name });
741741+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Height descriptor \xe2\x80\x9ch\xe2\x80\x9d is not allowed." srcset_value element_name))));
742742 false
743743 | _ ->
744744 (* Unknown descriptor - find context in srcset_value *)
···770770 with Not_found -> srcset_value
771771 in
772772 Message_collector.add_typed collector
773773- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number followed by \xe2\x80\x9cw\xe2\x80\x9d or \xe2\x80\x9cx\xe2\x80\x9d but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name found_desc context });
773773+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number followed by \xe2\x80\x9cw\xe2\x80\x9d or \xe2\x80\x9cx\xe2\x80\x9d but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name found_desc context))));
774774 false
775775 end
776776···806806 (* Check for empty srcset *)
807807 if String.trim value = "" then begin
808808 Message_collector.add_typed collector
809809- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Must contain one or more image candidate strings." value element_name })
809809+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Must contain one or more image candidate strings." value element_name))))
810810 end;
811811812812 (* Check for leading comma *)
813813 if String.length value > 0 && value.[0] = ',' then begin
814814 Message_collector.add_typed collector
815815- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Starts with empty image-candidate string." value element_name })
815815+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Starts with empty image-candidate string." value element_name))))
816816 end;
817817818818 (* Check for trailing comma(s) / empty entries *)
···829829 if trailing_commas > 1 then
830830 (* Multiple trailing commas: "Empty image-candidate string at" *)
831831 Message_collector.add_typed collector
832832- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Empty image-candidate string at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name value })
832832+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Empty image-candidate string at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name value))))
833833 else
834834 (* Single trailing comma: "Ends with empty image-candidate string." *)
835835 Message_collector.add_typed collector
836836- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Ends with empty image-candidate string." value element_name })
836836+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Ends with empty image-candidate string." value element_name))))
837837 end;
838838839839 List.iter (fun entry ->
···851851 let scheme_colon = scheme ^ ":" in
852852 if url_lower = scheme_colon then
853853 Message_collector.add_typed collector
854854- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad image-candidate URL: \xe2\x80\x9c%s\xe2\x80\x9d: Expected a slash (\"/\")." value element_name url })
854854+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad image-candidate URL: \xe2\x80\x9c%s\xe2\x80\x9d: Expected a slash (\"/\")." value element_name url))))
855855 ) special_schemes
856856 in
857857 match parts with
···863863 begin match Hashtbl.find_opt seen_descriptors "explicit-1x" with
864864 | Some first_url ->
865865 Message_collector.add_typed collector
866866- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Density for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to density for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name url first_url })
866866+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Density for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to density for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name url first_url))))
867867 | None ->
868868 Hashtbl.add seen_descriptors "implicit-1x" url
869869 end
···874874 if rest <> [] then begin
875875 let extra_desc = List.hd rest in
876876 Message_collector.add_typed collector
877877- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected single descriptor but found extraneous descriptor \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name extra_desc value })
877877+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected single descriptor but found extraneous descriptor \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name extra_desc value))))
878878 end;
879879880880 let desc_lower = String.lowercase_ascii (String.trim desc) in
···913913 value
914914 in
915915 Message_collector.add_typed collector
916916- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" value element_name trimmed_desc entry_context })
916916+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" value element_name trimmed_desc entry_context))))
917917 end
918918 end;
919919···925925 begin match Hashtbl.find_opt seen_descriptors normalized with
926926 | Some first_url ->
927927 Message_collector.add_typed collector
928928- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: %s for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to %s for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name dup_type url (String.lowercase_ascii dup_type) first_url })
928928+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: %s for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to %s for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name dup_type url (String.lowercase_ascii dup_type) first_url))))
929929 | None ->
930930 begin match (if is_1x then Hashtbl.find_opt seen_descriptors "implicit-1x" else None) with
931931 | Some first_url ->
932932 (* Explicit 1x conflicts with implicit 1x *)
933933 Message_collector.add_typed collector
934934- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: %s for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to %s for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name dup_type url (String.lowercase_ascii dup_type) first_url })
934934+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: %s for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to %s for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name dup_type url (String.lowercase_ascii dup_type) first_url))))
935935 | None ->
936936 Hashtbl.add seen_descriptors normalized url;
937937 if is_1x then Hashtbl.add seen_descriptors "explicit-1x" url
···946946 (* Check: if w descriptor used and no sizes, that's an error for img and source *)
947947 if !has_w_descriptor && not has_sizes then
948948 Message_collector.add_typed collector
949949- (Error_code.Srcset_w_without_sizes);
949949+ (`Srcset `W_without_sizes);
950950951951 (* Check: if sizes is present, all entries must have width descriptors *)
952952 (match !no_descriptor_url with
953953 | Some url when has_sizes ->
954954 Message_collector.add_typed collector
955955- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: No width specified for image \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" value element_name url })
955955+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: No width specified for image \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" value element_name url))))
956956 | _ -> ());
957957958958 (* Check: if sizes is present and srcset uses x descriptors, that's an error.
959959 Only report if we haven't already reported the detailed error. *)
960960 if has_sizes && !has_x_descriptor && not !x_with_sizes_error_reported then
961961 Message_collector.add_typed collector
962962- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width." value element_name });
962962+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width." value element_name))));
963963964964 (* Check for mixing w and x descriptors *)
965965 if !has_w_descriptor && !has_x_descriptor then
966966 Message_collector.add_typed collector
967967- (Error_code.Bad_attr_value_generic { message = Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Mixing width and density descriptors is not allowed." value element_name })
967967+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Mixing width and density descriptors is not allowed." value element_name))))
968968969969let start_element _state ~name ~namespace ~attrs collector =
970970 let name_lower = String.lowercase_ascii name in
···973973 if namespace <> None && name_lower = "image" then begin
974974 if get_attr "srcset" attrs <> None then
975975 Message_collector.add_typed collector
976976- (Error_code.Attr_not_allowed_on_element { attr = "srcset"; element = "image" })
976976+ (`Attr (`Not_allowed (`Attr "srcset", `Elem "image")))
977977 end;
978978979979 if namespace <> None then ()
···998998 (* Error: sizes without srcset on img *)
999999 if name_lower = "img" && has_sizes && not has_srcset then
10001000 Message_collector.add_typed collector
10011001- (Error_code.Sizes_without_srcset)
10011001+ (`Srcset `Sizes_without_srcset)
10021002 end
10031003 end
10041004
+17-17
lib/html5_checker/specialized/svg_checker.ml
···290290 (* xmlns on any SVG element must be the SVG namespace *)
291291 if value <> svg_ns_url then
292292 Message_collector.add_typed collector
293293- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
293293+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
294294 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for the attribute \xe2\x80\x9cxmlns\xe2\x80\x9d (only \xe2\x80\x9c%s\xe2\x80\x9d permitted here)."
295295- value svg_ns_url })
295295+ value svg_ns_url))))
296296 | "xmlns:xlink" ->
297297 if value <> "http://www.w3.org/1999/xlink" then
298298 Message_collector.add_typed collector
299299- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
299299+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
300300 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for the attribute \xe2\x80\x9cxmlns:link\xe2\x80\x9d (only \xe2\x80\x9chttp://www.w3.org/1999/xlink\xe2\x80\x9d permitted here)."
301301- value })
301301+ value))))
302302 | _ when String.starts_with ~prefix:"xmlns:" attr && attr <> "xmlns:xlink" ->
303303 (* Other xmlns declarations are not allowed in HTML-embedded SVG *)
304304 Message_collector.add_typed collector
305305- (Error_code.Attr_not_allowed_here { attr })
305305+ (`Attr (`Not_allowed_here (`Attr attr)))
306306 | _ -> ()
307307308308(* Validate SVG path data *)
···322322 let ctx_end = min (String.length d) (!i + 1) in
323323 let context = String.sub d !context_start (ctx_end - !context_start) in
324324 Message_collector.add_typed collector
325325- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
325325+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
326326 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9cd\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad SVG path data: Expected command but found \xe2\x80\x9c#\xe2\x80\x9d (context: \xe2\x80\x9c%s\xe2\x80\x9d)."
327327- d element context });
327327+ d element context))));
328328 i := len (* Stop processing *)
329329 | _ ->
330330 incr i
···342342 let ctx_start = max 0 (pos - 10) in
343343 let context = String.sub d ctx_start (flag_end - ctx_start) in
344344 Message_collector.add_typed collector
345345- (Error_code.Bad_attr_value_generic { message = Printf.sprintf
345345+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf
346346 "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9cd\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad SVG path data: Expected \xe2\x80\x9c0\xe2\x80\x9d or \xe2\x80\x9c1\xe2\x80\x9d for large-arc-flag for \xe2\x80\x9ca\xe2\x80\x9d command but found \xe2\x80\x9c%s\xe2\x80\x9d instead (context: \xe2\x80\x9c%s\xe2\x80\x9d)."
347347- d element flag context })
347347+ d element flag context))))
348348 end
349349 with Not_found -> ()
350350···364364 | parent :: _ when String.lowercase_ascii parent = "a" ->
365365 if List.mem name_lower a_disallowed_children then
366366 Message_collector.add_typed collector
367367- (Error_code.Element_not_allowed_as_child { child = name_lower; parent = "a" })
367367+ (`Element (`Not_allowed_as_child (`Child name_lower, `Parent "a")))
368368 | _ -> ());
369369370370 (* 2. Track missing-glyph in font *)
···381381 p = "lineargradient" || p = "radialgradient") -> ()
382382 | parent :: _ ->
383383 Message_collector.add_typed collector
384384- (Error_code.Element_not_allowed_as_child { child = name; parent })
384384+ (`Element (`Not_allowed_as_child (`Child name, `Parent parent)))
385385 | [] -> ()
386386 end;
387387···390390 match state.element_stack with
391391 | parent :: _ when String.lowercase_ascii parent = "use" ->
392392 Message_collector.add_typed collector
393393- (Error_code.Element_not_allowed_as_child { child = name; parent })
393393+ (`Element (`Not_allowed_as_child (`Child name, `Parent parent)))
394394 | _ -> ()
395395 end;
396396···402402 | fect :: _ ->
403403 if List.mem name_lower fect.seen_funcs then
404404 Message_collector.add_typed collector
405405- (Error_code.Element_not_allowed_as_child { child = name; parent = "feComponentTransfer" })
405405+ (`Element (`Not_allowed_as_child (`Child name, `Parent "feComponentTransfer")))
406406 else
407407 fect.seen_funcs <- name_lower :: fect.seen_funcs
408408 | [] -> ()
···427427 (* Check xml:* attributes - most are not allowed *)
428428 else if attr_lower = "xml:id" || attr_lower = "xml:base" then
429429 Message_collector.add_typed collector
430430- (Error_code.Attr_not_allowed_on_element { attr; element = name })
430430+ (`Attr (`Not_allowed (`Attr attr, `Elem name)))
431431 (* Validate path data *)
432432 else if attr_lower = "d" && name_lower = "path" then
433433 validate_path_data value name collector
434434 (* Check if attribute is valid for this element *)
435435 else if not (is_valid_attr name_lower attr_lower) then
436436 Message_collector.add_typed collector
437437- (Error_code.Attr_not_allowed_on_element { attr; element = name })
437437+ (`Attr (`Not_allowed (`Attr attr, `Elem name)))
438438 ) attrs;
439439440440 (* Check required attributes *)
···443443 List.iter (fun req_attr ->
444444 if not (List.exists (fun (a, _) -> String.lowercase_ascii a = req_attr) attrs) then
445445 Message_collector.add_typed collector
446446- (Error_code.Missing_required_svg_attr { element = name_lower; attr = req_attr })
446446+ (`Svg (`Missing_attr (`Elem name_lower, `Attr req_attr)))
447447 ) req_attrs
448448 | None -> ())
449449 end
···464464 | Some children ->
465465 List.iter (fun child ->
466466 Message_collector.add_typed collector
467467- (Error_code.Missing_required_child { parent = "font"; child })
467467+ (`Element (`Missing_child (`Parent "font", `Child child)))
468468 ) children
469469 | None -> ()
470470 end;
+24-24
lib/html5_checker/specialized/table_checker.ml
···3636 let colspan =
3737 if colspan > max_colspan then (
3838 Message_collector.add_typed collector
3939- (Error_code.Generic { message = Printf.sprintf
3939+ (`Generic (Printf.sprintf
4040 {|The value of the "colspan" attribute must be less than or equal to %d.|}
4141- max_colspan });
4141+ max_colspan));
4242 max_colspan)
4343 else colspan
4444 in
4545 let rowspan =
4646 if rowspan > max_rowspan then (
4747 Message_collector.add_typed collector
4848- (Error_code.Generic { message = Printf.sprintf
4848+ (`Generic (Printf.sprintf
4949 {|The value of the "rowspan" attribute must be less than or equal to %d.|}
5050- max_rowspan });
5050+ max_rowspan));
5151 max_rowspan)
5252 else rowspan
5353 in
···7575(** Emit error for horizontal cell overlap *)
7676let err_on_horizontal_overlap cell1 cell2 collector =
7777 if cells_overlap_horizontally cell1 cell2 then (
7878- Message_collector.add_typed collector Error_code.Table_cell_overlap;
7979- Message_collector.add_typed collector Error_code.Table_cell_overlap)
7878+ Message_collector.add_typed collector (`Table `Cell_overlap);
7979+ Message_collector.add_typed collector (`Table `Cell_overlap))
80808181(** Check if cell spans past end of row group *)
8282let err_if_not_rowspan_zero cell ~row_group_type:_ collector =
8383 if cell.bottom <> rowspan_zero_magic then
8484- Message_collector.add_typed collector Error_code.Table_cell_spans_rowgroup
8484+ Message_collector.add_typed collector (`Table `Cell_spans_rowgroup)
85858686(** {1 Column Range Tracking} *)
8787···206206let end_row_in_group group collector =
207207 (if not group.row_had_cells then
208208 Message_collector.add_typed collector
209209- (Error_code.Table_row_no_cells { row = group.current_row + 1 }));
209209+ (`Table (`Row_no_cells (`Row (group.current_row + 1)))));
210210211211 find_insertion_point group;
212212 group.cells_on_current_row <- [||];
···385385 let span = parse_non_negative_int attrs "span" in
386386 if span > max_colspan then (
387387 Message_collector.add_typed collector
388388- (Error_code.Generic { message = Printf.sprintf
389389- {|The value of the "span" attribute must be less than or equal to %d.|} max_colspan });
388388+ (`Generic (Printf.sprintf
389389+ {|The value of the "span" attribute must be less than or equal to %d.|} max_colspan));
390390 max_colspan)
391391 else span
392392···471471 if table.hard_width then (
472472 if row_width > table.column_count then
473473 Message_collector.add_typed collector
474474- (Error_code.Generic { message = Printf.sprintf
474474+ (`Generic (Printf.sprintf
475475 {|A table row was %d columns wide and exceeded the column count established using column markup (%d).|}
476476- row_width table.column_count })
476476+ row_width table.column_count))
477477 else if row_width < table.column_count then
478478 Message_collector.add_typed collector
479479- (Error_code.Generic { message = Printf.sprintf
479479+ (`Generic (Printf.sprintf
480480 {|A table row was %d columns wide, which is less than the column count established using column markup (%d).|}
481481- row_width table.column_count }))
481481+ row_width table.column_count)))
482482 else if table.column_count = -1 then
483483 table.column_count <- row_width
484484 else (
485485 if row_width > table.column_count then
486486 Message_collector.add_typed collector
487487- (Error_code.Generic { message = Printf.sprintf
487487+ (`Generic (Printf.sprintf
488488 {|A table row was %d columns wide and exceeded the column count established by the first row (%d).|}
489489- row_width table.column_count })
489489+ row_width table.column_count))
490490 else if row_width < table.column_count then
491491 Message_collector.add_typed collector
492492- (Error_code.Generic { message = Printf.sprintf
492492+ (`Generic (Printf.sprintf
493493 {|A table row was %d columns wide, which is less than the column count established by the first row (%d).|}
494494- row_width table.column_count }))
494494+ row_width table.column_count)))
495495496496(** End a row *)
497497let end_row table collector =
···621621 | InColgroup ->
622622 if table.pending_colgroup_span > 0 then
623623 Message_collector.add_typed collector
624624- (Error_code.Generic { message = Printf.sprintf
624624+ (`Generic (Printf.sprintf
625625 "A col element causes a span attribute with value %d to be ignored on the \
626626 parent colgroup."
627627- table.pending_colgroup_span });
627627+ table.pending_colgroup_span));
628628 table.pending_colgroup_span <- 0;
629629 table.state <- InColInColgroup;
630630 let span = abs (parse_span attrs collector) in
···663663 (fun heading ->
664664 if not (Hashtbl.mem table.header_ids heading) then
665665 Message_collector.add_typed collector
666666- (Error_code.Generic { message = Printf.sprintf
666666+ (`Generic (Printf.sprintf
667667 {|The "headers" attribute on the element "%s" refers to the ID "%s", but there is no "th" element with that ID in the same table.|}
668668- cell.element_name heading }))
668668+ cell.element_name heading)))
669669 cell.headers)
670670 !(table.cells_with_headers);
671671···675675 | None -> ()
676676 | Some r ->
677677 Message_collector.add_typed collector
678678- (Error_code.Table_column_no_cells { column = r.right; element = r.element });
678678+ (`Table (`Column_no_cells (`Column r.right, `Elem r.element)));
679679 check_ranges r.next
680680 in
681681 check_ranges table.first_col_range
···739739let end_document state collector =
740740 if !(state.tables) <> [] then
741741 Message_collector.add_typed collector
742742- (Error_code.Generic { message = "Unclosed table element at end of document." })
742742+ (`Generic "Unclosed table element at end of document.")
743743744744let checker =
745745 (module struct
+2-2
lib/html5_checker/specialized/title_checker.ml
···6262 (* Check if title was empty *)
6363 if not state.title_has_content then
6464 Message_collector.add_typed collector
6565- (Error_code.Element_must_not_be_empty { element = "title" });
6565+ (`Element (`Must_not_be_empty (`Elem "title")));
6666 state.in_title <- false
6767 | "head" ->
6868 (* Check if head had a title element *)
6969 if state.in_head && not state.has_title then
7070 Message_collector.add_typed collector
7171- (Error_code.Missing_required_child { parent = "head"; child = "title" });
7171+ (`Element (`Missing_child (`Parent "head", `Child "title")));
7272 state.in_head <- false
7373 | _ -> ()
7474 end
···5050 String.sub attr_name 0 5 = "data-" then
5151 let suffix = String.sub attr_name 5 (String.length attr_name - 5) in
5252 if String.exists (fun c -> c >= 'A' && c <= 'Z') suffix then
5353- Message_collector.add_typed collector Error_code.Data_attr_uppercase
5353+ Message_collector.add_typed collector (`Attr `Data_uppercase)
5454 ) attrs
55555656let start_element state ~name ~namespace ~attrs collector =
···6666 let parent_lower = String.lowercase_ascii parent in
6767 if not (is_child_allowed ~parent:parent_lower ~child:name_lower) then
6868 Message_collector.add_typed collector
6969- (Error_code.Element_not_allowed_as_child { child = name_lower; parent = parent_lower })
6969+ (`Element (`Not_allowed_as_child (`Child name_lower, `Parent parent_lower)))
7070 | [] -> ());
71717272 (* Handle figure content model *)
···8484 (* Flow content appearing in figure *)
8585 if fig.has_figcaption && not fig.figcaption_at_start then
8686 Message_collector.add_typed collector
8787- (Error_code.Element_not_allowed_as_child { child = name_lower; parent = "figure" })
8787+ (`Element (`Not_allowed_as_child (`Child name_lower, `Parent "figure")))
8888 else if not fig.has_figcaption then
8989 fig.has_content_before_figcaption <- true
9090 end
···123123 | fig :: _ ->
124124 if fig.has_figcaption && not fig.figcaption_at_start then
125125 Message_collector.add_typed collector
126126- (Error_code.Text_not_allowed { parent = "figure" })
126126+ (`Element (`Text_not_allowed (`Parent "figure")))
127127 else if not fig.has_figcaption then
128128 fig.has_content_before_figcaption <- true
129129 | [] -> ()
130130 end
131131 else if not (is_text_allowed parent_lower) then
132132 Message_collector.add_typed collector
133133- (Error_code.Text_not_allowed { parent = parent_lower })
133133+ (`Element (`Text_not_allowed (`Parent parent_lower)))
134134 end
135135136136let end_document _state _collector = ()
+20-42
test/expected_message.ml
···8080 if Str.string_match re msg 0 then
8181 let child = Str.matched_group 1 msg in
8282 let parent = Str.matched_group 2 msg in
8383- Some (Html5_checker.Error_code.Element_not_allowed_as_child { child; parent },
8383+ Some ((`Element (`Not_allowed_as_child (`Child child, `Parent parent)) : Html5_checker.Error_code.t),
8484 Some child, None)
8585 else None
8686···9090 if Str.string_match re msg 0 then
9191 let attr = Str.matched_group 1 msg in
9292 let element = Str.matched_group 2 msg in
9393- Some (Html5_checker.Error_code.Attr_not_allowed_on_element { attr; element },
9393+ Some ((`Attr (`Not_allowed (`Attr attr, `Elem element)) : Html5_checker.Error_code.t),
9494 Some element, Some attr)
9595 else None
9696···9999 let re = Str.regexp {|Attribute "\([^"]+\)" not allowed here|} in
100100 if Str.string_match re msg 0 then
101101 let attr = Str.matched_group 1 msg in
102102- Some (Html5_checker.Error_code.Attr_not_allowed_here { attr },
102102+ Some ((`Attr (`Not_allowed_here (`Attr attr)) : Html5_checker.Error_code.t),
103103 None, Some attr)
104104 else None
105105···109109 if Str.string_match re msg 0 then
110110 let element = Str.matched_group 1 msg in
111111 let attr = Str.matched_group 2 msg in
112112- Some (Html5_checker.Error_code.Missing_required_attr { element; attr },
112112+ Some ((`Attr (`Missing (`Elem element, `Attr attr)) : Html5_checker.Error_code.t),
113113 Some element, Some attr)
114114 else None
115115···119119 if Str.string_match re msg 0 then
120120 let parent = Str.matched_group 1 msg in
121121 let child = Str.matched_group 2 msg in
122122- Some (Html5_checker.Error_code.Missing_required_child { parent; child },
122122+ Some ((`Element (`Missing_child (`Parent parent, `Child child)) : Html5_checker.Error_code.t),
123123 Some parent, None)
124124 else None
125125···128128 let re = Str.regexp {|Duplicate ID "\([^"]+\)"|} in
129129 if Str.string_match re msg 0 then
130130 let id = Str.matched_group 1 msg in
131131- Some (Html5_checker.Error_code.Duplicate_id { id },
131131+ Some ((`Attr (`Duplicate_id (`Id id)) : Html5_checker.Error_code.t),
132132 None, None)
133133 else None
134134···137137 let re = Str.regexp {|The "\([^"]+\)" element is obsolete|} in
138138 if Str.string_match re msg 0 then
139139 let element = Str.matched_group 1 msg in
140140- Some (Html5_checker.Error_code.Obsolete_element { element; suggestion = "" },
140140+ Some ((`Element (`Obsolete (`Elem element, `Suggestion "")) : Html5_checker.Error_code.t),
141141 Some element, None)
142142 else None
143143···147147 if Str.string_match re msg 0 then
148148 let attr = Str.matched_group 1 msg in
149149 let element = Str.matched_group 2 msg in
150150- Some (Html5_checker.Error_code.Obsolete_attr { attr; element; suggestion = None },
150150+ Some ((`Element (`Obsolete_attr (`Elem element, `Attr attr, `Suggestion None)) : Html5_checker.Error_code.t),
151151 Some element, Some attr)
152152 else None
153153···156156 let re = Str.regexp {|Stray end tag "\([^"]+\)"|} in
157157 if Str.string_match re msg 0 then
158158 let tag = Str.matched_group 1 msg in
159159- Some (Html5_checker.Error_code.Stray_end_tag { tag },
159159+ Some ((`Tag (`Stray_end (`Tag tag)) : Html5_checker.Error_code.t),
160160 Some tag, None)
161161 else None
162162···165165 let re = Str.regexp {|Stray start tag "\([^"]+\)"|} in
166166 if Str.string_match re msg 0 then
167167 let tag = Str.matched_group 1 msg in
168168- Some (Html5_checker.Error_code.Stray_start_tag { tag },
168168+ Some ((`Tag (`Stray_start (`Tag tag)) : Html5_checker.Error_code.t),
169169 Some tag, None)
170170 else None
171171···175175 if Str.string_match re msg 0 then
176176 let role = Str.matched_group 1 msg in
177177 let reason = Str.matched_group 2 msg in
178178- Some (Html5_checker.Error_code.Unnecessary_role { role; element = ""; reason },
178178+ Some ((`Aria (`Unnecessary_role (`Role role, `Elem "", `Reason reason)) : Html5_checker.Error_code.t),
179179 None, None)
180180 else None
181181···185185 if Str.string_match re msg 0 then
186186 let role = Str.matched_group 1 msg in
187187 let element = Str.matched_group 2 msg in
188188- Some (Html5_checker.Error_code.Bad_role { element; role },
188188+ Some ((`Aria (`Bad_role (`Elem element, `Role role)) : Html5_checker.Error_code.t),
189189 Some element, Some "role")
190190 else None
191191···196196 let attr = Str.matched_group 1 msg in
197197 let element = Str.matched_group 2 msg in
198198 let condition = Str.matched_group 3 msg in
199199- Some (Html5_checker.Error_code.Aria_must_not_be_specified { attr; element; condition },
199199+ Some ((`Aria (`Must_not_specify (`Attr attr, `Elem element, `Condition condition)) : Html5_checker.Error_code.t),
200200 Some element, Some attr)
201201 else None
202202···207207 let attr = Str.matched_group 1 msg in
208208 let element = Str.matched_group 2 msg in
209209 let condition = Str.matched_group 3 msg in
210210- Some (Html5_checker.Error_code.Aria_must_not_be_used { attr; element; condition },
210210+ Some ((`Aria (`Must_not_use (`Attr attr, `Elem element, `Condition condition)) : Html5_checker.Error_code.t),
211211 Some element, Some attr)
212212 else None
213213···225225 String.trim (String.sub msg (colon_pos + 1) (String.length msg - colon_pos - 1))
226226 with Not_found -> ""
227227 in
228228- Some (Html5_checker.Error_code.Bad_attr_value { element; attr; value; reason },
228228+ Some ((`Attr (`Bad_value (`Elem element, `Attr attr, `Value value, `Reason reason)) : Html5_checker.Error_code.t),
229229 Some element, Some attr)
230230 else None
231231···234234 let re = Str.regexp {|End tag "\([^"]+\)" implied, but there were open elements|} in
235235 if Str.string_match re msg 0 then
236236 let tag = Str.matched_group 1 msg in
237237- Some (Html5_checker.Error_code.End_tag_implied_open_elements { tag },
237237+ Some ((`Tag (`End_implied_open (`Tag tag)) : Html5_checker.Error_code.t),
238238 Some tag, None)
239239 else None
240240···243243 let re = Str.regexp {|No "\([^"]+\)" element in scope but a "\([^"]+\)" end tag seen|} in
244244 if Str.string_match re msg 0 then
245245 let tag = Str.matched_group 1 msg in
246246- Some (Html5_checker.Error_code.No_element_in_scope { tag },
246246+ Some ((`Tag (`Not_in_scope (`Tag tag)) : Html5_checker.Error_code.t),
247247 Some tag, None)
248248 else None
249249···252252 let re = Str.regexp {|Start tag "\([^"]+\)" seen in "table"|} in
253253 if Str.string_match re msg 0 then
254254 let tag = Str.matched_group 1 msg in
255255- Some (Html5_checker.Error_code.Start_tag_in_table { tag },
255255+ Some ((`Tag (`Start_in_table (`Tag tag)) : Html5_checker.Error_code.t),
256256 Some tag, None)
257257 else None
258258···330330331331(** Compare error codes for semantic equality *)
332332let error_codes_match code1 code2 =
333333- match (code1, code2) with
334334- | (Html5_checker.Error_code.Element_not_allowed_as_child { child = c1; parent = p1 },
335335- Html5_checker.Error_code.Element_not_allowed_as_child { child = c2; parent = p2 }) ->
336336- String.lowercase_ascii c1 = String.lowercase_ascii c2 &&
337337- String.lowercase_ascii p1 = String.lowercase_ascii p2
338338- | (Html5_checker.Error_code.Attr_not_allowed_on_element { attr = a1; element = e1 },
339339- Html5_checker.Error_code.Attr_not_allowed_on_element { attr = a2; element = e2 }) ->
340340- String.lowercase_ascii a1 = String.lowercase_ascii a2 &&
341341- String.lowercase_ascii e1 = String.lowercase_ascii e2
342342- | (Html5_checker.Error_code.Missing_required_attr { element = e1; attr = a1 },
343343- Html5_checker.Error_code.Missing_required_attr { element = e2; attr = a2 }) ->
344344- String.lowercase_ascii e1 = String.lowercase_ascii e2 &&
345345- String.lowercase_ascii a1 = String.lowercase_ascii a2
346346- | (Html5_checker.Error_code.Duplicate_id { id = i1 },
347347- Html5_checker.Error_code.Duplicate_id { id = i2 }) ->
348348- i1 = i2
349349- | (Html5_checker.Error_code.Stray_end_tag { tag = t1 },
350350- Html5_checker.Error_code.Stray_end_tag { tag = t2 }) ->
351351- String.lowercase_ascii t1 = String.lowercase_ascii t2
352352- | (Html5_checker.Error_code.Stray_start_tag { tag = t1 },
353353- Html5_checker.Error_code.Stray_start_tag { tag = t2 }) ->
354354- String.lowercase_ascii t1 = String.lowercase_ascii t2
355355- (* For other cases, fall back to structural equality *)
356356- | (c1, c2) -> c1 = c2
333333+ (* Use structural equality for all polymorphic variant error codes *)
334334+ code1 = code2
357335358336let matches ~strictness ~expected ~actual =
359337 let expected_norm = normalize_quotes expected.message in