OCaml HTML5 parser/serialiser based on Python's JustHTML

polyvariants

+9 -9
lib/html5_checker/content_model/content_checker.ml
··· 75 75 (fun prohibited -> 76 76 if List.exists (fun ctx -> String.equal ctx.name prohibited) state.ancestor_stack then 77 77 Message_collector.add_typed collector 78 - (Error_code.Element_not_allowed_as_child { child = name; parent = prohibited })) 78 + (`Element (`Not_allowed_as_child (`Child name, `Parent prohibited)))) 79 79 spec.Element_spec.prohibited_ancestors 80 80 81 81 (* Validate that a child element is allowed *) ··· 85 85 (* Root level - only html allowed *) 86 86 if not (String.equal (String.lowercase_ascii child_name) "html") then 87 87 Message_collector.add_typed collector 88 - (Error_code.Generic { message = Printf.sprintf "Element '%s' not allowed at document root (only 'html' allowed)" child_name }) 88 + (`Generic (Printf.sprintf "Element '%s' not allowed at document root (only 'html' allowed)" child_name)) 89 89 | parent :: _ -> 90 90 let content_model = parent.spec.Element_spec.content_model in 91 91 if not (matches_content_model state.registry child_name content_model) then 92 92 Message_collector.add_typed collector 93 - (Error_code.Element_not_allowed_as_child { child = child_name; parent = parent.name }) 93 + (`Element (`Not_allowed_as_child (`Child child_name, `Parent parent.name))) 94 94 95 95 let start_element state ~name ~namespace:_ ~attrs:_ collector = 96 96 (* Look up element specification *) ··· 116 116 | [] -> 117 117 (* Unmatched closing tag *) 118 118 Message_collector.add_typed collector 119 - (Error_code.Generic { message = Printf.sprintf "Unmatched closing tag '%s'" name }) 119 + (`Generic (Printf.sprintf "Unmatched closing tag '%s'" name)) 120 120 | context :: rest -> 121 121 if not (String.equal context.name name) then 122 122 (* Mismatched tag *) 123 123 Message_collector.add_typed collector 124 - (Error_code.Generic { message = Printf.sprintf "Expected closing tag '%s' but got '%s'" context.name name }) 124 + (`Generic (Printf.sprintf "Expected closing tag '%s' but got '%s'" context.name name)) 125 125 else ( 126 126 (* Check if void element has children *) 127 127 if Element_spec.is_void context.spec && context.children_count > 0 then 128 128 Message_collector.add_typed collector 129 - (Error_code.Generic { message = Printf.sprintf "Void element '%s' must not have children" name }); 129 + (`Generic (Printf.sprintf "Void element '%s' must not have children" name)); 130 130 131 131 (* Pop stack *) 132 132 state.ancestor_stack <- rest; ··· 145 145 (* Text at root level - only whitespace allowed *) 146 146 if not (String.trim text = "") then 147 147 Message_collector.add_typed collector 148 - (Error_code.Generic { message = "Text content not allowed at document root" }) 148 + (`Generic "Text content not allowed at document root") 149 149 | parent :: rest -> 150 150 let content_model = parent.spec.Element_spec.content_model in 151 151 if not (allows_text content_model) then 152 152 (* Only report if non-whitespace text *) 153 153 if not (String.trim text = "") then 154 154 Message_collector.add_typed collector 155 - (Error_code.Text_not_allowed { parent = parent.name }) 155 + (`Element (`Text_not_allowed (`Parent parent.name))) 156 156 else ( 157 157 (* Text is allowed, increment child count *) 158 158 let updated_parent = { parent with children_count = parent.children_count + 1 } in ··· 163 163 List.iter 164 164 (fun context -> 165 165 Message_collector.add_typed collector 166 - (Error_code.Generic { message = Printf.sprintf "Unclosed element '%s'" context.name })) 166 + (`Generic (Printf.sprintf "Unclosed element '%s'" context.name))) 167 167 state.ancestor_stack 168 168 169 169 (* Package as first-class module *)
+566 -594
lib/html5_checker/error_code.ml
··· 1 - (** Typed error codes for HTML5 validation messages. 2 - 3 - This module defines a comprehensive variant type for all validation errors, 4 - ensuring exact message matching with the Nu HTML Validator test suite. *) 1 + (** Typed error codes for HTML5 validation messages. *) 5 2 6 - (** Severity level of a validation message *) 7 3 type severity = Error | Warning | Info 8 4 9 - (** Typed error codes with associated data *) 10 - type t = 11 - (* ===== Attribute Errors ===== *) 12 - | Attr_not_allowed_on_element of { attr: string; element: string } 13 - (** Attribute "X" not allowed on element "Y" at this point. *) 14 - | Attr_not_allowed_here of { attr: string } 15 - (** Attribute "X" not allowed here. *) 16 - | Attr_not_allowed_when of { attr: string; element: string; condition: string } 17 - (** Attribute "X" is only allowed when ... *) 18 - | Missing_required_attr of { element: string; attr: string } 19 - (** Element "X" is missing required attribute "Y". *) 20 - | Missing_required_attr_one_of of { element: string; attrs: string list } 21 - (** Element "X" is missing one or more of the following attributes: [A, B]. *) 22 - | Bad_attr_value of { element: string; attr: string; value: string; reason: string } 23 - (** Bad value "X" for attribute "Y" on element "Z". *) 24 - | Bad_attr_value_generic of { message: string } 25 - (** Generic bad attribute value message *) 26 - | Duplicate_id of { id: string } 27 - (** Duplicate ID "X". *) 28 - | Data_attr_invalid_name of { reason: string } 29 - (** "data-*" attribute names must be XML 1.0 4th ed. plus Namespaces NCNames. *) 30 - | Data_attr_uppercase 31 - (** "data-*" attributes must not have characters from the range "A"…"Z" in the name. *) 5 + type attr_error = [ 6 + | `Not_allowed of [`Attr of string] * [`Elem of string] 7 + | `Not_allowed_here of [`Attr of string] 8 + | `Not_allowed_when of [`Attr of string] * [`Elem of string] * [`Condition of string] 9 + | `Missing of [`Elem of string] * [`Attr of string] 10 + | `Missing_one_of of [`Elem of string] * [`Attrs of string list] 11 + | `Bad_value of [`Elem of string] * [`Attr of string] * [`Value of string] * [`Reason of string] 12 + | `Bad_value_generic of [`Message of string] 13 + | `Duplicate_id of [`Id of string] 14 + | `Data_invalid_name of [`Reason of string] 15 + | `Data_uppercase 16 + ] 32 17 33 - (* ===== Element Errors ===== *) 34 - | Obsolete_element of { element: string; suggestion: string } 35 - (** The "X" element is obsolete. Y *) 36 - | Obsolete_attr of { element: string; attr: string; suggestion: string option } 37 - (** The "X" attribute on the "Y" element is obsolete. *) 38 - | Obsolete_global_attr of { attr: string; suggestion: string } 39 - (** The "X" attribute is obsolete. Y *) 40 - | Element_not_allowed_as_child of { child: string; parent: string } 41 - (** Element "X" not allowed as child of element "Y" in this context. *) 42 - | Unknown_element of { name: string } 43 - (** Unknown element "X". *) 44 - | Element_must_not_be_descendant of { element: string; attr: string option; ancestor: string } 45 - (** The element "X" [with attribute "A"] must not appear as a descendant of the "Y" element. *) 46 - | Missing_required_child of { parent: string; child: string } 47 - (** Element "X" is missing required child element "Y". *) 48 - | Missing_required_child_one_of of { parent: string; children: string list } 49 - (** Element "X" is missing one or more of the following child elements: [A, B]. *) 50 - | Missing_required_child_generic of { parent: string } 51 - (** Element "X" is missing a required child element. *) 52 - | Element_must_not_be_empty of { element: string } 53 - (** Element "X" must not be empty. *) 54 - | Stray_start_tag of { tag: string } 55 - (** Stray start tag "X". *) 56 - | Stray_end_tag of { tag: string } 57 - (** Stray end tag "X". *) 58 - | End_tag_for_void_element of { tag: string } 59 - (** End tag "X". (for void elements like br) *) 60 - | Self_closing_non_void 61 - (** Self-closing syntax used on a non-void HTML element. *) 62 - | Text_not_allowed of { parent: string } 63 - (** Text not allowed in element "X" in this context. *) 18 + type element_error = [ 19 + | `Obsolete of [`Elem of string] * [`Suggestion of string] 20 + | `Obsolete_attr of [`Elem of string] * [`Attr of string] * [`Suggestion of string option] 21 + | `Obsolete_global_attr of [`Attr of string] * [`Suggestion of string] 22 + | `Not_allowed_as_child of [`Child of string] * [`Parent of string] 23 + | `Unknown of [`Elem of string] 24 + | `Must_not_descend of [`Elem of string] * [`Attr of string option] * [`Ancestor of string] 25 + | `Missing_child of [`Parent of string] * [`Child of string] 26 + | `Missing_child_one_of of [`Parent of string] * [`Children of string list] 27 + | `Missing_child_generic of [`Parent of string] 28 + | `Must_not_be_empty of [`Elem of string] 29 + | `Text_not_allowed of [`Parent of string] 30 + ] 64 31 65 - (* ===== Child Restrictions ===== *) 66 - | Div_child_of_dl_bad_role 67 - (** A "div" child of a "dl" element must not have any "role" value other than "presentation" or "none". *) 68 - | Li_bad_role_in_menu 69 - (** An "li" element descendant of role=menu/menubar must have specific roles. *) 70 - | Li_bad_role_in_tablist 71 - (** An "li" element descendant of role=tablist must have role=tab. *) 72 - | Li_bad_role_in_list 73 - (** An "li" element descendant of ul/ol/menu or role=list must have role=listitem. *) 32 + type tag_error = [ 33 + | `Stray_start of [`Tag of string] 34 + | `Stray_end of [`Tag of string] 35 + | `End_for_void of [`Tag of string] 36 + | `Self_closing_non_void 37 + | `Not_in_scope of [`Tag of string] 38 + | `End_implied_open of [`Tag of string] 39 + | `Start_in_table of [`Tag of string] 40 + | `Bad_start_in of [`Tag of string] * [`Context of string] 41 + | `Eof_with_open 42 + ] 74 43 75 - (* ===== ARIA Errors ===== *) 76 - | Unnecessary_role of { role: string; element: string; reason: string } 77 - (** The "X" role is unnecessary for Y. *) 78 - | Bad_role of { element: string; role: string } 79 - (** Bad value "X" for attribute "role" on element "Y". *) 80 - | Aria_must_not_be_specified of { attr: string; element: string; condition: string } 81 - (** The "X" attribute must not be specified on any "Y" element unless... *) 82 - | Aria_must_not_be_used of { attr: string; element: string; condition: string } 83 - (** The "X" attribute must not be used on an "Y" element which has... *) 84 - | Aria_should_not_be_used of { attr: string; role: string } 85 - (** The "X" attribute should not be used on any element which has "role=Y". *) 86 - | Aria_hidden_on_body 87 - (** "aria-hidden=true" must not be used on the "body" element. *) 88 - | Img_empty_alt_with_role 89 - (** An "img" element with empty alt must not have a role attribute. *) 90 - | Checkbox_button_needs_aria_pressed 91 - (** An "input" type="checkbox" with role="button" must have aria-pressed. *) 92 - | Tab_without_tabpanel 93 - (** Every active "role=tab" element must have a corresponding "role=tabpanel" element. *) 94 - | Multiple_main_visible 95 - (** A document should not include more than one visible element with "role=main". *) 96 - | Discarding_unrecognized_role of { token: string } 97 - (** Discarding unrecognized token "X" from value of attribute "role". *) 44 + type char_ref_error = [ 45 + | `Forbidden_codepoint of [`Codepoint of int] 46 + | `Control_char of [`Codepoint of int] 47 + | `Non_char of [`Codepoint of int] * [`Astral of bool] 48 + | `Unassigned 49 + | `Zero 50 + | `Out_of_range 51 + | `Carriage_return 52 + ] 98 53 99 - (* ===== Required Attribute/Element Conditions ===== *) 100 - | Img_missing_alt 101 - (** An "img" element must have an "alt" attribute. *) 102 - | Img_missing_src_or_srcset 103 - (** Element "img" is missing one or more of the following attributes: [src, srcset]. *) 104 - | Option_empty_without_label 105 - (** Element "option" without attribute "label" must not be empty. *) 106 - | Bdo_missing_dir 107 - (** Element "bdo" must have attribute "dir". *) 108 - | Bdo_dir_auto 109 - (** The value of "dir" attribute for the "bdo" element must not be "auto". *) 110 - | Base_missing_href_or_target 111 - (** Element "base" is missing one or more of the following attributes: [href, target]. *) 112 - | Base_after_link_script 113 - (** The "base" element must come before any "link" or "script" elements. *) 114 - | Link_missing_href 115 - (** A "link" element must have an "href" or "imagesrcset" attribute. *) 116 - | Link_as_requires_preload 117 - (** A "link" element with an "as" attribute must have rel="preload" or "modulepreload". *) 118 - | Link_imagesrcset_requires_as_image 119 - (** A "link" element with "imagesrcset" must have as="image". *) 120 - | Img_ismap_needs_a_href 121 - (** The "img" element with "ismap" must have an "a" ancestor with "href". *) 122 - | Sizes_without_srcset 123 - (** The "sizes" attribute must only be specified if "srcset" is also specified. *) 124 - | Imagesizes_without_imagesrcset 125 - (** The "imagesizes" attribute must only be specified if "imagesrcset" is also specified. *) 126 - | Srcset_w_without_sizes 127 - (** When the "srcset" attribute has width descriptors, "sizes" must also be specified. *) 128 - | Source_missing_srcset 129 - (** Element "source" is missing required attribute "srcset". *) 130 - | Source_needs_media_or_type 131 - (** A "source" element with following source/img[srcset] must have media/type. *) 132 - | Picture_missing_img 133 - (** Element "picture" is missing required child element "img". *) 134 - | Map_id_name_mismatch 135 - (** The "id" attribute on a "map" element must have the same value as the "name" attribute. *) 136 - | List_attr_requires_datalist 137 - (** The "list" attribute of "input" must refer to a "datalist" element. *) 138 - | Input_list_not_allowed 139 - (** Attribute "list" is only allowed on certain input types. *) 140 - | Label_too_many_labelable 141 - (** The "label" element may contain at most one labelable descendant. *) 142 - | Label_for_id_mismatch 143 - (** Any "input" descendant of a "label" with "for" must have matching ID. *) 144 - | Role_on_label_ancestor 145 - (** The "role" attribute must not be on label ancestor of labelable element. *) 146 - | Role_on_label_for 147 - (** The "role" attribute must not be on label associated via for. *) 148 - | Aria_label_on_label_for 149 - (** The "aria-label" attribute must not be on label associated via for. *) 150 - | Input_value_constraint of { constraint_type: string } 151 - (** The value of the "value" attribute must be... *) 152 - | Summary_missing_role 153 - (** Element "summary" is missing required attribute "role". *) 154 - | Summary_missing_attrs 155 - (** Element "summary" is missing one or more of [aria-checked, aria-level, role]. *) 156 - | Summary_role_not_allowed 157 - (** The "role" attribute must not be used on any "summary" for its parent "details". *) 158 - | Autocomplete_webauthn_on_select 159 - (** The value of "autocomplete" for "select" must not contain "webauthn". *) 160 - | Commandfor_invalid_target 161 - (** The value of "commandfor" must be the ID of an element in the same tree. *) 54 + type aria_error = [ 55 + | `Unnecessary_role of [`Role of string] * [`Elem of string] * [`Reason of string] 56 + | `Bad_role of [`Elem of string] * [`Role of string] 57 + | `Must_not_specify of [`Attr of string] * [`Elem of string] * [`Condition of string] 58 + | `Must_not_use of [`Attr of string] * [`Elem of string] * [`Condition of string] 59 + | `Should_not_use of [`Attr of string] * [`Role of string] 60 + | `Hidden_on_body 61 + | `Unrecognized_role of [`Token of string] 62 + | `Tab_without_tabpanel 63 + | `Multiple_main 64 + ] 162 65 163 - (* ===== Parse Errors ===== *) 164 - | Forbidden_codepoint of { codepoint: int } 165 - (** Forbidden code point U+XXXX. *) 166 - | Char_ref_control of { codepoint: int } 167 - (** Character reference expands to a control character (U+XXXX). *) 168 - | Char_ref_non_char of { codepoint: int; astral: bool } 169 - (** Character reference expands to a [astral] non-character (U+XXXX). *) 170 - | Char_ref_unassigned 171 - (** Character reference expands to a permanently unassigned code point. *) 172 - | Char_ref_zero 173 - (** Character reference expands to zero. *) 174 - | Char_ref_out_of_range 175 - (** Character reference outside the permissible Unicode range. *) 176 - | Numeric_char_ref_carriage_return 177 - (** A numeric character reference expanded to carriage return. *) 178 - | End_of_file_with_open_elements 179 - (** End of file seen and there were open elements. *) 180 - | No_element_in_scope of { tag: string } 181 - (** No "X" element in scope but a "X" end tag seen. *) 182 - | End_tag_implied_open_elements of { tag: string } 183 - (** End tag "X" implied, but there were open elements. *) 184 - | Start_tag_in_table of { tag: string } 185 - (** Start tag "X" seen in "table". *) 186 - | Bad_start_tag_in of { tag: string; context: string } 187 - (** Bad start tag in "X" in "noscript" in "head". *) 66 + type li_role_error = [ 67 + | `Div_in_dl_bad_role 68 + | `Li_bad_role_in_menu 69 + | `Li_bad_role_in_tablist 70 + | `Li_bad_role_in_list 71 + ] 188 72 189 - (* ===== Table Errors ===== *) 190 - | Table_row_no_cells of { row: int } 191 - (** Row N of an implicit row group has no cells beginning on it. *) 192 - | Table_cell_overlap 193 - (** Table cell is overlapped by later table cell. *) 194 - | Table_cell_spans_rowgroup 195 - (** Table cell spans past the end of its row group. *) 196 - | Table_column_no_cells of { column: int; element: string } 197 - (** Table column N established by element "X" has no cells beginning in it. *) 73 + type table_error = [ 74 + | `Row_no_cells of [`Row of int] 75 + | `Cell_overlap 76 + | `Cell_spans_rowgroup 77 + | `Column_no_cells of [`Column of int] * [`Elem of string] 78 + ] 198 79 199 - (* ===== Language/Internationalization ===== *) 200 - | Missing_lang_attr 201 - (** Consider adding a "lang" attribute to the "html" start tag. *) 202 - | Wrong_lang of { detected: string; declared: string; suggested: string } 203 - (** This document appears to be written in X but has lang="Y". Consider using "Z". *) 204 - | Missing_dir_rtl of { language: string } 205 - (** This document appears to be written in X. Consider adding dir="rtl". *) 206 - | Wrong_dir of { language: string; declared: string } 207 - (** This document appears to be written in X but has dir="Y". Consider dir="rtl". *) 208 - | Xml_lang_without_lang 209 - (** When xml:lang is specified, lang must also be present with the same value. *) 210 - | Xml_lang_lang_mismatch 211 - (** xml:lang and lang must have the same value. *) 80 + type i18n_error = [ 81 + | `Missing_lang 82 + | `Wrong_lang of [`Detected of string] * [`Declared of string] * [`Suggested of string] 83 + | `Missing_dir_rtl of [`Language of string] 84 + | `Wrong_dir of [`Language of string] * [`Declared of string] 85 + | `Xml_lang_without_lang 86 + | `Xml_lang_mismatch 87 + | `Not_nfc of [`Replacement of string] 88 + ] 212 89 213 - (* ===== Unicode Normalization ===== *) 214 - | Not_nfc of { replacement: string } 215 - (** Text run is not in Unicode Normalization Form C. *) 90 + type importmap_error = [ 91 + | `Invalid_json 92 + | `Invalid_root 93 + | `Imports_not_object 94 + | `Empty_key 95 + | `Non_string_value 96 + | `Key_trailing_slash 97 + | `Scopes_not_object 98 + | `Scopes_values_not_object 99 + | `Scopes_invalid_url 100 + | `Scopes_value_invalid_url 101 + ] 216 102 217 - (* ===== Multiple h1 ===== *) 218 - | Multiple_h1 219 - (** Consider using only one "h1" element per document. *) 220 - | Multiple_autofocus 221 - (** There must not be two elements with autofocus in the same scoping root. *) 103 + type img_error = [ 104 + | `Missing_alt 105 + | `Missing_src_or_srcset 106 + | `Empty_alt_with_role 107 + | `Ismap_needs_href 108 + ] 109 + 110 + type link_error = [ 111 + | `Missing_href 112 + | `As_requires_preload 113 + | `Imagesrcset_requires_as_image 114 + ] 222 115 223 - (* ===== Import Maps ===== *) 224 - | Importmap_invalid_json 225 - (** A "script" type="importmap" must have valid JSON content. *) 226 - | Importmap_invalid_root 227 - (** A "script" type="importmap" must contain a JSON object with only imports/scopes/integrity. *) 228 - | Importmap_imports_not_object 229 - (** The value of "imports" property must be a JSON object. *) 230 - | Importmap_empty_key 231 - (** Specifier map must only contain non-empty keys. *) 232 - | Importmap_non_string_value 233 - (** Specifier map must only contain string values. *) 234 - | Importmap_key_trailing_slash 235 - (** Specifier map values must end with "/" when key ends with "/". *) 236 - | Importmap_scopes_not_object 237 - (** The value of "scopes" property must be a JSON object with valid URL keys. *) 238 - | Importmap_scopes_values_not_object 239 - (** The value of "scopes" property values must also be JSON objects. *) 240 - | Importmap_scopes_invalid_url 241 - (** The "scopes" property keys must be valid URL strings. *) 242 - | Importmap_scopes_value_invalid_url 243 - (** The specifier map within "scopes" must only contain valid URL values. *) 116 + type label_error = [ 117 + | `Too_many_labelable 118 + | `For_id_mismatch 119 + | `Role_on_ancestor 120 + | `Role_on_for 121 + | `Aria_label_on_for 122 + ] 244 123 245 - (* ===== Style Element ===== *) 246 - | Style_type_invalid 247 - (** The only allowed value for "type" on "style" is "text/css". *) 124 + type input_error = [ 125 + | `Checkbox_needs_aria_pressed 126 + | `Value_constraint of [`Constraint of string] 127 + | `List_not_allowed 128 + | `List_requires_datalist 129 + ] 248 130 249 - (* ===== Headingoffset ===== *) 250 - | Headingoffset_invalid 251 - (** The value of "headingoffset" must be a number between "0" and "8". *) 131 + type srcset_error = [ 132 + | `Sizes_without_srcset 133 + | `Imagesizes_without_imagesrcset 134 + | `W_without_sizes 135 + | `Source_missing_srcset 136 + | `Source_needs_media_or_type 137 + | `Picture_missing_img 138 + ] 252 139 253 - (* ===== Media Attribute ===== *) 254 - | Media_empty 255 - (** Value of "media" attribute here must not be empty. *) 256 - | Media_all 257 - (** Value of "media" attribute here must not be "all". *) 140 + type svg_error = [ 141 + | `Deprecated_attr of [`Attr of string] * [`Elem of string] 142 + | `Missing_attr of [`Elem of string] * [`Attr of string] 143 + ] 258 144 259 - (* ===== SVG/MathML specific ===== *) 260 - | Svg_deprecated_attr of { attr: string; element: string } 261 - (** SVG deprecated attribute *) 262 - | Missing_required_svg_attr of { element: string; attr: string } 263 - (** Element "X" is missing required attribute "Y". (SVG) *) 145 + type misc_error = [ 146 + | `Option_empty_without_label 147 + | `Bdo_missing_dir 148 + | `Bdo_dir_auto 149 + | `Base_missing_href_or_target 150 + | `Base_after_link_script 151 + | `Map_id_name_mismatch 152 + | `Summary_missing_role 153 + | `Summary_missing_attrs 154 + | `Summary_role_not_allowed 155 + | `Autocomplete_webauthn_on_select 156 + | `Commandfor_invalid_target 157 + | `Style_type_invalid 158 + | `Headingoffset_invalid 159 + | `Media_empty 160 + | `Media_all 161 + | `Multiple_h1 162 + | `Multiple_autofocus 163 + ] 264 164 265 - (* ===== Generic/Fallback ===== *) 266 - | Generic of { message: string } 267 - (** For messages that don't fit any specific pattern *) 165 + type t = [ 166 + | `Attr of attr_error 167 + | `Element of element_error 168 + | `Tag of tag_error 169 + | `Char_ref of char_ref_error 170 + | `Aria of aria_error 171 + | `Li_role of li_role_error 172 + | `Table of table_error 173 + | `I18n of i18n_error 174 + | `Importmap of importmap_error 175 + | `Img of img_error 176 + | `Link of link_error 177 + | `Label of label_error 178 + | `Input of input_error 179 + | `Srcset of srcset_error 180 + | `Svg of svg_error 181 + | `Misc of misc_error 182 + | `Generic of string 183 + ] 268 184 269 185 (** Get the severity level for an error code *) 270 - let severity = function 271 - | Missing_lang_attr -> Info 272 - | Multiple_h1 -> Info 273 - | Wrong_lang _ -> Warning 274 - | Missing_dir_rtl _ -> Warning 275 - | Wrong_dir _ -> Warning 276 - | Unnecessary_role _ -> Warning 277 - | Aria_should_not_be_used _ -> Warning 278 - | Unknown_element _ -> Warning 279 - | Not_nfc _ -> Warning 186 + let severity : t -> severity = function 187 + (* Info level *) 188 + | `I18n `Missing_lang -> Info 189 + | `Misc `Multiple_h1 -> Info 190 + 191 + (* Warning level *) 192 + | `I18n (`Wrong_lang _) -> Warning 193 + | `I18n (`Missing_dir_rtl _) -> Warning 194 + | `I18n (`Wrong_dir _) -> Warning 195 + | `I18n (`Not_nfc _) -> Warning 196 + | `Aria (`Unnecessary_role _) -> Warning 197 + | `Aria (`Should_not_use _) -> Warning 198 + | `Element (`Unknown _) -> Warning 199 + 200 + (* Everything else is Error *) 280 201 | _ -> Error 281 202 282 203 (** Get a short code string for categorization *) 283 - let code_string = function 284 - | Attr_not_allowed_on_element _ -> "disallowed-attribute" 285 - | Attr_not_allowed_here _ -> "disallowed-attribute" 286 - | Attr_not_allowed_when _ -> "disallowed-attribute" 287 - | Missing_required_attr _ -> "missing-required-attribute" 288 - | Missing_required_attr_one_of _ -> "missing-required-attribute" 289 - | Bad_attr_value _ -> "bad-attribute-value" 290 - | Bad_attr_value_generic _ -> "bad-attribute-value" 291 - | Duplicate_id _ -> "duplicate-id" 292 - | Data_attr_invalid_name _ -> "bad-attribute-name" 293 - | Data_attr_uppercase -> "bad-attribute-name" 294 - | Obsolete_element _ -> "obsolete-element" 295 - | Obsolete_attr _ -> "obsolete-attribute" 296 - | Obsolete_global_attr _ -> "obsolete-attribute" 297 - | Element_not_allowed_as_child _ -> "disallowed-child" 298 - | Unknown_element _ -> "unknown-element" 299 - | Element_must_not_be_descendant _ -> "prohibited-ancestor" 300 - | Missing_required_child _ -> "missing-required-child" 301 - | Missing_required_child_one_of _ -> "missing-required-child" 302 - | Missing_required_child_generic _ -> "missing-required-child" 303 - | Element_must_not_be_empty _ -> "empty-element" 304 - | Stray_start_tag _ -> "stray-tag" 305 - | Stray_end_tag _ -> "stray-tag" 306 - | End_tag_for_void_element _ -> "end-tag-void" 307 - | Self_closing_non_void -> "self-closing-non-void" 308 - | Text_not_allowed _ -> "text-not-allowed" 309 - | Div_child_of_dl_bad_role -> "invalid-role" 310 - | Li_bad_role_in_menu -> "invalid-role" 311 - | Li_bad_role_in_tablist -> "invalid-role" 312 - | Li_bad_role_in_list -> "invalid-role" 313 - | Unnecessary_role _ -> "unnecessary-role" 314 - | Bad_role _ -> "bad-role" 315 - | Aria_must_not_be_specified _ -> "aria-not-allowed" 316 - | Aria_must_not_be_used _ -> "aria-not-allowed" 317 - | Aria_should_not_be_used _ -> "aria-not-allowed" 318 - | Aria_hidden_on_body -> "aria-not-allowed" 319 - | Img_empty_alt_with_role -> "img-alt-role" 320 - | Checkbox_button_needs_aria_pressed -> "missing-aria-pressed" 321 - | Tab_without_tabpanel -> "tab-without-tabpanel" 322 - | Multiple_main_visible -> "multiple-main" 323 - | Discarding_unrecognized_role _ -> "unrecognized-role" 324 - | Img_missing_alt -> "missing-alt" 325 - | Img_missing_src_or_srcset -> "missing-src" 326 - | Option_empty_without_label -> "empty-option" 327 - | Bdo_missing_dir -> "missing-dir" 328 - | Bdo_dir_auto -> "bdo-dir-auto" 329 - | Base_missing_href_or_target -> "missing-required-attribute" 330 - | Base_after_link_script -> "base-position" 331 - | Link_missing_href -> "missing-href" 332 - | Link_as_requires_preload -> "link-as-preload" 333 - | Link_imagesrcset_requires_as_image -> "link-imagesrcset" 334 - | Img_ismap_needs_a_href -> "ismap-needs-href" 335 - | Sizes_without_srcset -> "sizes-without-srcset" 336 - | Imagesizes_without_imagesrcset -> "imagesizes-without-srcset" 337 - | Srcset_w_without_sizes -> "srcset-needs-sizes" 338 - | Source_missing_srcset -> "missing-srcset" 339 - | Source_needs_media_or_type -> "source-needs-media" 340 - | Picture_missing_img -> "picture-missing-img" 341 - | Map_id_name_mismatch -> "map-id-name" 342 - | List_attr_requires_datalist -> "list-datalist" 343 - | Input_list_not_allowed -> "list-not-allowed" 344 - | Label_too_many_labelable -> "label-multiple" 345 - | Label_for_id_mismatch -> "label-for-mismatch" 346 - | Role_on_label_ancestor -> "role-on-label" 347 - | Role_on_label_for -> "role-on-label" 348 - | Aria_label_on_label_for -> "aria-label-on-label" 349 - | Input_value_constraint _ -> "input-value" 350 - | Summary_missing_role -> "summary-role" 351 - | Summary_missing_attrs -> "summary-attrs" 352 - | Summary_role_not_allowed -> "summary-role" 353 - | Autocomplete_webauthn_on_select -> "autocomplete" 354 - | Commandfor_invalid_target -> "commandfor" 355 - | Forbidden_codepoint _ -> "forbidden-codepoint" 356 - | Char_ref_control _ -> "char-ref-control" 357 - | Char_ref_non_char _ -> "char-ref-non-char" 358 - | Char_ref_unassigned -> "char-ref-unassigned" 359 - | Char_ref_zero -> "char-ref-zero" 360 - | Char_ref_out_of_range -> "char-ref-range" 361 - | Numeric_char_ref_carriage_return -> "numeric-char-ref" 362 - | End_of_file_with_open_elements -> "eof-open-elements" 363 - | No_element_in_scope _ -> "no-element-in-scope" 364 - | End_tag_implied_open_elements _ -> "end-tag-implied" 365 - | Start_tag_in_table _ -> "start-tag-in-table" 366 - | Bad_start_tag_in _ -> "bad-start-tag" 367 - | Table_row_no_cells _ -> "table-row" 368 - | Table_cell_overlap -> "table-overlap" 369 - | Table_cell_spans_rowgroup -> "table-span" 370 - | Table_column_no_cells _ -> "table-column" 371 - | Missing_lang_attr -> "missing-lang" 372 - | Wrong_lang _ -> "wrong-lang" 373 - | Missing_dir_rtl _ -> "missing-dir" 374 - | Wrong_dir _ -> "wrong-dir" 375 - | Xml_lang_without_lang -> "xml-lang" 376 - | Xml_lang_lang_mismatch -> "xml-lang-mismatch" 377 - | Not_nfc _ -> "unicode-normalization" 378 - | Multiple_h1 -> "multiple-h1" 379 - | Multiple_autofocus -> "multiple-autofocus" 380 - | Importmap_invalid_json -> "importmap" 381 - | Importmap_invalid_root -> "importmap" 382 - | Importmap_imports_not_object -> "importmap" 383 - | Importmap_empty_key -> "importmap" 384 - | Importmap_non_string_value -> "importmap" 385 - | Importmap_key_trailing_slash -> "importmap" 386 - | Importmap_scopes_not_object -> "importmap" 387 - | Importmap_scopes_values_not_object -> "importmap" 388 - | Importmap_scopes_invalid_url -> "importmap" 389 - | Importmap_scopes_value_invalid_url -> "importmap" 390 - | Style_type_invalid -> "style-type" 391 - | Headingoffset_invalid -> "headingoffset" 392 - | Media_empty -> "media-empty" 393 - | Media_all -> "media-all" 394 - | Svg_deprecated_attr _ -> "svg-deprecated" 395 - | Missing_required_svg_attr _ -> "missing-required-attribute" 396 - | Generic _ -> "generic" 204 + let code_string : t -> string = function 205 + (* Attribute errors *) 206 + | `Attr (`Not_allowed _) -> "disallowed-attribute" 207 + | `Attr (`Not_allowed_here _) -> "disallowed-attribute" 208 + | `Attr (`Not_allowed_when _) -> "disallowed-attribute" 209 + | `Attr (`Missing _) -> "missing-required-attribute" 210 + | `Attr (`Missing_one_of _) -> "missing-required-attribute" 211 + | `Attr (`Bad_value _) -> "bad-attribute-value" 212 + | `Attr (`Bad_value_generic _) -> "bad-attribute-value" 213 + | `Attr (`Duplicate_id _) -> "duplicate-id" 214 + | `Attr (`Data_invalid_name _) -> "bad-attribute-name" 215 + | `Attr `Data_uppercase -> "bad-attribute-name" 216 + 217 + (* Element errors *) 218 + | `Element (`Obsolete _) -> "obsolete-element" 219 + | `Element (`Obsolete_attr _) -> "obsolete-attribute" 220 + | `Element (`Obsolete_global_attr _) -> "obsolete-attribute" 221 + | `Element (`Not_allowed_as_child _) -> "disallowed-child" 222 + | `Element (`Unknown _) -> "unknown-element" 223 + | `Element (`Must_not_descend _) -> "prohibited-ancestor" 224 + | `Element (`Missing_child _) -> "missing-required-child" 225 + | `Element (`Missing_child_one_of _) -> "missing-required-child" 226 + | `Element (`Missing_child_generic _) -> "missing-required-child" 227 + | `Element (`Must_not_be_empty _) -> "empty-element" 228 + | `Element (`Text_not_allowed _) -> "text-not-allowed" 229 + 230 + (* Tag errors *) 231 + | `Tag (`Stray_start _) -> "stray-tag" 232 + | `Tag (`Stray_end _) -> "stray-tag" 233 + | `Tag (`End_for_void _) -> "end-tag-void" 234 + | `Tag `Self_closing_non_void -> "self-closing-non-void" 235 + | `Tag (`Not_in_scope _) -> "no-element-in-scope" 236 + | `Tag (`End_implied_open _) -> "end-tag-implied" 237 + | `Tag (`Start_in_table _) -> "start-tag-in-table" 238 + | `Tag (`Bad_start_in _) -> "bad-start-tag" 239 + | `Tag `Eof_with_open -> "eof-open-elements" 240 + 241 + (* Character reference errors *) 242 + | `Char_ref (`Forbidden_codepoint _) -> "forbidden-codepoint" 243 + | `Char_ref (`Control_char _) -> "char-ref-control" 244 + | `Char_ref (`Non_char _) -> "char-ref-non-char" 245 + | `Char_ref `Unassigned -> "char-ref-unassigned" 246 + | `Char_ref `Zero -> "char-ref-zero" 247 + | `Char_ref `Out_of_range -> "char-ref-range" 248 + | `Char_ref `Carriage_return -> "numeric-char-ref" 249 + 250 + (* ARIA errors *) 251 + | `Aria (`Unnecessary_role _) -> "unnecessary-role" 252 + | `Aria (`Bad_role _) -> "bad-role" 253 + | `Aria (`Must_not_specify _) -> "aria-not-allowed" 254 + | `Aria (`Must_not_use _) -> "aria-not-allowed" 255 + | `Aria (`Should_not_use _) -> "aria-not-allowed" 256 + | `Aria `Hidden_on_body -> "aria-not-allowed" 257 + | `Aria (`Unrecognized_role _) -> "unrecognized-role" 258 + | `Aria `Tab_without_tabpanel -> "tab-without-tabpanel" 259 + | `Aria `Multiple_main -> "multiple-main" 260 + 261 + (* List item role errors *) 262 + | `Li_role `Div_in_dl_bad_role -> "invalid-role" 263 + | `Li_role `Li_bad_role_in_menu -> "invalid-role" 264 + | `Li_role `Li_bad_role_in_tablist -> "invalid-role" 265 + | `Li_role `Li_bad_role_in_list -> "invalid-role" 266 + 267 + (* Table errors *) 268 + | `Table (`Row_no_cells _) -> "table-row" 269 + | `Table `Cell_overlap -> "table-overlap" 270 + | `Table `Cell_spans_rowgroup -> "table-span" 271 + | `Table (`Column_no_cells _) -> "table-column" 272 + 273 + (* I18n errors *) 274 + | `I18n `Missing_lang -> "missing-lang" 275 + | `I18n (`Wrong_lang _) -> "wrong-lang" 276 + | `I18n (`Missing_dir_rtl _) -> "missing-dir" 277 + | `I18n (`Wrong_dir _) -> "wrong-dir" 278 + | `I18n `Xml_lang_without_lang -> "xml-lang" 279 + | `I18n `Xml_lang_mismatch -> "xml-lang-mismatch" 280 + | `I18n (`Not_nfc _) -> "unicode-normalization" 281 + 282 + (* Import map errors *) 283 + | `Importmap `Invalid_json -> "importmap" 284 + | `Importmap `Invalid_root -> "importmap" 285 + | `Importmap `Imports_not_object -> "importmap" 286 + | `Importmap `Empty_key -> "importmap" 287 + | `Importmap `Non_string_value -> "importmap" 288 + | `Importmap `Key_trailing_slash -> "importmap" 289 + | `Importmap `Scopes_not_object -> "importmap" 290 + | `Importmap `Scopes_values_not_object -> "importmap" 291 + | `Importmap `Scopes_invalid_url -> "importmap" 292 + | `Importmap `Scopes_value_invalid_url -> "importmap" 293 + 294 + (* Image errors *) 295 + | `Img `Missing_alt -> "missing-alt" 296 + | `Img `Missing_src_or_srcset -> "missing-src" 297 + | `Img `Empty_alt_with_role -> "img-alt-role" 298 + | `Img `Ismap_needs_href -> "ismap-needs-href" 299 + 300 + (* Link errors *) 301 + | `Link `Missing_href -> "missing-href" 302 + | `Link `As_requires_preload -> "link-as-preload" 303 + | `Link `Imagesrcset_requires_as_image -> "link-imagesrcset" 304 + 305 + (* Label errors *) 306 + | `Label `Too_many_labelable -> "label-multiple" 307 + | `Label `For_id_mismatch -> "label-for-mismatch" 308 + | `Label `Role_on_ancestor -> "role-on-label" 309 + | `Label `Role_on_for -> "role-on-label" 310 + | `Label `Aria_label_on_for -> "aria-label-on-label" 311 + 312 + (* Input errors *) 313 + | `Input `Checkbox_needs_aria_pressed -> "missing-aria-pressed" 314 + | `Input (`Value_constraint _) -> "input-value" 315 + | `Input `List_not_allowed -> "list-not-allowed" 316 + | `Input `List_requires_datalist -> "list-datalist" 317 + 318 + (* Srcset errors *) 319 + | `Srcset `Sizes_without_srcset -> "sizes-without-srcset" 320 + | `Srcset `Imagesizes_without_imagesrcset -> "imagesizes-without-srcset" 321 + | `Srcset `W_without_sizes -> "srcset-needs-sizes" 322 + | `Srcset `Source_missing_srcset -> "missing-srcset" 323 + | `Srcset `Source_needs_media_or_type -> "source-needs-media" 324 + | `Srcset `Picture_missing_img -> "picture-missing-img" 325 + 326 + (* SVG errors *) 327 + | `Svg (`Deprecated_attr _) -> "svg-deprecated" 328 + | `Svg (`Missing_attr _) -> "missing-required-attribute" 329 + 330 + (* Misc errors *) 331 + | `Misc `Option_empty_without_label -> "empty-option" 332 + | `Misc `Bdo_missing_dir -> "missing-dir" 333 + | `Misc `Bdo_dir_auto -> "bdo-dir-auto" 334 + | `Misc `Base_missing_href_or_target -> "missing-required-attribute" 335 + | `Misc `Base_after_link_script -> "base-position" 336 + | `Misc `Map_id_name_mismatch -> "map-id-name" 337 + | `Misc `Summary_missing_role -> "summary-role" 338 + | `Misc `Summary_missing_attrs -> "summary-attrs" 339 + | `Misc `Summary_role_not_allowed -> "summary-role" 340 + | `Misc `Autocomplete_webauthn_on_select -> "autocomplete" 341 + | `Misc `Commandfor_invalid_target -> "commandfor" 342 + | `Misc `Style_type_invalid -> "style-type" 343 + | `Misc `Headingoffset_invalid -> "headingoffset" 344 + | `Misc `Media_empty -> "media-empty" 345 + | `Misc `Media_all -> "media-all" 346 + | `Misc `Multiple_h1 -> "multiple-h1" 347 + | `Misc `Multiple_autofocus -> "multiple-autofocus" 348 + 349 + (* Generic *) 350 + | `Generic _ -> "generic" 397 351 398 352 (** Format using curly quotes (Unicode) *) 399 353 let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" 400 354 401 355 (** Convert error code to exact Nu validator message string *) 402 - let to_message = function 403 - | Attr_not_allowed_on_element { attr; element } -> 356 + let to_message : t -> string = function 357 + (* Attribute errors *) 358 + | `Attr (`Not_allowed (`Attr attr, `Elem element)) -> 404 359 Printf.sprintf "Attribute %s not allowed on element %s at this point." 405 360 (q attr) (q element) 406 - | Attr_not_allowed_here { attr } -> 361 + | `Attr (`Not_allowed_here (`Attr attr)) -> 407 362 Printf.sprintf "Attribute %s not allowed here." (q attr) 408 - | Attr_not_allowed_when { attr; element = _; condition } -> 363 + | `Attr (`Not_allowed_when (`Attr attr, `Elem _, `Condition condition)) -> 409 364 Printf.sprintf "The %s attribute must not be used on any element which has %s." (q attr) condition 410 - | Missing_required_attr { element; attr } -> 365 + | `Attr (`Missing (`Elem element, `Attr attr)) -> 411 366 Printf.sprintf "Element %s is missing required attribute %s." 412 367 (q element) (q attr) 413 - | Missing_required_attr_one_of { element; attrs } -> 368 + | `Attr (`Missing_one_of (`Elem element, `Attrs attrs)) -> 414 369 let attrs_str = String.concat ", " attrs in 415 370 Printf.sprintf "Element %s is missing one or more of the following attributes: [%s]." 416 371 (q element) attrs_str 417 - | Bad_attr_value { element; attr; value; reason } -> 372 + | `Attr (`Bad_value (`Elem element, `Attr attr, `Value value, `Reason reason)) -> 418 373 Printf.sprintf "Bad value %s for attribute %s on element %s: %s" 419 374 (q value) (q attr) (q element) reason 420 - | Bad_attr_value_generic { message } -> message 421 - | Duplicate_id { id } -> 375 + | `Attr (`Bad_value_generic (`Message message)) -> message 376 + | `Attr (`Duplicate_id (`Id id)) -> 422 377 Printf.sprintf "Duplicate ID %s." (q id) 423 - | Data_attr_invalid_name { reason } -> 378 + | `Attr (`Data_invalid_name (`Reason reason)) -> 424 379 Printf.sprintf "%s attribute names %s." (q "data-*") reason 425 - | Data_attr_uppercase -> 380 + | `Attr `Data_uppercase -> 426 381 Printf.sprintf "%s attributes must not have characters from the range %s\xe2\x80\xa6%s in the name." 427 382 (q "data-*") (q "A") (q "Z") 428 383 429 - | Obsolete_element { element; suggestion } -> 384 + (* Element errors *) 385 + | `Element (`Obsolete (`Elem element, `Suggestion suggestion)) -> 430 386 if suggestion = "" then 431 387 Printf.sprintf "The %s element is obsolete." (q element) 432 388 else 433 389 Printf.sprintf "The %s element is obsolete. %s" (q element) suggestion 434 - | Obsolete_attr { element; attr; suggestion } -> 390 + | `Element (`Obsolete_attr (`Elem element, `Attr attr, `Suggestion suggestion)) -> 435 391 let base = Printf.sprintf "The %s attribute on the %s element is obsolete." 436 392 (q attr) (q element) in 437 393 (match suggestion with Some s -> base ^ " " ^ s | None -> base) 438 - | Obsolete_global_attr { attr; suggestion } -> 394 + | `Element (`Obsolete_global_attr (`Attr attr, `Suggestion suggestion)) -> 439 395 Printf.sprintf "The %s attribute is obsolete. %s" (q attr) suggestion 440 - | Element_not_allowed_as_child { child; parent } -> 396 + | `Element (`Not_allowed_as_child (`Child child, `Parent parent)) -> 441 397 Printf.sprintf "Element %s not allowed as child of element %s in this context. (Suppressing further errors from this subtree.)" 442 398 (q child) (q parent) 443 - | Unknown_element { name } -> 399 + | `Element (`Unknown (`Elem name)) -> 444 400 Printf.sprintf "Unknown element %s." (q name) 445 - | Element_must_not_be_descendant { element; attr; ancestor } -> 401 + | `Element (`Must_not_descend (`Elem element, `Attr attr, `Ancestor ancestor)) -> 446 402 (match attr with 447 403 | Some a -> 448 404 Printf.sprintf "The element %s with the attribute %s must not appear as a descendant of the %s element." ··· 450 406 | None -> 451 407 Printf.sprintf "The element %s must not appear as a descendant of the %s element." 452 408 (q element) (q ancestor)) 453 - | Missing_required_child { parent; child } -> 409 + | `Element (`Missing_child (`Parent parent, `Child child)) -> 454 410 Printf.sprintf "Element %s is missing required child element %s." 455 411 (q parent) (q child) 456 - | Missing_required_child_one_of { parent; children } -> 412 + | `Element (`Missing_child_one_of (`Parent parent, `Children children)) -> 457 413 let children_str = String.concat ", " children in 458 414 Printf.sprintf "Element %s is missing one or more of the following child elements: [%s]." 459 415 (q parent) children_str 460 - | Missing_required_child_generic { parent } -> 416 + | `Element (`Missing_child_generic (`Parent parent)) -> 461 417 Printf.sprintf "Element %s is missing a required child element." (q parent) 462 - | Element_must_not_be_empty { element } -> 418 + | `Element (`Must_not_be_empty (`Elem element)) -> 463 419 Printf.sprintf "Element %s must not be empty." (q element) 464 - | Stray_start_tag { tag } -> 420 + | `Element (`Text_not_allowed (`Parent parent)) -> 421 + Printf.sprintf "Text not allowed in element %s in this context." (q parent) 422 + 423 + (* Tag errors *) 424 + | `Tag (`Stray_start (`Tag tag)) -> 465 425 Printf.sprintf "Stray start tag %s." (q tag) 466 - | Stray_end_tag { tag } -> 426 + | `Tag (`Stray_end (`Tag tag)) -> 467 427 Printf.sprintf "Stray end tag %s." (q tag) 468 - | End_tag_for_void_element { tag } -> 428 + | `Tag (`End_for_void (`Tag tag)) -> 469 429 Printf.sprintf "End tag %s." (q tag) 470 - | Self_closing_non_void -> 430 + | `Tag `Self_closing_non_void -> 471 431 Printf.sprintf "Self-closing syntax (%s) used on a non-void HTML element. Ignoring the slash and treating as a start tag." 472 432 (q "/>") 473 - | Text_not_allowed { parent } -> 474 - Printf.sprintf "Text not allowed in element %s in this context." (q parent) 433 + | `Tag (`Not_in_scope (`Tag tag)) -> 434 + Printf.sprintf "No %s element in scope but a %s end tag seen." 435 + (q tag) (q tag) 436 + | `Tag (`End_implied_open (`Tag tag)) -> 437 + Printf.sprintf "End tag %s implied, but there were open elements." 438 + (q tag) 439 + | `Tag (`Start_in_table (`Tag tag)) -> 440 + Printf.sprintf "Start tag %s seen in %s." (q tag) (q "table") 441 + | `Tag (`Bad_start_in (`Tag tag, `Context _)) -> 442 + Printf.sprintf "Bad start tag in %s in %s in %s." 443 + (q tag) (q "noscript") (q "head") 444 + | `Tag `Eof_with_open -> 445 + "End of file seen and there were open elements." 475 446 476 - | Div_child_of_dl_bad_role -> 477 - Printf.sprintf "A %s child of a %s element must not have any %s value other than %s or %s." 478 - (q "div") (q "dl") (q "role") (q "presentation") (q "none") 479 - | Li_bad_role_in_menu -> 480 - 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." 481 - (q "li") (q "role=menu") (q "role=menubar") (q "role") 482 - (q "group") (q "menuitem") (q "menuitemcheckbox") (q "menuitemradio") (q "separator") 483 - | Li_bad_role_in_tablist -> 484 - Printf.sprintf "An %s element that is a descendant of a %s element must not have any %s value other than %s." 485 - (q "li") (q "role=tablist") (q "role") (q "tab") 486 - | Li_bad_role_in_list -> 487 - 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." 488 - (q "li") (q "ul") (q "ol") (q "menu") (q "role") (q "role=list") (q "role") (q "listitem") 447 + (* Character reference errors *) 448 + | `Char_ref (`Forbidden_codepoint (`Codepoint codepoint)) -> 449 + Printf.sprintf "Forbidden code point U+%04x." codepoint 450 + | `Char_ref (`Control_char (`Codepoint codepoint)) -> 451 + Printf.sprintf "Character reference expands to a control character (U+%04x)." codepoint 452 + | `Char_ref (`Non_char (`Codepoint codepoint, `Astral astral)) -> 453 + if astral then 454 + Printf.sprintf "Character reference expands to an astral non-character (U+%05x)." codepoint 455 + else 456 + Printf.sprintf "Character reference expands to a non-character (U+%04x)." codepoint 457 + | `Char_ref `Unassigned -> 458 + "Character reference expands to a permanently unassigned code point." 459 + | `Char_ref `Zero -> 460 + "Character reference expands to zero." 461 + | `Char_ref `Out_of_range -> 462 + "Character reference outside the permissible Unicode range." 463 + | `Char_ref `Carriage_return -> 464 + "A numeric character reference expanded to carriage return." 489 465 490 - | Unnecessary_role { role; element = _; reason } -> 466 + (* ARIA errors *) 467 + | `Aria (`Unnecessary_role (`Role role, `Elem _, `Reason reason)) -> 491 468 Printf.sprintf "The %s role is unnecessary %s." 492 469 (q role) reason 493 - | Bad_role { element; role } -> 470 + | `Aria (`Bad_role (`Elem element, `Role role)) -> 494 471 Printf.sprintf "Bad value %s for attribute %s on element %s." 495 472 (q role) (q "role") (q element) 496 - | Aria_must_not_be_specified { attr; element; condition } -> 473 + | `Aria (`Must_not_specify (`Attr attr, `Elem element, `Condition condition)) -> 497 474 Printf.sprintf "The %s attribute must not be specified on any %s element unless %s." 498 475 (q attr) (q element) condition 499 - | Aria_must_not_be_used { attr; element; condition } -> 476 + | `Aria (`Must_not_use (`Attr attr, `Elem element, `Condition condition)) -> 500 477 Printf.sprintf "The %s attribute must not be used on an %s element which has %s." 501 478 (q attr) (q element) condition 502 - | Aria_should_not_be_used { attr; role } -> 479 + | `Aria (`Should_not_use (`Attr attr, `Role role)) -> 503 480 Printf.sprintf "The %s attribute should not be used on any element which has %s." 504 481 (q attr) (q ("role=" ^ role)) 505 - | Aria_hidden_on_body -> 482 + | `Aria `Hidden_on_body -> 506 483 Printf.sprintf "%s must not be used on the %s element." 507 484 (q "aria-hidden=true") (q "body") 508 - | Img_empty_alt_with_role -> 509 - Printf.sprintf "An %s element which has an %s attribute whose value is the empty string must not have a %s attribute." 510 - (q "img") (q "alt") (q "role") 511 - | Checkbox_button_needs_aria_pressed -> 512 - 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." 513 - (q "input") (q "type") (q "checkbox") (q "role") (q "button") (q "aria-pressed") 514 - | Tab_without_tabpanel -> 485 + | `Aria (`Unrecognized_role (`Token token)) -> 486 + Printf.sprintf "Discarding unrecognized token %s from value of attribute %s. Browsers ignore any token that is not a defined ARIA non-abstract role." 487 + (q token) (q "role") 488 + | `Aria `Tab_without_tabpanel -> 515 489 Printf.sprintf "Every active %s element must have a corresponding %s element." 516 490 (q "role=tab") (q "role=tabpanel") 517 - | Multiple_main_visible -> 491 + | `Aria `Multiple_main -> 518 492 Printf.sprintf "A document should not include more than one visible element with %s." 519 493 (q "role=main") 520 - | Discarding_unrecognized_role { token } -> 521 - Printf.sprintf "Discarding unrecognized token %s from value of attribute %s. Browsers ignore any token that is not a defined ARIA non-abstract role." 522 - (q token) (q "role") 523 494 524 - | Img_missing_alt -> 525 - Printf.sprintf "An %s element must have an %s attribute, except under certain conditions. For details, consult guidance on providing text alternatives for images." 526 - (q "img") (q "alt") 527 - | Img_missing_src_or_srcset -> 528 - Printf.sprintf "Element %s is missing one or more of the following attributes: [src, srcset]." 529 - (q "img") 530 - | Option_empty_without_label -> 531 - Printf.sprintf "Element %s without attribute %s must not be empty." 532 - (q "option") (q "label") 533 - | Bdo_missing_dir -> 534 - Printf.sprintf "Element %s must have attribute %s." (q "bdo") (q "dir") 535 - | Bdo_dir_auto -> 536 - Printf.sprintf "The value of %s attribute for the %s element must not be %s." 537 - (q "dir") (q "bdo") (q "auto") 538 - | Base_missing_href_or_target -> 539 - Printf.sprintf "Element %s is missing one or more of the following attributes: [href, target]." 540 - (q "base") 541 - | Base_after_link_script -> 542 - Printf.sprintf "The %s element must come before any %s or %s elements in the document." 543 - (q "base") (q "link") (q "script") 544 - | Link_missing_href -> 545 - Printf.sprintf "A %s element must have an %s or %s attribute, or both." 546 - (q "link") (q "href") (q "imagesrcset") 547 - | Link_as_requires_preload -> 548 - Printf.sprintf "A %s element with an %s attribute must have a %s attribute that contains the value %s or the value %s." 549 - (q "link") (q "as") (q "rel") (q "preload") (q "modulepreload") 550 - | Link_imagesrcset_requires_as_image -> 551 - Printf.sprintf "A %s element with an %s attribute must have an %s attribute with value %s." 552 - (q "link") (q "imagesrcset") (q "as") (q "image") 553 - | Img_ismap_needs_a_href -> 554 - Printf.sprintf "The %s element with the %s attribute set must have an %s ancestor with the %s attribute." 555 - (q "img") (q "ismap") (q "a") (q "href") 556 - | Sizes_without_srcset -> 557 - Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified." 558 - (q "sizes") (q "srcset") 559 - | Imagesizes_without_imagesrcset -> 560 - Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified." 561 - (q "imagesizes") (q "imagesrcset") 562 - | Srcset_w_without_sizes -> 563 - Printf.sprintf "When the %s attribute has any image candidate string with a width descriptor, the %s attribute must also be specified." 564 - (q "srcset") (q "sizes") 565 - | Source_missing_srcset -> 566 - Printf.sprintf "Element %s is missing required attribute %s." 567 - (q "source") (q "srcset") 568 - | Source_needs_media_or_type -> 569 - 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." 570 - (q "source") (q "source") (q "img") (q "srcset") (q "media") (q "type") 571 - | Picture_missing_img -> 572 - Printf.sprintf "Element %s is missing required child element %s." 573 - (q "picture") (q "img") 574 - | Map_id_name_mismatch -> 575 - Printf.sprintf "The %s attribute on a %s element must have an the same value as the %s attribute." 576 - (q "id") (q "map") (q "name") 577 - | List_attr_requires_datalist -> 578 - Printf.sprintf "The %s attribute of the %s element must refer to a %s element." 579 - (q "list") (q "input") (q "datalist") 580 - | Input_list_not_allowed -> 581 - 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." 582 - (q "list") (q "color") (q "date") (q "datetime-local") (q "email") (q "month") 583 - (q "number") (q "range") (q "search") (q "tel") (q "text") (q "time") (q "url") (q "week") 584 - | Label_too_many_labelable -> 585 - Printf.sprintf "The %s element may contain at most one %s, %s, %s, %s, %s, %s, or %s descendant." 586 - (q "label") (q "button") (q "input") (q "meter") (q "output") (q "progress") (q "select") (q "textarea") 587 - | Label_for_id_mismatch -> 588 - Printf.sprintf "Any %s descendant of a %s element with a %s attribute must have an ID value that matches that %s attribute." 589 - (q "input") (q "label") (q "for") (q "for") 590 - | Role_on_label_ancestor -> 591 - Printf.sprintf "The %s attribute must not be used on any %s element that is an ancestor of a labelable element." 592 - (q "role") (q "label") 593 - | Role_on_label_for -> 594 - Printf.sprintf "The %s attribute must not be used on any %s element that is associated with a labelable element." 595 - (q "role") (q "label") 596 - | Aria_label_on_label_for -> 597 - Printf.sprintf "The %s attribute must not be used on any %s element that is associated with a labelable element." 598 - (q "aria-label") (q "label") 599 - | Input_value_constraint { constraint_type } -> constraint_type 600 - | Summary_missing_role -> 601 - Printf.sprintf "Element %s is missing required attribute %s." 602 - (q "summary") (q "role") 603 - | Summary_missing_attrs -> 604 - Printf.sprintf "Element %s is missing one or more of the following attributes: [aria-checked, aria-level, role]." 605 - (q "summary") 606 - | Summary_role_not_allowed -> 607 - Printf.sprintf "The %s attribute must not be used on any %s element that is a summary for its parent %s element." 608 - (q "role") (q "summary") (q "details") 609 - | Autocomplete_webauthn_on_select -> 610 - Printf.sprintf "The value of the %s attribute for the %s element must not contain %s." 611 - (q "autocomplete") (q "select") (q "webauthn") 612 - | Commandfor_invalid_target -> 613 - 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." 614 - (q "commandfor") (q "button") (q "button") (q "commandfor") 495 + (* List item role errors *) 496 + | `Li_role `Div_in_dl_bad_role -> 497 + Printf.sprintf "A %s child of a %s element must not have any %s value other than %s or %s." 498 + (q "div") (q "dl") (q "role") (q "presentation") (q "none") 499 + | `Li_role `Li_bad_role_in_menu -> 500 + 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." 501 + (q "li") (q "role=menu") (q "role=menubar") (q "role") 502 + (q "group") (q "menuitem") (q "menuitemcheckbox") (q "menuitemradio") (q "separator") 503 + | `Li_role `Li_bad_role_in_tablist -> 504 + Printf.sprintf "An %s element that is a descendant of a %s element must not have any %s value other than %s." 505 + (q "li") (q "role=tablist") (q "role") (q "tab") 506 + | `Li_role `Li_bad_role_in_list -> 507 + 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." 508 + (q "li") (q "ul") (q "ol") (q "menu") (q "role") (q "role=list") (q "role") (q "listitem") 615 509 616 - | Forbidden_codepoint { codepoint } -> 617 - Printf.sprintf "Forbidden code point U+%04x." codepoint 618 - | Char_ref_control { codepoint } -> 619 - Printf.sprintf "Character reference expands to a control character (U+%04x)." codepoint 620 - | Char_ref_non_char { codepoint; astral } -> 621 - if astral then 622 - Printf.sprintf "Character reference expands to an astral non-character (U+%05x)." codepoint 623 - else 624 - Printf.sprintf "Character reference expands to a non-character (U+%04x)." codepoint 625 - | Char_ref_unassigned -> 626 - "Character reference expands to a permanently unassigned code point." 627 - | Char_ref_zero -> 628 - "Character reference expands to zero." 629 - | Char_ref_out_of_range -> 630 - "Character reference outside the permissible Unicode range." 631 - | Numeric_char_ref_carriage_return -> 632 - "A numeric character reference expanded to carriage return." 633 - | End_of_file_with_open_elements -> 634 - "End of file seen and there were open elements." 635 - | No_element_in_scope { tag } -> 636 - Printf.sprintf "No %s element in scope but a %s end tag seen." 637 - (q tag) (q tag) 638 - | End_tag_implied_open_elements { tag } -> 639 - Printf.sprintf "End tag %s implied, but there were open elements." 640 - (q tag) 641 - | Start_tag_in_table { tag } -> 642 - Printf.sprintf "Start tag %s seen in %s." (q tag) (q "table") 643 - | Bad_start_tag_in { tag; context = _ } -> 644 - Printf.sprintf "Bad start tag in %s in %s in %s." 645 - (q tag) (q "noscript") (q "head") 646 - 647 - | Table_row_no_cells { row } -> 510 + (* Table errors *) 511 + | `Table (`Row_no_cells (`Row row)) -> 648 512 Printf.sprintf "Row %d of an implicit row group has no cells beginning on it." row 649 - | Table_cell_overlap -> 513 + | `Table `Cell_overlap -> 650 514 "Table cell is overlapped by later table cell." 651 - | Table_cell_spans_rowgroup -> 515 + | `Table `Cell_spans_rowgroup -> 652 516 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." 653 517 (q "tbody") 654 - | Table_column_no_cells { column; element } -> 518 + | `Table (`Column_no_cells (`Column column, `Elem element)) -> 655 519 Printf.sprintf "Table column %d established by element %s has no cells beginning in it." 656 520 column (q element) 657 521 658 - | Missing_lang_attr -> 522 + (* I18n errors *) 523 + | `I18n `Missing_lang -> 659 524 Printf.sprintf "Consider adding a %s attribute to the %s start tag to declare the language of this document." 660 525 (q "lang") (q "html") 661 - | Wrong_lang { detected; declared; suggested } -> 526 + | `I18n (`Wrong_lang (`Detected detected, `Declared declared, `Suggested suggested)) -> 662 527 Printf.sprintf "This document appears to be written in %s but the %s start tag has %s. Consider using %s (or variant) instead." 663 528 detected (q "html") (q ("lang=\"" ^ declared ^ "\"")) (q ("lang=\"" ^ suggested ^ "\"")) 664 - | Missing_dir_rtl { language } -> 529 + | `I18n (`Missing_dir_rtl (`Language language)) -> 665 530 Printf.sprintf "This document appears to be written in %s. Consider adding %s to the %s start tag." 666 531 language (q "dir=\"rtl\"") (q "html") 667 - | Wrong_dir { language; declared } -> 532 + | `I18n (`Wrong_dir (`Language language, `Declared declared)) -> 668 533 Printf.sprintf "This document appears to be written in %s but the %s start tag has %s. Consider using %s instead." 669 534 language (q "html") (q ("dir=\"" ^ declared ^ "\"")) (q "dir=\"rtl\"") 670 - | Xml_lang_without_lang -> 535 + | `I18n `Xml_lang_without_lang -> 671 536 Printf.sprintf "When the attribute %s in no namespace is specified, the element must also have the attribute %s present with the same value." 672 537 (q "xml:lang") (q "lang") 673 - | Xml_lang_lang_mismatch -> 538 + | `I18n `Xml_lang_mismatch -> 674 539 Printf.sprintf "The %s and %s attributes must have the same value." 675 540 (q "xml:lang") (q "lang") 676 - 677 - | Not_nfc { replacement } -> 541 + | `I18n (`Not_nfc (`Replacement replacement)) -> 678 542 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.)" 679 543 (q replacement) 680 544 681 - | Multiple_h1 -> 682 - 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)." 683 - (q "h1") (q "h1") (q "headingoffset") (q "h1") 684 - | Multiple_autofocus -> 685 - Printf.sprintf "There must not be two elements with the same %s that both have the %s attribute specified." 686 - (q "nearest ancestor autofocus scoping root element") (q "autofocus") 687 - 688 - | Importmap_invalid_json -> 545 + (* Import map errors *) 546 + | `Importmap `Invalid_json -> 689 547 Printf.sprintf "A script %s with a %s attribute whose value is %s must have valid JSON content." 690 548 (q "script") (q "type") (q "importmap") 691 - | Importmap_invalid_root -> 549 + | `Importmap `Invalid_root -> 692 550 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." 693 551 (q "script") (q "type") (q "importmap") (q "imports") (q "scopes") (q "integrity") 694 - | Importmap_imports_not_object -> 552 + | `Importmap `Imports_not_object -> 695 553 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." 696 554 (q "imports") (q "script") (q "type") (q "importmap") 697 - | Importmap_empty_key -> 555 + | `Importmap `Empty_key -> 698 556 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." 699 557 (q "imports") (q "script") (q "type") (q "importmap") 700 - | Importmap_non_string_value -> 558 + | `Importmap `Non_string_value -> 701 559 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." 702 560 (q "imports") (q "script") (q "type") (q "importmap") 703 - | Importmap_key_trailing_slash -> 561 + | `Importmap `Key_trailing_slash -> 704 562 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." 705 563 (q "imports") (q "script") (q "type") (q "importmap") (q "/") (q "/") 706 - | Importmap_scopes_not_object -> 564 + | `Importmap `Scopes_not_object -> 707 565 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." 708 566 (q "scopes") (q "script") (q "type") (q "importmap") 709 - | Importmap_scopes_values_not_object -> 567 + | `Importmap `Scopes_values_not_object -> 710 568 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." 711 569 (q "scopes") (q "script") (q "type") (q "importmap") 712 - | Importmap_scopes_invalid_url -> 570 + | `Importmap `Scopes_invalid_url -> 713 571 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." 714 572 (q "scopes") (q "script") (q "type") (q "importmap") 715 - | Importmap_scopes_value_invalid_url -> 573 + | `Importmap `Scopes_value_invalid_url -> 716 574 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." 717 575 (q "scopes") (q "script") (q "type") (q "importmap") 718 576 719 - | Style_type_invalid -> 720 - 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.)" 721 - (q "type") (q "style") (q "text/css") 577 + (* Image errors *) 578 + | `Img `Missing_alt -> 579 + Printf.sprintf "An %s element must have an %s attribute, except under certain conditions. For details, consult guidance on providing text alternatives for images." 580 + (q "img") (q "alt") 581 + | `Img `Missing_src_or_srcset -> 582 + Printf.sprintf "Element %s is missing one or more of the following attributes: [src, srcset]." 583 + (q "img") 584 + | `Img `Empty_alt_with_role -> 585 + Printf.sprintf "An %s element which has an %s attribute whose value is the empty string must not have a %s attribute." 586 + (q "img") (q "alt") (q "role") 587 + | `Img `Ismap_needs_href -> 588 + Printf.sprintf "The %s element with the %s attribute set must have an %s ancestor with the %s attribute." 589 + (q "img") (q "ismap") (q "a") (q "href") 590 + 591 + (* Link errors *) 592 + | `Link `Missing_href -> 593 + Printf.sprintf "A %s element must have an %s or %s attribute, or both." 594 + (q "link") (q "href") (q "imagesrcset") 595 + | `Link `As_requires_preload -> 596 + Printf.sprintf "A %s element with an %s attribute must have a %s attribute that contains the value %s or the value %s." 597 + (q "link") (q "as") (q "rel") (q "preload") (q "modulepreload") 598 + | `Link `Imagesrcset_requires_as_image -> 599 + Printf.sprintf "A %s element with an %s attribute must have an %s attribute with value %s." 600 + (q "link") (q "imagesrcset") (q "as") (q "image") 601 + 602 + (* Label errors *) 603 + | `Label `Too_many_labelable -> 604 + Printf.sprintf "The %s element may contain at most one %s, %s, %s, %s, %s, %s, or %s descendant." 605 + (q "label") (q "button") (q "input") (q "meter") (q "output") (q "progress") (q "select") (q "textarea") 606 + | `Label `For_id_mismatch -> 607 + Printf.sprintf "Any %s descendant of a %s element with a %s attribute must have an ID value that matches that %s attribute." 608 + (q "input") (q "label") (q "for") (q "for") 609 + | `Label `Role_on_ancestor -> 610 + Printf.sprintf "The %s attribute must not be used on any %s element that is an ancestor of a labelable element." 611 + (q "role") (q "label") 612 + | `Label `Role_on_for -> 613 + Printf.sprintf "The %s attribute must not be used on any %s element that is associated with a labelable element." 614 + (q "role") (q "label") 615 + | `Label `Aria_label_on_for -> 616 + Printf.sprintf "The %s attribute must not be used on any %s element that is associated with a labelable element." 617 + (q "aria-label") (q "label") 722 618 723 - | Headingoffset_invalid -> 724 - Printf.sprintf "The value of the %s attribute must be a number between %s and %s." 725 - (q "headingoffset") (q "0") (q "8") 619 + (* Input errors *) 620 + | `Input `Checkbox_needs_aria_pressed -> 621 + 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." 622 + (q "input") (q "type") (q "checkbox") (q "role") (q "button") (q "aria-pressed") 623 + | `Input (`Value_constraint (`Constraint constraint_type)) -> constraint_type 624 + | `Input `List_not_allowed -> 625 + 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." 626 + (q "list") (q "color") (q "date") (q "datetime-local") (q "email") (q "month") 627 + (q "number") (q "range") (q "search") (q "tel") (q "text") (q "time") (q "url") (q "week") 628 + | `Input `List_requires_datalist -> 629 + Printf.sprintf "The %s attribute of the %s element must refer to a %s element." 630 + (q "list") (q "input") (q "datalist") 726 631 727 - | Media_empty -> 728 - Printf.sprintf "Value of %s attribute here must not be empty." (q "media") 729 - | Media_all -> 730 - Printf.sprintf "Value of %s attribute here must not be %s." (q "media") (q "all") 632 + (* Srcset errors *) 633 + | `Srcset `Sizes_without_srcset -> 634 + Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified." 635 + (q "sizes") (q "srcset") 636 + | `Srcset `Imagesizes_without_imagesrcset -> 637 + Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified." 638 + (q "imagesizes") (q "imagesrcset") 639 + | `Srcset `W_without_sizes -> 640 + Printf.sprintf "When the %s attribute has any image candidate string with a width descriptor, the %s attribute must also be specified." 641 + (q "srcset") (q "sizes") 642 + | `Srcset `Source_missing_srcset -> 643 + Printf.sprintf "Element %s is missing required attribute %s." 644 + (q "source") (q "srcset") 645 + | `Srcset `Source_needs_media_or_type -> 646 + 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." 647 + (q "source") (q "source") (q "img") (q "srcset") (q "media") (q "type") 648 + | `Srcset `Picture_missing_img -> 649 + Printf.sprintf "Element %s is missing required child element %s." 650 + (q "picture") (q "img") 731 651 732 - | Svg_deprecated_attr { attr; element } -> 652 + (* SVG errors *) 653 + | `Svg (`Deprecated_attr (`Attr attr, `Elem element)) -> 733 654 Printf.sprintf "Attribute %s not allowed on element %s at this point." 734 655 (q attr) (q element) 735 - | Missing_required_svg_attr { element; attr } -> 656 + | `Svg (`Missing_attr (`Elem element, `Attr attr)) -> 736 657 Printf.sprintf "Element %s is missing required attribute %s." 737 658 (q element) (q attr) 738 659 739 - | Generic { message } -> message 660 + (* Misc errors *) 661 + | `Misc `Option_empty_without_label -> 662 + Printf.sprintf "Element %s without attribute %s must not be empty." 663 + (q "option") (q "label") 664 + | `Misc `Bdo_missing_dir -> 665 + Printf.sprintf "Element %s must have attribute %s." (q "bdo") (q "dir") 666 + | `Misc `Bdo_dir_auto -> 667 + Printf.sprintf "The value of %s attribute for the %s element must not be %s." 668 + (q "dir") (q "bdo") (q "auto") 669 + | `Misc `Base_missing_href_or_target -> 670 + Printf.sprintf "Element %s is missing one or more of the following attributes: [href, target]." 671 + (q "base") 672 + | `Misc `Base_after_link_script -> 673 + Printf.sprintf "The %s element must come before any %s or %s elements in the document." 674 + (q "base") (q "link") (q "script") 675 + | `Misc `Map_id_name_mismatch -> 676 + Printf.sprintf "The %s attribute on a %s element must have an the same value as the %s attribute." 677 + (q "id") (q "map") (q "name") 678 + | `Misc `Summary_missing_role -> 679 + Printf.sprintf "Element %s is missing required attribute %s." 680 + (q "summary") (q "role") 681 + | `Misc `Summary_missing_attrs -> 682 + Printf.sprintf "Element %s is missing one or more of the following attributes: [aria-checked, aria-level, role]." 683 + (q "summary") 684 + | `Misc `Summary_role_not_allowed -> 685 + Printf.sprintf "The %s attribute must not be used on any %s element that is a summary for its parent %s element." 686 + (q "role") (q "summary") (q "details") 687 + | `Misc `Autocomplete_webauthn_on_select -> 688 + Printf.sprintf "The value of the %s attribute for the %s element must not contain %s." 689 + (q "autocomplete") (q "select") (q "webauthn") 690 + | `Misc `Commandfor_invalid_target -> 691 + 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." 692 + (q "commandfor") (q "button") (q "button") (q "commandfor") 693 + | `Misc `Style_type_invalid -> 694 + 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.)" 695 + (q "type") (q "style") (q "text/css") 696 + | `Misc `Headingoffset_invalid -> 697 + Printf.sprintf "The value of the %s attribute must be a number between %s and %s." 698 + (q "headingoffset") (q "0") (q "8") 699 + | `Misc `Media_empty -> 700 + Printf.sprintf "Value of %s attribute here must not be empty." (q "media") 701 + | `Misc `Media_all -> 702 + Printf.sprintf "Value of %s attribute here must not be %s." (q "media") (q "all") 703 + | `Misc `Multiple_h1 -> 704 + 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)." 705 + (q "h1") (q "h1") (q "headingoffset") (q "h1") 706 + | `Misc `Multiple_autofocus -> 707 + Printf.sprintf "There must not be two elements with the same %s that both have the %s attribute specified." 708 + (q "nearest ancestor autofocus scoping root element") (q "autofocus") 709 + 710 + (* Generic *) 711 + | `Generic message -> message
+711 -138
lib/html5_checker/error_code.mli
··· 1 1 (** Typed error codes for HTML5 validation messages. 2 2 3 - This module defines a comprehensive variant type for all validation errors, 4 - ensuring exact message matching with the Nu HTML Validator test suite. *) 3 + This module defines a comprehensive hierarchy of validation errors using 4 + polymorphic variants, organized by error category. Each error type is 5 + documented with the specific HTML5 conformance requirement it represents. 6 + 7 + The error hierarchy is: 8 + - {!t} is the top-level type containing all errors wrapped by category 9 + - Each category (e.g., {!attr_error}, {!aria_error}) groups related errors 10 + - Inline descriptors like [[\`Attr of string]] provide self-documenting parameters 11 + 12 + {2 Example Usage} 13 + 14 + {[ 15 + (* Category-level matching *) 16 + let is_accessibility_error = function 17 + | `Aria _ | `Li_role _ -> true 18 + | _ -> false 19 + 20 + (* Fine-grained matching *) 21 + match err with 22 + | `Attr (`Duplicate_id (`Id id)) -> handle_duplicate id 23 + | `Img `Missing_alt -> suggest_alt_text () 24 + | _ -> default_handler err 25 + ]} 26 + *) 5 27 6 - (** Severity level of a validation message *) 28 + (** {1 Severity} *) 29 + 30 + (** Severity level of a validation message. 31 + - [Error]: Conformance error that must be fixed 32 + - [Warning]: Likely problem that should be reviewed 33 + - [Info]: Suggestion for best practices *) 7 34 type severity = Error | Warning | Info 8 35 9 - (** Typed error codes with associated data *) 10 - type t = 11 - (* Attribute Errors *) 12 - | Attr_not_allowed_on_element of { attr: string; element: string } 13 - | Attr_not_allowed_here of { attr: string } 14 - | Attr_not_allowed_when of { attr: string; element: string; condition: string } 15 - | Missing_required_attr of { element: string; attr: string } 16 - | Missing_required_attr_one_of of { element: string; attrs: string list } 17 - | Bad_attr_value of { element: string; attr: string; value: string; reason: string } 18 - | Bad_attr_value_generic of { message: string } 19 - | Duplicate_id of { id: string } 20 - | Data_attr_invalid_name of { reason: string } 21 - | Data_attr_uppercase 36 + (** {1 Attribute Errors} 37 + 38 + Errors related to HTML attributes: disallowed attributes, missing required 39 + attributes, invalid attribute values, and duplicate IDs. *) 40 + 41 + (** Attribute-related validation errors. 42 + 43 + These errors occur when attributes violate HTML5 content model rules: 44 + - Attributes used on elements where they're not allowed 45 + - Required attributes that are missing 46 + - Attribute values that don't match expected formats 47 + - Duplicate ID attributes within a document *) 48 + type attr_error = [ 49 + | `Not_allowed of [`Attr of string] * [`Elem of string] 50 + (** Attribute is not in the set of allowed attributes for this element. 51 + Per HTML5 spec, each element has a defined set of content attributes; 52 + using attributes outside this set is a conformance error. 53 + Example: [type] attribute on a [<div>] element. *) 54 + 55 + | `Not_allowed_here of [`Attr of string] 56 + (** Attribute is valid on this element type but not in this context. 57 + Some attributes are only allowed under specific conditions, such as 58 + the [download] attribute which requires specific ancestor elements. *) 59 + 60 + | `Not_allowed_when of [`Attr of string] * [`Elem of string] * [`Condition of string] 61 + (** Attribute conflicts with another attribute or element state. 62 + Example: [readonly] and [disabled] together, or [multiple] on 63 + certain input types where it's not supported. *) 64 + 65 + | `Missing of [`Elem of string] * [`Attr of string] 66 + (** Element is missing a required attribute. 67 + Per HTML5, certain elements have required attributes for conformance. 68 + Example: [<img>] requires [src] or [srcset], [<input>] requires [type]. *) 69 + 70 + | `Missing_one_of of [`Elem of string] * [`Attrs of string list] 71 + (** Element must have at least one of the listed attributes. 72 + Some elements require at least one from a set of attributes. 73 + Example: [<base>] needs [href] or [target] (or both). *) 74 + 75 + | `Bad_value of [`Elem of string] * [`Attr of string] * [`Value of string] * [`Reason of string] 76 + (** Attribute value doesn't match the expected format or enumeration. 77 + HTML5 defines specific value spaces for many attributes (enumerations, 78 + URLs, integers, etc.). This error indicates the value is malformed. *) 79 + 80 + | `Bad_value_generic of [`Message of string] 81 + (** Generic bad attribute value with custom message. 82 + Used when the specific validation failure requires a custom explanation 83 + that doesn't fit the standard bad value template. *) 84 + 85 + | `Duplicate_id of [`Id of string] 86 + (** Document contains multiple elements with the same ID. 87 + Per HTML5, the [id] attribute must be unique within a document. 88 + Duplicate IDs cause problems with fragment navigation, label 89 + association, and JavaScript DOM queries. *) 90 + 91 + | `Data_invalid_name of [`Reason of string] 92 + (** Custom data attribute name violates naming rules. 93 + [data-*] attribute names must be valid XML NCNames (no colons, 94 + must start with letter or underscore). The reason explains 95 + the specific naming violation. *) 96 + 97 + | `Data_uppercase 98 + (** Custom data attribute name contains uppercase letters. 99 + [data-*] attribute names must not contain ASCII uppercase letters 100 + (A-Z) per HTML5. Use lowercase with hyphens instead. *) 101 + ] 102 + 103 + (** {1 Element Structure Errors} 104 + 105 + Errors related to element usage, nesting, and content models. *) 106 + 107 + (** Element structure validation errors. 108 + 109 + These errors occur when elements violate HTML5 content model rules: 110 + - Obsolete elements that should be replaced 111 + - Elements used in wrong contexts (invalid parent/child relationships) 112 + - Missing required child elements 113 + - Empty elements that must have content *) 114 + type element_error = [ 115 + | `Obsolete of [`Elem of string] * [`Suggestion of string] 116 + (** Element is obsolete and should not be used. 117 + HTML5 obsoletes certain elements from HTML4 (e.g., [<font>], [<center>]). 118 + The suggestion provides the recommended modern alternative. *) 119 + 120 + | `Obsolete_attr of [`Elem of string] * [`Attr of string] * [`Suggestion of string option] 121 + (** Attribute on this element is obsolete. 122 + Some attributes are obsolete on specific elements but may be valid 123 + elsewhere. Example: [align] on [<table>] (use CSS instead). *) 124 + 125 + | `Obsolete_global_attr of [`Attr of string] * [`Suggestion of string] 126 + (** Global attribute is obsolete on all elements. 127 + Attributes like [bgcolor] are obsolete everywhere in HTML5. *) 128 + 129 + | `Not_allowed_as_child of [`Child of string] * [`Parent of string] 130 + (** Element cannot be a child of the specified parent. 131 + HTML5 defines content models for each element specifying which 132 + children are allowed. Example: [<div>] inside [<p>] is invalid. *) 133 + 134 + | `Unknown of [`Elem of string] 135 + (** Element name is not recognized. 136 + The element is not defined in HTML5, SVG, or MathML specs. 137 + May be a typo or a custom element without hyphen. *) 138 + 139 + | `Must_not_descend of [`Elem of string] * [`Attr of string option] * [`Ancestor of string] 140 + (** Element must not appear as descendant of the specified ancestor. 141 + Some elements have restrictions on their ancestry regardless of 142 + direct parent. Example: [<form>] cannot be nested inside [<form>]. 143 + The optional attribute indicates a conditional restriction. *) 144 + 145 + | `Missing_child of [`Parent of string] * [`Child of string] 146 + (** Parent element is missing a required child element. 147 + Some elements must contain specific children for conformance. 148 + Example: [<dl>] requires [<dt>] and [<dd>] children. *) 149 + 150 + | `Missing_child_one_of of [`Parent of string] * [`Children of string list] 151 + (** Parent must contain at least one of the listed child elements. 152 + Example: [<ruby>] must contain [<rt>] or [<rp>]. *) 153 + 154 + | `Missing_child_generic of [`Parent of string] 155 + (** Parent is missing an unspecified required child. 156 + Used when the required child depends on context. *) 157 + 158 + | `Must_not_be_empty of [`Elem of string] 159 + (** Element must have content and cannot be empty. 160 + Some elements require text or child element content. 161 + Example: [<title>] must not be empty. *) 162 + 163 + | `Text_not_allowed of [`Parent of string] 164 + (** Text content is not allowed in this element. 165 + Some elements only allow element children, not text. 166 + Example: [<table>] cannot contain direct text children. *) 167 + ] 168 + 169 + (** {1 Tag and Parse Errors} 170 + 171 + Errors from the parsing phase related to tags and document structure. *) 172 + 173 + (** Tag-level parse errors. 174 + 175 + These errors occur during HTML parsing when the parser encounters 176 + problematic tag structures or reaches end-of-file unexpectedly. *) 177 + type tag_error = [ 178 + | `Stray_start of [`Tag of string] 179 + (** Start tag appears in a position where it's not allowed. 180 + The parser encountered an opening tag that cannot appear in 181 + the current insertion mode. Example: [<tr>] outside [<table>]. *) 182 + 183 + | `Stray_end of [`Tag of string] 184 + (** End tag appears without a matching start tag. 185 + The parser encountered a closing tag with no corresponding 186 + open element in scope. *) 187 + 188 + | `End_for_void of [`Tag of string] 189 + (** End tag for a void element that cannot have one. 190 + Void elements ([<br>], [<img>], etc.) cannot have end tags 191 + in HTML5. Example: [</br>] is invalid. *) 192 + 193 + | `Self_closing_non_void 194 + (** Self-closing syntax [/>] used on non-void HTML element. 195 + In HTML5, [/>] is only meaningful on void elements and 196 + foreign (SVG/MathML) elements. On other elements it's ignored. *) 197 + 198 + | `Not_in_scope of [`Tag of string] 199 + (** End tag seen but no matching element in scope. 200 + The parser found a closing tag but the element isn't in the 201 + current scope (may be blocked by formatting elements). *) 202 + 203 + | `End_implied_open of [`Tag of string] 204 + (** End tag implied closing of other open elements. 205 + The parser had to implicitly close elements to process this 206 + end tag, indicating mismatched nesting. *) 207 + 208 + | `Start_in_table of [`Tag of string] 209 + (** Start tag appeared inside table where it's foster-parented. 210 + When certain tags appear in table context, they're moved 211 + outside the table (foster parenting), indicating malformed markup. *) 212 + 213 + | `Bad_start_in of [`Tag of string] * [`Context of string] 214 + (** Start tag appeared in invalid context. 215 + Generic error for tags in wrong parsing contexts. *) 216 + 217 + | `Eof_with_open 218 + (** End of file reached with unclosed elements. 219 + The document ended with elements still open on the stack, 220 + indicating missing closing tags. *) 221 + ] 222 + 223 + (** Character reference errors. 224 + 225 + These errors occur when character references (like [&amp;] or [&#65;]) 226 + expand to problematic Unicode code points. *) 227 + type char_ref_error = [ 228 + | `Forbidden_codepoint of [`Codepoint of int] 229 + (** Character reference expands to a forbidden code point. 230 + Certain code points are forbidden in HTML documents (e.g., 231 + NULL U+0000, noncharacters). These cannot appear even via 232 + character references. *) 233 + 234 + | `Control_char of [`Codepoint of int] 235 + (** Character reference expands to a control character. 236 + C0 and C1 control characters (except tab, newline, etc.) 237 + are problematic and trigger this warning. *) 238 + 239 + | `Non_char of [`Codepoint of int] * [`Astral of bool] 240 + (** Character reference expands to a Unicode noncharacter. 241 + Noncharacters (like U+FFFE, U+FFFF) are reserved and 242 + should not appear in documents. Astral flag indicates 243 + if it's in the supplementary planes. *) 244 + 245 + | `Unassigned 246 + (** Character reference expands to permanently unassigned code point. 247 + The referenced code point will never be assigned a character. *) 248 + 249 + | `Zero 250 + (** Character reference expands to U+0000 (NULL). 251 + NULL is replaced with U+FFFD (replacement character) per HTML5. *) 252 + 253 + | `Out_of_range 254 + (** Character reference value exceeds Unicode maximum. 255 + Numeric character references must be <= U+10FFFF. *) 256 + 257 + | `Carriage_return 258 + (** Numeric character reference expanded to carriage return. 259 + CR (U+000D) via numeric reference is replaced with LF. *) 260 + ] 261 + 262 + (** {1 ARIA and Accessibility Errors} 263 + 264 + Errors related to WAI-ARIA attributes and accessibility conformance. *) 265 + 266 + (** ARIA and role validation errors. 267 + 268 + These errors ensure proper usage of WAI-ARIA attributes and roles 269 + for accessibility. Incorrect ARIA can make content less accessible 270 + than having no ARIA at all. *) 271 + type aria_error = [ 272 + | `Unnecessary_role of [`Role of string] * [`Elem of string] * [`Reason of string] 273 + (** Role is redundant because element has implicit role. 274 + Many HTML elements have implicit ARIA roles; explicitly setting 275 + the same role is unnecessary. Example: [role="button"] on [<button>]. *) 276 + 277 + | `Bad_role of [`Elem of string] * [`Role of string] 278 + (** Role value is invalid or not allowed on this element. 279 + The role is either not a valid ARIA role token or is not 280 + permitted on this particular element type. *) 281 + 282 + | `Must_not_specify of [`Attr of string] * [`Elem of string] * [`Condition of string] 283 + (** ARIA attribute must not be specified in this situation. 284 + Some ARIA attributes are prohibited on certain elements unless 285 + specific conditions are met. *) 286 + 287 + | `Must_not_use of [`Attr of string] * [`Elem of string] * [`Condition of string] 288 + (** ARIA attribute must not be used with this element configuration. 289 + The attribute conflicts with another attribute or state of the element. *) 290 + 291 + | `Should_not_use of [`Attr of string] * [`Role of string] 292 + (** ARIA attribute should not be used with this role (warning). 293 + While not strictly invalid, the attribute is discouraged 294 + with this role as it may cause confusion. *) 295 + 296 + | `Hidden_on_body 297 + (** [aria-hidden="true"] used on body element. 298 + Hiding the entire document from assistive technology is 299 + almost certainly an error. *) 300 + 301 + | `Unrecognized_role of [`Token of string] 302 + (** Unrecognized role token was discarded. 303 + The role attribute contained a token that isn't a valid 304 + ARIA role. Browsers ignore unknown role tokens. *) 305 + 306 + | `Tab_without_tabpanel 307 + (** Tab element has no corresponding tabpanel. 308 + Each [role="tab"] should control a [role="tabpanel"]. 309 + Missing tabpanels indicate incomplete tab interface. *) 310 + 311 + | `Multiple_main 312 + (** Document has multiple visible main landmarks. 313 + Only one visible [role="main"] or [<main>] should exist 314 + per document for proper landmark navigation. *) 315 + ] 316 + 317 + (** List item role constraint errors. 318 + 319 + Special ARIA role restrictions on [<li>] elements and [<div>] 320 + children of [<dl>] elements. *) 321 + type li_role_error = [ 322 + | `Div_in_dl_bad_role 323 + (** [<div>] child of [<dl>] has invalid role. 324 + When [<div>] is used to group [<dt>]/[<dd>] pairs in a [<dl>], 325 + it may only have [role="presentation"] or [role="none"]. *) 326 + 327 + | `Li_bad_role_in_menu 328 + (** [<li>] in menu/menubar has invalid role. 329 + [<li>] descendants of [role="menu"] or [role="menubar"] must 330 + have roles like [menuitem], [menuitemcheckbox], etc. *) 331 + 332 + | `Li_bad_role_in_tablist 333 + (** [<li>] in tablist has invalid role. 334 + [<li>] descendants of [role="tablist"] must have [role="tab"]. *) 335 + 336 + | `Li_bad_role_in_list 337 + (** [<li>] in list context has invalid role. 338 + [<li>] in [<ul>], [<ol>], [<menu>], or [role="list"] must 339 + have [role="listitem"] or no explicit role. *) 340 + ] 341 + 342 + (** {1 Table Errors} 343 + 344 + Errors in HTML table structure and cell spanning. *) 345 + 346 + (** Table structure validation errors. 347 + 348 + These errors indicate problems with table structure that may 349 + cause incorrect rendering or accessibility issues. *) 350 + type table_error = [ 351 + | `Row_no_cells of [`Row of int] 352 + (** Table row has no cells starting on it. 353 + The specified row number (1-indexed) in an implicit row group 354 + has no cells beginning on that row, possibly due to rowspan. *) 355 + 356 + | `Cell_overlap 357 + (** Table cells overlap due to spanning. 358 + A cell's rowspan/colspan causes it to overlap with another cell, 359 + making the table structure ambiguous. *) 360 + 361 + | `Cell_spans_rowgroup 362 + (** Cell's rowspan extends past its row group. 363 + A cell's rowspan would extend beyond the [<tbody>], [<thead>], 364 + or [<tfoot>] containing it; the span is clipped. *) 365 + 366 + | `Column_no_cells of [`Column of int] * [`Elem of string] 367 + (** Table column has no cells. 368 + A column established by [<col>] or [<colgroup>] has no cells 369 + beginning in it, indicating mismatched column definitions. *) 370 + ] 371 + 372 + (** {1 Internationalization Errors} 373 + 374 + Errors related to language declaration and text direction. *) 375 + 376 + (** Language and internationalization validation errors. 377 + 378 + These errors help ensure documents properly declare their language 379 + and text direction for accessibility and correct rendering. *) 380 + type i18n_error = [ 381 + | `Missing_lang 382 + (** Document has no language declaration. 383 + The [<html>] element should have a [lang] attribute declaring 384 + the document's primary language for accessibility. *) 385 + 386 + | `Wrong_lang of [`Detected of string] * [`Declared of string] * [`Suggested of string] 387 + (** Declared language doesn't match detected content language. 388 + Automatic language detection suggests the [lang] attribute 389 + value is incorrect for the actual content. *) 390 + 391 + | `Missing_dir_rtl of [`Language of string] 392 + (** RTL language content lacks [dir="rtl"]. 393 + Content detected as a right-to-left language should have 394 + explicit direction declaration. *) 395 + 396 + | `Wrong_dir of [`Language of string] * [`Declared of string] 397 + (** Text direction doesn't match detected language direction. 398 + The [dir] attribute value conflicts with the detected 399 + language's natural direction. *) 400 + 401 + | `Xml_lang_without_lang 402 + (** [xml:lang] present but [lang] is missing. 403 + When [xml:lang] is specified (for XHTML compatibility), 404 + the [lang] attribute must also be present with the same value. *) 405 + 406 + | `Xml_lang_mismatch 407 + (** [xml:lang] and [lang] attribute values don't match. 408 + Both attributes must have identical values when present. *) 409 + 410 + | `Not_nfc of [`Replacement of string] 411 + (** Text is not in Unicode Normalization Form C. 412 + HTML5 requires NFC normalization. The replacement string 413 + shows the correctly normalized form. *) 414 + ] 415 + 416 + (** {1 Import Map Errors} 417 + 418 + Errors in [<script type="importmap">] JSON content. *) 419 + 420 + (** Import map validation errors. 421 + 422 + These errors occur when validating the JSON content of 423 + [<script type="importmap">] elements per the Import Maps spec. *) 424 + type importmap_error = [ 425 + | `Invalid_json 426 + (** Import map content is not valid JSON. 427 + The script content must be parseable as JSON. *) 22 428 23 - (* Element Errors *) 24 - | Obsolete_element of { element: string; suggestion: string } 25 - | Obsolete_attr of { element: string; attr: string; suggestion: string option } 26 - | Obsolete_global_attr of { attr: string; suggestion: string } 27 - | Element_not_allowed_as_child of { child: string; parent: string } 28 - | Unknown_element of { name: string } 29 - | Element_must_not_be_descendant of { element: string; attr: string option; ancestor: string } 30 - | Missing_required_child of { parent: string; child: string } 31 - | Missing_required_child_one_of of { parent: string; children: string list } 32 - | Missing_required_child_generic of { parent: string } 33 - | Element_must_not_be_empty of { element: string } 34 - | Stray_start_tag of { tag: string } 35 - | Stray_end_tag of { tag: string } 36 - | End_tag_for_void_element of { tag: string } 37 - | Self_closing_non_void 38 - | Text_not_allowed of { parent: string } 429 + | `Invalid_root 430 + (** Import map root is not a valid object. 431 + The JSON must be an object with only [imports], [scopes], 432 + and [integrity] properties. *) 39 433 40 - (* Child Restrictions *) 41 - | Div_child_of_dl_bad_role 42 - | Li_bad_role_in_menu 43 - | Li_bad_role_in_tablist 44 - | Li_bad_role_in_list 434 + | `Imports_not_object 435 + (** The [imports] property is not a JSON object. 436 + [imports] must be an object mapping specifiers to URLs. *) 45 437 46 - (* ARIA Errors *) 47 - | Unnecessary_role of { role: string; element: string; reason: string } 48 - | Bad_role of { element: string; role: string } 49 - | Aria_must_not_be_specified of { attr: string; element: string; condition: string } 50 - | Aria_must_not_be_used of { attr: string; element: string; condition: string } 51 - | Aria_should_not_be_used of { attr: string; role: string } 52 - | Aria_hidden_on_body 53 - | Img_empty_alt_with_role 54 - | Checkbox_button_needs_aria_pressed 55 - | Tab_without_tabpanel 56 - | Multiple_main_visible 57 - | Discarding_unrecognized_role of { token: string } 438 + | `Empty_key 439 + (** Specifier map contains an empty string key. 440 + Module specifier keys must be non-empty strings. *) 58 441 59 - (* Required Attribute/Element Conditions *) 60 - | Img_missing_alt 61 - | Img_missing_src_or_srcset 62 - | Option_empty_without_label 63 - | Bdo_missing_dir 64 - | Bdo_dir_auto 65 - | Base_missing_href_or_target 66 - | Base_after_link_script 67 - | Link_missing_href 68 - | Link_as_requires_preload 69 - | Link_imagesrcset_requires_as_image 70 - | Img_ismap_needs_a_href 71 - | Sizes_without_srcset 72 - | Imagesizes_without_imagesrcset 73 - | Srcset_w_without_sizes 74 - | Source_missing_srcset 75 - | Source_needs_media_or_type 76 - | Picture_missing_img 77 - | Map_id_name_mismatch 78 - | List_attr_requires_datalist 79 - | Input_list_not_allowed 80 - | Label_too_many_labelable 81 - | Label_for_id_mismatch 82 - | Role_on_label_ancestor 83 - | Role_on_label_for 84 - | Aria_label_on_label_for 85 - | Input_value_constraint of { constraint_type: string } 86 - | Summary_missing_role 87 - | Summary_missing_attrs 88 - | Summary_role_not_allowed 89 - | Autocomplete_webauthn_on_select 90 - | Commandfor_invalid_target 442 + | `Non_string_value 443 + (** Specifier map contains a non-string value. 444 + All values in the specifier map must be strings (URLs). *) 91 445 92 - (* Parse Errors *) 93 - | Forbidden_codepoint of { codepoint: int } 94 - | Char_ref_control of { codepoint: int } 95 - | Char_ref_non_char of { codepoint: int; astral: bool } 96 - | Char_ref_unassigned 97 - | Char_ref_zero 98 - | Char_ref_out_of_range 99 - | Numeric_char_ref_carriage_return 100 - | End_of_file_with_open_elements 101 - | No_element_in_scope of { tag: string } 102 - | End_tag_implied_open_elements of { tag: string } 103 - | Start_tag_in_table of { tag: string } 104 - | Bad_start_tag_in of { tag: string; context: string } 446 + | `Key_trailing_slash 447 + (** Specifier with trailing [/] maps to URL without trailing [/]. 448 + When a specifier key ends with [/], its value must also 449 + end with [/] for proper prefix matching. *) 105 450 106 - (* Table Errors *) 107 - | Table_row_no_cells of { row: int } 108 - | Table_cell_overlap 109 - | Table_cell_spans_rowgroup 110 - | Table_column_no_cells of { column: int; element: string } 451 + | `Scopes_not_object 452 + (** The [scopes] property is not a JSON object. 453 + [scopes] must be an object with URL keys. *) 111 454 112 - (* Language/Internationalization *) 113 - | Missing_lang_attr 114 - | Wrong_lang of { detected: string; declared: string; suggested: string } 115 - | Missing_dir_rtl of { language: string } 116 - | Wrong_dir of { language: string; declared: string } 117 - | Xml_lang_without_lang 118 - | Xml_lang_lang_mismatch 455 + | `Scopes_values_not_object 456 + (** A [scopes] entry value is not a JSON object. 457 + Each scope must map to a specifier map object. *) 119 458 120 - (* Unicode Normalization *) 121 - | Not_nfc of { replacement: string } 459 + | `Scopes_invalid_url 460 + (** A [scopes] key is not a valid URL. 461 + Scope keys must be valid URL strings. *) 122 462 123 - (* Multiple h1 *) 124 - | Multiple_h1 125 - | Multiple_autofocus 463 + | `Scopes_value_invalid_url 464 + (** A specifier value in [scopes] is not a valid URL. 465 + URL values in scope specifier maps must be valid. *) 466 + ] 126 467 127 - (* Import Maps *) 128 - | Importmap_invalid_json 129 - | Importmap_invalid_root 130 - | Importmap_imports_not_object 131 - | Importmap_empty_key 132 - | Importmap_non_string_value 133 - | Importmap_key_trailing_slash 134 - | Importmap_scopes_not_object 135 - | Importmap_scopes_values_not_object 136 - | Importmap_scopes_invalid_url 137 - | Importmap_scopes_value_invalid_url 468 + (** {1 Element-Specific Errors} 138 469 139 - (* Style Element *) 140 - | Style_type_invalid 470 + Validation errors specific to particular HTML elements. *) 141 471 142 - (* Headingoffset *) 143 - | Headingoffset_invalid 472 + (** Image element ([<img>]) validation errors. *) 473 + type img_error = [ 474 + | `Missing_alt 475 + (** Image lacks [alt] attribute for accessibility. 476 + Per WCAG and HTML5, images must have [alt] text describing 477 + their content, or [alt=""] for decorative images. *) 144 478 145 - (* Media Attribute *) 146 - | Media_empty 147 - | Media_all 479 + | `Missing_src_or_srcset 480 + (** Image has neither [src] nor [srcset]. 481 + An [<img>] must have at least one image source specified. *) 482 + 483 + | `Empty_alt_with_role 484 + (** Image with [alt=""] has a [role] attribute. 485 + Decorative images (empty [alt]) must not have [role] because 486 + they should be hidden from assistive technology. *) 487 + 488 + | `Ismap_needs_href 489 + (** Image with [ismap] lacks [<a href>] ancestor. 490 + Server-side image maps require a link wrapper to function. *) 491 + ] 492 + 493 + (** Link element ([<link>]) validation errors. *) 494 + type link_error = [ 495 + | `Missing_href 496 + (** [<link>] has no [href] or [imagesrcset]. 497 + A link element must have a resource to link to. *) 498 + 499 + | `As_requires_preload 500 + (** [<link as="...">] used without [rel="preload"]. 501 + The [as] attribute is only meaningful for preload/modulepreload. *) 502 + 503 + | `Imagesrcset_requires_as_image 504 + (** [<link imagesrcset>] used without [as="image"]. 505 + Image srcset preloading requires [as="image"]. *) 506 + ] 148 507 149 - (* SVG/MathML specific *) 150 - | Svg_deprecated_attr of { attr: string; element: string } 151 - | Missing_required_svg_attr of { element: string; attr: string } 508 + (** Label element ([<label>]) validation errors. *) 509 + type label_error = [ 510 + | `Too_many_labelable 511 + (** Label contains multiple labelable descendants. 512 + A [<label>] should associate with exactly one form control. *) 152 513 153 - (* Generic/Fallback *) 154 - | Generic of { message: string } 514 + | `For_id_mismatch 515 + (** Label's [for] doesn't match descendant input's [id]. 516 + When a [<label>] has both [for] and a descendant input, 517 + the input's [id] must match the [for] value. *) 518 + 519 + | `Role_on_ancestor 520 + (** [<label>] with role is ancestor of labelable element. 521 + Adding [role] to a label that wraps a form control 522 + breaks the implicit label association. *) 155 523 156 - (** Get the severity level for an error code *) 524 + | `Role_on_for 525 + (** [<label>] with role uses [for] association. 526 + Labels with explicit [for] association must not have [role]. *) 527 + 528 + | `Aria_label_on_for 529 + (** [<label>] with [aria-label] uses [for] association. 530 + [aria-label] on a label associated via [for] creates 531 + conflicting accessible names. *) 532 + ] 533 + 534 + (** Input element ([<input>]) validation errors. *) 535 + type input_error = [ 536 + | `Checkbox_needs_aria_pressed 537 + (** Checkbox with [role="button"] lacks [aria-pressed]. 538 + When a checkbox is styled as a toggle button, it needs 539 + [aria-pressed] to convey the toggle state. *) 540 + 541 + | `Value_constraint of [`Constraint of string] 542 + (** Input [value] doesn't meet type-specific constraints. 543 + Different input types have different value format requirements 544 + (dates, numbers, emails, etc.). *) 545 + 546 + | `List_not_allowed 547 + (** [list] attribute used on incompatible input type. 548 + The [list] attribute for datalist binding is only valid 549 + on certain input types (text, search, url, etc.). *) 550 + 551 + | `List_requires_datalist 552 + (** [list] attribute doesn't reference a [<datalist>]. 553 + The [list] attribute must contain the ID of a datalist element. *) 554 + ] 555 + 556 + (** Responsive image ([srcset]/[sizes]) validation errors. *) 557 + type srcset_error = [ 558 + | `Sizes_without_srcset 559 + (** [sizes] used without [srcset]. 560 + The [sizes] attribute is meaningless without [srcset]. *) 561 + 562 + | `Imagesizes_without_imagesrcset 563 + (** [imagesizes] used without [imagesrcset]. 564 + On [<link>], [imagesizes] requires [imagesrcset]. *) 565 + 566 + | `W_without_sizes 567 + (** [srcset] with width descriptors lacks [sizes]. 568 + When using width descriptors ([w]) in [srcset], the [sizes] 569 + attribute must specify the rendered size. *) 570 + 571 + | `Source_missing_srcset 572 + (** [<source>] in [<picture>] lacks [srcset]. 573 + Picture source elements must have a srcset. *) 574 + 575 + | `Source_needs_media_or_type 576 + (** [<source>] needs [media] or [type] to differentiate. 577 + When multiple sources exist, each must have selection criteria. *) 578 + 579 + | `Picture_missing_img 580 + (** [<picture>] lacks required [<img>] child. 581 + A picture element must contain an img as the fallback. *) 582 + ] 583 + 584 + (** SVG element validation errors. *) 585 + type svg_error = [ 586 + | `Deprecated_attr of [`Attr of string] * [`Elem of string] 587 + (** SVG attribute is deprecated. 588 + Certain SVG presentation attributes are deprecated in 589 + favor of CSS properties. *) 590 + 591 + | `Missing_attr of [`Elem of string] * [`Attr of string] 592 + (** SVG element missing required attribute. 593 + Some SVG elements have required attributes for valid rendering. *) 594 + ] 595 + 596 + (** Miscellaneous element-specific errors. 597 + 598 + These errors are specific to individual elements that don't 599 + warrant their own category. *) 600 + type misc_error = [ 601 + | `Option_empty_without_label 602 + (** [<option>] without [label] attribute is empty. 603 + Options need either text content or a label attribute. *) 604 + 605 + | `Bdo_missing_dir 606 + (** [<bdo>] element lacks required [dir] attribute. 607 + The bidirectional override element must specify direction. *) 608 + 609 + | `Bdo_dir_auto 610 + (** [<bdo>] has [dir="auto"] which is invalid. 611 + BDO requires explicit [ltr] or [rtl], not auto-detection. *) 612 + 613 + | `Base_missing_href_or_target 614 + (** [<base>] has neither [href] nor [target]. 615 + A base element must specify at least one of these. *) 616 + 617 + | `Base_after_link_script 618 + (** [<base>] appears after [<link>] or [<script>]. 619 + The base URL must be established before other URL resolution. *) 620 + 621 + | `Map_id_name_mismatch 622 + (** [<map>] [id] and [name] attributes don't match. 623 + For image maps, both attributes must have the same value. *) 624 + 625 + | `Summary_missing_role 626 + (** Non-default [<summary>] lacks [role] attribute. 627 + Custom summary content outside details needs explicit role. *) 628 + 629 + | `Summary_missing_attrs 630 + (** Non-default [<summary>] missing required ARIA attributes. 631 + Custom summary implementations need proper ARIA. *) 632 + 633 + | `Summary_role_not_allowed 634 + (** [<summary>] for its parent [<details>] has [role]. 635 + Default summary for details must not override its role. *) 636 + 637 + | `Autocomplete_webauthn_on_select 638 + (** [<select>] has [autocomplete] containing [webauthn]. 639 + WebAuthn autocomplete tokens are not valid for select elements. *) 640 + 641 + | `Commandfor_invalid_target 642 + (** [commandfor] doesn't reference a valid element ID. 643 + The invoker must reference an element in the same tree. *) 644 + 645 + | `Style_type_invalid 646 + (** [<style type>] has value other than [text/css]. 647 + HTML5 only supports CSS in style elements. *) 648 + 649 + | `Headingoffset_invalid 650 + (** [headingoffset] value is out of range. 651 + Must be an integer between 0 and 8. *) 652 + 653 + | `Media_empty 654 + (** [media] attribute is empty string. 655 + Media queries must be non-empty if the attribute is present. *) 656 + 657 + | `Media_all 658 + (** [media] attribute is just ["all"]. 659 + Using [media="all"] is pointless; omit the attribute instead. *) 660 + 661 + | `Multiple_h1 662 + (** Document contains multiple [<h1>] elements. 663 + Best practice is one [<h1>] per document unless using 664 + [headingoffset] to indicate sectioning. *) 665 + 666 + | `Multiple_autofocus 667 + (** Multiple elements have [autofocus] in same scope. 668 + Only one element should have autofocus per scoping root. *) 669 + ] 670 + 671 + (** {1 Top-Level Error Type} *) 672 + 673 + (** All HTML5 validation errors, organized by category. 674 + 675 + Pattern match on the outer constructor to handle error categories, 676 + or match through to specific errors as needed. 677 + 678 + {[ 679 + let severity_of_category = function 680 + | `Aria _ -> may_be_warning 681 + | `I18n _ -> usually_info_or_warning 682 + | _ -> usually_error 683 + ]} *) 684 + type t = [ 685 + | `Attr of attr_error 686 + (** Attribute validation errors *) 687 + | `Element of element_error 688 + (** Element structure errors *) 689 + | `Tag of tag_error 690 + (** Tag-level parse errors *) 691 + | `Char_ref of char_ref_error 692 + (** Character reference errors *) 693 + | `Aria of aria_error 694 + (** ARIA and accessibility errors *) 695 + | `Li_role of li_role_error 696 + (** List item role constraints *) 697 + | `Table of table_error 698 + (** Table structure errors *) 699 + | `I18n of i18n_error 700 + (** Language and direction errors *) 701 + | `Importmap of importmap_error 702 + (** Import map JSON errors *) 703 + | `Img of img_error 704 + (** Image element errors *) 705 + | `Link of link_error 706 + (** Link element errors *) 707 + | `Label of label_error 708 + (** Label element errors *) 709 + | `Input of input_error 710 + (** Input element errors *) 711 + | `Srcset of srcset_error 712 + (** Responsive image errors *) 713 + | `Svg of svg_error 714 + (** SVG-specific errors *) 715 + | `Misc of misc_error 716 + (** Miscellaneous element errors *) 717 + | `Generic of string 718 + (** Fallback for messages without specific error codes *) 719 + ] 720 + 721 + (** {1 Functions} *) 722 + 723 + (** Get the severity level for an error. 724 + Most errors are [Error]; some ARIA and i18n issues are [Warning] or [Info]. *) 157 725 val severity : t -> severity 158 726 159 - (** Get a short code string for categorization *) 727 + (** Get a short categorization code string. 728 + Useful for filtering, grouping, or machine-readable output. 729 + Example: ["disallowed-attribute"], ["missing-alt"], ["aria-not-allowed"]. *) 160 730 val code_string : t -> string 161 731 162 - (** Convert error code to exact Nu validator message string *) 732 + (** Convert error to human-readable message. 733 + Produces messages matching the Nu HTML Validator format with 734 + proper Unicode curly quotes around identifiers. *) 163 735 val to_message : t -> string 164 736 165 - (** Format a string with curly quotes *) 737 + (** Format a string with Unicode curly quotes. 738 + Wraps the string in U+201C and U+201D ("..."). *) 166 739 val q : string -> string
+2 -2
lib/html5_checker/html5_checker.ml
··· 42 42 let dummy_doc = Html5rw.parse (Bytesrw.Bytes.Reader.of_string "") in 43 43 { doc = dummy_doc; msgs = Message_collector.messages collector; system_id } 44 44 | Error msg -> 45 - Message_collector.add_typed collector (Error_code.Generic { message = msg }); 45 + Message_collector.add_typed collector (`Generic msg); 46 46 let dummy_doc = Html5rw.parse (Bytesrw.Bytes.Reader.of_string "") in 47 47 { doc = dummy_doc; msgs = Message_collector.messages collector; system_id } 48 48 end ··· 62 62 63 63 (* Special case: emit missing-lang warning for specific test file *) 64 64 if is_missing_lang_test system_id then 65 - Message_collector.add_typed collector Error_code.Missing_lang_attr; 65 + Message_collector.add_typed collector (`I18n `Missing_lang); 66 66 67 67 { doc; msgs = Message_collector.messages collector; system_id } 68 68 end
+1 -1
lib/html5_checker/semantic/autofocus_checker.ml
··· 69 69 | ctx :: _ -> 70 70 ctx.autofocus_count <- ctx.autofocus_count + 1; 71 71 if ctx.autofocus_count > 1 then 72 - Message_collector.add_typed collector Error_code.Multiple_autofocus 72 + Message_collector.add_typed collector (`Misc `Multiple_autofocus) 73 73 | [] -> () 74 74 end 75 75 end
+2 -7
lib/html5_checker/semantic/form_checker.ml
··· 26 26 let check_autocomplete_value value element_name collector = 27 27 (* webauthn is not allowed on select, only on input and textarea *) 28 28 if element_name = "select" && contains_webauthn value then begin 29 - Message_collector.add_typed collector Error_code.Autocomplete_webauthn_on_select 29 + Message_collector.add_typed collector (`Misc `Autocomplete_webauthn_on_select) 30 30 end else begin 31 31 (* Use the proper autocomplete validator from dt_autocomplete *) 32 32 match Dt_autocomplete.validate_autocomplete value with ··· 35 35 (* Nu validator prefixes autocomplete errors with "Bad autocomplete detail tokens (any): " for select/textarea, but not for input *) 36 36 let reason = if element_name = "input" then msg else "Bad autocomplete detail tokens (any): " ^ msg in 37 37 Message_collector.add_typed collector 38 - (Error_code.Bad_attr_value { 39 - element = element_name; 40 - attr = "autocomplete"; 41 - value; 42 - reason 43 - }) 38 + (`Attr (`Bad_value (`Elem element_name, `Attr "autocomplete", `Value value, `Reason reason))) 44 39 end 45 40 46 41 let start_element _state ~name ~namespace:_ ~attrs collector =
+17 -33
lib/html5_checker/semantic/id_checker.ml
··· 100 100 (* Check for empty ID *) 101 101 if String.length id = 0 then 102 102 Message_collector.add_typed collector 103 - (Error_code.Bad_attr_value_generic { 104 - message = "Bad value \"\" for attribute \"id\": An ID must not be the empty string." 105 - }) 103 + (`Attr (`Bad_value_generic (`Message "Bad value \"\" for attribute \"id\": An ID must not be the empty string."))) 106 104 (* Check for whitespace in ID *) 107 105 else if contains_whitespace id then 108 106 Message_collector.add_typed collector 109 - (Error_code.Bad_attr_value_generic { 110 - message = Printf.sprintf "Bad value %s for attribute \"id\": An ID must not contain whitespace." 111 - (Error_code.q id) 112 - }) 107 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute \"id\": An ID must not contain whitespace." 108 + (Error_code.q id))))) 113 109 (* Check for duplicate ID *) 114 110 else if Hashtbl.mem state.ids id then 115 - Message_collector.add_typed collector (Error_code.Duplicate_id { id }) 111 + Message_collector.add_typed collector (`Attr (`Duplicate_id (`Id id))) 116 112 else 117 113 (* Store the ID *) 118 114 Hashtbl.add state.ids id () ··· 148 144 else 149 145 (* Empty hash name: "#" *) 150 146 Message_collector.add_typed collector 151 - (Error_code.Bad_attr_value { 152 - element; 153 - attr = name; 154 - value; 155 - reason = Printf.sprintf "Bad hash-name reference: A hash-name reference must have at least one character after %s." 156 - (Error_code.q "#") 157 - }) 147 + (`Attr (`Bad_value (`Elem element, `Attr name, `Value value, 148 + `Reason (Printf.sprintf "Bad hash-name reference: A hash-name reference must have at least one character after %s." 149 + (Error_code.q "#"))))) 158 150 | None -> 159 151 if String.length value > 0 then 160 152 (* Missing # prefix *) 161 153 Message_collector.add_typed collector 162 - (Error_code.Bad_attr_value { 163 - element; 164 - attr = name; 165 - value; 166 - reason = Printf.sprintf "Bad hash-name reference: A hash-name reference must start with %s." 167 - (Error_code.q "#") 168 - }) 154 + (`Attr (`Bad_value (`Elem element, `Attr name, `Value value, 155 + `Reason (Printf.sprintf "Bad hash-name reference: A hash-name reference must start with %s." 156 + (Error_code.q "#"))))) 169 157 end 170 158 171 159 | "name" when element = "map" -> ··· 201 189 let name_opt = List.find_map (fun (n, v) -> if n = "name" then Some v else None) attrs in 202 190 match id_opt, name_opt with 203 191 | Some id_val, Some name_val when id_val <> name_val -> 204 - Message_collector.add_typed collector Error_code.Map_id_name_mismatch 192 + Message_collector.add_typed collector (`Misc `Map_id_name_mismatch) 205 193 | _ -> () 206 194 end 207 195 ··· 217 205 if not (Hashtbl.mem state.ids ref.referenced_id) then begin 218 206 (* Use specific error for list attribute on input *) 219 207 if ref.attribute = "list" && ref.referring_element = "input" then 220 - Message_collector.add_typed collector Error_code.List_attr_requires_datalist 208 + Message_collector.add_typed collector (`Input `List_requires_datalist) 221 209 else if ref.attribute = "commandfor" then 222 - Message_collector.add_typed collector Error_code.Commandfor_invalid_target 210 + Message_collector.add_typed collector (`Misc `Commandfor_invalid_target) 223 211 else 224 212 (* Use generic for dangling references - format may vary *) 225 213 Message_collector.add_typed collector 226 - (Error_code.Generic { 227 - message = Printf.sprintf "The %s attribute on the %s element refers to ID %s which does not exist in the document." 228 - (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id) 229 - }) 214 + (`Generic (Printf.sprintf "The %s attribute on the %s element refers to ID %s which does not exist in the document." 215 + (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id))) 230 216 end 231 217 ) state.references; 232 218 ··· 234 220 List.iter (fun ref -> 235 221 if not (Hashtbl.mem state.map_names ref.referenced_id) then 236 222 Message_collector.add_typed collector 237 - (Error_code.Generic { 238 - message = Printf.sprintf "The %s attribute on the %s element refers to map name %s which does not exist in the document." 239 - (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id) 240 - }) 223 + (`Generic (Printf.sprintf "The %s attribute on the %s element refers to map name %s which does not exist in the document." 224 + (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id))) 241 225 ) state.usemap_references 242 226 243 227 let checker = (module struct
+4 -12
lib/html5_checker/semantic/lang_detecting_checker.ml
··· 322 322 if original_declared = "" then begin 323 323 (* No lang attribute - suggest adding one *) 324 324 Message_collector.add_typed collector 325 - (Error_code.Wrong_lang { 326 - detected = detected_name; 327 - declared = ""; 328 - suggested = suggested_code 329 - }) 325 + (`I18n (`Wrong_lang (`Detected detected_name, `Declared "", `Suggested suggested_code))) 330 326 end 331 327 else if base_declared <> base_detected && 332 328 (* Don't warn for zh variants *) 333 329 not (base_declared = "zh" && base_detected = "zh") then begin 334 330 Message_collector.add_typed collector 335 - (Error_code.Wrong_lang { 336 - detected = detected_name; 337 - declared = original_declared; 338 - suggested = suggested_code 339 - }) 331 + (`I18n (`Wrong_lang (`Detected detected_name, `Declared original_declared, `Suggested suggested_code))) 340 332 end; 341 333 342 334 (* Check dir attribute for RTL languages *) ··· 344 336 match state.html_dir with 345 337 | None -> 346 338 Message_collector.add_typed collector 347 - (Error_code.Missing_dir_rtl { language = detected_name }) 339 + (`I18n (`Missing_dir_rtl (`Language detected_name))) 348 340 | Some dir when String.lowercase_ascii dir <> "rtl" -> 349 341 Message_collector.add_typed collector 350 - (Error_code.Wrong_dir { language = detected_name; declared = dir }) 342 + (`I18n (`Wrong_dir (`Language detected_name, `Declared dir))) 351 343 | _ -> () 352 344 end 353 345 | _ -> ()
+5 -17
lib/html5_checker/semantic/nesting_checker.ml
··· 263 263 | None -> ancestor 264 264 in 265 265 Message_collector.add_typed collector 266 - (Error_code.Element_not_allowed_as_child { 267 - child = name; 268 - parent 269 - }) 266 + (`Element (`Not_allowed_as_child (`Child name, `Parent parent))) 270 267 end else 271 268 (* Nesting violation: use "must not be descendant" format *) 272 269 Message_collector.add_typed collector 273 - (Error_code.Element_must_not_be_descendant { 274 - element = name; 275 - attr; 276 - ancestor 277 - }) 270 + (`Element (`Must_not_descend (`Elem name, `Attr attr, `Ancestor ancestor))) 278 271 end 279 272 ) special_ancestors 280 273 end ··· 286 279 | "area" -> 287 280 if (state.ancestor_mask land map_mask) = 0 then 288 281 Message_collector.add_typed collector 289 - (Error_code.Generic { 290 - message = Printf.sprintf "The %s element must have a %s ancestor." 291 - (Error_code.q "area") (Error_code.q "map") 292 - }) 282 + (`Generic (Printf.sprintf "The %s element must have a %s ancestor." 283 + (Error_code.q "area") (Error_code.q "map"))) 293 284 | _ -> () 294 285 295 286 (** Check for metadata-only elements appearing outside valid contexts. ··· 304 295 | parent :: _ -> 305 296 (* style inside any other element is not allowed *) 306 297 Message_collector.add_typed collector 307 - (Error_code.Element_not_allowed_as_child { 308 - child = "style"; 309 - parent = parent.name 310 - }) 298 + (`Element (`Not_allowed_as_child (`Child "style", `Parent parent.name))) 311 299 | [] -> () (* at root level, would be caught elsewhere *) 312 300 end 313 301 | _ -> ()
+5 -5
lib/html5_checker/semantic/obsolete_checker.ml
··· 269 269 | None -> () 270 270 | Some suggestion -> 271 271 Message_collector.add_typed collector 272 - (Error_code.Obsolete_element { element = name; suggestion })); 272 + (`Element (`Obsolete (`Elem name, `Suggestion suggestion)))); 273 273 274 274 (* Check for obsolete attributes *) 275 275 List.iter (fun (attr_name, _attr_value) -> ··· 281 281 error from nesting_checker takes precedence *) 282 282 if state.in_head then 283 283 Message_collector.add_typed collector 284 - (Error_code.Attr_not_allowed_on_element { attr = attr_name; element = name }) 284 + (`Attr (`Not_allowed (`Attr attr_name, `Elem name))) 285 285 end else begin 286 286 (* Check specific obsolete attributes for this element *) 287 287 (match Hashtbl.find_opt obsolete_attributes attr_lower with ··· 291 291 | None -> () 292 292 | Some suggestion -> 293 293 Message_collector.add_typed collector 294 - (Error_code.Obsolete_attr { element = name; attr = attr_name; suggestion = Some suggestion }))); 294 + (`Element (`Obsolete_attr (`Elem name, `Attr attr_name, `Suggestion (Some suggestion)))))); 295 295 296 296 (* Check obsolete style attributes *) 297 297 (match Hashtbl.find_opt obsolete_style_attrs attr_lower with ··· 299 299 | Some elements -> 300 300 if List.mem name_lower elements then 301 301 Message_collector.add_typed collector 302 - (Error_code.Obsolete_attr { element = name; attr = attr_name; suggestion = Some "Use CSS instead." })); 302 + (`Element (`Obsolete_attr (`Elem name, `Attr attr_name, `Suggestion (Some "Use CSS instead."))))); 303 303 304 304 (* Check obsolete global attributes *) 305 305 (match Hashtbl.find_opt obsolete_global_attrs attr_lower with 306 306 | None -> () 307 307 | Some suggestion -> 308 308 Message_collector.add_typed collector 309 - (Error_code.Obsolete_global_attr { attr = attr_name; suggestion })) 309 + (`Element (`Obsolete_global_attr (`Attr attr_name, `Suggestion suggestion)))) 310 310 end 311 311 ) attrs 312 312 end
+2 -5
lib/html5_checker/semantic/option_checker.ml
··· 45 45 (* Report error for empty label attribute value *) 46 46 if label_empty then 47 47 Message_collector.add_typed collector 48 - (Error_code.Bad_attr_value { 49 - element = "option"; attr = "label"; value = ""; 50 - reason = "Bad non-empty string: Must not be empty." 51 - }); 48 + (`Attr (`Bad_value (`Elem "option", `Attr "label", `Value "", `Reason "Bad non-empty string: Must not be empty."))); 52 49 let ctx = { has_text = false; has_label; label_empty } in 53 50 state.option_stack <- ctx :: state.option_stack 54 51 end ··· 69 66 (* Note: empty label error is already reported at start_element, 70 67 so only report empty option without label when there's no label attribute at all *) 71 68 if not ctx.has_text && not ctx.has_label then 72 - Message_collector.add_typed collector Error_code.Option_empty_without_label 69 + Message_collector.add_typed collector (`Misc `Option_empty_without_label) 73 70 | [] -> () 74 71 end 75 72 end
+29 -41
lib/html5_checker/semantic/required_attr_checker.ml
··· 27 27 let check_img_element state attrs collector = 28 28 (* Check for required src OR srcset attribute *) 29 29 if not (has_attr "src" attrs) && not (has_attr "srcset" attrs) then 30 - Message_collector.add_typed collector Error_code.Img_missing_src_or_srcset; 30 + Message_collector.add_typed collector (`Img `Missing_src_or_srcset); 31 31 32 32 (* Check for alt attribute - always required *) 33 33 if not (has_attr "alt" attrs) then 34 - Message_collector.add_typed collector Error_code.Img_missing_alt; 34 + Message_collector.add_typed collector (`Img `Missing_alt); 35 35 36 36 (* Check ismap requires 'a' ancestor with href *) 37 37 if has_attr "ismap" attrs && not state.in_a_with_href then 38 - Message_collector.add_typed collector Error_code.Img_ismap_needs_a_href 38 + Message_collector.add_typed collector (`Img `Ismap_needs_href) 39 39 40 40 let check_area_element attrs collector = 41 41 (* area with href requires alt *) 42 42 if has_attr "href" attrs && not (has_attr "alt" attrs) then 43 43 Message_collector.add_typed collector 44 - (Error_code.Missing_required_attr { element = "area"; attr = "alt" }) 44 + (`Attr (`Missing (`Elem "area", `Attr "alt"))) 45 45 46 46 let check_input_element attrs collector = 47 47 match get_attr "type" attrs with ··· 49 49 (* input[type=image] requires alt *) 50 50 if not (has_attr "alt" attrs) then 51 51 Message_collector.add_typed collector 52 - (Error_code.Missing_required_attr { element = "input"; attr = "alt" }) 52 + (`Attr (`Missing (`Elem "input", `Attr "alt"))) 53 53 | Some "hidden" -> 54 54 (* input[type=hidden] should not have required attribute *) 55 55 if has_attr "required" attrs then 56 56 Message_collector.add_typed collector 57 - (Error_code.Attr_not_allowed_when { 58 - attr = "required"; 59 - element = "input"; 60 - condition = "the type attribute is hidden" 61 - }) 57 + (`Attr (`Not_allowed_when (`Attr "required", `Elem "input", `Condition "the type attribute is hidden"))) 62 58 | Some "file" -> 63 59 (* input[type=file] should not have value attribute *) 64 60 if has_attr "value" attrs then 65 61 Message_collector.add_typed collector 66 - (Error_code.Attr_not_allowed_when { 67 - attr = "value"; 68 - element = "input"; 69 - condition = "the type attribute is file" 70 - }) 62 + (`Attr (`Not_allowed_when (`Attr "value", `Elem "input", `Condition "the type attribute is file"))) 71 63 | _ -> () 72 64 73 65 let check_script_element attrs _collector = ··· 100 92 in 101 93 102 94 if not valid then 95 + let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" in 103 96 Message_collector.add_typed collector 104 - (Error_code.Generic { 105 - 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." 106 - (Error_code.q "meta") (Error_code.q "charset") (Error_code.q "name") 107 - (Error_code.q "content") (Error_code.q "http-equiv") (Error_code.q "content") 108 - }) 97 + (`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." 98 + (q "meta") (q "charset") (q "name") 99 + (q "content") (q "http-equiv") (q "content"))) 109 100 110 101 let check_link_element attrs collector = 111 102 (* link[rel="stylesheet"] requires href *) 112 103 match get_attr "rel" attrs with 113 104 | Some rel when String.equal rel "stylesheet" -> 114 105 if not (has_attr "href" attrs) then 115 - Message_collector.add_typed collector Error_code.Link_missing_href 106 + Message_collector.add_typed collector (`Link `Missing_href) 116 107 | _ -> () 117 108 118 109 let check_a_element attrs collector = 119 110 (* a[download] requires href *) 120 111 if has_attr "download" attrs && not (has_attr "href" attrs) then 121 112 Message_collector.add_typed collector 122 - (Error_code.Missing_required_attr { element = "a"; attr = "href" }) 113 + (`Attr (`Missing (`Elem "a", `Attr "href"))) 123 114 124 115 let check_map_element attrs collector = 125 116 (* map requires name *) 126 117 if not (has_attr "name" attrs) then 127 118 Message_collector.add_typed collector 128 - (Error_code.Missing_required_attr { element = "map"; attr = "name" }) 119 + (`Attr (`Missing (`Elem "map", `Attr "name"))) 129 120 130 121 let check_object_element attrs collector = 131 122 (* object requires data attribute (or type attribute alone is not sufficient) *) ··· 133 124 let has_type = has_attr "type" attrs in 134 125 if not has_data && has_type then 135 126 Message_collector.add_typed collector 136 - (Error_code.Missing_required_attr { element = "object"; attr = "data" }) 127 + (`Attr (`Missing (`Elem "object", `Attr "data"))) 137 128 138 129 let check_popover_element element_name attrs collector = 139 130 (* popover attribute must have valid value *) ··· 142 133 let value_lower = String.lowercase_ascii value in 143 134 (* Valid values: empty string, auto, manual, hint *) 144 135 if value_lower <> "" && value_lower <> "auto" && value_lower <> "manual" && value_lower <> "hint" then 136 + let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" in 145 137 Message_collector.add_typed collector 146 - (Error_code.Bad_attr_value_generic { 147 - message = Printf.sprintf "Bad value %s for attribute %s on element %s." 148 - (Error_code.q value) (Error_code.q "popover") (Error_code.q element_name) 149 - }) 138 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s." 139 + (q value) (q "popover") (q element_name))))) 150 140 | None -> () 151 141 152 142 let check_meter_element attrs collector = 153 143 (* meter requires value attribute *) 154 144 if not (has_attr "value" attrs) then 155 145 Message_collector.add_typed collector 156 - (Error_code.Missing_required_attr { element = "meter"; attr = "value" }) 146 + (`Attr (`Missing (`Elem "meter", `Attr "value"))) 157 147 else begin 158 148 (* Validate min <= value constraint *) 159 149 match get_attr "value" attrs, get_attr "min" attrs with ··· 162 152 let value = float_of_string value_str in 163 153 let min_val = float_of_string min_str in 164 154 if min_val > value then 155 + let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" in 165 156 Message_collector.add_typed collector 166 - (Error_code.Generic { 167 - message = Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute." 168 - (Error_code.q "min") (Error_code.q "value") 169 - }) 157 + (`Generic (Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute." 158 + (q "min") (q "value"))) 170 159 with _ -> ()) 171 160 | _ -> () 172 161 end ··· 183 172 | Some max_str -> (try float_of_string max_str with _ -> 1.0) 184 173 in 185 174 if value > max_val then 175 + let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d" in 186 176 (* Check which message to use based on whether max is present *) 187 177 if has_attr "max" attrs then 188 178 Message_collector.add_typed collector 189 - (Error_code.Generic { 179 + (`Generic ( 190 180 (* Note: double space before "value" matches Nu validator quirk *) 191 - message = Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute." 192 - (Error_code.q "value") (Error_code.q "max") 193 - }) 181 + Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute." 182 + (q "value") (q "max"))) 194 183 else 195 184 Message_collector.add_typed collector 196 - (Error_code.Generic { 185 + (`Generic ( 197 186 (* Note: double space before "value" matches Nu validator quirk *) 198 - message = Printf.sprintf "The value of the %s attribute must be less than or equal to one when the %s attribute is absent." 199 - (Error_code.q "value") (Error_code.q "max") 200 - }) 187 + Printf.sprintf "The value of the %s attribute must be less than or equal to one when the %s attribute is absent." 188 + (q "value") (q "max"))) 201 189 with _ -> ()) 202 190 203 191 let start_element state ~name ~namespace:_ ~attrs collector =
+34 -34
lib/html5_checker/specialized/aria_checker.ml
··· 452 452 List.iter (fun role -> 453 453 if not (Hashtbl.mem valid_aria_roles role) then 454 454 Message_collector.add_typed collector 455 - (Error_code.Discarding_unrecognized_role { token = role }) 455 + (`Aria (`Unrecognized_role (`Token role))) 456 456 ) explicit_roles; 457 457 458 458 (* Get implicit role for this element *) ··· 484 484 let first_role = List.hd explicit_roles in 485 485 if first_role <> "none" && first_role <> "presentation" then 486 486 Message_collector.add_typed collector 487 - (Error_code.Bad_role { element = name; role = first_role }) 487 + (`Aria (`Bad_role (`Elem name, `Role first_role))) 488 488 end; 489 489 490 490 (* Check br/wbr aria-* attribute restrictions - not allowed *) ··· 494 494 if String.length attr_lower > 5 && String.sub attr_lower 0 5 = "aria-" && 495 495 attr_lower <> "aria-hidden" then 496 496 Message_collector.add_typed collector 497 - (Error_code.Attr_not_allowed_on_element { attr = attr_name; element = name }) 497 + (`Attr (`Not_allowed (`Attr attr_name, `Elem name))) 498 498 ) attrs 499 499 end; 500 500 ··· 504 504 (* Generate error if element cannot have accessible name but has one *) 505 505 if has_aria_label && not can_have_name then 506 506 Message_collector.add_typed collector 507 - (Error_code.Aria_must_not_be_specified { attr = "aria-label"; element = name; 508 - 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" }); 507 + (`Aria (`Must_not_specify (`Attr "aria-label", `Elem name, 508 + `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"))); 509 509 510 510 if has_aria_labelledby && not can_have_name then 511 511 Message_collector.add_typed collector 512 - (Error_code.Aria_must_not_be_specified { attr = "aria-labelledby"; element = name; 513 - 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" }); 512 + (`Aria (`Must_not_specify (`Attr "aria-labelledby", `Elem name, 513 + `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"))); 514 514 515 515 if has_aria_braillelabel && not can_have_name then 516 516 Message_collector.add_typed collector 517 - (Error_code.Aria_must_not_be_specified { attr = "aria-braillelabel"; element = name; 518 - 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" }); 517 + (`Aria (`Must_not_specify (`Attr "aria-braillelabel", `Elem name, 518 + `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"))); 519 519 520 520 (* Check for img with empty alt having role attribute *) 521 521 if name_lower = "img" then begin ··· 524 524 | Some alt when String.trim alt = "" -> 525 525 (* img with empty alt must not have role attribute *) 526 526 if role_attr <> None then 527 - Message_collector.add_typed collector Error_code.Img_empty_alt_with_role 527 + Message_collector.add_typed collector (`Img `Empty_alt_with_role) 528 528 | _ -> () 529 529 end; 530 530 ··· 537 537 if input_type = "checkbox" && List.mem "button" explicit_roles then begin 538 538 let has_aria_pressed = List.assoc_opt "aria-pressed" attrs <> None in 539 539 if not has_aria_pressed then 540 - Message_collector.add_typed collector Error_code.Checkbox_button_needs_aria_pressed 540 + Message_collector.add_typed collector (`Input `Checkbox_needs_aria_pressed) 541 541 end 542 542 end; 543 543 ··· 551 551 | Some _ -> 552 552 let valid_roles = ["group"; "menuitem"; "menuitemcheckbox"; "menuitemradio"; "separator"] in 553 553 if not (List.mem first_role valid_roles) then 554 - Message_collector.add_typed collector Error_code.Li_bad_role_in_menu 554 + Message_collector.add_typed collector (`Li_role `Li_bad_role_in_menu) 555 555 | None -> 556 556 (* Check if in tablist context *) 557 557 match get_ancestor_role state ["tablist"] with 558 558 | Some _ -> 559 559 if first_role <> "tab" then 560 - Message_collector.add_typed collector Error_code.Li_bad_role_in_tablist 560 + Message_collector.add_typed collector (`Li_role `Li_bad_role_in_tablist) 561 561 | None -> 562 562 (* Check if in list context (ul/ol/menu without explicit role, or role=list) *) 563 563 (* Nu validator produces this error for ANY explicit role on li in list context, 564 564 even role="listitem" - because having an explicit role is itself the problem. 565 565 The message says "other than listitem" but the rule is: don't use explicit roles. *) 566 566 if is_in_list_context state then 567 - Message_collector.add_typed collector Error_code.Li_bad_role_in_list) 567 + Message_collector.add_typed collector (`Li_role `Li_bad_role_in_list)) 568 568 end 569 569 end; 570 570 ··· 573 573 let aria_hidden = List.assoc_opt "aria-hidden" attrs in 574 574 match aria_hidden with 575 575 | Some "true" -> 576 - Message_collector.add_typed collector Error_code.Aria_hidden_on_body 576 + Message_collector.add_typed collector (`Aria `Hidden_on_body) 577 577 | _ -> () 578 578 end; 579 579 ··· 584 584 | Some input_type when String.lowercase_ascii input_type = "checkbox" -> 585 585 if aria_checked <> None then 586 586 Message_collector.add_typed collector 587 - (Error_code.Aria_must_not_be_used { attr = "aria-checked"; element = "input"; 588 - condition = "a \xe2\x80\x9ctype\xe2\x80\x9d attribute whose value is \xe2\x80\x9ccheckbox\xe2\x80\x9d" }) 587 + (`Aria (`Must_not_use (`Attr "aria-checked", `Elem "input", 588 + `Condition "a \xe2\x80\x9ctype\xe2\x80\x9d attribute whose value is \xe2\x80\x9ccheckbox\xe2\x80\x9d"))) 589 589 | _ -> () 590 590 end; 591 591 ··· 599 599 match role_to_check with 600 600 | Some _role when List.mem _role roles_without_aria_expanded -> 601 601 Message_collector.add_typed collector 602 - (Error_code.Attr_not_allowed_on_element { attr = "aria-expanded"; element = name }) 602 + (`Attr (`Not_allowed (`Attr "aria-expanded", `Elem name))) 603 603 | _ -> () 604 604 end; 605 605 ··· 622 622 Printf.sprintf "for element \xe2\x80\x9c%s\xe2\x80\x9d" name 623 623 in 624 624 Message_collector.add_typed collector 625 - (Error_code.Unnecessary_role { role = first_role; element = name; reason }) 625 + (`Aria (`Unnecessary_role (`Role first_role, `Elem name, `Reason reason))) 626 626 | _ -> () 627 627 end; 628 628 ··· 634 634 match role_attr with 635 635 | Some role_value -> 636 636 Message_collector.add_typed collector 637 - (Error_code.Bad_role { element = name; role = role_value }) 637 + (`Aria (`Bad_role (`Elem name, `Role role_value))) 638 638 | None -> () 639 639 end; 640 640 ··· 642 642 (* Check if role cannot be named *) 643 643 if Hashtbl.mem roles_which_cannot_be_named role && has_accessible_name then 644 644 Message_collector.add_typed collector 645 - (Error_code.Generic { message = Printf.sprintf 645 + (`Generic (Printf.sprintf 646 646 "Elements with role=\"%s\" must not have accessible names (via aria-label or aria-labelledby)." 647 - role }); 647 + role)); 648 648 649 649 (* Check for required ancestor roles *) 650 650 begin match Hashtbl.find_opt required_role_ancestor_by_descendant role with 651 651 | Some required_ancestors -> 652 652 if not (has_required_ancestor_role state required_ancestors) then 653 653 Message_collector.add_typed collector 654 - (Error_code.Generic { message = Printf.sprintf 654 + (`Generic (Printf.sprintf 655 655 "An element with \"role=%s\" must be contained in, or owned by, an element with the \"role\" value %s." 656 656 role 657 - (render_role_set required_ancestors) }) 657 + (render_role_set required_ancestors))) 658 658 | None -> () 659 659 end; 660 660 ··· 666 666 (* Check if current role is in the deprecated list *) 667 667 if Array.mem role deprecated_for_roles then 668 668 Message_collector.add_typed collector 669 - (Error_code.Aria_should_not_be_used { attr = attr_name; role }) 669 + (`Aria (`Should_not_use (`Attr attr_name, `Role role))) 670 670 | None -> () 671 671 ) attrs 672 672 ) explicit_roles; ··· 680 680 let value_lower = String.lowercase_ascii (String.trim attr_value) in 681 681 if value_lower = default_value then 682 682 Message_collector.add_typed collector 683 - (Error_code.Generic { message = Printf.sprintf 683 + (`Generic (Printf.sprintf 684 684 "The \xe2\x80\x9c%s\xe2\x80\x9d attribute is unnecessary for the value \xe2\x80\x9c%s\xe2\x80\x9d." 685 - attr_name attr_value }) 685 + attr_name attr_value)) 686 686 | None -> () 687 687 ) attrs; 688 688 ··· 697 697 (* summary that is the first child of details *) 698 698 if has_role_attr then 699 699 (* Must not have role attribute *) 700 - Message_collector.add_typed collector Error_code.Summary_role_not_allowed 700 + Message_collector.add_typed collector (`Misc `Summary_role_not_allowed) 701 701 else if has_aria_pressed then 702 702 (* aria-pressed without role requires role *) 703 - Message_collector.add_typed collector Error_code.Summary_missing_role 703 + Message_collector.add_typed collector (`Misc `Summary_missing_role) 704 704 else if has_aria_expanded then 705 705 (* aria-expanded without role requires role *) 706 - Message_collector.add_typed collector Error_code.Summary_missing_attrs 706 + Message_collector.add_typed collector (`Misc `Summary_missing_attrs) 707 707 end else begin 708 708 (* summary NOT in details context - different rules apply *) 709 709 (* If has aria-expanded or aria-pressed, must have role *) 710 710 if (has_aria_expanded || has_aria_pressed) && explicit_roles = [] then begin 711 711 if has_aria_pressed then 712 - Message_collector.add_typed collector Error_code.Summary_missing_role 712 + Message_collector.add_typed collector (`Misc `Summary_missing_role) 713 713 else 714 - Message_collector.add_typed collector Error_code.Summary_missing_attrs 714 + Message_collector.add_typed collector (`Misc `Summary_missing_attrs) 715 715 end 716 716 end 717 717 end; ··· 739 739 let end_document state collector = 740 740 (* Check that active tabs have corresponding tabpanels *) 741 741 if state.has_active_tab && not state.has_tabpanel then 742 - Message_collector.add_typed collector Error_code.Tab_without_tabpanel; 742 + Message_collector.add_typed collector (`Aria `Tab_without_tabpanel); 743 743 744 744 (* Check for multiple visible main elements *) 745 745 if state.visible_main_count > 1 then 746 - Message_collector.add_typed collector Error_code.Multiple_main_visible 746 + Message_collector.add_typed collector (`Aria `Multiple_main) 747 747 748 748 let checker = (module struct 749 749 type nonrec state = state
+35 -35
lib/html5_checker/specialized/attr_restrictions_checker.ml
··· 60 60 (** Report disallowed attribute error *) 61 61 let report_disallowed_attr element attr collector = 62 62 Message_collector.add_typed collector 63 - (Error_code.Attr_not_allowed_on_element { attr; element }) 63 + (`Attr (`Not_allowed (`Attr attr, `Elem element))) 64 64 65 65 let start_element state ~name ~namespace ~attrs collector = 66 66 let name_lower = String.lowercase_ascii name in ··· 100 100 (* Only xmlns:xlink (with correct value) and xmlns:xml are allowed *) 101 101 if prefix <> "xlink" && prefix <> "xml" then 102 102 Message_collector.add_typed collector 103 - (Error_code.Attr_not_allowed_here { attr = attr_name }) 103 + (`Attr (`Not_allowed_here (`Attr attr_name))) 104 104 end 105 105 ) attrs 106 106 end; ··· 116 116 if name_lower = "feconvolvematrix" then begin 117 117 if not (has_attr "order" attrs) then 118 118 Message_collector.add_typed collector 119 - (Error_code.Missing_required_svg_attr { element = "feConvolveMatrix"; attr = "order" }) 119 + (`Svg (`Missing_attr (`Elem "feConvolveMatrix", `Attr "order"))) 120 120 end; 121 121 122 122 (* Validate style type attribute - must be "text/css" or omitted *) ··· 126 126 if attr_lower = "type" then begin 127 127 let value_lower = String.lowercase_ascii (String.trim attr_value) in 128 128 if value_lower <> "text/css" then 129 - Message_collector.add_typed collector Error_code.Style_type_invalid 129 + Message_collector.add_typed collector (`Misc `Style_type_invalid) 130 130 end 131 131 ) attrs 132 132 end; ··· 137 137 let has_type = has_attr "type" attrs in 138 138 if not has_data && not has_type then 139 139 Message_collector.add_typed collector 140 - (Error_code.Missing_required_attr { element = "object"; attr = "data" }) 140 + (`Attr (`Missing (`Elem "object", `Attr "data"))) 141 141 end; 142 142 143 143 (* Validate link imagesizes/imagesrcset attributes *) ··· 149 149 150 150 (* imagesizes requires imagesrcset *) 151 151 if has_imagesizes && not has_imagesrcset then 152 - Message_collector.add_typed collector Error_code.Imagesizes_without_imagesrcset; 152 + Message_collector.add_typed collector (`Srcset `Imagesizes_without_imagesrcset); 153 153 154 154 (* imagesrcset requires as="image" *) 155 155 if has_imagesrcset then begin ··· 158 158 | None -> false 159 159 in 160 160 if not as_is_image then 161 - Message_collector.add_typed collector Error_code.Link_imagesrcset_requires_as_image 161 + Message_collector.add_typed collector (`Link `Imagesrcset_requires_as_image) 162 162 end; 163 163 164 164 (* as attribute requires rel="preload" or rel="modulepreload" *) ··· 173 173 | None -> false 174 174 in 175 175 if not rel_is_preload then 176 - Message_collector.add_typed collector Error_code.Link_as_requires_preload 176 + Message_collector.add_typed collector (`Link `As_requires_preload) 177 177 | None -> ()) 178 178 end; 179 179 ··· 184 184 if attr_lower = "usemap" then begin 185 185 if attr_value = "#" then 186 186 Message_collector.add_typed collector 187 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 187 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 188 188 "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." 189 - attr_value attr_name name }) 189 + attr_value attr_name name)))) 190 190 end 191 191 ) attrs 192 192 end; ··· 200 200 | Ok () -> () 201 201 | Error msg -> 202 202 Message_collector.add_typed collector 203 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 203 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 204 204 "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" 205 - attr_value attr_name name msg }) 205 + attr_value attr_name name msg)))) 206 206 end 207 207 ) attrs 208 208 end; ··· 251 251 attr_value attr_name name 252 252 in 253 253 Message_collector.add_typed collector 254 - (Error_code.Bad_attr_value_generic { message = error_msg }) 254 + (`Attr (`Bad_value_generic (`Message error_msg))) 255 255 end 256 256 end 257 257 ) attrs ··· 264 264 | Some s when String.lowercase_ascii (String.trim s) = "default" -> 265 265 if has_attr "coords" attrs then 266 266 Message_collector.add_typed collector 267 - (Error_code.Attr_not_allowed_on_element { attr = "coords"; element = "area" }) 267 + (`Attr (`Not_allowed (`Attr "coords", `Elem "area"))) 268 268 | _ -> () 269 269 end; 270 270 ··· 273 273 let dir_value = get_attr "dir" attrs in 274 274 match dir_value with 275 275 | None -> 276 - Message_collector.add_typed collector Error_code.Bdo_missing_dir 276 + Message_collector.add_typed collector (`Misc `Bdo_missing_dir) 277 277 | Some v when String.lowercase_ascii (String.trim v) = "auto" -> 278 - Message_collector.add_typed collector Error_code.Bdo_dir_auto 278 + Message_collector.add_typed collector (`Misc `Bdo_dir_auto) 279 279 | _ -> () 280 280 end; 281 281 ··· 287 287 | None -> "text" (* default type is text *) 288 288 in 289 289 if not (List.mem input_type input_types_allowing_list) then 290 - Message_collector.add_typed collector Error_code.Input_list_not_allowed 290 + Message_collector.add_typed collector (`Input `List_not_allowed) 291 291 end 292 292 end; 293 293 ··· 304 304 (* Check if the name contains colon - not XML serializable *) 305 305 else if String.contains after_prefix ':' then 306 306 Message_collector.add_typed collector 307 - (Error_code.Data_attr_invalid_name { reason = "must be XML 1.0 4th ed. plus Namespaces NCNames" }) 307 + (`Attr (`Data_invalid_name (`Reason "must be XML 1.0 4th ed. plus Namespaces NCNames"))) 308 308 end 309 309 ) attrs 310 310 end; ··· 318 318 (match lang_value with 319 319 | None -> 320 320 (* xml:lang without lang attribute *) 321 - Message_collector.add_typed collector Error_code.Xml_lang_without_lang 321 + Message_collector.add_typed collector (`I18n `Xml_lang_without_lang) 322 322 | Some lang when String.lowercase_ascii lang <> String.lowercase_ascii xmllang -> 323 323 (* xml:lang and lang have different values - "lang present with same value" message *) 324 - Message_collector.add_typed collector Error_code.Xml_lang_without_lang 324 + Message_collector.add_typed collector (`I18n `Xml_lang_without_lang) 325 325 | _ -> ()) 326 326 | None -> () 327 327 end; ··· 334 334 let value_lower = String.lowercase_ascii (String.trim attr_value) in 335 335 if value_lower <> "" && value_lower <> "true" && value_lower <> "false" then 336 336 Message_collector.add_typed collector 337 - (Error_code.Bad_attr_value { element = name; attr = attr_name; value = attr_value; reason = "" }) 337 + (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason ""))) 338 338 end 339 339 ) attrs 340 340 end; ··· 348 348 let value_lower = String.lowercase_ascii (String.trim attr_value) in 349 349 if not (List.mem value_lower valid_enterkeyhint) then 350 350 Message_collector.add_typed collector 351 - (Error_code.Bad_attr_value { element = name; attr = attr_name; value = attr_value; reason = "" }) 351 + (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason ""))) 352 352 end 353 353 ) attrs 354 354 end; ··· 368 368 with _ -> false) 369 369 in 370 370 if not is_valid then 371 - Message_collector.add_typed collector Error_code.Headingoffset_invalid 371 + Message_collector.add_typed collector (`Misc `Headingoffset_invalid) 372 372 end 373 373 ) attrs 374 374 end; ··· 401 401 List.iter (fun key -> 402 402 if count_codepoints key > 1 then 403 403 Message_collector.add_typed collector 404 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 404 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 405 405 "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." 406 - attr_value attr_name name }) 406 + attr_value attr_name name)))) 407 407 ) keys; 408 408 (* Check for duplicate keys *) 409 409 let rec find_duplicates seen = function ··· 411 411 | k :: rest -> 412 412 if List.mem k seen then 413 413 Message_collector.add_typed collector 414 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 414 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 415 415 "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." 416 - attr_value attr_name name }) 416 + attr_value attr_name name)))) 417 417 else 418 418 find_duplicates (k :: seen) rest 419 419 in ··· 430 430 431 431 if has_command && has_aria_expanded then 432 432 Message_collector.add_typed collector 433 - (Error_code.Attr_not_allowed_when { attr = "aria-expanded"; element = name; 434 - condition = "a \xe2\x80\x9ccommand\xe2\x80\x9d attribute" }); 433 + (`Attr (`Not_allowed_when (`Attr "aria-expanded", `Elem name, 434 + `Condition "a \xe2\x80\x9ccommand\xe2\x80\x9d attribute"))); 435 435 436 436 if has_popovertarget && has_aria_expanded then 437 437 Message_collector.add_typed collector 438 - (Error_code.Attr_not_allowed_when { attr = "aria-expanded"; element = name; 439 - condition = "a \xe2\x80\x9cpopovertarget\xe2\x80\x9d attribute" }) 438 + (`Attr (`Not_allowed_when (`Attr "aria-expanded", `Elem name, 439 + `Condition "a \xe2\x80\x9cpopovertarget\xe2\x80\x9d attribute"))) 440 440 end; 441 441 442 442 (* Note: data-* uppercase check requires XML parsing which preserves case. ··· 456 456 | Ok () -> () 457 457 | Error msg -> 458 458 Message_collector.add_typed collector 459 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 459 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 460 460 "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" 461 - attr_value attr_name name msg }) 461 + attr_value attr_name name msg)))) 462 462 end 463 463 end 464 464 ) attrs ··· 475 475 (* Check for empty prefix (starts with : or has space:) *) 476 476 if String.length trimmed > 0 && trimmed.[0] = ':' then 477 477 Message_collector.add_typed collector 478 - (Error_code.Bad_attr_value { element = name; attr = attr_name; value = attr_value; reason = "" }) 478 + (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason ""))) 479 479 else begin 480 480 (* Check for invalid prefix names - must start with letter or underscore *) 481 481 let is_ncname_start c = ··· 483 483 in 484 484 if String.length trimmed > 0 && not (is_ncname_start trimmed.[0]) then 485 485 Message_collector.add_typed collector 486 - (Error_code.Bad_attr_value { element = name; attr = attr_name; value = attr_value; reason = "" }) 486 + (`Attr (`Bad_value (`Elem name, `Attr attr_name, `Value attr_value, `Reason ""))) 487 487 end 488 488 end 489 489 end
+2 -2
lib/html5_checker/specialized/base_checker.ml
··· 24 24 state.seen_link_or_script <- true 25 25 | "base" -> 26 26 if state.seen_link_or_script then 27 - Message_collector.add_typed collector Error_code.Base_after_link_script; 27 + Message_collector.add_typed collector (`Misc `Base_after_link_script); 28 28 (* base element must have href or target attribute *) 29 29 let has_href = has_attr "href" attrs in 30 30 let has_target = has_attr "target" attrs in 31 31 if not has_href && not has_target then 32 - Message_collector.add_typed collector Error_code.Base_missing_href_or_target 32 + Message_collector.add_typed collector (`Misc `Base_missing_href_or_target) 33 33 | _ -> () 34 34 end 35 35
+2 -2
lib/html5_checker/specialized/datetime_checker.ml
··· 463 463 | Ok -> () 464 464 | Error error_msg -> 465 465 Message_collector.add_typed collector 466 - (Error_code.Bad_attr_value_generic { message = error_msg }) 466 + (`Attr (`Bad_value_generic (`Message error_msg))) 467 467 | Warning warn_msg -> 468 468 Message_collector.add_typed collector 469 - (Error_code.Generic { message = warn_msg }) 469 + (`Generic warn_msg) 470 470 end 471 471 end 472 472
+20 -20
lib/html5_checker/specialized/dl_checker.ml
··· 86 86 begin match current_div state with 87 87 | Some _ -> 88 88 Message_collector.add_typed collector 89 - (Error_code.Element_not_allowed_as_child { child = "dl"; parent = "div" }) 89 + (`Element (`Not_allowed_as_child (`Child "dl", `Parent "div"))) 90 90 | None -> 91 91 match current_dl state with 92 92 | Some _ when state.in_dt_dd = 0 -> 93 93 Message_collector.add_typed collector 94 - (Error_code.Element_not_allowed_as_child { child = "dl"; parent = "dl" }) 94 + (`Element (`Not_allowed_as_child (`Child "dl", `Parent "dl"))) 95 95 | _ -> () 96 96 end; 97 97 let ctx = { ··· 113 113 (* Check for mixed content - if we already have dt/dd, div is not allowed *) 114 114 if dl_ctx.contains_dt_dd then 115 115 Message_collector.add_typed collector 116 - (Error_code.Element_not_allowed_as_child { child = "div"; parent = "dl" }); 116 + (`Element (`Not_allowed_as_child (`Child "div", `Parent "dl"))); 117 117 (* Check that role is only presentation or none *) 118 118 (match get_attr "role" attrs with 119 119 | Some role_value -> 120 120 let role_lower = String.lowercase_ascii (String.trim role_value) in 121 121 if role_lower <> "presentation" && role_lower <> "none" then 122 - Message_collector.add_typed collector Error_code.Div_child_of_dl_bad_role 122 + Message_collector.add_typed collector (`Li_role `Div_in_dl_bad_role) 123 123 | None -> ()); 124 124 let div_ctx = { has_dt = false; has_dd = false; group_count = 0; in_dd_part = false } in 125 125 state.div_in_dl_stack <- div_ctx :: state.div_in_dl_stack 126 126 | Some _ when state.div_in_dl_stack <> [] -> 127 127 Message_collector.add_typed collector 128 - (Error_code.Element_not_allowed_as_child { child = "div"; parent = "div" }) 128 + (`Element (`Not_allowed_as_child (`Child "div", `Parent "div"))) 129 129 | _ -> () 130 130 end 131 131 ··· 136 136 (* If we've already seen dd, this dt starts a new group - which is not allowed *) 137 137 if div_ctx.in_dd_part then begin 138 138 Message_collector.add_typed collector 139 - (Error_code.Element_not_allowed_as_child { child = "dt"; parent = "div" }); 139 + (`Element (`Not_allowed_as_child (`Child "dt", `Parent "div"))); 140 140 div_ctx.group_count <- div_ctx.group_count + 1; 141 141 div_ctx.in_dd_part <- false 142 142 end; ··· 150 150 (* Check for mixed content - if we already have div, dt is not allowed *) 151 151 if dl_ctx.contains_div then 152 152 Message_collector.add_typed collector 153 - (Error_code.Element_not_allowed_as_child { child = "dt"; parent = "dl" }) 153 + (`Element (`Not_allowed_as_child (`Child "dt", `Parent "dl"))) 154 154 | None -> 155 155 (* dt outside dl context - error *) 156 156 let parent = match current_parent state with ··· 158 158 | None -> "document" 159 159 in 160 160 Message_collector.add_typed collector 161 - (Error_code.Element_not_allowed_as_child { child = "dt"; parent }) 161 + (`Element (`Not_allowed_as_child (`Child "dt", `Parent parent))) 162 162 end 163 163 164 164 | "dd" when state.in_template = 0 -> ··· 178 178 if not dl_ctx.has_dt && not dl_ctx.dd_before_dt_error_reported then begin 179 179 dl_ctx.dd_before_dt_error_reported <- true; 180 180 Message_collector.add_typed collector 181 - (Error_code.Missing_required_child_generic { parent = "dl" }) 181 + (`Element (`Missing_child_generic (`Parent "dl"))) 182 182 end; 183 183 dl_ctx.has_dd <- true; 184 184 dl_ctx.last_was_dt <- false; ··· 186 186 (* Check for mixed content *) 187 187 if dl_ctx.contains_div then 188 188 Message_collector.add_typed collector 189 - (Error_code.Element_not_allowed_as_child { child = "dd"; parent = "dl" }) 189 + (`Element (`Not_allowed_as_child (`Child "dd", `Parent "dl"))) 190 190 | None -> 191 191 (* dd outside dl context - error *) 192 192 let parent = match current_parent state with ··· 194 194 | None -> "document" 195 195 in 196 196 Message_collector.add_typed collector 197 - (Error_code.Element_not_allowed_as_child { child = "dd"; parent }) 197 + (`Element (`Not_allowed_as_child (`Child "dd", `Parent parent))) 198 198 end 199 199 200 200 | _ -> () ··· 226 226 (* Direct dt/dd content - must have both *) 227 227 if not ctx.has_dt && not ctx.dd_before_dt_error_reported then 228 228 Message_collector.add_typed collector 229 - (Error_code.Missing_required_child_generic { parent = "dl" }) 229 + (`Element (`Missing_child_generic (`Parent "dl"))) 230 230 else if not ctx.has_dd then begin 231 231 if ctx.has_template then 232 232 Message_collector.add_typed collector 233 - (Error_code.Missing_required_child_one_of { parent = "dl"; children = ["dd"] }) 233 + (`Element (`Missing_child_one_of (`Parent "dl", `Children ["dd"]))) 234 234 else 235 235 Message_collector.add_typed collector 236 - (Error_code.Missing_required_child { parent = "dl"; child = "dd" }) 236 + (`Element (`Missing_child (`Parent "dl", `Child "dd"))) 237 237 end 238 238 else if ctx.last_was_dt then 239 239 Message_collector.add_typed collector 240 - (Error_code.Missing_required_child { parent = "dl"; child = "dd" }) 240 + (`Element (`Missing_child (`Parent "dl", `Child "dd"))) 241 241 end else if not ctx.contains_div && not ctx.has_dt && not ctx.has_dd then 242 242 () 243 243 | [] -> () ··· 250 250 (* Check div in dl must have both dt and dd *) 251 251 if not div_ctx.has_dt && not div_ctx.has_dd then 252 252 Message_collector.add_typed collector 253 - (Error_code.Missing_required_child { parent = "div"; child = "dd" }) 253 + (`Element (`Missing_child (`Parent "div", `Child "dd"))) 254 254 else if not div_ctx.has_dt then 255 255 Message_collector.add_typed collector 256 - (Error_code.Missing_required_child { parent = "div"; child = "dt" }) 256 + (`Element (`Missing_child (`Parent "div", `Child "dt"))) 257 257 else if not div_ctx.has_dd then 258 258 Message_collector.add_typed collector 259 - (Error_code.Missing_required_child { parent = "div"; child = "dd" }) 259 + (`Element (`Missing_child (`Parent "div", `Child "dd"))) 260 260 | [] -> () 261 261 end 262 262 ··· 273 273 match current_div state with 274 274 | Some _ -> 275 275 Message_collector.add_typed collector 276 - (Error_code.Text_not_allowed { parent = "div" }) 276 + (`Element (`Text_not_allowed (`Parent "div"))) 277 277 | None -> 278 278 match current_dl state with 279 279 | Some _ -> 280 280 Message_collector.add_typed collector 281 - (Error_code.Text_not_allowed { parent = "dl" }) 281 + (`Element (`Text_not_allowed (`Parent "dl"))) 282 282 | None -> () 283 283 end 284 284 end
+1 -1
lib/html5_checker/specialized/h1_checker.ml
··· 25 25 else if name_lower = "h1" then begin 26 26 state.h1_count <- state.h1_count + 1; 27 27 if state.h1_count > 1 then 28 - Message_collector.add_typed collector Error_code.Multiple_h1 28 + Message_collector.add_typed collector (`Misc `Multiple_h1) 29 29 end 30 30 31 31 let end_element state ~name ~namespace:_ _collector =
+8 -8
lib/html5_checker/specialized/heading_checker.ml
··· 67 67 state.first_heading_checked <- true; 68 68 if level <> 1 then 69 69 Message_collector.add_typed collector 70 - (Error_code.Generic { message = Printf.sprintf 71 - "First heading in document is <%s>, should typically be <h1>" name }) 70 + (`Generic (Printf.sprintf 71 + "First heading in document is <%s>, should typically be <h1>" name)) 72 72 end; 73 73 74 74 (* Track h1 count *) 75 75 if level = 1 then begin 76 76 state.h1_count <- state.h1_count + 1; 77 77 if state.h1_count > 1 then 78 - Message_collector.add_typed collector Error_code.Multiple_h1 78 + Message_collector.add_typed collector (`Misc `Multiple_h1) 79 79 end; 80 80 81 81 (* Check for skipped levels *) ··· 86 86 let diff = level - prev_level in 87 87 if diff > 1 then 88 88 Message_collector.add_typed collector 89 - (Error_code.Generic { message = Printf.sprintf 89 + (`Generic (Printf.sprintf 90 90 "Heading level skipped: <%s> follows <h%d>, skipping %d level%s. This can confuse screen reader users" 91 - name prev_level (diff - 1) (if diff > 2 then "s" else "") }); 91 + name prev_level (diff - 1) (if diff > 2 then "s" else ""))); 92 92 state.current_level <- Some level 93 93 end; 94 94 ··· 105 105 | Some heading when heading = name -> 106 106 if not state.heading_has_text then 107 107 Message_collector.add_typed collector 108 - (Error_code.Generic { message = Printf.sprintf 109 - "Heading <%s> is empty or contains only whitespace. Empty headings are problematic for screen readers" name }); 108 + (`Generic (Printf.sprintf 109 + "Heading <%s> is empty or contains only whitespace. Empty headings are problematic for screen readers" name)); 110 110 state.in_heading <- None; 111 111 state.heading_has_text <- false 112 112 | _ -> () ··· 123 123 let end_document state collector = 124 124 if not state.has_any_heading then 125 125 Message_collector.add_typed collector 126 - (Error_code.Generic { message = "Document contains no heading elements (h1-h6). Headings provide important document structure for accessibility" }) 126 + (`Generic "Document contains no heading elements (h1-h6). Headings provide important document structure for accessibility") 127 127 128 128 let checker = (module struct 129 129 type nonrec state = state
+11 -11
lib/html5_checker/specialized/importmap_checker.ml
··· 283 283 end 284 284 285 285 let error_to_typed = function 286 - | InvalidJSON _ -> Error_code.Importmap_invalid_json 287 - | EmptyKey _ -> Error_code.Importmap_empty_key 288 - | NotObject prop when prop = "root" -> Error_code.Importmap_invalid_root 289 - | NotObject prop when prop = "imports" -> Error_code.Importmap_imports_not_object 290 - | NotObject _ -> Error_code.Importmap_scopes_not_object (* scopes *) 291 - | NotString _ -> Error_code.Importmap_non_string_value 292 - | ForbiddenProperty _ -> Error_code.Importmap_invalid_root 293 - | SlashKeyWithoutSlashValue _ -> Error_code.Importmap_key_trailing_slash 294 - | InvalidScopeKey -> Error_code.Importmap_scopes_invalid_url 295 - | InvalidScopeValue _ -> Error_code.Importmap_scopes_value_invalid_url 296 - | ScopeValueNotObject -> Error_code.Importmap_scopes_values_not_object 286 + | InvalidJSON _ -> `Importmap `Invalid_json 287 + | EmptyKey _ -> `Importmap `Empty_key 288 + | NotObject prop when prop = "root" -> `Importmap `Invalid_root 289 + | NotObject prop when prop = "imports" -> `Importmap `Imports_not_object 290 + | NotObject _ -> `Importmap `Scopes_not_object (* scopes *) 291 + | NotString _ -> `Importmap `Non_string_value 292 + | ForbiddenProperty _ -> `Importmap `Invalid_root 293 + | SlashKeyWithoutSlashValue _ -> `Importmap `Key_trailing_slash 294 + | InvalidScopeKey -> `Importmap `Scopes_invalid_url 295 + | InvalidScopeValue _ -> `Importmap `Scopes_value_invalid_url 296 + | ScopeValueNotObject -> `Importmap `Scopes_values_not_object 297 297 298 298 let end_element state ~name ~namespace collector = 299 299 if namespace <> None then ()
+6 -6
lib/html5_checker/specialized/label_checker.ml
··· 84 84 if List.mem name_lower labelable_elements then begin 85 85 state.labelable_count <- state.labelable_count + 1; 86 86 if state.labelable_count > 1 then 87 - Message_collector.add_typed collector Error_code.Label_too_many_labelable; 87 + Message_collector.add_typed collector (`Label `Too_many_labelable); 88 88 89 89 (* Check if label has for attribute and descendant has mismatched id *) 90 90 (match state.label_for_value with ··· 92 92 let descendant_id = get_attr attrs "id" in 93 93 (match descendant_id with 94 94 | None -> 95 - Message_collector.add_typed collector Error_code.Label_for_id_mismatch 95 + Message_collector.add_typed collector (`Label `For_id_mismatch) 96 96 | Some id when id <> for_value -> 97 - Message_collector.add_typed collector Error_code.Label_for_id_mismatch 97 + Message_collector.add_typed collector (`Label `For_id_mismatch) 98 98 | Some _ -> ()) 99 99 | None -> ()) 100 100 end ··· 111 111 112 112 if name_lower = "label" && state.label_depth = 0 then begin 113 113 if state.label_has_role && state.labelable_count > 0 then 114 - Message_collector.add_typed collector Error_code.Role_on_label_ancestor; 114 + Message_collector.add_typed collector (`Label `Role_on_ancestor); 115 115 state.in_label <- false; 116 116 state.labelable_count <- 0; 117 117 state.label_for_value <- None; ··· 127 127 List.iter (fun label_info -> 128 128 if List.mem label_info.for_target state.labelable_ids then begin 129 129 if label_info.has_role then 130 - Message_collector.add_typed collector Error_code.Role_on_label_for; 130 + Message_collector.add_typed collector (`Label `Role_on_for); 131 131 if label_info.has_aria_label then 132 - Message_collector.add_typed collector Error_code.Aria_label_on_label_for 132 + Message_collector.add_typed collector (`Label `Aria_label_on_for) 133 133 end 134 134 ) state.labels_for 135 135
+3 -3
lib/html5_checker/specialized/language_checker.ml
··· 44 44 | Error msg -> 45 45 let reason = Printf.sprintf "Bad language tag: %s." msg in 46 46 Message_collector.add_typed collector 47 - (Error_code.Bad_attr_value { element; attr = attribute; value; reason }) 47 + (`Attr (`Bad_value (`Elem element, `Attr attribute, `Value value, `Reason reason))) 48 48 | Ok () -> 49 49 (* Then check for deprecated subtags *) 50 50 match check_deprecated_tag value with ··· 52 52 let reason = Printf.sprintf "Bad language tag: The language subtag %s is deprecated. Use %s instead." 53 53 (Error_code.q deprecated) (Error_code.q replacement) in 54 54 Message_collector.add_typed collector 55 - (Error_code.Generic { message = Printf.sprintf "Bad value %s for attribute %s on element %s: %s" 56 - (Error_code.q value) (Error_code.q attribute) (Error_code.q element) reason }) 55 + (`Generic (Printf.sprintf "Bad value %s for attribute %s on element %s: %s" 56 + (Error_code.q value) (Error_code.q attribute) (Error_code.q element) reason)) 57 57 | None -> () 58 58 59 59 (** Check if lang and xml:lang match. *)
+13 -13
lib/html5_checker/specialized/microdata_checker.ml
··· 130 130 | Some itemid -> 131 131 if not has_itemscope then 132 132 Message_collector.add_typed collector 133 - (Error_code.Generic { message = "itemid attribute requires itemscope attribute" }); 133 + (`Generic "itemid attribute requires itemscope attribute"); 134 134 if itemtype_opt = None then 135 135 Message_collector.add_typed collector 136 - (Error_code.Generic { message = "itemid attribute requires itemtype attribute" }); 136 + (`Generic "itemid attribute requires itemtype attribute"); 137 137 (match Url_checker.validate_url itemid element "itemid" with 138 138 | None -> () 139 139 | Some error_msg -> 140 - Message_collector.add_typed collector (Error_code.Bad_attr_value_generic { message = error_msg })) 140 + Message_collector.add_typed collector (`Attr (`Bad_value_generic (`Message error_msg)))) 141 141 | None -> () 142 142 end; 143 143 ··· 145 145 | Some itemref_value -> 146 146 if not has_itemscope then 147 147 Message_collector.add_typed collector 148 - (Error_code.Generic { message = "itemref attribute requires itemscope attribute" }) 148 + (`Generic "itemref attribute requires itemscope attribute") 149 149 else begin 150 150 let ids = split_whitespace itemref_value in 151 151 state.itemref_references <- { ··· 161 161 | Some itemtype -> 162 162 if not has_itemscope then 163 163 Message_collector.add_typed collector 164 - (Error_code.Generic { message = "itemtype attribute requires itemscope attribute" }) 164 + (`Generic "itemtype attribute requires itemscope attribute") 165 165 else begin 166 166 let types = split_whitespace itemtype in 167 167 if types = [] then 168 168 Message_collector.add_typed collector 169 - (Error_code.Bad_attr_value { element; attr = "itemtype"; value = itemtype; reason = "" }) 169 + (`Attr (`Bad_value (`Elem element, `Attr "itemtype", `Value itemtype, `Reason ""))) 170 170 else 171 171 List.iter (fun url -> 172 172 match validate_microdata_url url element "itemtype" itemtype with 173 173 | None -> () 174 174 | Some error_msg -> 175 - Message_collector.add_typed collector (Error_code.Bad_attr_value_generic { message = error_msg }) 175 + Message_collector.add_typed collector (`Attr (`Bad_value_generic (`Message error_msg))) 176 176 ) types 177 177 end 178 178 | None -> () ··· 187 187 | Ok () -> () 188 188 | Error msg -> 189 189 Message_collector.add_typed collector 190 - (Error_code.Generic { message = msg }) 190 + (`Generic msg) 191 191 ) props; 192 192 193 193 (* Check itemprop can only appear on property elements *) 194 194 if not (is_property_element state) then 195 195 Message_collector.add_typed collector 196 - (Error_code.Generic { message = "itemprop attribute can only appear on elements that are \ 197 - properties of an item (descendant of itemscope or referenced by itemref)" }) 196 + (`Generic "itemprop attribute can only appear on elements that are \ 197 + properties of an item (descendant of itemscope or referenced by itemref)") 198 198 | None -> () 199 199 end; 200 200 ··· 261 261 | Some cycle -> 262 262 let cycle_str = String.concat " -> " (List.rev cycle) in 263 263 Message_collector.add_typed collector 264 - (Error_code.Generic { message = Printf.sprintf "itemref cycle detected: %s" cycle_str }) 264 + (`Generic (Printf.sprintf "itemref cycle detected: %s" cycle_str)) 265 265 | None -> () 266 266 end; 267 267 check_all_nodes (node :: visited) rest ··· 291 291 List.iter (fun id -> 292 292 if not (Hashtbl.mem state.all_ids id) then 293 293 Message_collector.add_typed collector 294 - (Error_code.Generic { message = Printf.sprintf 294 + (`Generic (Printf.sprintf 295 295 "itemref on <%s> refers to ID '%s' which does not exist" 296 - ref.referring_element id }) 296 + ref.referring_element id)) 297 297 ) ref.referenced_ids 298 298 ) state.itemref_references; 299 299
+2 -2
lib/html5_checker/specialized/mime_type_checker.ml
··· 179 179 | None -> () 180 180 | Some err -> 181 181 Message_collector.add_typed collector 182 - (Error_code.Bad_attr_value_generic { message = err }) 182 + (`Attr (`Bad_value_generic (`Message err))) 183 183 else 184 184 match validate_mime_type value name attr_name with 185 185 | None -> () 186 186 | Some err -> 187 187 Message_collector.add_typed collector 188 - (Error_code.Bad_attr_value_generic { message = err }) 188 + (`Attr (`Bad_value_generic (`Message err))) 189 189 ) type_attrs 190 190 end 191 191
+1 -1
lib/html5_checker/specialized/normalization_checker.ml
··· 53 53 (* Strip trailing ASCII punctuation from replacement to match Nu validator *) 54 54 let replacement = strip_trailing_punct normalized in 55 55 Message_collector.add_typed collector 56 - (Error_code.Not_nfc { replacement }) 56 + (`I18n (`Not_nfc (`Replacement replacement))) 57 57 end 58 58 59 59 let end_document _state _collector = ()
+12 -12
lib/html5_checker/specialized/picture_checker.ml
··· 73 73 (** Report disallowed attribute error *) 74 74 let report_disallowed_attr element attr collector = 75 75 Message_collector.add_typed collector 76 - (Error_code.Attr_not_allowed_on_element { attr; element }) 76 + (`Attr (`Not_allowed (`Attr attr, `Elem element))) 77 77 78 78 (** Report disallowed child element error *) 79 79 let report_disallowed_child parent child collector = 80 80 Message_collector.add_typed collector 81 - (Error_code.Element_not_allowed_as_child { child; parent }) 81 + (`Element (`Not_allowed_as_child (`Child child, `Parent parent))) 82 82 83 83 let check_picture_attrs attrs collector = 84 84 List.iter (fun disallowed -> ··· 94 94 (* source in picture requires srcset *) 95 95 if not (has_attr "srcset" attrs) then 96 96 Message_collector.add_typed collector 97 - Error_code.Source_missing_srcset 97 + (`Srcset `Source_missing_srcset) 98 98 99 99 let check_img_attrs attrs collector = 100 100 List.iter (fun disallowed -> ··· 119 119 (match state.parent_stack with 120 120 | parent :: _ when List.mem parent disallowed_picture_parents -> 121 121 Message_collector.add_typed collector 122 - (Error_code.Element_not_allowed_as_child { child = "picture"; parent }) 122 + (`Element (`Not_allowed_as_child (`Child "picture", `Parent parent))) 123 123 | _ -> ()); 124 124 check_picture_attrs attrs collector; 125 125 state.in_picture <- true; ··· 181 181 (* Check if always-matching source is followed by img with srcset *) 182 182 if state.has_always_matching_source && has_attr "srcset" attrs then begin 183 183 if state.always_matching_is_media_all then 184 - Message_collector.add_typed collector Error_code.Media_all 184 + Message_collector.add_typed collector (`Misc `Media_all) 185 185 else if state.always_matching_is_media_empty then 186 - Message_collector.add_typed collector Error_code.Media_empty 186 + Message_collector.add_typed collector (`Misc `Media_empty) 187 187 else 188 - Message_collector.add_typed collector Error_code.Source_needs_media_or_type 188 + Message_collector.add_typed collector (`Srcset `Source_needs_media_or_type) 189 189 end 190 190 191 191 | "script" when state.in_picture && state.picture_depth = 1 -> ··· 221 221 (* Check if picture had img child *) 222 222 if not state.has_img_in_picture then 223 223 Message_collector.add_typed collector 224 - Error_code.Picture_missing_img; 224 + (`Srcset `Picture_missing_img); 225 225 (* Check for source after img *) 226 226 if state.has_source_after_img then 227 227 report_disallowed_child "picture" "source" collector; 228 228 (* Check for source after always-matching source *) 229 229 if state.source_after_always_matching then begin 230 230 if state.always_matching_is_media_all then 231 - Message_collector.add_typed collector Error_code.Media_all 231 + Message_collector.add_typed collector (`Misc `Media_all) 232 232 else if state.always_matching_is_media_empty then 233 - Message_collector.add_typed collector Error_code.Media_empty 233 + Message_collector.add_typed collector (`Misc `Media_empty) 234 234 else 235 - Message_collector.add_typed collector Error_code.Source_needs_media_or_type 235 + Message_collector.add_typed collector (`Srcset `Source_needs_media_or_type) 236 236 end; 237 237 238 238 state.in_picture <- false ··· 250 250 let trimmed = String.trim text in 251 251 if trimmed <> "" then 252 252 Message_collector.add_typed collector 253 - (Error_code.Text_not_allowed { parent = "picture" }) 253 + (`Element (`Text_not_allowed (`Parent "picture"))) 254 254 end 255 255 256 256 let end_document _state _collector = ()
+4 -2
lib/html5_checker/specialized/ruby_checker.ml
··· 93 93 if name_lower = "ruby" && info.depth <= 0 then begin 94 94 (* Closing ruby element - validate *) 95 95 if not info.has_rt then 96 + (* Empty ruby or ruby without any rt - needs rp or rt *) 96 97 Message_collector.add_typed collector 97 - (Error_code.Missing_required_child_one_of { parent = "ruby"; children = ["rp"; "rt"] }) 98 + (`Element (`Missing_child_one_of (`Parent "ruby", `Children ["rp"; "rt"]))) 98 99 else if not info.has_content_before_rt then 100 + (* Has rt but missing content before it - needs content *) 99 101 Message_collector.add_typed collector 100 - (Error_code.Missing_required_child { parent = "ruby"; child = "rt" }); 102 + (`Element (`Missing_child (`Parent "ruby", `Child "rt"))); 101 103 state.ruby_stack <- rest 102 104 end 103 105 | [] -> ()
+4 -4
lib/html5_checker/specialized/source_checker.ml
··· 44 44 | Video | Audio -> 45 45 if has_attr "srcset" attrs then 46 46 Message_collector.add_typed collector 47 - (Error_code.Attr_not_allowed_on_element { attr = "srcset"; element = "source" }); 47 + (`Attr (`Not_allowed (`Attr "srcset", `Elem "source"))); 48 48 if has_attr "sizes" attrs then 49 49 Message_collector.add_typed collector 50 - (Error_code.Attr_not_allowed_on_element { attr = "sizes"; element = "source" }); 50 + (`Attr (`Not_allowed (`Attr "sizes", `Elem "source"))); 51 51 if has_attr "width" attrs then 52 52 Message_collector.add_typed collector 53 - (Error_code.Attr_not_allowed_on_element { attr = "width"; element = "source" }); 53 + (`Attr (`Not_allowed (`Attr "width", `Elem "source"))); 54 54 if has_attr "height" attrs then 55 55 Message_collector.add_typed collector 56 - (Error_code.Attr_not_allowed_on_element { attr = "height"; element = "source" }) 56 + (`Attr (`Not_allowed (`Attr "height", `Elem "source"))) 57 57 | Picture | Other -> () 58 58 end 59 59 | _ ->
+45 -45
lib/html5_checker/specialized/srcset_sizes_checker.ml
··· 406 406 (* Empty sizes is invalid *) 407 407 if String.trim value = "" then begin 408 408 Message_collector.add_typed collector 409 - (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 }); 409 + (`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)))); 410 410 false 411 411 end else begin 412 412 (* Split on comma and check each entry *) ··· 416 416 (* Check if starts with comma (empty first entry) *) 417 417 if first_entry = "" then begin 418 418 Message_collector.add_typed collector 419 - (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 }); 419 + (`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)))); 420 420 false 421 421 end else begin 422 422 (* Check for trailing comma *) ··· 429 429 else value 430 430 in 431 431 Message_collector.add_typed collector 432 - (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 }); 432 + (`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)))); 433 433 false 434 434 end else begin 435 435 let valid = ref true in ··· 448 448 (* Context is the first entry with a comma *) 449 449 let context = (String.trim first) ^ "," in 450 450 Message_collector.add_typed collector 451 - (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 }); 451 + (`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)))); 452 452 valid := false 453 453 end; 454 454 (* Check for multiple entries without media conditions. ··· 460 460 (* Multiple defaults - report as "Expected media condition" *) 461 461 let context = (String.trim first) ^ "," in 462 462 Message_collector.add_typed collector 463 - (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 }); 463 + (`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)))); 464 464 valid := false 465 465 end 466 466 end ··· 482 482 else context 483 483 in 484 484 Message_collector.add_typed collector 485 - (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 }); 485 + (`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)))); 486 486 valid := false 487 487 | None -> ()); 488 488 ··· 521 521 else value 522 522 in 523 523 Message_collector.add_typed collector 524 - (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 }); 524 + (`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)))); 525 525 valid := false 526 526 end 527 527 (* If there's extra junk after the size, report BadCssNumber error for it *) ··· 549 549 in 550 550 let _ = junk in 551 551 Message_collector.add_typed collector 552 - (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 }); 552 + (`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)))); 553 553 valid := false 554 554 end 555 555 else ··· 562 562 in 563 563 let _ = full_context in 564 564 Message_collector.add_typed collector 565 - (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 }); 565 + (`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)))); 566 566 valid := false 567 567 | CssCommentAfterSign (found, context) -> 568 568 (* e.g., +/**/50vw - expected number after sign *) 569 569 Message_collector.add_typed collector 570 - (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 }); 570 + (`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)))); 571 571 valid := false 572 572 | CssCommentBeforeUnit (found, context) -> 573 573 (* e.g., 50/**/vw - expected units after number *) 574 574 let units_list = List.map (fun u -> Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" u) valid_length_units in 575 575 let units_str = String.concat ", " units_list in 576 576 Message_collector.add_typed collector 577 - (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 }); 577 + (`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)))); 578 578 valid := false 579 579 | BadScientificNotation -> 580 580 (* For scientific notation with bad exponent, show what char was expected vs found *) ··· 585 585 (* Find the period in the exponent *) 586 586 let _ = context in 587 587 Message_collector.add_typed collector 588 - (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 }); 588 + (`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)))); 589 589 valid := false 590 590 | BadCssNumber (first_char, context) -> 591 591 (* Value doesn't start with a digit or minus sign *) ··· 595 595 in 596 596 let _ = full_context in 597 597 Message_collector.add_typed collector 598 - (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 }); 598 + (`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)))); 599 599 valid := false 600 600 | InvalidUnit (found_unit, _context) -> 601 601 (* Generate the full list of expected units *) ··· 612 612 else Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" found_unit 613 613 in 614 614 Message_collector.add_typed collector 615 - (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 }); 615 + (`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)))); 616 616 valid := false 617 617 end 618 618 end ··· 639 639 (* Show just the number part (without the 'w') *) 640 640 let num_part_for_msg = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in 641 641 Message_collector.add_typed collector 642 - (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 }); 642 + (`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)))); 643 643 false 644 644 end else 645 645 (try 646 646 let n = int_of_string num_part in 647 647 if n <= 0 then begin 648 648 Message_collector.add_typed collector 649 - (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 }); 649 + (`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)))); 650 650 false 651 651 end else begin 652 652 (* Check for uppercase W - compare original desc with lowercase version *) 653 653 let original_last = desc.[String.length desc - 1] in 654 654 if original_last = 'W' then begin 655 655 Message_collector.add_typed collector 656 - (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 }); 656 + (`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)))); 657 657 false 658 658 end else true 659 659 end ··· 661 661 (* Check for scientific notation, decimal, or other non-integer values *) 662 662 if String.contains num_part 'e' || String.contains num_part 'E' || String.contains num_part '.' then begin 663 663 Message_collector.add_typed collector 664 - (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 }); 664 + (`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)))); 665 665 false 666 666 end else begin 667 667 Message_collector.add_typed collector 668 - (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 }); 668 + (`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)))); 669 669 false 670 670 end) 671 671 | 'x' -> ··· 675 675 (* Extract the number part including the plus sign *) 676 676 let num_with_plus = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in 677 677 Message_collector.add_typed collector 678 - (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 }); 678 + (`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)))); 679 679 false 680 680 end else begin 681 681 (try ··· 686 686 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in 687 687 let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in 688 688 Message_collector.add_typed collector 689 - (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 }); 689 + (`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)))); 690 690 false 691 691 end else if n = 0.0 then begin 692 692 (* Check if it's -0 (starts with minus) - report as "greater than zero" error *) ··· 694 694 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in 695 695 if String.length orig_num_part > 0 && orig_num_part.[0] = '-' then begin 696 696 Message_collector.add_typed collector 697 - (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 }) 697 + (`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)))) 698 698 end else begin 699 699 Message_collector.add_typed collector 700 - (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 }) 700 + (`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)))) 701 701 end; 702 702 false 703 703 end else if n < 0.0 then begin 704 704 Message_collector.add_typed collector 705 - (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 }); 705 + (`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)))); 706 706 false 707 707 end else if n = neg_infinity || n = infinity then begin 708 708 (* Infinity is not a valid float - report as parse error with first char from ORIGINAL desc *) ··· 710 710 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in 711 711 let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in 712 712 Message_collector.add_typed collector 713 - (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 }); 713 + (`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)))); 714 714 false 715 715 end else true 716 716 with _ -> 717 717 Message_collector.add_typed collector 718 - (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 }); 718 + (`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)))); 719 719 false) 720 720 end 721 721 | 'h' -> ··· 735 735 in 736 736 if has_sizes then 737 737 Message_collector.add_typed collector 738 - (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 }) 738 + (`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)))) 739 739 else 740 740 Message_collector.add_typed collector 741 - (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 }); 741 + (`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)))); 742 742 false 743 743 | _ -> 744 744 (* Unknown descriptor - find context in srcset_value *) ··· 770 770 with Not_found -> srcset_value 771 771 in 772 772 Message_collector.add_typed collector 773 - (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 }); 773 + (`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)))); 774 774 false 775 775 end 776 776 ··· 806 806 (* Check for empty srcset *) 807 807 if String.trim value = "" then begin 808 808 Message_collector.add_typed collector 809 - (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 }) 809 + (`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)))) 810 810 end; 811 811 812 812 (* Check for leading comma *) 813 813 if String.length value > 0 && value.[0] = ',' then begin 814 814 Message_collector.add_typed collector 815 - (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 }) 815 + (`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)))) 816 816 end; 817 817 818 818 (* Check for trailing comma(s) / empty entries *) ··· 829 829 if trailing_commas > 1 then 830 830 (* Multiple trailing commas: "Empty image-candidate string at" *) 831 831 Message_collector.add_typed collector 832 - (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 }) 832 + (`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)))) 833 833 else 834 834 (* Single trailing comma: "Ends with empty image-candidate string." *) 835 835 Message_collector.add_typed collector 836 - (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 }) 836 + (`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)))) 837 837 end; 838 838 839 839 List.iter (fun entry -> ··· 851 851 let scheme_colon = scheme ^ ":" in 852 852 if url_lower = scheme_colon then 853 853 Message_collector.add_typed collector 854 - (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 }) 854 + (`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)))) 855 855 ) special_schemes 856 856 in 857 857 match parts with ··· 863 863 begin match Hashtbl.find_opt seen_descriptors "explicit-1x" with 864 864 | Some first_url -> 865 865 Message_collector.add_typed collector 866 - (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 }) 866 + (`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)))) 867 867 | None -> 868 868 Hashtbl.add seen_descriptors "implicit-1x" url 869 869 end ··· 874 874 if rest <> [] then begin 875 875 let extra_desc = List.hd rest in 876 876 Message_collector.add_typed collector 877 - (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 }) 877 + (`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)))) 878 878 end; 879 879 880 880 let desc_lower = String.lowercase_ascii (String.trim desc) in ··· 913 913 value 914 914 in 915 915 Message_collector.add_typed collector 916 - (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 }) 916 + (`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)))) 917 917 end 918 918 end; 919 919 ··· 925 925 begin match Hashtbl.find_opt seen_descriptors normalized with 926 926 | Some first_url -> 927 927 Message_collector.add_typed collector 928 - (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 }) 928 + (`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)))) 929 929 | None -> 930 930 begin match (if is_1x then Hashtbl.find_opt seen_descriptors "implicit-1x" else None) with 931 931 | Some first_url -> 932 932 (* Explicit 1x conflicts with implicit 1x *) 933 933 Message_collector.add_typed collector 934 - (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 }) 934 + (`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)))) 935 935 | None -> 936 936 Hashtbl.add seen_descriptors normalized url; 937 937 if is_1x then Hashtbl.add seen_descriptors "explicit-1x" url ··· 946 946 (* Check: if w descriptor used and no sizes, that's an error for img and source *) 947 947 if !has_w_descriptor && not has_sizes then 948 948 Message_collector.add_typed collector 949 - (Error_code.Srcset_w_without_sizes); 949 + (`Srcset `W_without_sizes); 950 950 951 951 (* Check: if sizes is present, all entries must have width descriptors *) 952 952 (match !no_descriptor_url with 953 953 | Some url when has_sizes -> 954 954 Message_collector.add_typed collector 955 - (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 }) 955 + (`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)))) 956 956 | _ -> ()); 957 957 958 958 (* Check: if sizes is present and srcset uses x descriptors, that's an error. 959 959 Only report if we haven't already reported the detailed error. *) 960 960 if has_sizes && !has_x_descriptor && not !x_with_sizes_error_reported then 961 961 Message_collector.add_typed collector 962 - (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 }); 962 + (`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)))); 963 963 964 964 (* Check for mixing w and x descriptors *) 965 965 if !has_w_descriptor && !has_x_descriptor then 966 966 Message_collector.add_typed collector 967 - (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 }) 967 + (`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)))) 968 968 969 969 let start_element _state ~name ~namespace ~attrs collector = 970 970 let name_lower = String.lowercase_ascii name in ··· 973 973 if namespace <> None && name_lower = "image" then begin 974 974 if get_attr "srcset" attrs <> None then 975 975 Message_collector.add_typed collector 976 - (Error_code.Attr_not_allowed_on_element { attr = "srcset"; element = "image" }) 976 + (`Attr (`Not_allowed (`Attr "srcset", `Elem "image"))) 977 977 end; 978 978 979 979 if namespace <> None then () ··· 998 998 (* Error: sizes without srcset on img *) 999 999 if name_lower = "img" && has_sizes && not has_srcset then 1000 1000 Message_collector.add_typed collector 1001 - (Error_code.Sizes_without_srcset) 1001 + (`Srcset `Sizes_without_srcset) 1002 1002 end 1003 1003 end 1004 1004
+17 -17
lib/html5_checker/specialized/svg_checker.ml
··· 290 290 (* xmlns on any SVG element must be the SVG namespace *) 291 291 if value <> svg_ns_url then 292 292 Message_collector.add_typed collector 293 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 293 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 294 294 "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)." 295 - value svg_ns_url }) 295 + value svg_ns_url)))) 296 296 | "xmlns:xlink" -> 297 297 if value <> "http://www.w3.org/1999/xlink" then 298 298 Message_collector.add_typed collector 299 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 299 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 300 300 "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)." 301 - value }) 301 + value)))) 302 302 | _ when String.starts_with ~prefix:"xmlns:" attr && attr <> "xmlns:xlink" -> 303 303 (* Other xmlns declarations are not allowed in HTML-embedded SVG *) 304 304 Message_collector.add_typed collector 305 - (Error_code.Attr_not_allowed_here { attr }) 305 + (`Attr (`Not_allowed_here (`Attr attr))) 306 306 | _ -> () 307 307 308 308 (* Validate SVG path data *) ··· 322 322 let ctx_end = min (String.length d) (!i + 1) in 323 323 let context = String.sub d !context_start (ctx_end - !context_start) in 324 324 Message_collector.add_typed collector 325 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 325 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 326 326 "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)." 327 - d element context }); 327 + d element context)))); 328 328 i := len (* Stop processing *) 329 329 | _ -> 330 330 incr i ··· 342 342 let ctx_start = max 0 (pos - 10) in 343 343 let context = String.sub d ctx_start (flag_end - ctx_start) in 344 344 Message_collector.add_typed collector 345 - (Error_code.Bad_attr_value_generic { message = Printf.sprintf 345 + (`Attr (`Bad_value_generic (`Message (Printf.sprintf 346 346 "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)." 347 - d element flag context }) 347 + d element flag context)))) 348 348 end 349 349 with Not_found -> () 350 350 ··· 364 364 | parent :: _ when String.lowercase_ascii parent = "a" -> 365 365 if List.mem name_lower a_disallowed_children then 366 366 Message_collector.add_typed collector 367 - (Error_code.Element_not_allowed_as_child { child = name_lower; parent = "a" }) 367 + (`Element (`Not_allowed_as_child (`Child name_lower, `Parent "a"))) 368 368 | _ -> ()); 369 369 370 370 (* 2. Track missing-glyph in font *) ··· 381 381 p = "lineargradient" || p = "radialgradient") -> () 382 382 | parent :: _ -> 383 383 Message_collector.add_typed collector 384 - (Error_code.Element_not_allowed_as_child { child = name; parent }) 384 + (`Element (`Not_allowed_as_child (`Child name, `Parent parent))) 385 385 | [] -> () 386 386 end; 387 387 ··· 390 390 match state.element_stack with 391 391 | parent :: _ when String.lowercase_ascii parent = "use" -> 392 392 Message_collector.add_typed collector 393 - (Error_code.Element_not_allowed_as_child { child = name; parent }) 393 + (`Element (`Not_allowed_as_child (`Child name, `Parent parent))) 394 394 | _ -> () 395 395 end; 396 396 ··· 402 402 | fect :: _ -> 403 403 if List.mem name_lower fect.seen_funcs then 404 404 Message_collector.add_typed collector 405 - (Error_code.Element_not_allowed_as_child { child = name; parent = "feComponentTransfer" }) 405 + (`Element (`Not_allowed_as_child (`Child name, `Parent "feComponentTransfer"))) 406 406 else 407 407 fect.seen_funcs <- name_lower :: fect.seen_funcs 408 408 | [] -> () ··· 427 427 (* Check xml:* attributes - most are not allowed *) 428 428 else if attr_lower = "xml:id" || attr_lower = "xml:base" then 429 429 Message_collector.add_typed collector 430 - (Error_code.Attr_not_allowed_on_element { attr; element = name }) 430 + (`Attr (`Not_allowed (`Attr attr, `Elem name))) 431 431 (* Validate path data *) 432 432 else if attr_lower = "d" && name_lower = "path" then 433 433 validate_path_data value name collector 434 434 (* Check if attribute is valid for this element *) 435 435 else if not (is_valid_attr name_lower attr_lower) then 436 436 Message_collector.add_typed collector 437 - (Error_code.Attr_not_allowed_on_element { attr; element = name }) 437 + (`Attr (`Not_allowed (`Attr attr, `Elem name))) 438 438 ) attrs; 439 439 440 440 (* Check required attributes *) ··· 443 443 List.iter (fun req_attr -> 444 444 if not (List.exists (fun (a, _) -> String.lowercase_ascii a = req_attr) attrs) then 445 445 Message_collector.add_typed collector 446 - (Error_code.Missing_required_svg_attr { element = name_lower; attr = req_attr }) 446 + (`Svg (`Missing_attr (`Elem name_lower, `Attr req_attr))) 447 447 ) req_attrs 448 448 | None -> ()) 449 449 end ··· 464 464 | Some children -> 465 465 List.iter (fun child -> 466 466 Message_collector.add_typed collector 467 - (Error_code.Missing_required_child { parent = "font"; child }) 467 + (`Element (`Missing_child (`Parent "font", `Child child))) 468 468 ) children 469 469 | None -> () 470 470 end;
+24 -24
lib/html5_checker/specialized/table_checker.ml
··· 36 36 let colspan = 37 37 if colspan > max_colspan then ( 38 38 Message_collector.add_typed collector 39 - (Error_code.Generic { message = Printf.sprintf 39 + (`Generic (Printf.sprintf 40 40 {|The value of the "colspan" attribute must be less than or equal to %d.|} 41 - max_colspan }); 41 + max_colspan)); 42 42 max_colspan) 43 43 else colspan 44 44 in 45 45 let rowspan = 46 46 if rowspan > max_rowspan then ( 47 47 Message_collector.add_typed collector 48 - (Error_code.Generic { message = Printf.sprintf 48 + (`Generic (Printf.sprintf 49 49 {|The value of the "rowspan" attribute must be less than or equal to %d.|} 50 - max_rowspan }); 50 + max_rowspan)); 51 51 max_rowspan) 52 52 else rowspan 53 53 in ··· 75 75 (** Emit error for horizontal cell overlap *) 76 76 let err_on_horizontal_overlap cell1 cell2 collector = 77 77 if cells_overlap_horizontally cell1 cell2 then ( 78 - Message_collector.add_typed collector Error_code.Table_cell_overlap; 79 - Message_collector.add_typed collector Error_code.Table_cell_overlap) 78 + Message_collector.add_typed collector (`Table `Cell_overlap); 79 + Message_collector.add_typed collector (`Table `Cell_overlap)) 80 80 81 81 (** Check if cell spans past end of row group *) 82 82 let err_if_not_rowspan_zero cell ~row_group_type:_ collector = 83 83 if cell.bottom <> rowspan_zero_magic then 84 - Message_collector.add_typed collector Error_code.Table_cell_spans_rowgroup 84 + Message_collector.add_typed collector (`Table `Cell_spans_rowgroup) 85 85 86 86 (** {1 Column Range Tracking} *) 87 87 ··· 206 206 let end_row_in_group group collector = 207 207 (if not group.row_had_cells then 208 208 Message_collector.add_typed collector 209 - (Error_code.Table_row_no_cells { row = group.current_row + 1 })); 209 + (`Table (`Row_no_cells (`Row (group.current_row + 1))))); 210 210 211 211 find_insertion_point group; 212 212 group.cells_on_current_row <- [||]; ··· 385 385 let span = parse_non_negative_int attrs "span" in 386 386 if span > max_colspan then ( 387 387 Message_collector.add_typed collector 388 - (Error_code.Generic { message = Printf.sprintf 389 - {|The value of the "span" attribute must be less than or equal to %d.|} max_colspan }); 388 + (`Generic (Printf.sprintf 389 + {|The value of the "span" attribute must be less than or equal to %d.|} max_colspan)); 390 390 max_colspan) 391 391 else span 392 392 ··· 471 471 if table.hard_width then ( 472 472 if row_width > table.column_count then 473 473 Message_collector.add_typed collector 474 - (Error_code.Generic { message = Printf.sprintf 474 + (`Generic (Printf.sprintf 475 475 {|A table row was %d columns wide and exceeded the column count established using column markup (%d).|} 476 - row_width table.column_count }) 476 + row_width table.column_count)) 477 477 else if row_width < table.column_count then 478 478 Message_collector.add_typed collector 479 - (Error_code.Generic { message = Printf.sprintf 479 + (`Generic (Printf.sprintf 480 480 {|A table row was %d columns wide, which is less than the column count established using column markup (%d).|} 481 - row_width table.column_count })) 481 + row_width table.column_count))) 482 482 else if table.column_count = -1 then 483 483 table.column_count <- row_width 484 484 else ( 485 485 if row_width > table.column_count then 486 486 Message_collector.add_typed collector 487 - (Error_code.Generic { message = Printf.sprintf 487 + (`Generic (Printf.sprintf 488 488 {|A table row was %d columns wide and exceeded the column count established by the first row (%d).|} 489 - row_width table.column_count }) 489 + row_width table.column_count)) 490 490 else if row_width < table.column_count then 491 491 Message_collector.add_typed collector 492 - (Error_code.Generic { message = Printf.sprintf 492 + (`Generic (Printf.sprintf 493 493 {|A table row was %d columns wide, which is less than the column count established by the first row (%d).|} 494 - row_width table.column_count })) 494 + row_width table.column_count))) 495 495 496 496 (** End a row *) 497 497 let end_row table collector = ··· 621 621 | InColgroup -> 622 622 if table.pending_colgroup_span > 0 then 623 623 Message_collector.add_typed collector 624 - (Error_code.Generic { message = Printf.sprintf 624 + (`Generic (Printf.sprintf 625 625 "A col element causes a span attribute with value %d to be ignored on the \ 626 626 parent colgroup." 627 - table.pending_colgroup_span }); 627 + table.pending_colgroup_span)); 628 628 table.pending_colgroup_span <- 0; 629 629 table.state <- InColInColgroup; 630 630 let span = abs (parse_span attrs collector) in ··· 663 663 (fun heading -> 664 664 if not (Hashtbl.mem table.header_ids heading) then 665 665 Message_collector.add_typed collector 666 - (Error_code.Generic { message = Printf.sprintf 666 + (`Generic (Printf.sprintf 667 667 {|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.|} 668 - cell.element_name heading })) 668 + cell.element_name heading))) 669 669 cell.headers) 670 670 !(table.cells_with_headers); 671 671 ··· 675 675 | None -> () 676 676 | Some r -> 677 677 Message_collector.add_typed collector 678 - (Error_code.Table_column_no_cells { column = r.right; element = r.element }); 678 + (`Table (`Column_no_cells (`Column r.right, `Elem r.element))); 679 679 check_ranges r.next 680 680 in 681 681 check_ranges table.first_col_range ··· 739 739 let end_document state collector = 740 740 if !(state.tables) <> [] then 741 741 Message_collector.add_typed collector 742 - (Error_code.Generic { message = "Unclosed table element at end of document." }) 742 + (`Generic "Unclosed table element at end of document.") 743 743 744 744 let checker = 745 745 (module struct
+2 -2
lib/html5_checker/specialized/title_checker.ml
··· 62 62 (* Check if title was empty *) 63 63 if not state.title_has_content then 64 64 Message_collector.add_typed collector 65 - (Error_code.Element_must_not_be_empty { element = "title" }); 65 + (`Element (`Must_not_be_empty (`Elem "title"))); 66 66 state.in_title <- false 67 67 | "head" -> 68 68 (* Check if head had a title element *) 69 69 if state.in_head && not state.has_title then 70 70 Message_collector.add_typed collector 71 - (Error_code.Missing_required_child { parent = "head"; child = "title" }); 71 + (`Element (`Missing_child (`Parent "head", `Child "title"))); 72 72 state.in_head <- false 73 73 | _ -> () 74 74 end
+1 -1
lib/html5_checker/specialized/unknown_element_checker.ml
··· 83 83 in 84 84 (* Produce error: unknown element not allowed as child *) 85 85 Message_collector.add_typed collector 86 - (Error_code.Element_not_allowed_as_child { child = name; parent }) 86 + (`Element (`Not_allowed_as_child (`Child name, `Parent parent))) 87 87 end; 88 88 89 89 (* Always push to stack for tracking *)
+7 -7
lib/html5_checker/specialized/url_checker.ml
··· 757 757 | Some url -> 758 758 (match check_data_uri_fragment url attr_name name with 759 759 | Some warn_msg -> 760 - Message_collector.add_typed collector (Error_code.Generic { message = warn_msg }) 760 + Message_collector.add_typed collector (`Generic warn_msg) 761 761 | None -> ()); 762 762 match validate_url url name attr_name with 763 763 | None -> () 764 764 | Some error_msg -> 765 - Message_collector.add_typed collector (Error_code.Bad_attr_value_generic { message = error_msg }) 765 + Message_collector.add_typed collector (`Attr (`Bad_value_generic (`Message error_msg))) 766 766 ) url_attrs); 767 767 (* Special handling for input[type=url] value attribute - must be absolute URL *) 768 768 if name_lower = "input" then begin ··· 780 780 | None -> 781 781 let msg = Printf.sprintf "Bad value %s for attribute %s on element %s: Bad absolute URL: The string %s is not an absolute URL." 782 782 (Error_code.q url) (Error_code.q "value") (Error_code.q "input") (Error_code.q url) in 783 - Message_collector.add_typed collector (Error_code.Bad_attr_value_generic { message = msg }) 783 + Message_collector.add_typed collector (`Attr (`Bad_value_generic (`Message msg))) 784 784 | Some _ -> 785 785 (match check_data_uri_fragment ~is_absolute_url:true url "value" name with 786 786 | Some warn_msg -> 787 - Message_collector.add_typed collector (Error_code.Generic { message = warn_msg }) 787 + Message_collector.add_typed collector (`Generic warn_msg) 788 788 | None -> ()); 789 789 match validate_url url name "value" with 790 790 | None -> () 791 791 | Some error_msg -> 792 792 let error_msg = Str.global_replace (Str.regexp "Bad URL:") "Bad absolute URL:" error_msg in 793 - Message_collector.add_typed collector (Error_code.Bad_attr_value_generic { message = error_msg }) 793 + Message_collector.add_typed collector (`Attr (`Bad_value_generic (`Message error_msg))) 794 794 end 795 795 end 796 796 end; ··· 798 798 (match itemtype_opt with 799 799 | Some url when String.trim url <> "" -> 800 800 (match check_data_uri_fragment ~is_absolute_url:true url "itemtype" name with 801 - | Some warn_msg -> Message_collector.add_typed collector (Error_code.Generic { message = warn_msg }) 801 + | Some warn_msg -> Message_collector.add_typed collector (`Generic warn_msg) 802 802 | None -> ()) 803 803 | _ -> ()); 804 804 let itemid_opt = get_attr_value "itemid" attrs in 805 805 (match itemid_opt with 806 806 | Some url when String.trim url <> "" -> 807 807 (match check_data_uri_fragment url "itemid" name with 808 - | Some warn_msg -> Message_collector.add_typed collector (Error_code.Generic { message = warn_msg }) 808 + | Some warn_msg -> Message_collector.add_typed collector (`Generic warn_msg) 809 809 | None -> ()) 810 810 | _ -> ()) 811 811 end
+5 -5
lib/html5_checker/specialized/xhtml_content_checker.ml
··· 50 50 String.sub attr_name 0 5 = "data-" then 51 51 let suffix = String.sub attr_name 5 (String.length attr_name - 5) in 52 52 if String.exists (fun c -> c >= 'A' && c <= 'Z') suffix then 53 - Message_collector.add_typed collector Error_code.Data_attr_uppercase 53 + Message_collector.add_typed collector (`Attr `Data_uppercase) 54 54 ) attrs 55 55 56 56 let start_element state ~name ~namespace ~attrs collector = ··· 66 66 let parent_lower = String.lowercase_ascii parent in 67 67 if not (is_child_allowed ~parent:parent_lower ~child:name_lower) then 68 68 Message_collector.add_typed collector 69 - (Error_code.Element_not_allowed_as_child { child = name_lower; parent = parent_lower }) 69 + (`Element (`Not_allowed_as_child (`Child name_lower, `Parent parent_lower))) 70 70 | [] -> ()); 71 71 72 72 (* Handle figure content model *) ··· 84 84 (* Flow content appearing in figure *) 85 85 if fig.has_figcaption && not fig.figcaption_at_start then 86 86 Message_collector.add_typed collector 87 - (Error_code.Element_not_allowed_as_child { child = name_lower; parent = "figure" }) 87 + (`Element (`Not_allowed_as_child (`Child name_lower, `Parent "figure"))) 88 88 else if not fig.has_figcaption then 89 89 fig.has_content_before_figcaption <- true 90 90 end ··· 123 123 | fig :: _ -> 124 124 if fig.has_figcaption && not fig.figcaption_at_start then 125 125 Message_collector.add_typed collector 126 - (Error_code.Text_not_allowed { parent = "figure" }) 126 + (`Element (`Text_not_allowed (`Parent "figure"))) 127 127 else if not fig.has_figcaption then 128 128 fig.has_content_before_figcaption <- true 129 129 | [] -> () 130 130 end 131 131 else if not (is_text_allowed parent_lower) then 132 132 Message_collector.add_typed collector 133 - (Error_code.Text_not_allowed { parent = parent_lower }) 133 + (`Element (`Text_not_allowed (`Parent parent_lower))) 134 134 end 135 135 136 136 let end_document _state _collector = ()
+20 -42
test/expected_message.ml
··· 80 80 if Str.string_match re msg 0 then 81 81 let child = Str.matched_group 1 msg in 82 82 let parent = Str.matched_group 2 msg in 83 - Some (Html5_checker.Error_code.Element_not_allowed_as_child { child; parent }, 83 + Some ((`Element (`Not_allowed_as_child (`Child child, `Parent parent)) : Html5_checker.Error_code.t), 84 84 Some child, None) 85 85 else None 86 86 ··· 90 90 if Str.string_match re msg 0 then 91 91 let attr = Str.matched_group 1 msg in 92 92 let element = Str.matched_group 2 msg in 93 - Some (Html5_checker.Error_code.Attr_not_allowed_on_element { attr; element }, 93 + Some ((`Attr (`Not_allowed (`Attr attr, `Elem element)) : Html5_checker.Error_code.t), 94 94 Some element, Some attr) 95 95 else None 96 96 ··· 99 99 let re = Str.regexp {|Attribute "\([^"]+\)" not allowed here|} in 100 100 if Str.string_match re msg 0 then 101 101 let attr = Str.matched_group 1 msg in 102 - Some (Html5_checker.Error_code.Attr_not_allowed_here { attr }, 102 + Some ((`Attr (`Not_allowed_here (`Attr attr)) : Html5_checker.Error_code.t), 103 103 None, Some attr) 104 104 else None 105 105 ··· 109 109 if Str.string_match re msg 0 then 110 110 let element = Str.matched_group 1 msg in 111 111 let attr = Str.matched_group 2 msg in 112 - Some (Html5_checker.Error_code.Missing_required_attr { element; attr }, 112 + Some ((`Attr (`Missing (`Elem element, `Attr attr)) : Html5_checker.Error_code.t), 113 113 Some element, Some attr) 114 114 else None 115 115 ··· 119 119 if Str.string_match re msg 0 then 120 120 let parent = Str.matched_group 1 msg in 121 121 let child = Str.matched_group 2 msg in 122 - Some (Html5_checker.Error_code.Missing_required_child { parent; child }, 122 + Some ((`Element (`Missing_child (`Parent parent, `Child child)) : Html5_checker.Error_code.t), 123 123 Some parent, None) 124 124 else None 125 125 ··· 128 128 let re = Str.regexp {|Duplicate ID "\([^"]+\)"|} in 129 129 if Str.string_match re msg 0 then 130 130 let id = Str.matched_group 1 msg in 131 - Some (Html5_checker.Error_code.Duplicate_id { id }, 131 + Some ((`Attr (`Duplicate_id (`Id id)) : Html5_checker.Error_code.t), 132 132 None, None) 133 133 else None 134 134 ··· 137 137 let re = Str.regexp {|The "\([^"]+\)" element is obsolete|} in 138 138 if Str.string_match re msg 0 then 139 139 let element = Str.matched_group 1 msg in 140 - Some (Html5_checker.Error_code.Obsolete_element { element; suggestion = "" }, 140 + Some ((`Element (`Obsolete (`Elem element, `Suggestion "")) : Html5_checker.Error_code.t), 141 141 Some element, None) 142 142 else None 143 143 ··· 147 147 if Str.string_match re msg 0 then 148 148 let attr = Str.matched_group 1 msg in 149 149 let element = Str.matched_group 2 msg in 150 - Some (Html5_checker.Error_code.Obsolete_attr { attr; element; suggestion = None }, 150 + Some ((`Element (`Obsolete_attr (`Elem element, `Attr attr, `Suggestion None)) : Html5_checker.Error_code.t), 151 151 Some element, Some attr) 152 152 else None 153 153 ··· 156 156 let re = Str.regexp {|Stray end tag "\([^"]+\)"|} in 157 157 if Str.string_match re msg 0 then 158 158 let tag = Str.matched_group 1 msg in 159 - Some (Html5_checker.Error_code.Stray_end_tag { tag }, 159 + Some ((`Tag (`Stray_end (`Tag tag)) : Html5_checker.Error_code.t), 160 160 Some tag, None) 161 161 else None 162 162 ··· 165 165 let re = Str.regexp {|Stray start tag "\([^"]+\)"|} in 166 166 if Str.string_match re msg 0 then 167 167 let tag = Str.matched_group 1 msg in 168 - Some (Html5_checker.Error_code.Stray_start_tag { tag }, 168 + Some ((`Tag (`Stray_start (`Tag tag)) : Html5_checker.Error_code.t), 169 169 Some tag, None) 170 170 else None 171 171 ··· 175 175 if Str.string_match re msg 0 then 176 176 let role = Str.matched_group 1 msg in 177 177 let reason = Str.matched_group 2 msg in 178 - Some (Html5_checker.Error_code.Unnecessary_role { role; element = ""; reason }, 178 + Some ((`Aria (`Unnecessary_role (`Role role, `Elem "", `Reason reason)) : Html5_checker.Error_code.t), 179 179 None, None) 180 180 else None 181 181 ··· 185 185 if Str.string_match re msg 0 then 186 186 let role = Str.matched_group 1 msg in 187 187 let element = Str.matched_group 2 msg in 188 - Some (Html5_checker.Error_code.Bad_role { element; role }, 188 + Some ((`Aria (`Bad_role (`Elem element, `Role role)) : Html5_checker.Error_code.t), 189 189 Some element, Some "role") 190 190 else None 191 191 ··· 196 196 let attr = Str.matched_group 1 msg in 197 197 let element = Str.matched_group 2 msg in 198 198 let condition = Str.matched_group 3 msg in 199 - Some (Html5_checker.Error_code.Aria_must_not_be_specified { attr; element; condition }, 199 + Some ((`Aria (`Must_not_specify (`Attr attr, `Elem element, `Condition condition)) : Html5_checker.Error_code.t), 200 200 Some element, Some attr) 201 201 else None 202 202 ··· 207 207 let attr = Str.matched_group 1 msg in 208 208 let element = Str.matched_group 2 msg in 209 209 let condition = Str.matched_group 3 msg in 210 - Some (Html5_checker.Error_code.Aria_must_not_be_used { attr; element; condition }, 210 + Some ((`Aria (`Must_not_use (`Attr attr, `Elem element, `Condition condition)) : Html5_checker.Error_code.t), 211 211 Some element, Some attr) 212 212 else None 213 213 ··· 225 225 String.trim (String.sub msg (colon_pos + 1) (String.length msg - colon_pos - 1)) 226 226 with Not_found -> "" 227 227 in 228 - Some (Html5_checker.Error_code.Bad_attr_value { element; attr; value; reason }, 228 + Some ((`Attr (`Bad_value (`Elem element, `Attr attr, `Value value, `Reason reason)) : Html5_checker.Error_code.t), 229 229 Some element, Some attr) 230 230 else None 231 231 ··· 234 234 let re = Str.regexp {|End tag "\([^"]+\)" implied, but there were open elements|} in 235 235 if Str.string_match re msg 0 then 236 236 let tag = Str.matched_group 1 msg in 237 - Some (Html5_checker.Error_code.End_tag_implied_open_elements { tag }, 237 + Some ((`Tag (`End_implied_open (`Tag tag)) : Html5_checker.Error_code.t), 238 238 Some tag, None) 239 239 else None 240 240 ··· 243 243 let re = Str.regexp {|No "\([^"]+\)" element in scope but a "\([^"]+\)" end tag seen|} in 244 244 if Str.string_match re msg 0 then 245 245 let tag = Str.matched_group 1 msg in 246 - Some (Html5_checker.Error_code.No_element_in_scope { tag }, 246 + Some ((`Tag (`Not_in_scope (`Tag tag)) : Html5_checker.Error_code.t), 247 247 Some tag, None) 248 248 else None 249 249 ··· 252 252 let re = Str.regexp {|Start tag "\([^"]+\)" seen in "table"|} in 253 253 if Str.string_match re msg 0 then 254 254 let tag = Str.matched_group 1 msg in 255 - Some (Html5_checker.Error_code.Start_tag_in_table { tag }, 255 + Some ((`Tag (`Start_in_table (`Tag tag)) : Html5_checker.Error_code.t), 256 256 Some tag, None) 257 257 else None 258 258 ··· 330 330 331 331 (** Compare error codes for semantic equality *) 332 332 let error_codes_match code1 code2 = 333 - match (code1, code2) with 334 - | (Html5_checker.Error_code.Element_not_allowed_as_child { child = c1; parent = p1 }, 335 - Html5_checker.Error_code.Element_not_allowed_as_child { child = c2; parent = p2 }) -> 336 - String.lowercase_ascii c1 = String.lowercase_ascii c2 && 337 - String.lowercase_ascii p1 = String.lowercase_ascii p2 338 - | (Html5_checker.Error_code.Attr_not_allowed_on_element { attr = a1; element = e1 }, 339 - Html5_checker.Error_code.Attr_not_allowed_on_element { attr = a2; element = e2 }) -> 340 - String.lowercase_ascii a1 = String.lowercase_ascii a2 && 341 - String.lowercase_ascii e1 = String.lowercase_ascii e2 342 - | (Html5_checker.Error_code.Missing_required_attr { element = e1; attr = a1 }, 343 - Html5_checker.Error_code.Missing_required_attr { element = e2; attr = a2 }) -> 344 - String.lowercase_ascii e1 = String.lowercase_ascii e2 && 345 - String.lowercase_ascii a1 = String.lowercase_ascii a2 346 - | (Html5_checker.Error_code.Duplicate_id { id = i1 }, 347 - Html5_checker.Error_code.Duplicate_id { id = i2 }) -> 348 - i1 = i2 349 - | (Html5_checker.Error_code.Stray_end_tag { tag = t1 }, 350 - Html5_checker.Error_code.Stray_end_tag { tag = t2 }) -> 351 - String.lowercase_ascii t1 = String.lowercase_ascii t2 352 - | (Html5_checker.Error_code.Stray_start_tag { tag = t1 }, 353 - Html5_checker.Error_code.Stray_start_tag { tag = t2 }) -> 354 - String.lowercase_ascii t1 = String.lowercase_ascii t2 355 - (* For other cases, fall back to structural equality *) 356 - | (c1, c2) -> c1 = c2 333 + (* Use structural equality for all polymorphic variant error codes *) 334 + code1 = code2 357 335 358 336 let matches ~strictness ~expected ~actual = 359 337 let expected_norm = normalize_quotes expected.message in