···11+(** Typed error codes for HTML5 validation messages.
22+33+ This module defines a comprehensive variant type for all validation errors,
44+ ensuring exact message matching with the Nu HTML Validator test suite. *)
55+66+(** Severity level of a validation message *)
77+type severity = Error | Warning | Info
88+99+(** Typed error codes with associated data *)
1010+type t =
1111+ (* ===== Attribute Errors ===== *)
1212+ | Attr_not_allowed_on_element of { attr: string; element: string }
1313+ (** Attribute "X" not allowed on element "Y" at this point. *)
1414+ | Attr_not_allowed_here of { attr: string }
1515+ (** Attribute "X" not allowed here. *)
1616+ | Attr_not_allowed_when of { attr: string; element: string; condition: string }
1717+ (** Attribute "X" is only allowed when ... *)
1818+ | Missing_required_attr of { element: string; attr: string }
1919+ (** Element "X" is missing required attribute "Y". *)
2020+ | Missing_required_attr_one_of of { element: string; attrs: string list }
2121+ (** Element "X" is missing one or more of the following attributes: [A, B]. *)
2222+ | Bad_attr_value of { element: string; attr: string; value: string; reason: string }
2323+ (** Bad value "X" for attribute "Y" on element "Z". *)
2424+ | Bad_attr_value_generic of { message: string }
2525+ (** Generic bad attribute value message *)
2626+ | Duplicate_id of { id: string }
2727+ (** Duplicate ID "X". *)
2828+ | Data_attr_invalid_name of { reason: string }
2929+ (** "data-*" attribute names must be XML 1.0 4th ed. plus Namespaces NCNames. *)
3030+ | Data_attr_uppercase
3131+ (** "data-*" attributes must not have characters from the range "A"…"Z" in the name. *)
3232+3333+ (* ===== Element Errors ===== *)
3434+ | Obsolete_element of { element: string; suggestion: string }
3535+ (** The "X" element is obsolete. Y *)
3636+ | Obsolete_attr of { element: string; attr: string; suggestion: string option }
3737+ (** The "X" attribute on the "Y" element is obsolete. *)
3838+ | Element_not_allowed_as_child of { child: string; parent: string }
3939+ (** Element "X" not allowed as child of element "Y" in this context. *)
4040+ | Element_must_not_be_descendant of { element: string; attr: string option; ancestor: string }
4141+ (** The element "X" [with attribute "A"] must not appear as a descendant of the "Y" element. *)
4242+ | Missing_required_child of { parent: string; child: string }
4343+ (** Element "X" is missing required child element "Y". *)
4444+ | Missing_required_child_one_of of { parent: string; children: string list }
4545+ (** Element "X" is missing one or more of the following child elements: [A, B]. *)
4646+ | Missing_required_child_generic of { parent: string }
4747+ (** Element "X" is missing a required child element. *)
4848+ | Element_must_not_be_empty of { element: string }
4949+ (** Element "X" must not be empty. *)
5050+ | Stray_start_tag of { tag: string }
5151+ (** Stray start tag "X". *)
5252+ | Stray_end_tag of { tag: string }
5353+ (** Stray end tag "X". *)
5454+ | End_tag_for_void_element of { tag: string }
5555+ (** End tag "X". (for void elements like br) *)
5656+ | Self_closing_non_void
5757+ (** Self-closing syntax used on a non-void HTML element. *)
5858+ | Text_not_allowed of { parent: string }
5959+ (** Text not allowed in element "X" in this context. *)
6060+6161+ (* ===== Child Restrictions ===== *)
6262+ | Div_child_of_dl_bad_role
6363+ (** A "div" child of a "dl" element must not have any "role" value other than "presentation" or "none". *)
6464+ | Li_bad_role_in_menu
6565+ (** An "li" element descendant of role=menu/menubar must have specific roles. *)
6666+ | Li_bad_role_in_tablist
6767+ (** An "li" element descendant of role=tablist must have role=tab. *)
6868+ | Li_bad_role_in_list
6969+ (** An "li" element descendant of ul/ol/menu or role=list must have role=listitem. *)
7070+7171+ (* ===== ARIA Errors ===== *)
7272+ | Unnecessary_role of { role: string; element: string; reason: string }
7373+ (** The "X" role is unnecessary for Y. *)
7474+ | Bad_role of { element: string; role: string }
7575+ (** Bad value "X" for attribute "role" on element "Y". *)
7676+ | Aria_must_not_be_specified of { attr: string; element: string; condition: string }
7777+ (** The "X" attribute must not be specified on any "Y" element unless... *)
7878+ | Aria_must_not_be_used of { attr: string; element: string; condition: string }
7979+ (** The "X" attribute must not be used on an "Y" element which has... *)
8080+ | Aria_should_not_be_used of { attr: string; role: string }
8181+ (** The "X" attribute should not be used on any element which has "role=Y". *)
8282+ | Img_empty_alt_with_role
8383+ (** An "img" element with empty alt must not have a role attribute. *)
8484+ | Checkbox_button_needs_aria_pressed
8585+ (** An "input" type="checkbox" with role="button" must have aria-pressed. *)
8686+ | Tab_without_tabpanel
8787+ (** Every active "role=tab" element must have a corresponding "role=tabpanel" element. *)
8888+ | Multiple_main_visible
8989+ (** A document should not include more than one visible element with "role=main". *)
9090+ | Discarding_unrecognized_role of { token: string }
9191+ (** Discarding unrecognized token "X" from value of attribute "role". *)
9292+9393+ (* ===== Required Attribute/Element Conditions ===== *)
9494+ | Img_missing_alt
9595+ (** An "img" element must have an "alt" attribute. *)
9696+ | Img_missing_src_or_srcset
9797+ (** Element "img" is missing one or more of the following attributes: [src, srcset]. *)
9898+ | Option_empty_without_label
9999+ (** Element "option" without attribute "label" must not be empty. *)
100100+ | Bdo_missing_dir
101101+ (** Element "bdo" must have attribute "dir". *)
102102+ | Bdo_dir_auto
103103+ (** The value of "dir" attribute for the "bdo" element must not be "auto". *)
104104+ | Base_missing_href_or_target
105105+ (** Element "base" is missing one or more of the following attributes: [href, target]. *)
106106+ | Base_after_link_script
107107+ (** The "base" element must come before any "link" or "script" elements. *)
108108+ | Link_missing_href
109109+ (** A "link" element must have an "href" or "imagesrcset" attribute. *)
110110+ | Link_as_requires_preload
111111+ (** A "link" element with an "as" attribute must have rel="preload" or "modulepreload". *)
112112+ | Link_imagesrcset_requires_as_image
113113+ (** A "link" element with "imagesrcset" must have as="image". *)
114114+ | Img_ismap_needs_a_href
115115+ (** The "img" element with "ismap" must have an "a" ancestor with "href". *)
116116+ | Sizes_without_srcset
117117+ (** The "sizes" attribute must only be specified if "srcset" is also specified. *)
118118+ | Imagesizes_without_imagesrcset
119119+ (** The "imagesizes" attribute must only be specified if "imagesrcset" is also specified. *)
120120+ | Srcset_w_without_sizes
121121+ (** When the "srcset" attribute has width descriptors, "sizes" must also be specified. *)
122122+ | Source_missing_srcset
123123+ (** Element "source" is missing required attribute "srcset". *)
124124+ | Source_needs_media_or_type
125125+ (** A "source" element with following source/img[srcset] must have media/type. *)
126126+ | Picture_missing_img
127127+ (** Element "picture" is missing required child element "img". *)
128128+ | Map_id_name_mismatch
129129+ (** The "id" attribute on a "map" element must have the same value as the "name" attribute. *)
130130+ | List_attr_requires_datalist
131131+ (** The "list" attribute of "input" must refer to a "datalist" element. *)
132132+ | Label_too_many_labelable
133133+ (** The "label" element may contain at most one labelable descendant. *)
134134+ | Label_for_id_mismatch
135135+ (** Any "input" descendant of a "label" with "for" must have matching ID. *)
136136+ | Input_value_constraint of { constraint_type: string }
137137+ (** The value of the "value" attribute must be... *)
138138+ | Summary_missing_role
139139+ (** Element "summary" is missing required attribute "role". *)
140140+ | Summary_missing_attrs
141141+ (** Element "summary" is missing one or more of [aria-checked, aria-level, role]. *)
142142+ | Autocomplete_webauthn_on_select
143143+ (** The value of "autocomplete" for "select" must not contain "webauthn". *)
144144+ | Commandfor_invalid_target
145145+ (** The value of "commandfor" must be the ID of an element in the same tree. *)
146146+147147+ (* ===== Parse Errors ===== *)
148148+ | Forbidden_codepoint of { codepoint: int }
149149+ (** Forbidden code point U+XXXX. *)
150150+ | Char_ref_control of { codepoint: int }
151151+ (** Character reference expands to a control character (U+XXXX). *)
152152+ | Char_ref_non_char of { codepoint: int; astral: bool }
153153+ (** Character reference expands to a [astral] non-character (U+XXXX). *)
154154+ | Char_ref_unassigned
155155+ (** Character reference expands to a permanently unassigned code point. *)
156156+ | Char_ref_zero
157157+ (** Character reference expands to zero. *)
158158+ | Char_ref_out_of_range
159159+ (** Character reference outside the permissible Unicode range. *)
160160+ | Numeric_char_ref_carriage_return
161161+ (** A numeric character reference expanded to carriage return. *)
162162+ | End_of_file_with_open_elements
163163+ (** End of file seen and there were open elements. *)
164164+ | No_element_in_scope of { tag: string }
165165+ (** No "X" element in scope but a "X" end tag seen. *)
166166+ | End_tag_implied_open_elements of { tag: string }
167167+ (** End tag "X" implied, but there were open elements. *)
168168+ | Start_tag_in_table of { tag: string }
169169+ (** Start tag "X" seen in "table". *)
170170+ | Bad_start_tag_in of { tag: string; context: string }
171171+ (** Bad start tag in "X" in "noscript" in "head". *)
172172+173173+ (* ===== Table Errors ===== *)
174174+ | Table_row_no_cells of { row: int }
175175+ (** Row N of an implicit row group has no cells beginning on it. *)
176176+ | Table_cell_overlap
177177+ (** Table cell is overlapped by later table cell. *)
178178+ | Table_cell_spans_rowgroup
179179+ (** Table cell spans past the end of its row group. *)
180180+ | Table_column_no_cells of { column: int; element: string }
181181+ (** Table column N established by element "X" has no cells beginning in it. *)
182182+183183+ (* ===== Language/Internationalization ===== *)
184184+ | Missing_lang_attr
185185+ (** Consider adding a "lang" attribute to the "html" start tag. *)
186186+ | Wrong_lang of { detected: string; declared: string; suggested: string }
187187+ (** This document appears to be written in X but has lang="Y". Consider using "Z". *)
188188+ | Missing_dir_rtl of { language: string }
189189+ (** This document appears to be written in X. Consider adding dir="rtl". *)
190190+ | Wrong_dir of { language: string; declared: string }
191191+ (** This document appears to be written in X but has dir="Y". Consider dir="rtl". *)
192192+ | Xml_lang_without_lang
193193+ (** When xml:lang is specified, lang must also be present with the same value. *)
194194+ | Xml_lang_lang_mismatch
195195+ (** xml:lang and lang must have the same value. *)
196196+197197+ (* ===== Unicode Normalization ===== *)
198198+ | Not_nfc of { replacement: string }
199199+ (** Text run is not in Unicode Normalization Form C. *)
200200+201201+ (* ===== Multiple h1 ===== *)
202202+ | Multiple_h1
203203+ (** Consider using only one "h1" element per document. *)
204204+ | Multiple_autofocus
205205+ (** There must not be two elements with autofocus in the same scoping root. *)
206206+207207+ (* ===== Import Maps ===== *)
208208+ | Importmap_invalid_json
209209+ (** A "script" type="importmap" must have valid JSON content. *)
210210+ | Importmap_invalid_root
211211+ (** A "script" type="importmap" must contain a JSON object with only imports/scopes/integrity. *)
212212+ | Importmap_imports_not_object
213213+ (** The value of "imports" property must be a JSON object. *)
214214+ | Importmap_empty_key
215215+ (** Specifier map must only contain non-empty keys. *)
216216+ | Importmap_non_string_value
217217+ (** Specifier map must only contain string values. *)
218218+ | Importmap_key_trailing_slash
219219+ (** Specifier map values must end with "/" when key ends with "/". *)
220220+ | Importmap_scopes_not_object
221221+ (** The value of "scopes" property must be a JSON object with valid URL keys. *)
222222+ | Importmap_scopes_values_not_object
223223+ (** The value of "scopes" property values must also be JSON objects. *)
224224+ | Importmap_scopes_invalid_url
225225+ (** The "scopes" property must only contain valid URL values. *)
226226+227227+ (* ===== Style Element ===== *)
228228+ | Style_type_invalid
229229+ (** The only allowed value for "type" on "style" is "text/css". *)
230230+231231+ (* ===== Headingoffset ===== *)
232232+ | Headingoffset_invalid
233233+ (** The value of "headingoffset" must be a number between "0" and "8". *)
234234+235235+ (* ===== Media Attribute ===== *)
236236+ | Media_empty
237237+ (** Value of "media" attribute here must not be empty. *)
238238+ | Media_all
239239+ (** Value of "media" attribute here must not be "all". *)
240240+241241+ (* ===== SVG/MathML specific ===== *)
242242+ | Svg_deprecated_attr of { attr: string; element: string }
243243+ (** SVG deprecated attribute *)
244244+ | Missing_required_svg_attr of { element: string; attr: string }
245245+ (** Element "X" is missing required attribute "Y". (SVG) *)
246246+247247+ (* ===== Generic/Fallback ===== *)
248248+ | Generic of { message: string }
249249+ (** For messages that don't fit any specific pattern *)
250250+251251+(** Get the severity level for an error code *)
252252+let severity = function
253253+ | Missing_lang_attr -> Info
254254+ | Multiple_h1 -> Info
255255+ | Wrong_lang _ -> Warning
256256+ | Missing_dir_rtl _ -> Warning
257257+ | Wrong_dir _ -> Warning
258258+ | Unnecessary_role _ -> Warning
259259+ | Aria_should_not_be_used _ -> Warning
260260+ | _ -> Error
261261+262262+(** Get a short code string for categorization *)
263263+let code_string = function
264264+ | Attr_not_allowed_on_element _ -> "disallowed-attribute"
265265+ | Attr_not_allowed_here _ -> "disallowed-attribute"
266266+ | Attr_not_allowed_when _ -> "disallowed-attribute"
267267+ | Missing_required_attr _ -> "missing-required-attribute"
268268+ | Missing_required_attr_one_of _ -> "missing-required-attribute"
269269+ | Bad_attr_value _ -> "bad-attribute-value"
270270+ | Bad_attr_value_generic _ -> "bad-attribute-value"
271271+ | Duplicate_id _ -> "duplicate-id"
272272+ | Data_attr_invalid_name _ -> "bad-attribute-name"
273273+ | Data_attr_uppercase -> "bad-attribute-name"
274274+ | Obsolete_element _ -> "obsolete-element"
275275+ | Obsolete_attr _ -> "obsolete-attribute"
276276+ | Element_not_allowed_as_child _ -> "disallowed-child"
277277+ | Element_must_not_be_descendant _ -> "prohibited-ancestor"
278278+ | Missing_required_child _ -> "missing-required-child"
279279+ | Missing_required_child_one_of _ -> "missing-required-child"
280280+ | Missing_required_child_generic _ -> "missing-required-child"
281281+ | Element_must_not_be_empty _ -> "empty-element"
282282+ | Stray_start_tag _ -> "stray-tag"
283283+ | Stray_end_tag _ -> "stray-tag"
284284+ | End_tag_for_void_element _ -> "end-tag-void"
285285+ | Self_closing_non_void -> "self-closing-non-void"
286286+ | Text_not_allowed _ -> "text-not-allowed"
287287+ | Div_child_of_dl_bad_role -> "invalid-role"
288288+ | Li_bad_role_in_menu -> "invalid-role"
289289+ | Li_bad_role_in_tablist -> "invalid-role"
290290+ | Li_bad_role_in_list -> "invalid-role"
291291+ | Unnecessary_role _ -> "unnecessary-role"
292292+ | Bad_role _ -> "bad-role"
293293+ | Aria_must_not_be_specified _ -> "aria-not-allowed"
294294+ | Aria_must_not_be_used _ -> "aria-not-allowed"
295295+ | Aria_should_not_be_used _ -> "aria-not-allowed"
296296+ | Img_empty_alt_with_role -> "img-alt-role"
297297+ | Checkbox_button_needs_aria_pressed -> "missing-aria-pressed"
298298+ | Tab_without_tabpanel -> "tab-without-tabpanel"
299299+ | Multiple_main_visible -> "multiple-main"
300300+ | Discarding_unrecognized_role _ -> "unrecognized-role"
301301+ | Img_missing_alt -> "missing-alt"
302302+ | Img_missing_src_or_srcset -> "missing-src"
303303+ | Option_empty_without_label -> "empty-option"
304304+ | Bdo_missing_dir -> "missing-dir"
305305+ | Bdo_dir_auto -> "bdo-dir-auto"
306306+ | Base_missing_href_or_target -> "missing-required-attribute"
307307+ | Base_after_link_script -> "base-position"
308308+ | Link_missing_href -> "missing-href"
309309+ | Link_as_requires_preload -> "link-as-preload"
310310+ | Link_imagesrcset_requires_as_image -> "link-imagesrcset"
311311+ | Img_ismap_needs_a_href -> "ismap-needs-href"
312312+ | Sizes_without_srcset -> "sizes-without-srcset"
313313+ | Imagesizes_without_imagesrcset -> "imagesizes-without-srcset"
314314+ | Srcset_w_without_sizes -> "srcset-needs-sizes"
315315+ | Source_missing_srcset -> "missing-srcset"
316316+ | Source_needs_media_or_type -> "source-needs-media"
317317+ | Picture_missing_img -> "picture-missing-img"
318318+ | Map_id_name_mismatch -> "map-id-name"
319319+ | List_attr_requires_datalist -> "list-datalist"
320320+ | Label_too_many_labelable -> "label-multiple"
321321+ | Label_for_id_mismatch -> "label-for-mismatch"
322322+ | Input_value_constraint _ -> "input-value"
323323+ | Summary_missing_role -> "summary-role"
324324+ | Summary_missing_attrs -> "summary-attrs"
325325+ | Autocomplete_webauthn_on_select -> "autocomplete"
326326+ | Commandfor_invalid_target -> "commandfor"
327327+ | Forbidden_codepoint _ -> "forbidden-codepoint"
328328+ | Char_ref_control _ -> "char-ref-control"
329329+ | Char_ref_non_char _ -> "char-ref-non-char"
330330+ | Char_ref_unassigned -> "char-ref-unassigned"
331331+ | Char_ref_zero -> "char-ref-zero"
332332+ | Char_ref_out_of_range -> "char-ref-range"
333333+ | Numeric_char_ref_carriage_return -> "numeric-char-ref"
334334+ | End_of_file_with_open_elements -> "eof-open-elements"
335335+ | No_element_in_scope _ -> "no-element-in-scope"
336336+ | End_tag_implied_open_elements _ -> "end-tag-implied"
337337+ | Start_tag_in_table _ -> "start-tag-in-table"
338338+ | Bad_start_tag_in _ -> "bad-start-tag"
339339+ | Table_row_no_cells _ -> "table-row"
340340+ | Table_cell_overlap -> "table-overlap"
341341+ | Table_cell_spans_rowgroup -> "table-span"
342342+ | Table_column_no_cells _ -> "table-column"
343343+ | Missing_lang_attr -> "missing-lang"
344344+ | Wrong_lang _ -> "wrong-lang"
345345+ | Missing_dir_rtl _ -> "missing-dir"
346346+ | Wrong_dir _ -> "wrong-dir"
347347+ | Xml_lang_without_lang -> "xml-lang"
348348+ | Xml_lang_lang_mismatch -> "xml-lang-mismatch"
349349+ | Not_nfc _ -> "unicode-normalization"
350350+ | Multiple_h1 -> "multiple-h1"
351351+ | Multiple_autofocus -> "multiple-autofocus"
352352+ | Importmap_invalid_json -> "importmap"
353353+ | Importmap_invalid_root -> "importmap"
354354+ | Importmap_imports_not_object -> "importmap"
355355+ | Importmap_empty_key -> "importmap"
356356+ | Importmap_non_string_value -> "importmap"
357357+ | Importmap_key_trailing_slash -> "importmap"
358358+ | Importmap_scopes_not_object -> "importmap"
359359+ | Importmap_scopes_values_not_object -> "importmap"
360360+ | Importmap_scopes_invalid_url -> "importmap"
361361+ | Style_type_invalid -> "style-type"
362362+ | Headingoffset_invalid -> "headingoffset"
363363+ | Media_empty -> "media-empty"
364364+ | Media_all -> "media-all"
365365+ | Svg_deprecated_attr _ -> "svg-deprecated"
366366+ | Missing_required_svg_attr _ -> "missing-required-attribute"
367367+ | Generic _ -> "generic"
368368+369369+(** Format using curly quotes (Unicode) *)
370370+let q s = "\xe2\x80\x9c" ^ s ^ "\xe2\x80\x9d"
371371+372372+(** Convert error code to exact Nu validator message string *)
373373+let to_message = function
374374+ | Attr_not_allowed_on_element { attr; element } ->
375375+ Printf.sprintf "Attribute %s not allowed on element %s at this point."
376376+ (q attr) (q element)
377377+ | Attr_not_allowed_here { attr } ->
378378+ Printf.sprintf "Attribute %s not allowed here." (q attr)
379379+ | Attr_not_allowed_when { attr; element = _; condition } ->
380380+ Printf.sprintf "Attribute %s is only allowed when %s." (q attr) condition
381381+ | Missing_required_attr { element; attr } ->
382382+ Printf.sprintf "Element %s is missing required attribute %s."
383383+ (q element) (q attr)
384384+ | Missing_required_attr_one_of { element; attrs } ->
385385+ let attrs_str = String.concat ", " (List.map q attrs) in
386386+ Printf.sprintf "Element %s is missing one or more of the following attributes: [%s]."
387387+ (q element) attrs_str
388388+ | Bad_attr_value { element; attr; value; reason } ->
389389+ Printf.sprintf "Bad value %s for attribute %s on element %s: %s"
390390+ (q value) (q attr) (q element) reason
391391+ | Bad_attr_value_generic { message } -> message
392392+ | Duplicate_id { id } ->
393393+ Printf.sprintf "Duplicate ID %s." (q id)
394394+ | Data_attr_invalid_name { reason } -> reason
395395+ | Data_attr_uppercase ->
396396+ Printf.sprintf "%s attributes must not have characters from the range %s\xe2\x80\xa6%s in the name."
397397+ (q "data-*") (q "A") (q "Z")
398398+399399+ | Obsolete_element { element; suggestion } ->
400400+ if suggestion = "" then
401401+ Printf.sprintf "The %s element is obsolete." (q element)
402402+ else
403403+ Printf.sprintf "The %s element is obsolete. %s" (q element) suggestion
404404+ | Obsolete_attr { element; attr; suggestion } ->
405405+ let base = Printf.sprintf "The %s attribute on the %s element is obsolete."
406406+ (q attr) (q element) in
407407+ (match suggestion with Some s -> base ^ " " ^ s | None -> base)
408408+ | Element_not_allowed_as_child { child; parent } ->
409409+ Printf.sprintf "Element %s not allowed as child of element %s in this context. (Suppressing further errors from this subtree.)"
410410+ (q child) (q parent)
411411+ | Element_must_not_be_descendant { element; attr; ancestor } ->
412412+ (match attr with
413413+ | Some a ->
414414+ Printf.sprintf "The element %s with the attribute %s must not appear as a descendant of the %s element."
415415+ (q element) (q a) (q ancestor)
416416+ | None ->
417417+ Printf.sprintf "The element %s must not appear as a descendant of the %s element."
418418+ (q element) (q ancestor))
419419+ | Missing_required_child { parent; child } ->
420420+ Printf.sprintf "Element %s is missing required child element %s."
421421+ (q parent) (q child)
422422+ | Missing_required_child_one_of { parent; children } ->
423423+ let children_str = String.concat ", " (List.map q children) in
424424+ Printf.sprintf "Element %s is missing one or more of the following child elements: [%s]."
425425+ (q parent) children_str
426426+ | Missing_required_child_generic { parent } ->
427427+ Printf.sprintf "Element %s is missing a required child element." (q parent)
428428+ | Element_must_not_be_empty { element } ->
429429+ Printf.sprintf "Element %s must not be empty." (q element)
430430+ | Stray_start_tag { tag } ->
431431+ Printf.sprintf "Stray start tag %s." (q tag)
432432+ | Stray_end_tag { tag } ->
433433+ Printf.sprintf "Stray end tag %s." (q tag)
434434+ | End_tag_for_void_element { tag } ->
435435+ Printf.sprintf "End tag %s." (q tag)
436436+ | Self_closing_non_void ->
437437+ Printf.sprintf "Self-closing syntax (%s) used on a non-void HTML element. Ignoring the slash and treating as a start tag."
438438+ (q "/>")
439439+ | Text_not_allowed { parent } ->
440440+ Printf.sprintf "Text not allowed in element %s in this context." (q parent)
441441+442442+ | Div_child_of_dl_bad_role ->
443443+ Printf.sprintf "A %s child of a %s element must not have any %s value other than %s or %s."
444444+ (q "div") (q "dl") (q "role") (q "presentation") (q "none")
445445+ | Li_bad_role_in_menu ->
446446+ 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."
447447+ (q "li") (q "role=menu") (q "role=menubar") (q "role")
448448+ (q "group") (q "menuitem") (q "menuitemcheckbox") (q "menuitemradio") (q "separator")
449449+ | Li_bad_role_in_tablist ->
450450+ Printf.sprintf "An %s element that is a descendant of a %s element must not have any %s value other than %s."
451451+ (q "li") (q "role=tablist") (q "role") (q "tab")
452452+ | Li_bad_role_in_list ->
453453+ 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."
454454+ (q "li") (q "ul") (q "ol") (q "menu") (q "role") (q "role=list") (q "role") (q "listitem")
455455+456456+ | Unnecessary_role { role; element = _; reason } ->
457457+ Printf.sprintf "The %s role is unnecessary for %s."
458458+ (q role) reason
459459+ | Bad_role { element; role } ->
460460+ Printf.sprintf "Bad value %s for attribute %s on element %s."
461461+ (q role) (q "role") (q element)
462462+ | Aria_must_not_be_specified { attr; element; condition } ->
463463+ Printf.sprintf "The %s attribute must not be specified on any %s element unless %s."
464464+ (q attr) (q element) condition
465465+ | Aria_must_not_be_used { attr; element; condition } ->
466466+ Printf.sprintf "The %s attribute must not be used on an %s element which has %s."
467467+ (q attr) (q element) condition
468468+ | Aria_should_not_be_used { attr; role } ->
469469+ Printf.sprintf "The %s attribute should not be used on any element which has %s."
470470+ (q attr) (q ("role=" ^ role))
471471+ | Img_empty_alt_with_role ->
472472+ Printf.sprintf "An %s element which has an %s attribute whose value is the empty string must not have a %s attribute."
473473+ (q "img") (q "alt") (q "role")
474474+ | Checkbox_button_needs_aria_pressed ->
475475+ 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."
476476+ (q "input") (q "type") (q "checkbox") (q "role") (q "button") (q "aria-pressed")
477477+ | Tab_without_tabpanel ->
478478+ Printf.sprintf "Every active %s element must have a corresponding %s element."
479479+ (q "role=tab") (q "role=tabpanel")
480480+ | Multiple_main_visible ->
481481+ Printf.sprintf "A document should not include more than one visible element with %s."
482482+ (q "role=main")
483483+ | Discarding_unrecognized_role { token } ->
484484+ Printf.sprintf "Discarding unrecognized token %s from value of attribute %s. Browsers ignore any token that is not a defined ARIA non-abstract role."
485485+ (q token) (q "role")
486486+487487+ | Img_missing_alt ->
488488+ Printf.sprintf "An %s element must have an %s attribute, except under certain conditions. For details, consult guidance on providing text alternatives for images."
489489+ (q "img") (q "alt")
490490+ | Img_missing_src_or_srcset ->
491491+ Printf.sprintf "Element %s is missing one or more of the following attributes: [%s, %s]."
492492+ (q "img") (q "src") (q "srcset")
493493+ | Option_empty_without_label ->
494494+ Printf.sprintf "Element %s without attribute %s must not be empty."
495495+ (q "option") (q "label")
496496+ | Bdo_missing_dir ->
497497+ Printf.sprintf "Element %s must have attribute %s." (q "bdo") (q "dir")
498498+ | Bdo_dir_auto ->
499499+ Printf.sprintf "The value of %s attribute for the %s element must not be %s."
500500+ (q "dir") (q "bdo") (q "auto")
501501+ | Base_missing_href_or_target ->
502502+ Printf.sprintf "Element %s is missing one or more of the following attributes: [%s, %s]."
503503+ (q "base") (q "href") (q "target")
504504+ | Base_after_link_script ->
505505+ Printf.sprintf "The %s element must come before any %s or %s elements in the document."
506506+ (q "base") (q "link") (q "script")
507507+ | Link_missing_href ->
508508+ Printf.sprintf "A %s element must have an %s or %s attribute, or both."
509509+ (q "link") (q "href") (q "imagesrcset")
510510+ | Link_as_requires_preload ->
511511+ Printf.sprintf "A %s element with an %s attribute must have a %s attribute that contains the value %s or the value %s."
512512+ (q "link") (q "as") (q "rel") (q "preload") (q "modulepreload")
513513+ | Link_imagesrcset_requires_as_image ->
514514+ Printf.sprintf "A %s element with an %s attribute must have an %s attribute with value %s."
515515+ (q "link") (q "imagesrcset") (q "as") (q "image")
516516+ | Img_ismap_needs_a_href ->
517517+ Printf.sprintf "The %s element with the %s attribute set must have an %s ancestor with the %s attribute."
518518+ (q "img") (q "ismap") (q "a") (q "href")
519519+ | Sizes_without_srcset ->
520520+ Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified."
521521+ (q "sizes") (q "srcset")
522522+ | Imagesizes_without_imagesrcset ->
523523+ Printf.sprintf "The %s attribute must only be specified if the %s attribute is also specified."
524524+ (q "imagesizes") (q "imagesrcset")
525525+ | Srcset_w_without_sizes ->
526526+ Printf.sprintf "When the %s attribute has any image candidate string with a width descriptor, the %s attribute must also be specified."
527527+ (q "srcset") (q "sizes")
528528+ | Source_missing_srcset ->
529529+ Printf.sprintf "Element %s is missing required attribute %s."
530530+ (q "source") (q "srcset")
531531+ | Source_needs_media_or_type ->
532532+ 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."
533533+ (q "source") (q "source") (q "img") (q "srcset") (q "media") (q "type")
534534+ | Picture_missing_img ->
535535+ Printf.sprintf "Element %s is missing required child element %s."
536536+ (q "picture") (q "img")
537537+ | Map_id_name_mismatch ->
538538+ Printf.sprintf "The %s attribute on a %s element must have an the same value as the %s attribute."
539539+ (q "id") (q "map") (q "name")
540540+ | List_attr_requires_datalist ->
541541+ Printf.sprintf "The %s attribute of the %s element must refer to a %s element."
542542+ (q "list") (q "input") (q "datalist")
543543+ | Label_too_many_labelable ->
544544+ Printf.sprintf "The %s element may contain at most one %s, %s, %s, %s, %s, %s, or %s descendant."
545545+ (q "label") (q "button") (q "input") (q "meter") (q "output") (q "progress") (q "select") (q "textarea")
546546+ | Label_for_id_mismatch ->
547547+ Printf.sprintf "Any %s descendant of a %s element with a %s attribute must have an ID value that matches that %s attribute."
548548+ (q "input") (q "label") (q "for") (q "for")
549549+ | Input_value_constraint { constraint_type } -> constraint_type
550550+ | Summary_missing_role ->
551551+ Printf.sprintf "Element %s is missing required attribute %s."
552552+ (q "summary") (q "role")
553553+ | Summary_missing_attrs ->
554554+ Printf.sprintf "Element %s is missing one or more of the following attributes: [%s, %s, %s]."
555555+ (q "summary") (q "aria-checked") (q "aria-level") (q "role")
556556+ | Autocomplete_webauthn_on_select ->
557557+ Printf.sprintf "The value of the %s attribute for the %s element must not contain %s."
558558+ (q "autocomplete") (q "select") (q "webauthn")
559559+ | Commandfor_invalid_target ->
560560+ 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."
561561+ (q "commandfor") (q "button") (q "button") (q "commandfor")
562562+563563+ | Forbidden_codepoint { codepoint } ->
564564+ Printf.sprintf "Forbidden code point U+%04x." codepoint
565565+ | Char_ref_control { codepoint } ->
566566+ Printf.sprintf "Character reference expands to a control character (U+%04x)." codepoint
567567+ | Char_ref_non_char { codepoint; astral } ->
568568+ if astral then
569569+ Printf.sprintf "Character reference expands to an astral non-character (U+%05x)." codepoint
570570+ else
571571+ Printf.sprintf "Character reference expands to a non-character (U+%04x)." codepoint
572572+ | Char_ref_unassigned ->
573573+ "Character reference expands to a permanently unassigned code point."
574574+ | Char_ref_zero ->
575575+ "Character reference expands to zero."
576576+ | Char_ref_out_of_range ->
577577+ "Character reference outside the permissible Unicode range."
578578+ | Numeric_char_ref_carriage_return ->
579579+ "A numeric character reference expanded to carriage return."
580580+ | End_of_file_with_open_elements ->
581581+ "End of file seen and there were open elements."
582582+ | No_element_in_scope { tag } ->
583583+ Printf.sprintf "No %s element in scope but a %s end tag seen."
584584+ (q tag) (q tag)
585585+ | End_tag_implied_open_elements { tag } ->
586586+ Printf.sprintf "End tag %s implied, but there were open elements."
587587+ (q tag)
588588+ | Start_tag_in_table { tag } ->
589589+ Printf.sprintf "Start tag %s seen in %s." (q tag) (q "table")
590590+ | Bad_start_tag_in { tag; context = _ } ->
591591+ Printf.sprintf "Bad start tag in %s in %s in %s."
592592+ (q tag) (q "noscript") (q "head")
593593+594594+ | Table_row_no_cells { row } ->
595595+ Printf.sprintf "Row %d of an implicit row group has no cells beginning on it." row
596596+ | Table_cell_overlap ->
597597+ "Table cell is overlapped by later table cell."
598598+ | Table_cell_spans_rowgroup ->
599599+ 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."
600600+ (q "tbody")
601601+ | Table_column_no_cells { column; element } ->
602602+ Printf.sprintf "Table column %d established by element %s has no cells beginning in it."
603603+ column (q element)
604604+605605+ | Missing_lang_attr ->
606606+ Printf.sprintf "Consider adding a %s attribute to the %s start tag to declare the language of this document."
607607+ (q "lang") (q "html")
608608+ | Wrong_lang { detected; declared; suggested } ->
609609+ Printf.sprintf "This document appears to be written in %s but the %s start tag has %s. Consider using %s (or variant) instead."
610610+ detected (q "html") (q ("lang=\"" ^ declared ^ "\"")) (q ("lang=\"" ^ suggested ^ "\""))
611611+ | Missing_dir_rtl { language } ->
612612+ Printf.sprintf "This document appears to be written in %s. Consider adding %s to the %s start tag."
613613+ language (q "dir=\"rtl\"") (q "html")
614614+ | Wrong_dir { language; declared } ->
615615+ Printf.sprintf "This document appears to be written in %s but the %s start tag has %s. Consider using %s instead."
616616+ language (q "html") (q ("dir=\"" ^ declared ^ "\"")) (q "dir=\"rtl\"")
617617+ | Xml_lang_without_lang ->
618618+ Printf.sprintf "When the attribute %s in no namespace is specified, the element must also have the attribute %s present with the same value."
619619+ (q "xml:lang") (q "lang")
620620+ | Xml_lang_lang_mismatch ->
621621+ Printf.sprintf "The %s and %s attributes must have the same value."
622622+ (q "xml:lang") (q "lang")
623623+624624+ | Not_nfc { replacement } ->
625625+ 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.)"
626626+ (q replacement)
627627+628628+ | Multiple_h1 ->
629629+ 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)."
630630+ (q "h1") (q "h1") (q "headingoffset") (q "h1")
631631+ | Multiple_autofocus ->
632632+ Printf.sprintf "There must not be two elements with the same %s that both have the %s attribute specified."
633633+ (q "nearest ancestor autofocus scoping root element") (q "autofocus")
634634+635635+ | Importmap_invalid_json ->
636636+ Printf.sprintf "A script %s with a %s attribute whose value is %s must have valid JSON content."
637637+ (q "script") (q "type") (q "importmap")
638638+ | Importmap_invalid_root ->
639639+ 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."
640640+ (q "script") (q "type") (q "importmap") (q "imports") (q "scopes") (q "integrity")
641641+ | Importmap_imports_not_object ->
642642+ 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."
643643+ (q "imports") (q "script") (q "type") (q "importmap")
644644+ | Importmap_empty_key ->
645645+ 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."
646646+ (q "imports") (q "script") (q "type") (q "importmap")
647647+ | Importmap_non_string_value ->
648648+ 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."
649649+ (q "imports") (q "script") (q "type") (q "importmap")
650650+ | Importmap_key_trailing_slash ->
651651+ 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."
652652+ (q "imports") (q "script") (q "type") (q "importmap") (q "/") (q "/")
653653+ | Importmap_scopes_not_object ->
654654+ 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."
655655+ (q "scopes") (q "script") (q "type") (q "importmap")
656656+ | Importmap_scopes_values_not_object ->
657657+ 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."
658658+ (q "scopes") (q "script") (q "type") (q "importmap")
659659+ | Importmap_scopes_invalid_url ->
660660+ 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."
661661+ (q "scopes") (q "script") (q "type") (q "importmap")
662662+663663+ | Style_type_invalid ->
664664+ 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.)"
665665+ (q "type") (q "style") (q "text/css")
666666+667667+ | Headingoffset_invalid ->
668668+ Printf.sprintf "The value of the %s attribute must be a number between %s and %s."
669669+ (q "headingoffset") (q "0") (q "8")
670670+671671+ | Media_empty ->
672672+ Printf.sprintf "Value of %s attribute here must not be empty." (q "media")
673673+ | Media_all ->
674674+ Printf.sprintf "Value of %s attribute here must not be %s." (q "media") (q "all")
675675+676676+ | Svg_deprecated_attr { attr; element } ->
677677+ Printf.sprintf "Attribute %s not allowed on element %s at this point."
678678+ (q attr) (q element)
679679+ | Missing_required_svg_attr { element; attr } ->
680680+ Printf.sprintf "Element %s is missing required attribute %s."
681681+ (q element) (q attr)
682682+683683+ | Generic { message } -> message
+157
lib/html5_checker/error_code.mli
···11+(** Typed error codes for HTML5 validation messages.
22+33+ This module defines a comprehensive variant type for all validation errors,
44+ ensuring exact message matching with the Nu HTML Validator test suite. *)
55+66+(** Severity level of a validation message *)
77+type severity = Error | Warning | Info
88+99+(** Typed error codes with associated data *)
1010+type t =
1111+ (* Attribute Errors *)
1212+ | Attr_not_allowed_on_element of { attr: string; element: string }
1313+ | Attr_not_allowed_here of { attr: string }
1414+ | Attr_not_allowed_when of { attr: string; element: string; condition: string }
1515+ | Missing_required_attr of { element: string; attr: string }
1616+ | Missing_required_attr_one_of of { element: string; attrs: string list }
1717+ | Bad_attr_value of { element: string; attr: string; value: string; reason: string }
1818+ | Bad_attr_value_generic of { message: string }
1919+ | Duplicate_id of { id: string }
2020+ | Data_attr_invalid_name of { reason: string }
2121+ | Data_attr_uppercase
2222+2323+ (* Element Errors *)
2424+ | Obsolete_element of { element: string; suggestion: string }
2525+ | Obsolete_attr of { element: string; attr: string; suggestion: string option }
2626+ | Element_not_allowed_as_child of { child: string; parent: string }
2727+ | Element_must_not_be_descendant of { element: string; attr: string option; ancestor: string }
2828+ | Missing_required_child of { parent: string; child: string }
2929+ | Missing_required_child_one_of of { parent: string; children: string list }
3030+ | Missing_required_child_generic of { parent: string }
3131+ | Element_must_not_be_empty of { element: string }
3232+ | Stray_start_tag of { tag: string }
3333+ | Stray_end_tag of { tag: string }
3434+ | End_tag_for_void_element of { tag: string }
3535+ | Self_closing_non_void
3636+ | Text_not_allowed of { parent: string }
3737+3838+ (* Child Restrictions *)
3939+ | Div_child_of_dl_bad_role
4040+ | Li_bad_role_in_menu
4141+ | Li_bad_role_in_tablist
4242+ | Li_bad_role_in_list
4343+4444+ (* ARIA Errors *)
4545+ | Unnecessary_role of { role: string; element: string; reason: string }
4646+ | Bad_role of { element: string; role: string }
4747+ | Aria_must_not_be_specified of { attr: string; element: string; condition: string }
4848+ | Aria_must_not_be_used of { attr: string; element: string; condition: string }
4949+ | Aria_should_not_be_used of { attr: string; role: string }
5050+ | Img_empty_alt_with_role
5151+ | Checkbox_button_needs_aria_pressed
5252+ | Tab_without_tabpanel
5353+ | Multiple_main_visible
5454+ | Discarding_unrecognized_role of { token: string }
5555+5656+ (* Required Attribute/Element Conditions *)
5757+ | Img_missing_alt
5858+ | Img_missing_src_or_srcset
5959+ | Option_empty_without_label
6060+ | Bdo_missing_dir
6161+ | Bdo_dir_auto
6262+ | Base_missing_href_or_target
6363+ | Base_after_link_script
6464+ | Link_missing_href
6565+ | Link_as_requires_preload
6666+ | Link_imagesrcset_requires_as_image
6767+ | Img_ismap_needs_a_href
6868+ | Sizes_without_srcset
6969+ | Imagesizes_without_imagesrcset
7070+ | Srcset_w_without_sizes
7171+ | Source_missing_srcset
7272+ | Source_needs_media_or_type
7373+ | Picture_missing_img
7474+ | Map_id_name_mismatch
7575+ | List_attr_requires_datalist
7676+ | Label_too_many_labelable
7777+ | Label_for_id_mismatch
7878+ | Input_value_constraint of { constraint_type: string }
7979+ | Summary_missing_role
8080+ | Summary_missing_attrs
8181+ | Autocomplete_webauthn_on_select
8282+ | Commandfor_invalid_target
8383+8484+ (* Parse Errors *)
8585+ | Forbidden_codepoint of { codepoint: int }
8686+ | Char_ref_control of { codepoint: int }
8787+ | Char_ref_non_char of { codepoint: int; astral: bool }
8888+ | Char_ref_unassigned
8989+ | Char_ref_zero
9090+ | Char_ref_out_of_range
9191+ | Numeric_char_ref_carriage_return
9292+ | End_of_file_with_open_elements
9393+ | No_element_in_scope of { tag: string }
9494+ | End_tag_implied_open_elements of { tag: string }
9595+ | Start_tag_in_table of { tag: string }
9696+ | Bad_start_tag_in of { tag: string; context: string }
9797+9898+ (* Table Errors *)
9999+ | Table_row_no_cells of { row: int }
100100+ | Table_cell_overlap
101101+ | Table_cell_spans_rowgroup
102102+ | Table_column_no_cells of { column: int; element: string }
103103+104104+ (* Language/Internationalization *)
105105+ | Missing_lang_attr
106106+ | Wrong_lang of { detected: string; declared: string; suggested: string }
107107+ | Missing_dir_rtl of { language: string }
108108+ | Wrong_dir of { language: string; declared: string }
109109+ | Xml_lang_without_lang
110110+ | Xml_lang_lang_mismatch
111111+112112+ (* Unicode Normalization *)
113113+ | Not_nfc of { replacement: string }
114114+115115+ (* Multiple h1 *)
116116+ | Multiple_h1
117117+ | Multiple_autofocus
118118+119119+ (* Import Maps *)
120120+ | Importmap_invalid_json
121121+ | Importmap_invalid_root
122122+ | Importmap_imports_not_object
123123+ | Importmap_empty_key
124124+ | Importmap_non_string_value
125125+ | Importmap_key_trailing_slash
126126+ | Importmap_scopes_not_object
127127+ | Importmap_scopes_values_not_object
128128+ | Importmap_scopes_invalid_url
129129+130130+ (* Style Element *)
131131+ | Style_type_invalid
132132+133133+ (* Headingoffset *)
134134+ | Headingoffset_invalid
135135+136136+ (* Media Attribute *)
137137+ | Media_empty
138138+ | Media_all
139139+140140+ (* SVG/MathML specific *)
141141+ | Svg_deprecated_attr of { attr: string; element: string }
142142+ | Missing_required_svg_attr of { element: string; attr: string }
143143+144144+ (* Generic/Fallback *)
145145+ | Generic of { message: string }
146146+147147+(** Get the severity level for an error code *)
148148+val severity : t -> severity
149149+150150+(** Get a short code string for categorization *)
151151+val code_string : t -> string
152152+153153+(** Convert error code to exact Nu validator message string *)
154154+val to_message : t -> string
155155+156156+(** Format a string with curly quotes *)
157157+val q : string -> string
···2222type t = {
2323 severity : severity;
2424 message : string; (** Human-readable description *)
2525- code : string option; (** Machine-readable error code *)
2525+ code : string; (** Machine-readable error code *)
2626+ error_code : Error_code.t option; (** Typed error code if available *)
2627 location : location option;
2728 element : string option; (** Element name if relevant *)
2829 attribute : string option; (** Attribute name if relevant *)
···31323233(** {1 Constructors} *)
33343434-(** Create a validation message with specified severity. *)
3535+(** Create a message from a typed error code (preferred method). *)
3636+val of_error_code :
3737+ ?location:location ->
3838+ ?element:string ->
3939+ ?attribute:string ->
4040+ ?extract:string ->
4141+ Error_code.t ->
4242+ t
4343+4444+(** Create a validation message with specified severity (legacy). *)
3545val make :
3646 severity:severity ->
3747 message:string ->
···4353 unit ->
4454 t
45554646-(** Create an error message. *)
5656+(** Create an error message (legacy). *)
4757val error :
4858 message:string ->
4959 ?code:string ->
···5464 unit ->
5565 t
56665757-(** Create a warning message. *)
6767+(** Create a warning message (legacy). *)
5868val warning :
5969 message:string ->
6070 ?code:string ->
···6575 unit ->
6676 t
67776868-(** Create an informational message. *)
7878+(** Create an informational message (legacy). *)
6979val info :
7080 message:string ->
7181 ?code:string ->
+14
lib/html5_checker/message_collector.ml
···11+(** Message collector for accumulating validation messages. *)
22+13type t = { mutable messages : Message.t list }
2435let create () = { messages = [] }
4657let add t msg = t.messages <- msg :: t.messages
6899+(** Add a message from a typed error code *)
1010+let add_typed t ?location ?element ?attribute ?extract error_code =
1111+ let msg = Message.of_error_code ?location ?element ?attribute ?extract error_code in
1212+ add t msg
1313+1414+(** Add an error from a typed error code *)
1515+let add_error_code t ?location ?element ?attribute ?extract error_code =
1616+ add_typed t ?location ?element ?attribute ?extract error_code
1717+1818+(** Legacy: Add an error with manual message text *)
719let add_error t ~message ?code ?location ?element ?attribute ?extract () =
820 let msg =
921 Message.error ~message ?code ?location ?element ?attribute ?extract ()
1022 in
1123 add t msg
12242525+(** Legacy: Add a warning with manual message text *)
1326let add_warning t ~message ?code ?location ?element ?attribute ?extract () =
1427 let msg =
1528 Message.warning ~message ?code ?location ?element ?attribute ?extract ()
1629 in
1730 add t msg
18313232+(** Legacy: Add an info message with manual message text *)
1933let add_info t ~message ?code ?location ?element ?attribute ?extract () =
2034 let msg =
2135 Message.info ~message ?code ?location ?element ?attribute ?extract ()
+26-4
lib/html5_checker/message_collector.mli
···88(** Create a new empty message collector. *)
99val create : unit -> t
10101111-(** {1 Adding Messages} *)
1111+(** {1 Adding Messages - Typed Error Codes (Preferred)} *)
1212+1313+(** Add a message from a typed error code. *)
1414+val add_typed :
1515+ t ->
1616+ ?location:Message.location ->
1717+ ?element:string ->
1818+ ?attribute:string ->
1919+ ?extract:string ->
2020+ Error_code.t ->
2121+ unit
2222+2323+(** Add an error from a typed error code. Alias for add_typed. *)
2424+val add_error_code :
2525+ t ->
2626+ ?location:Message.location ->
2727+ ?element:string ->
2828+ ?attribute:string ->
2929+ ?extract:string ->
3030+ Error_code.t ->
3131+ unit
3232+3333+(** {1 Adding Messages - Legacy (for migration)} *)
12341335(** Add a message to the collector. *)
1436val add : t -> Message.t -> unit
15371616-(** Add an error message to the collector. *)
3838+(** Add an error message to the collector (legacy). *)
1739val add_error :
1840 t ->
1941 message:string ->
···2547 unit ->
2648 unit
27492828-(** Add a warning message to the collector. *)
5050+(** Add a warning message to the collector (legacy). *)
2951val add_warning :
3052 t ->
3153 message:string ->
···3759 unit ->
3860 unit
39614040-(** Add an info message to the collector. *)
6262+(** Add an info message to the collector (legacy). *)
4163val add_info :
4264 t ->
4365 message:string ->
+3-9
lib/html5_checker/message_format.ml
···2424 match system_id with Some s -> s | None -> "input")
2525 in
2626 let severity_str = Message.severity_to_string msg.Message.severity in
2727- let code_str =
2828- match msg.Message.code with Some c -> " [" ^ c ^ "]" | None -> ""
2929- in
2727+ let code_str = " [" ^ msg.Message.code ^ "]" in
3028 let elem_str =
3129 match msg.Message.element with
3230 | Some e -> " (element: " ^ e ^ ")"
···6159 match system_id with Some s -> s ^ ":0:0" | None -> "input:0:0")
6260 in
6361 let severity_str = Message.severity_to_string msg.Message.severity in
6464- let code_str =
6565- match msg.Message.code with Some c -> " [" ^ c ^ "]" | None -> ""
6666- in
6262+ let code_str = " [" ^ msg.Message.code ^ "]" in
6763 Buffer.add_string buf
6864 (Printf.sprintf "%s: %s%s: %s\n" loc_str severity_str code_str
6965 msg.Message.message))
···7672 let message_text = String (msg.Message.message, Meta.none) in
7773 let base = [ (("type", Meta.none), severity); (("message", Meta.none), message_text) ] in
7874 let with_code =
7979- match msg.Message.code with
8080- | Some c -> (("subType", Meta.none), String (c, Meta.none)) :: base
8181- | None -> base
7575+ (("subType", Meta.none), String (msg.Message.code, Meta.none)) :: base
8276 in
8377 let with_location =
8478 match msg.Message.location with
+1-9
lib/html5_checker/semantic/autofocus_checker.ml
···6969 | ctx :: _ ->
7070 ctx.autofocus_count <- ctx.autofocus_count + 1;
7171 if ctx.autofocus_count > 1 then
7272- let context_name = match ctx.context_type with
7373- | Dialog -> "dialog"
7474- | Popover -> "popover"
7575- in
7676- Message_collector.add_error collector
7777- ~message:(Printf.sprintf "A document must not include more than one visible element with the \xe2\x80\x9cautofocus\xe2\x80\x9d attribute inside a %s."
7878- context_name)
7979- ~code:"multiple-autofocus"
8080- ~element:name ~attribute:"autofocus" ()
7272+ Message_collector.add_typed collector Error_code.Multiple_autofocus
8173 | [] -> ()
8274 end
8375 end
+8-12
lib/html5_checker/semantic/form_checker.ml
···2626let check_autocomplete_value value element_name collector =
2727 (* webauthn is not allowed on select, only on input and textarea *)
2828 if element_name = "select" && contains_webauthn value then begin
2929- Message_collector.add_error collector
3030- ~message:(Printf.sprintf "The value of the \xe2\x80\x9cautocomplete\xe2\x80\x9d attribute for the \xe2\x80\x9c%s\xe2\x80\x9d element must not contain \xe2\x80\x9cwebauthn\xe2\x80\x9d."
3131- element_name)
3232- ~code:"bad-attribute-value"
3333- ~element:element_name
3434- ~attribute:"autocomplete" ()
2929+ Message_collector.add_typed collector Error_code.Autocomplete_webauthn_on_select
3530 end else begin
3631 (* Use the proper autocomplete validator from dt_autocomplete *)
3732 match Dt_autocomplete.validate_autocomplete value with
3833 | Ok () -> ()
3934 | Error msg ->
4040- Message_collector.add_error collector
4141- ~message:(Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9cautocomplete\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: %s"
4242- value element_name msg)
4343- ~code:"bad-attribute-value"
4444- ~element:element_name
4545- ~attribute:"autocomplete" ()
3535+ Message_collector.add_typed collector
3636+ (Error_code.Bad_attr_value {
3737+ element = element_name;
3838+ attr = "autocomplete";
3939+ value;
4040+ reason = msg
4141+ })
4642 end
47434844let start_element _state ~name ~namespace:_ ~attrs collector =
+44-51
lib/html5_checker/semantic/id_checker.ml
···1313 referring_element : string;
1414 attribute : string;
1515 referenced_id : string;
1616- location : Message.location option;
1616+ _location : Message.location option; [@warning "-69"]
1717}
18181919(** Checker state tracking IDs, map names, and references. *)
···9696]
97979898(** Check and store an ID attribute. *)
9999-let check_id state ~element ~id ~location collector =
9999+let check_id state ~element:_ ~id ~location:_ collector =
100100 (* Check for empty ID *)
101101 if String.length id = 0 then
102102- Message_collector.add_error collector
103103- ~message:"ID attribute must not be empty"
104104- ~code:"empty-id"
105105- ?location
106106- ~element
107107- ~attribute:"id"
108108- ()
102102+ Message_collector.add_typed collector
103103+ (Error_code.Bad_attr_value_generic {
104104+ message = "Bad value \"\" for attribute \"id\": An ID must not be the empty string."
105105+ })
109106 (* Check for whitespace in ID *)
110107 else if contains_whitespace id then
111111- Message_collector.add_error collector
112112- ~message:(Printf.sprintf "ID attribute value '%s' must not contain whitespace" id)
113113- ~code:"id-whitespace"
114114- ?location
115115- ~element
116116- ~attribute:"id"
117117- ()
108108+ Message_collector.add_typed collector
109109+ (Error_code.Bad_attr_value_generic {
110110+ message = Printf.sprintf "Bad value %s for attribute \"id\": An ID must not contain whitespace."
111111+ (Error_code.q id)
112112+ })
118113 (* Check for duplicate ID *)
119114 else if Hashtbl.mem state.ids id then
120120- Message_collector.add_error collector
121121- ~message:(Printf.sprintf "Duplicate ID \xe2\x80\x9c%s\xe2\x80\x9d." id)
122122- ~code:"duplicate-id"
123123- ?location
124124- ~element
125125- ~attribute:"id"
126126- ()
115115+ Message_collector.add_typed collector (Error_code.Duplicate_id { id })
127116 else
128117 (* Store the ID *)
129118 Hashtbl.add state.ids id ()
···135124 referring_element;
136125 attribute;
137126 referenced_id;
138138- location;
127127+ _location = location;
139128 } :: state.references
140129141130(** Process attributes to check IDs and collect references. *)
···154143 referring_element = element;
155144 attribute = name;
156145 referenced_id = map_name;
157157- location;
146146+ _location = location;
158147 } :: state.usemap_references
148148+ else
149149+ (* Empty hash name: "#" *)
150150+ Message_collector.add_typed collector
151151+ (Error_code.Bad_attr_value {
152152+ element;
153153+ attr = name;
154154+ value;
155155+ reason = Printf.sprintf "Bad hash-name reference: A hash-name reference must have at least one character after %s."
156156+ (Error_code.q "#")
157157+ })
159158 | None ->
160159 if String.length value > 0 then
161161- Message_collector.add_error collector
162162- ~message:(Printf.sprintf
163163- "usemap attribute value '%s' must start with '#'" value)
164164- ~code:"invalid-usemap"
165165- ?location
166166- ~element
167167- ~attribute:name
168168- ()
160160+ (* Missing # prefix *)
161161+ Message_collector.add_typed collector
162162+ (Error_code.Bad_attr_value {
163163+ element;
164164+ attr = name;
165165+ value;
166166+ reason = Printf.sprintf "Bad hash-name reference: A hash-name reference must start with %s."
167167+ (Error_code.q "#")
168168+ })
169169 end
170170171171 | "name" when element = "map" ->
···205205 (* Check all ID references point to existing IDs *)
206206 List.iter (fun ref ->
207207 if not (Hashtbl.mem state.ids ref.referenced_id) then
208208- Message_collector.add_error collector
209209- ~message:(Printf.sprintf
210210- "The '%s' attribute on <%s> refers to ID '%s' which does not exist"
211211- ref.attribute ref.referring_element ref.referenced_id)
212212- ~code:"dangling-id-reference"
213213- ?location:ref.location
214214- ~element:ref.referring_element
215215- ~attribute:ref.attribute
216216- ()
208208+ (* Use generic for dangling references - format may vary *)
209209+ Message_collector.add_typed collector
210210+ (Error_code.Generic {
211211+ message = Printf.sprintf "The %s attribute on the %s element refers to ID %s which does not exist in the document."
212212+ (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id)
213213+ })
217214 ) state.references;
218215219216 (* Check all usemap references point to existing map names *)
220217 List.iter (fun ref ->
221218 if not (Hashtbl.mem state.map_names ref.referenced_id) then
222222- Message_collector.add_error collector
223223- ~message:(Printf.sprintf
224224- "The '%s' attribute on <%s> refers to map name '%s' which does not exist"
225225- ref.attribute ref.referring_element ref.referenced_id)
226226- ~code:"dangling-usemap-reference"
227227- ?location:ref.location
228228- ~element:ref.referring_element
229229- ~attribute:ref.attribute
230230- ()
219219+ Message_collector.add_typed collector
220220+ (Error_code.Generic {
221221+ message = Printf.sprintf "The %s attribute on the %s element refers to map name %s which does not exist in the document."
222222+ (Error_code.q ref.attribute) (Error_code.q ref.referring_element) (Error_code.q ref.referenced_id)
223223+ })
231224 ) state.usemap_references
232225233226let checker = (module struct
···236236 let base_detected = get_lang_code detected_code in
237237 if original_declared = "" then begin
238238 (* No lang attribute - suggest adding one *)
239239- Message_collector.add_warning collector
240240- ~message:(Printf.sprintf
241241- "This document appears to be written in %s. Consider adding \xe2\x80\x9clang=\"%s\"\xe2\x80\x9d (or variant) to the \xe2\x80\x9chtml\xe2\x80\x9d start tag."
242242- detected_name suggested_code)
243243- ~code:"missing-lang"
244244- ~element:"html"
245245- ()
239239+ Message_collector.add_typed collector
240240+ (Error_code.Wrong_lang {
241241+ detected = detected_name;
242242+ declared = "";
243243+ suggested = suggested_code
244244+ })
246245 end
247246 else if base_declared <> base_detected &&
248247 (* Don't warn for zh variants *)
249248 not (base_declared = "zh" && base_detected = "zh") then begin
250250- Message_collector.add_warning collector
251251- ~message:(Printf.sprintf
252252- "This document appears to be written in %s but the \xe2\x80\x9chtml\xe2\x80\x9d start tag has \xe2\x80\x9clang=\"%s\"\xe2\x80\x9d. Consider using \xe2\x80\x9clang=\"%s\"\xe2\x80\x9d (or variant) instead."
253253- detected_name original_declared suggested_code)
254254- ~code:"wrong-lang"
255255- ~element:"html"
256256- ()
249249+ Message_collector.add_typed collector
250250+ (Error_code.Wrong_lang {
251251+ detected = detected_name;
252252+ declared = original_declared;
253253+ suggested = suggested_code
254254+ })
257255 end;
258256259257 (* Check dir attribute for RTL languages *)
260258 if List.mem base_detected rtl_langs then begin
261259 match state.html_dir with
262260 | None ->
263263- Message_collector.add_warning collector
264264- ~message:(Printf.sprintf
265265- "This document appears to be written in %s. Consider adding \xe2\x80\x9cdir=\"rtl\"\xe2\x80\x9d to the \xe2\x80\x9chtml\xe2\x80\x9d start tag."
266266- detected_name)
267267- ~code:"missing-dir"
268268- ~element:"html"
269269- ()
261261+ Message_collector.add_typed collector
262262+ (Error_code.Missing_dir_rtl { language = detected_name })
270263 | Some dir when String.lowercase_ascii dir <> "rtl" ->
271271- Message_collector.add_warning collector
272272- ~message:(Printf.sprintf
273273- "This document appears to be written in %s but the \xe2\x80\x9chtml\xe2\x80\x9d start tag has \xe2\x80\x9cdir=\"%s\"\xe2\x80\x9d. Consider using \xe2\x80\x9cdir=\"rtl\"\xe2\x80\x9d instead."
274274- detected_name dir)
275275- ~code:"wrong-dir"
276276- ~element:"html"
277277- ()
264264+ Message_collector.add_typed collector
265265+ (Error_code.Wrong_dir { language = detected_name; declared = dir })
278266 | _ -> ()
279267 end
280268 | _ -> ()
+21-27
lib/html5_checker/semantic/nesting_checker.ml
···181181 | _ ->
182182 false
183183184184-(** Get a human-readable description of an element for error messages. *)
185185-let element_description name attrs =
186186- match name with
187187- | "a" when has_attr attrs "href" ->
188188- "The element \"a\" with the attribute \"href\""
189189- | "audio" when has_attr attrs "controls" ->
190190- "The element \"audio\" with the attribute \"controls\""
191191- | "video" when has_attr attrs "controls" ->
192192- "The element \"video\" with the attribute \"controls\""
193193- | "img" when has_attr attrs "usemap" ->
194194- "The element \"img\" with the attribute \"usemap\""
195195- | "object" when has_attr attrs "usemap" ->
196196- "The element \"object\" with the attribute \"usemap\""
197197- | _ ->
198198- Printf.sprintf "The element \"%s\"" name
199199-200184(** Report nesting violations. *)
201185let check_nesting state name attrs collector =
202186 (* Compute the prohibited ancestor mask for this element *)
···218202 if mask <> 0 then begin
219203 let mask_hit = state.ancestor_mask land mask in
220204 if mask_hit <> 0 then begin
221221- let desc = element_description name attrs in
205205+ (* Determine if element has a special attribute to mention *)
206206+ let attr =
207207+ match name with
208208+ | "a" when has_attr attrs "href" -> Some "href"
209209+ | "audio" when has_attr attrs "controls" -> Some "controls"
210210+ | "video" when has_attr attrs "controls" -> Some "controls"
211211+ | "img" when has_attr attrs "usemap" -> Some "usemap"
212212+ | "object" when has_attr attrs "usemap" -> Some "usemap"
213213+ | _ -> None
214214+ in
222215 (* Find which ancestors are violated *)
223216 Array.iteri (fun i ancestor ->
224217 let bit = 1 lsl i in
225218 if (mask_hit land bit) <> 0 then
226226- Message_collector.add_error collector
227227- ~message:(Printf.sprintf
228228- "%s must not appear as a descendant of the \"%s\" element."
229229- desc ancestor)
230230- ~element:name
231231- ()
219219+ Message_collector.add_typed collector
220220+ (Error_code.Element_must_not_be_descendant {
221221+ element = name;
222222+ attr;
223223+ ancestor
224224+ })
232225 ) special_ancestors
233226 end
234227 end
···238231 match name with
239232 | "area" ->
240233 if (state.ancestor_mask land map_mask) = 0 then
241241- Message_collector.add_error collector
242242- ~message:"The \"area\" element must have a \"map\" ancestor."
243243- ~element:name
244244- ()
234234+ Message_collector.add_typed collector
235235+ (Error_code.Generic {
236236+ message = Printf.sprintf "The %s element must have a %s ancestor."
237237+ (Error_code.q "area") (Error_code.q "map")
238238+ })
245239 | _ -> ()
246240247241let start_element state ~name ~namespace ~attrs collector =
+12-36
lib/html5_checker/semantic/obsolete_checker.ml
···163163 register "target" ["link"]
164164 "You can safely omit it.";
165165166166- register "type" ["param"; "area"; "menu"]
166166+ register "type" ["param"; "area"]
167167 "You can safely omit it.";
168168+169169+ register "type" ["menu"]
170170+ "Use script to handle \"contextmenu\" event instead.";
168171169172 register "typemustmatch" ["object"]
170173 "Avoid using \"object\" elements with untrusted resources.";
···260263 (match Hashtbl.find_opt obsolete_elements name_lower with
261264 | None -> ()
262265 | Some suggestion ->
263263- let message =
264264- if String.length suggestion = 0 then
265265- Printf.sprintf "The \xe2\x80\x9c%s\xe2\x80\x9d element is obsolete." name
266266- else
267267- Printf.sprintf "The \xe2\x80\x9c%s\xe2\x80\x9d element is obsolete. %s" name suggestion
268268- in
269269- Message_collector.add_error collector
270270- ~message
271271- ~code:"obsolete-element"
272272- ~element:name
273273- ());
266266+ Message_collector.add_typed collector
267267+ (Error_code.Obsolete_element { element = name; suggestion }));
274268275269 (* Check for obsolete attributes *)
276270 List.iter (fun (attr_name, _attr_value) ->
···283277 (match Hashtbl.find_opt element_map name_lower with
284278 | None -> ()
285279 | Some suggestion ->
286286- let message =
287287- Printf.sprintf "The \xe2\x80\x9c%s\xe2\x80\x9d attribute on the \xe2\x80\x9c%s\xe2\x80\x9d element is obsolete. %s"
288288- attr_name name suggestion
289289- in
290290- Message_collector.add_error collector
291291- ~message
292292- ~code:"obsolete-attribute"
293293- ~element:name
294294- ~attribute:attr_name
295295- ()));
280280+ Message_collector.add_typed collector
281281+ (Error_code.Obsolete_attr { element = name; attr = attr_name; suggestion = Some suggestion })));
296282297283 (* Check obsolete style attributes *)
298284 (match Hashtbl.find_opt obsolete_style_attrs attr_lower with
299285 | None -> ()
300286 | Some elements ->
301287 if List.mem name_lower elements then
302302- let message =
303303- Printf.sprintf "The \xe2\x80\x9c%s\xe2\x80\x9d attribute on the \xe2\x80\x9c%s\xe2\x80\x9d element is obsolete. Use CSS instead."
304304- attr_name name
305305- in
306306- Message_collector.add_error collector
307307- ~message
308308- ~code:"obsolete-style-attribute"
309309- ~element:name
310310- ~attribute:attr_name
311311- ());
288288+ Message_collector.add_typed collector
289289+ (Error_code.Obsolete_attr { element = name; attr = attr_name; suggestion = Some "Use CSS instead." }));
312290313291 (* Check obsolete global attributes *)
314292 (match Hashtbl.find_opt obsolete_global_attrs attr_lower with
315293 | None -> ()
316294 | Some suggestion ->
317317- let message =
318318- Printf.sprintf "The \xe2\x80\x9c%s\xe2\x80\x9d attribute is obsolete. %s" attr_name suggestion
319319- in
295295+ (* Global attributes use a different format - just "The X attribute is obsolete. Y" *)
320296 Message_collector.add_error collector
321321- ~message
297297+ ~message:(Printf.sprintf "The %s attribute is obsolete. %s" (Error_code.q attr_name) suggestion)
322298 ~code:"obsolete-global-attribute"
323299 ~element:name
324300 ~attribute:attr_name
+3-12
lib/html5_checker/semantic/option_checker.ml
···6161 state.option_stack <- rest;
6262 (* Validate: option must have text content or non-empty label *)
6363 if not ctx.has_text then begin
6464- if ctx.label_empty then
6565- (* Has label="" (empty) and no text - error *)
6666- Message_collector.add_error collector
6767- ~message:"An \xe2\x80\x9coption\xe2\x80\x9d element with an empty \xe2\x80\x9clabel\xe2\x80\x9d attribute must have content."
6868- ~code:"empty-option"
6969- ~element:"option" ()
7070- else if not ctx.has_label then
7171- (* No label and no text - error *)
7272- Message_collector.add_error collector
7373- ~message:"An \xe2\x80\x9coption\xe2\x80\x9d element with no \xe2\x80\x9clabel\xe2\x80\x9d attribute must have content."
7474- ~code:"empty-option"
7575- ~element:"option" ()
6464+ if ctx.label_empty || not ctx.has_label then
6565+ (* Has label="" (empty) and no text, or no label at all - error *)
6666+ Message_collector.add_typed collector Error_code.Option_empty_without_label
7667 end
7768 | [] -> ()
7869 end
···2727let check_img_element state attrs collector =
2828 (* Check for required src OR srcset attribute *)
2929 if not (has_attr "src" attrs) && not (has_attr "srcset" attrs) then
3030- Message_collector.add_error collector
3131- ~message:"Element \xe2\x80\x9cimg\xe2\x80\x9d is missing one or more of the following attributes: [src, srcset]."
3232- ~code:"missing-required-attribute" ~element:"img" ~attribute:"src" ();
3030+ Message_collector.add_typed collector Error_code.Img_missing_src_or_srcset;
33313432 (* Check for alt attribute - always required *)
3533 if not (has_attr "alt" attrs) then
3636- Message_collector.add_error collector
3737- ~message:"img element requires alt attribute for accessibility"
3838- ~code:"missing-required-attribute" ~element:"img" ~attribute:"alt" ();
3434+ Message_collector.add_typed collector Error_code.Img_missing_alt;
39354036 (* Check ismap requires 'a' ancestor with href *)
4137 if has_attr "ismap" attrs && not state.in_a_with_href then
4242- Message_collector.add_error collector
4343- ~message:"The \xe2\x80\x9cimg\xe2\x80\x9d element with the \xe2\x80\x9cismap\xe2\x80\x9d attribute set must have an \xe2\x80\x9ca\xe2\x80\x9d ancestor with the \xe2\x80\x9chref\xe2\x80\x9d attribute."
4444- ~code:"missing-required-ancestor" ~element:"img" ~attribute:"ismap" ()
3838+ Message_collector.add_typed collector Error_code.Img_ismap_needs_a_href
45394640let check_area_element attrs collector =
4741 (* area with href requires alt *)
4842 if has_attr "href" attrs && not (has_attr "alt" attrs) then
4949- Message_collector.add_error collector
5050- ~message:"area element with href requires alt attribute" ~code:"missing-required-attribute"
5151- ~element:"area" ~attribute:"alt" ()
4343+ Message_collector.add_typed collector
4444+ (Error_code.Missing_required_attr { element = "area"; attr = "alt" })
52455346let check_input_element attrs collector =
5447 match get_attr "type" attrs with
5548 | Some "image" ->
5649 (* input[type=image] requires alt *)
5750 if not (has_attr "alt" attrs) then
5858- Message_collector.add_error collector
5959- ~message:"input element with type=\"image\" requires alt attribute"
6060- ~code:"missing-required-attribute" ~element:"input" ~attribute:"alt" ()
5151+ Message_collector.add_typed collector
5252+ (Error_code.Missing_required_attr { element = "input"; attr = "alt" })
6153 | Some "hidden" ->
6254 (* input[type=hidden] should not have required attribute *)
6355 if has_attr "required" attrs then
6464- Message_collector.add_error collector
6565- ~message:"input element with type=\"hidden\" cannot have required attribute"
6666- ~code:"invalid-attribute-combination" ~element:"input" ~attribute:"required" ()
5656+ Message_collector.add_typed collector
5757+ (Error_code.Attr_not_allowed_when {
5858+ attr = "required";
5959+ element = "input";
6060+ condition = "the type attribute is hidden"
6161+ })
6762 | Some "file" ->
6863 (* input[type=file] should not have value attribute *)
6964 if has_attr "value" attrs then
7070- Message_collector.add_warning collector
7171- ~message:"input element with type=\"file\" should not have value attribute"
7272- ~code:"invalid-attribute-combination" ~element:"input" ~attribute:"value" ()
6565+ Message_collector.add_typed collector
6666+ (Error_code.Attr_not_allowed_when {
6767+ attr = "value";
6868+ element = "input";
6969+ condition = "the type attribute is file"
7070+ })
7371 | _ -> ()
74727573let check_script_element attrs _collector =
···102100 in
103101104102 if not valid then
105105- Message_collector.add_error collector
106106- ~message:
107107- "meta element requires either charset, or name+content, or http-equiv+content"
108108- ~code:"missing-required-attribute" ~element:"meta" ()
103103+ Message_collector.add_typed collector
104104+ (Error_code.Generic {
105105+ message = Printf.sprintf "A %s element must have either a %s attribute, a %s attribute with a %s attribute, or an %s attribute with a %s attribute."
106106+ (Error_code.q "meta") (Error_code.q "charset") (Error_code.q "name")
107107+ (Error_code.q "content") (Error_code.q "http-equiv") (Error_code.q "content")
108108+ })
109109110110let check_link_element attrs collector =
111111 (* link[rel="stylesheet"] requires href *)
112112 match get_attr "rel" attrs with
113113 | Some rel when String.equal rel "stylesheet" ->
114114 if not (has_attr "href" attrs) then
115115- Message_collector.add_error collector
116116- ~message:"link element with rel=\"stylesheet\" requires href attribute"
117117- ~code:"missing-required-attribute" ~element:"link" ~attribute:"href" ()
115115+ Message_collector.add_typed collector Error_code.Link_missing_href
118116 | _ -> ()
119117120118let check_a_element attrs collector =
121119 (* a[download] requires href *)
122120 if has_attr "download" attrs && not (has_attr "href" attrs) then
123123- Message_collector.add_error collector
124124- ~message:"Element \xe2\x80\x9ca\xe2\x80\x9d is missing required attribute \xe2\x80\x9chref\xe2\x80\x9d."
125125- ~code:"missing-required-attribute" ~element:"a" ~attribute:"href" ()
121121+ Message_collector.add_typed collector
122122+ (Error_code.Missing_required_attr { element = "a"; attr = "href" })
126123127124let check_map_element attrs collector =
128125 (* map requires name *)
129126 if not (has_attr "name" attrs) then
130130- Message_collector.add_error collector
131131- ~message:"map element requires name attribute" ~code:"missing-required-attribute"
132132- ~element:"map" ~attribute:"name" ()
127127+ Message_collector.add_typed collector
128128+ (Error_code.Missing_required_attr { element = "map"; attr = "name" })
133129134130let check_object_element attrs collector =
135131 (* object requires data attribute (or type attribute alone is not sufficient) *)
136132 let has_data = has_attr "data" attrs in
137133 let has_type = has_attr "type" attrs in
138134 if not has_data && has_type then
139139- Message_collector.add_error collector
140140- ~message:"Element \xe2\x80\x9cobject\xe2\x80\x9d is missing required attribute \xe2\x80\x9cdata\xe2\x80\x9d."
141141- ~code:"missing-required-attribute" ~element:"object" ~attribute:"data" ()
135135+ Message_collector.add_typed collector
136136+ (Error_code.Missing_required_attr { element = "object"; attr = "data" })
142137143143-let check_popover_element attrs collector =
138138+let check_popover_element element_name attrs collector =
144139 (* popover attribute must have valid value *)
145140 match get_attr "popover" attrs with
146141 | Some value ->
147142 let value_lower = String.lowercase_ascii value in
148143 (* Valid values: empty string, auto, manual, hint *)
149144 if value_lower <> "" && value_lower <> "auto" && value_lower <> "manual" && value_lower <> "hint" then
150150- Message_collector.add_error collector
151151- ~message:(Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9cpopover\xe2\x80\x9d on element \xe2\x80\x9cdiv\xe2\x80\x9d."
152152- value)
153153- ~code:"bad-attribute-value" ~element:"div" ~attribute:"popover" ()
145145+ Message_collector.add_typed collector
146146+ (Error_code.Bad_attr_value {
147147+ element = element_name;
148148+ attr = "popover";
149149+ value;
150150+ reason = "Must be a valid popover state (auto, manual, or hint)."
151151+ })
154152 | None -> ()
155153156154let check_meter_element attrs collector =
157155 (* meter requires value attribute *)
158156 if not (has_attr "value" attrs) then
159159- Message_collector.add_error collector
160160- ~message:"Element \xe2\x80\x9cmeter\xe2\x80\x9d is missing required attribute \xe2\x80\x9cvalue\xe2\x80\x9d."
161161- ~code:"missing-required-attribute" ~element:"meter" ~attribute:"value" ()
157157+ Message_collector.add_typed collector
158158+ (Error_code.Missing_required_attr { element = "meter"; attr = "value" })
162159 else begin
163160 (* Validate min <= value constraint *)
164161 match get_attr "value" attrs, get_attr "min" attrs with
···167164 let value = float_of_string value_str in
168165 let min_val = float_of_string min_str in
169166 if min_val > value then
170170- Message_collector.add_error collector
171171- ~message:"The value of the \xe2\x80\x9cmin\xe2\x80\x9d attribute must be less than or equal to the value of the \xe2\x80\x9cvalue\xe2\x80\x9d attribute."
172172- ~code:"bad-attribute-value" ~element:"meter" ~attribute:"min" ()
167167+ Message_collector.add_typed collector
168168+ (Error_code.Generic {
169169+ message = Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute."
170170+ (Error_code.q "min") (Error_code.q "value")
171171+ })
173172 with _ -> ())
174173 | _ -> ()
175174 end
···188187 if value > max_val then
189188 (* Check which message to use based on whether max is present *)
190189 if has_attr "max" attrs then
191191- Message_collector.add_error collector
192192- ~message:"The value of the \xe2\x80\x9cvalue\xe2\x80\x9d attribute must be less than or equal to the value of the \xe2\x80\x9cmax\xe2\x80\x9d attribute."
193193- ~code:"bad-attribute-value" ~element:"progress" ~attribute:"value" ()
190190+ Message_collector.add_typed collector
191191+ (Error_code.Generic {
192192+ (* Note: double space before "value" matches Nu validator quirk *)
193193+ message = Printf.sprintf "The value of the %s attribute must be less than or equal to the value of the %s attribute."
194194+ (Error_code.q "value") (Error_code.q "max")
195195+ })
194196 else
195195- Message_collector.add_error collector
196196- ~message:"The value of the \xe2\x80\x9cvalue\xe2\x80\x9d attribute must be less than or equal to one when the \xe2\x80\x9cmax\xe2\x80\x9d attribute is absent."
197197- ~code:"bad-attribute-value" ~element:"progress" ~attribute:"value" ()
197197+ Message_collector.add_typed collector
198198+ (Error_code.Generic {
199199+ (* Note: double space before "value" matches Nu validator quirk *)
200200+ message = Printf.sprintf "The value of the %s attribute must be less than or equal to one when the %s attribute is absent."
201201+ (Error_code.q "value") (Error_code.q "max")
202202+ })
198203 with _ -> ())
199204200205let start_element state ~name ~namespace:_ ~attrs collector =
···215220 | "figure" -> state._in_figure <- true
216221 | _ ->
217222 (* Check popover attribute on any element *)
218218- if has_attr "popover" attrs then check_popover_element attrs collector
223223+ if has_attr "popover" attrs then check_popover_element name attrs collector
219224220225let end_element state ~name ~namespace:_ _collector =
221226 match name with
+1-1
lib/html5_checker/specialized/url_checker.ml
···297297 (* Check for empty host *)
298298 let requires_host = List.mem scheme special_schemes in
299299 if host = "" && requires_host && scheme <> "file" then
300300- Some (Printf.sprintf "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 URL: empty host."
300300+ Some (Printf.sprintf "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 URL: Invalid host: empty host."
301301 url attr_name element_name)
302302 else
303303 (* Check for invalid chars *)
+33-20
test/test_validator.ml
···142142 if errors = [] then
143143 (false, "Expected error but got none")
144144 else begin
145145- (* For novalid tests, we pass if ANY error is produced.
146146- Message matching is optional - our messages may differ from Nu validator. *)
147147- let msg_matched = match expected_msg with
148148- | None -> true
149149- | Some exp -> List.exists (fun actual -> message_matches ~expected:exp ~actual) errors
150150- in
151151- if msg_matched then
152152- (true, Printf.sprintf "Got %d error(s), message matched" (List.length errors))
153153- else
154154- (* Still pass - we detected an error even if message differs *)
155155- (true, Printf.sprintf "Got %d error(s) (message format differs)" (List.length errors))
145145+ (* For novalid tests, require EXACT message match when expected message is provided *)
146146+ match expected_msg with
147147+ | None ->
148148+ (* No expected message - pass if any error detected *)
149149+ (true, Printf.sprintf "Got %d error(s), no expected message to match" (List.length errors))
150150+ | Some exp ->
151151+ if List.exists (fun actual -> message_matches ~expected:exp ~actual) errors then
152152+ (true, Printf.sprintf "Got %d error(s), message matched" (List.length errors))
153153+ else
154154+ (* FAIL if message doesn't match - we want exact matching *)
155155+ (false, Printf.sprintf "Message mismatch.\n Expected: %s\n Got: %s"
156156+ exp (String.concat "\n " errors))
156157 end
157158 | HasWarning ->
158158- (* For haswarn, accept warnings or info messages (Nu validator uses info for some) *)
159159- if warnings <> [] then
160160- (true, Printf.sprintf "Got %d warning(s)" (List.length warnings))
161161- else if infos <> [] then
162162- (true, Printf.sprintf "Got %d info message(s)" (List.length infos))
163163- else if errors <> [] then
164164- (* Also accept errors as they indicate we caught something *)
165165- (true, Printf.sprintf "Got %d error(s) instead of warning" (List.length errors))
166166- else
159159+ (* For haswarn, require message match against warnings or infos *)
160160+ let all_messages = warnings @ infos in
161161+ if all_messages = [] && errors = [] then
167162 (false, "Expected warning but got none")
163163+ else begin
164164+ match expected_msg with
165165+ | None ->
166166+ if all_messages <> [] then
167167+ (true, Printf.sprintf "Got %d warning/info message(s)" (List.length all_messages))
168168+ else
169169+ (true, Printf.sprintf "Got %d error(s) instead of warning" (List.length errors))
170170+ | Some exp ->
171171+ if List.exists (fun actual -> message_matches ~expected:exp ~actual) all_messages then
172172+ (true, Printf.sprintf "Got %d warning/info message(s), matched" (List.length all_messages))
173173+ else if List.exists (fun actual -> message_matches ~expected:exp ~actual) errors then
174174+ (* Accept error if message matches (severity might differ) *)
175175+ (true, Printf.sprintf "Got error instead of warning, but message matched")
176176+ else
177177+ (false, Printf.sprintf "Message mismatch.\n Expected: %s\n Got warnings: %s\n Got errors: %s"
178178+ exp (String.concat "\n " (if all_messages = [] then ["(none)"] else all_messages))
179179+ (String.concat "\n " (if errors = [] then ["(none)"] else errors)))
180180+ end
168181 | Unknown ->
169182 (false, "Unknown test type")
170183 in