OCaml HTML5 parser/serialiser based on Python's JustHTML
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. *)
5
6(** Severity level of a validation message *)
7type severity = Error | Warning | Info
8
9(** Typed error codes with associated data *)
10type 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. *)
32
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. *)
64
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. *)
74
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". *)
98
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. *)
162
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". *)
188
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. *)
198
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. *)
212
213 (* ===== Unicode Normalization ===== *)
214 | Not_nfc of { replacement: string }
215 (** Text run is not in Unicode Normalization Form C. *)
216
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. *)
222
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. *)
244
245 (* ===== Style Element ===== *)
246 | Style_type_invalid
247 (** The only allowed value for "type" on "style" is "text/css". *)
248
249 (* ===== Headingoffset ===== *)
250 | Headingoffset_invalid
251 (** The value of "headingoffset" must be a number between "0" and "8". *)
252
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". *)
258
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) *)
264
265 (* ===== Generic/Fallback ===== *)
266 | Generic of { message: string }
267 (** For messages that don't fit any specific pattern *)
268
269(** Get the severity level for an error code *)
270let 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
280 | _ -> Error
281
282(** Get a short code string for categorization *)
283let 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"
397
398(** Format using curly quotes (Unicode) *)
399let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d"
400
401(** Convert error code to exact Nu validator message string *)
402let to_message = function
403 | Attr_not_allowed_on_element { attr; element } ->
404 Printf.sprintf "Attribute %s not allowed on element %s at this point."
405 (q attr) (q element)
406 | Attr_not_allowed_here { attr } ->
407 Printf.sprintf "Attribute %s not allowed here." (q attr)
408 | Attr_not_allowed_when { attr; element = _; condition } ->
409 Printf.sprintf "The %s attribute must not be used on any element which has %s." (q attr) condition
410 | Missing_required_attr { element; attr } ->
411 Printf.sprintf "Element %s is missing required attribute %s."
412 (q element) (q attr)
413 | Missing_required_attr_one_of { element; attrs } ->
414 let attrs_str = String.concat ", " attrs in
415 Printf.sprintf "Element %s is missing one or more of the following attributes: [%s]."
416 (q element) attrs_str
417 | Bad_attr_value { element; attr; value; reason } ->
418 Printf.sprintf "Bad value %s for attribute %s on element %s: %s"
419 (q value) (q attr) (q element) reason
420 | Bad_attr_value_generic { message } -> message
421 | Duplicate_id { id } ->
422 Printf.sprintf "Duplicate ID %s." (q id)
423 | Data_attr_invalid_name { reason } ->
424 Printf.sprintf "%s attribute names %s." (q "data-*") reason
425 | Data_attr_uppercase ->
426 Printf.sprintf "%s attributes must not have characters from the range %s\xe2\x80\xa6%s in the name."
427 (q "data-*") (q "A") (q "Z")
428
429 | Obsolete_element { element; suggestion } ->
430 if suggestion = "" then
431 Printf.sprintf "The %s element is obsolete." (q element)
432 else
433 Printf.sprintf "The %s element is obsolete. %s" (q element) suggestion
434 | Obsolete_attr { element; attr; suggestion } ->
435 let base = Printf.sprintf "The %s attribute on the %s element is obsolete."
436 (q attr) (q element) in
437 (match suggestion with Some s -> base ^ " " ^ s | None -> base)
438 | Obsolete_global_attr { attr; suggestion } ->
439 Printf.sprintf "The %s attribute is obsolete. %s" (q attr) suggestion
440 | Element_not_allowed_as_child { child; parent } ->
441 Printf.sprintf "Element %s not allowed as child of element %s in this context. (Suppressing further errors from this subtree.)"
442 (q child) (q parent)
443 | Unknown_element { name } ->
444 Printf.sprintf "Unknown element %s." (q name)
445 | Element_must_not_be_descendant { element; attr; ancestor } ->
446 (match attr with
447 | Some a ->
448 Printf.sprintf "The element %s with the attribute %s must not appear as a descendant of the %s element."
449 (q element) (q a) (q ancestor)
450 | None ->
451 Printf.sprintf "The element %s must not appear as a descendant of the %s element."
452 (q element) (q ancestor))
453 | Missing_required_child { parent; child } ->
454 Printf.sprintf "Element %s is missing required child element %s."
455 (q parent) (q child)
456 | Missing_required_child_one_of { parent; children } ->
457 let children_str = String.concat ", " children in
458 Printf.sprintf "Element %s is missing one or more of the following child elements: [%s]."
459 (q parent) children_str
460 | Missing_required_child_generic { parent } ->
461 Printf.sprintf "Element %s is missing a required child element." (q parent)
462 | Element_must_not_be_empty { element } ->
463 Printf.sprintf "Element %s must not be empty." (q element)
464 | Stray_start_tag { tag } ->
465 Printf.sprintf "Stray start tag %s." (q tag)
466 | Stray_end_tag { tag } ->
467 Printf.sprintf "Stray end tag %s." (q tag)
468 | End_tag_for_void_element { tag } ->
469 Printf.sprintf "End tag %s." (q tag)
470 | Self_closing_non_void ->
471 Printf.sprintf "Self-closing syntax (%s) used on a non-void HTML element. Ignoring the slash and treating as a start tag."
472 (q "/>")
473 | Text_not_allowed { parent } ->
474 Printf.sprintf "Text not allowed in element %s in this context." (q parent)
475
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")
489
490 | Unnecessary_role { role; element = _; reason } ->
491 Printf.sprintf "The %s role is unnecessary %s."
492 (q role) reason
493 | Bad_role { element; role } ->
494 Printf.sprintf "Bad value %s for attribute %s on element %s."
495 (q role) (q "role") (q element)
496 | Aria_must_not_be_specified { attr; element; condition } ->
497 Printf.sprintf "The %s attribute must not be specified on any %s element unless %s."
498 (q attr) (q element) condition
499 | Aria_must_not_be_used { attr; element; condition } ->
500 Printf.sprintf "The %s attribute must not be used on an %s element which has %s."
501 (q attr) (q element) condition
502 | Aria_should_not_be_used { attr; role } ->
503 Printf.sprintf "The %s attribute should not be used on any element which has %s."
504 (q attr) (q ("role=" ^ role))
505 | Aria_hidden_on_body ->
506 Printf.sprintf "%s must not be used on the %s element."
507 (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 ->
515 Printf.sprintf "Every active %s element must have a corresponding %s element."
516 (q "role=tab") (q "role=tabpanel")
517 | Multiple_main_visible ->
518 Printf.sprintf "A document should not include more than one visible element with %s."
519 (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
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")
615
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 } ->
648 Printf.sprintf "Row %d of an implicit row group has no cells beginning on it." row
649 | Table_cell_overlap ->
650 "Table cell is overlapped by later table cell."
651 | Table_cell_spans_rowgroup ->
652 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 (q "tbody")
654 | Table_column_no_cells { column; element } ->
655 Printf.sprintf "Table column %d established by element %s has no cells beginning in it."
656 column (q element)
657
658 | Missing_lang_attr ->
659 Printf.sprintf "Consider adding a %s attribute to the %s start tag to declare the language of this document."
660 (q "lang") (q "html")
661 | Wrong_lang { detected; declared; suggested } ->
662 Printf.sprintf "This document appears to be written in %s but the %s start tag has %s. Consider using %s (or variant) instead."
663 detected (q "html") (q ("lang=\"" ^ declared ^ "\"")) (q ("lang=\"" ^ suggested ^ "\""))
664 | Missing_dir_rtl { language } ->
665 Printf.sprintf "This document appears to be written in %s. Consider adding %s to the %s start tag."
666 language (q "dir=\"rtl\"") (q "html")
667 | Wrong_dir { language; declared } ->
668 Printf.sprintf "This document appears to be written in %s but the %s start tag has %s. Consider using %s instead."
669 language (q "html") (q ("dir=\"" ^ declared ^ "\"")) (q "dir=\"rtl\"")
670 | Xml_lang_without_lang ->
671 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 (q "xml:lang") (q "lang")
673 | Xml_lang_lang_mismatch ->
674 Printf.sprintf "The %s and %s attributes must have the same value."
675 (q "xml:lang") (q "lang")
676
677 | Not_nfc { replacement } ->
678 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 (q replacement)
680
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 ->
689 Printf.sprintf "A script %s with a %s attribute whose value is %s must have valid JSON content."
690 (q "script") (q "type") (q "importmap")
691 | Importmap_invalid_root ->
692 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 (q "script") (q "type") (q "importmap") (q "imports") (q "scopes") (q "integrity")
694 | Importmap_imports_not_object ->
695 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 (q "imports") (q "script") (q "type") (q "importmap")
697 | Importmap_empty_key ->
698 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 (q "imports") (q "script") (q "type") (q "importmap")
700 | Importmap_non_string_value ->
701 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 (q "imports") (q "script") (q "type") (q "importmap")
703 | Importmap_key_trailing_slash ->
704 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 (q "imports") (q "script") (q "type") (q "importmap") (q "/") (q "/")
706 | Importmap_scopes_not_object ->
707 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 (q "scopes") (q "script") (q "type") (q "importmap")
709 | Importmap_scopes_values_not_object ->
710 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 (q "scopes") (q "script") (q "type") (q "importmap")
712 | Importmap_scopes_invalid_url ->
713 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 (q "scopes") (q "script") (q "type") (q "importmap")
715 | Importmap_scopes_value_invalid_url ->
716 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 (q "scopes") (q "script") (q "type") (q "importmap")
718
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")
722
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")
726
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")
731
732 | Svg_deprecated_attr { attr; element } ->
733 Printf.sprintf "Attribute %s not allowed on element %s at this point."
734 (q attr) (q element)
735 | Missing_required_svg_attr { element; attr } ->
736 Printf.sprintf "Element %s is missing required attribute %s."
737 (q element) (q attr)
738
739 | Generic { message } -> message