···11(** Srcset and sizes attribute validation checker. *)
2233+(** Quote helper for consistent message formatting. *)
44+let q = Error_code.q
55+36(** Valid CSS length units for sizes attribute *)
47let valid_length_units = [
58 "em"; "ex"; "ch"; "rem"; "cap"; "ic";
···400403 (* Empty sizes is invalid *)
401404 if String.trim value = "" then begin
402405 Message_collector.add_typed collector
403403- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Must not be empty." element_name))));
406406+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Must not be empty." (q "") (q "sizes") (q element_name)))));
404407 false
405408 end else begin
406409 (* Split on comma and check each entry *)
···410413 (* Check if starts with comma (empty first entry) *)
411414 if first_entry = "" then begin
412415 Message_collector.add_typed collector
413413- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Starts with empty source size." value element_name))));
416416+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Starts with empty source size." (q value) (q "sizes") (q element_name)))));
414417 false
415418 end else begin
416419 (* Check for trailing comma *)
417420 let last_entry = String.trim (List.nth entries (List.length entries - 1)) in
418421 if List.length entries > 1 && last_entry = "" then begin
419419- (* Generate abbreviated context - show last ~25 chars with ellipsis if needed *)
420420- let context =
421421- if String.length value > 25 then
422422- "\xe2\x80\xa6" ^ String.sub value (String.length value - 25) 25
423423- else value
424424- in
425422 Message_collector.add_typed collector
426426- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context))));
423423+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Expected media condition before %s at %s." (q value) (q "sizes") (q element_name) (q "") (q value)))));
427424 false
428425 end else begin
429426 let valid = ref true in
···442439 (* Context is the first entry with a comma *)
443440 let context = (String.trim first) ^ "," in
444441 Message_collector.add_typed collector
445445- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context))));
442442+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Expected media condition before %s at %s." (q value) (q "sizes") (q element_name) (q "") (q context)))));
446443 valid := false
447444 end;
448445 (* Check for multiple entries without media conditions.
···454451 (* Multiple defaults - report as "Expected media condition" *)
455452 let context = (String.trim first) ^ "," in
456453 Message_collector.add_typed collector
457457- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context))));
454454+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Expected media condition before %s at %s." (q value) (q "sizes") (q element_name) (q "") (q context)))));
458455 valid := false
459456 end
460457 end
···468465 (* Check for invalid media condition *)
469466 (match has_invalid_media_condition trimmed with
470467 | Some err_msg ->
471471- (* Generate context: "entry," with ellipsis if needed *)
472472- let context = (String.trim entry) ^ "," in
473473- let context =
474474- if String.length context > 25 then
475475- "\xe2\x80\xa6" ^ String.sub context (String.length context - 25) 25
476476- else context
477477- in
468468+ let context = trimmed ^ "," in
478469 Message_collector.add_typed collector
479479- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: %s at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name err_msg context))));
470470+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: %s at %s." (q value) (q "sizes") (q element_name) err_msg (q context)))));
480471 valid := false
481472 | None -> ());
482473···508499 let prev_entries = List.filter (fun e -> String.trim e <> "" && e <> entry) entries in
509500 let context =
510501 if List.length prev_entries > 0 then
511511- let prev_value = String.concat ", " (List.map String.trim prev_entries) ^ "," in
512512- if String.length prev_value > 25 then
513513- "\xe2\x80\xa6" ^ String.sub prev_value (String.length prev_value - 25) 25
514514- else prev_value
502502+ String.concat ", " (List.map String.trim prev_entries) ^ ","
515503 else value
516504 in
517505 Message_collector.add_typed collector
518518- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected media condition before \xe2\x80\x9c\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name context))));
506506+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Expected media condition before %s at %s." (q value) (q "sizes") (q element_name) (q "") (q context)))));
519507 valid := false
520508 end
521509 (* If there's extra junk after the size, report BadCssNumber error for it *)
522510 else if extra_parts <> [] then begin
523523- let junk = String.concat " " extra_parts in
524511 let last_junk = List.nth extra_parts (List.length extra_parts - 1) in
525512 let first_char = if String.length last_junk > 0 then last_junk.[0] else 'x' in
526526- (* Context depends on whether this is the last entry:
527527- - For non-last entries: entry with trailing comma, truncated from beginning
528528- - For last entry: full value truncated from beginning (no trailing comma) *)
529513 let is_last_entry = idx = num_entries - 1 in
530514 let context =
531531- if is_last_entry then begin
532532- (* Last entry: use full value truncated *)
533533- if String.length value > 25 then
534534- "\xe2\x80\xa6" ^ String.sub value (String.length value - 25) 25
535535- else value
536536- end else begin
537537- (* Non-last entry: use entry with comma, truncated *)
538538- let entry_with_comma = trimmed ^ "," in
539539- if String.length entry_with_comma > 25 then
540540- "\xe2\x80\xa6" ^ String.sub entry_with_comma (String.length entry_with_comma - 25) 25
541541- else entry_with_comma
542542- end
515515+ if is_last_entry then value
516516+ else trimmed ^ ","
543517 in
544544- let _ = junk in
545518 Message_collector.add_typed collector
546546- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a minus sign or a digit but saw \xe2\x80\x9c%c\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name first_char context))));
519519+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Bad CSS number token: Expected a minus sign or a digit but saw %s instead at %s." (q value) (q "sizes") (q element_name) (q (String.make 1 first_char)) (q context)))));
547520 valid := false
548521 end
549522 else
···556529 in
557530 let _ = full_context in
558531 Message_collector.add_typed collector
559559- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected positive size value but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name size_val size_val))));
532532+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Expected positive size value but found %s at %s." (q value) (q "sizes") (q element_name) (q size_val) (q size_val)))));
560533 valid := false
561534 | CssCommentAfterSign (found, context) ->
562535 (* e.g., +/**/50vw - expected number after sign *)
563536 Message_collector.add_typed collector
564564- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected number but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name found context))));
537537+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Expected number but found %s at %s." (q value) (q "sizes") (q element_name) (q found) (q context)))));
565538 valid := false
566539 | CssCommentBeforeUnit (found, context) ->
567540 (* e.g., 50/**/vw - expected units after number *)
568568- let units_list = List.map (fun u -> Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" u) valid_length_units in
541541+ let units_list = List.map q valid_length_units in
569542 let units_str = String.concat ", " units_list in
570543 Message_collector.add_typed collector
571571- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected units (one of %s) but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name units_str found context))));
544544+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Expected units (one of %s) but found %s at %s." (q value) (q "sizes") (q element_name) units_str (q found) (q context)))));
572545 valid := false
573546 | BadScientificNotation ->
574547 (* For scientific notation with bad exponent, show what char was expected vs found *)
···579552 (* Find the period in the exponent *)
580553 let _ = context in
581554 Message_collector.add_typed collector
582582- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a digit but saw \xe2\x80\x9c.\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name size_val))));
555555+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Bad CSS number token: Expected a digit but saw %s instead at %s." (q value) (q "sizes") (q element_name) (q ".") (q size_val)))));
583556 valid := false
584557 | BadCssNumber (first_char, context) ->
585558 (* Value doesn't start with a digit or minus sign *)
···589562 in
590563 let _ = full_context in
591564 Message_collector.add_typed collector
592592- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Bad CSS number token: Expected a minus sign or a digit but saw \xe2\x80\x9c%c\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name first_char context))));
565565+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Bad CSS number token: Expected a minus sign or a digit but saw %s instead at %s." (q value) (q "sizes") (q element_name) (q (String.make 1 first_char)) (q context)))));
593566 valid := false
594567 | InvalidUnit (found_unit, _context) ->
595568 (* Generate the full list of expected units *)
596596- let units_list = List.map (fun u -> Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" u) valid_length_units in
569569+ let units_list = List.map q valid_length_units in
597570 let units_str = String.concat ", " units_list in
598571 (* Context should be the full entry, with comma only if there are multiple entries *)
599572 let full_context =
···603576 (* When found_unit is empty, say "no units" instead of quoting empty string *)
604577 let found_str =
605578 if found_unit = "" then "no units"
606606- else Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" found_unit
579579+ else q found_unit
607580 in
608581 Message_collector.add_typed collector
609609- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csizes\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad source size list: Expected units (one of %s) but found %s at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name units_str found_str full_context))));
582582+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad source size list: Expected units (one of %s) but found %s at %s." (q value) (q "sizes") (q element_name) units_str found_str (q full_context)))));
610583 valid := false
611584 end
612585 end
···633606 (* Show just the number part (without the 'w') *)
634607 let num_part_for_msg = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
635608 Message_collector.add_typed collector
636636- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number without leading plus sign but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part_for_msg srcset_value))));
609609+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected number without leading plus sign but found %s at %s." (q srcset_value) (q "srcset") (q element_name) (q num_part_for_msg) (q srcset_value)))));
637610 false
638611 end else
639612 (try
640613 let n = int_of_string num_part in
641614 if n <= 0 then begin
642615 Message_collector.add_typed collector
643643- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value))));
616616+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected number greater than zero but found %s at %s." (q srcset_value) (q "srcset") (q element_name) (q num_part) (q srcset_value)))));
644617 false
645618 end else begin
646619 (* Check for uppercase W - compare original desc with lowercase version *)
647620 let original_last = desc.[String.length desc - 1] in
648621 if original_last = 'W' then begin
649622 Message_collector.add_typed collector
650650- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" srcset_value element_name desc srcset_value))));
623623+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected width descriptor but found %s at %s. (When the %s attribute is present, all image candidate strings must specify a width.)" (q srcset_value) (q "srcset") (q element_name) (q desc) (q srcset_value) (q "sizes")))));
651624 false
652625 end else true
653626 end
···655628 (* Check for scientific notation, decimal, or other non-integer values *)
656629 if String.contains num_part 'e' || String.contains num_part 'E' || String.contains num_part '.' then begin
657630 Message_collector.add_typed collector
658658- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected integer but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value))));
631631+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected integer but found %s at %s." (q srcset_value) (q "srcset") (q element_name) (q num_part) (q srcset_value)))));
659632 false
660633 end else begin
661634 Message_collector.add_typed collector
662662- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Invalid width descriptor." srcset_value element_name))));
635635+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad srcset descriptor: Invalid width descriptor." (q srcset_value) (q "srcset") (q element_name)))));
663636 false
664637 end)
665638 | 'x' ->
···669642 (* Extract the number part including the plus sign *)
670643 let num_with_plus = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
671644 Message_collector.add_typed collector
672672- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number without leading plus sign but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_with_plus srcset_value))));
645645+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected number without leading plus sign but found %s at %s." (q srcset_value) (q "srcset") (q element_name) (q num_with_plus) (q srcset_value)))));
673646 false
674647 end else begin
675648 (try
···680653 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
681654 let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in
682655 Message_collector.add_typed collector
683683- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Expected a digit but saw \xe2\x80\x9c%s\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name first_char srcset_value))));
656656+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad positive floating point number: Expected a digit but saw %s instead at %s." (q srcset_value) (q "srcset") (q element_name) (q first_char) (q srcset_value)))));
684657 false
685658 end else if n = 0.0 then begin
686659 (* Check if it's -0 (starts with minus) - report as "greater than zero" error *)
···688661 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
689662 if String.length orig_num_part > 0 && orig_num_part.[0] = '-' then begin
690663 Message_collector.add_typed collector
691691- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name orig_num_part srcset_value))))
664664+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected number greater than zero but found %s at %s." (q srcset_value) (q "srcset") (q element_name) (q orig_num_part) (q srcset_value)))))
692665 end else begin
693666 Message_collector.add_typed collector
694694- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Zero is not a valid positive floating point number at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name srcset_value))))
667667+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad positive floating point number: Zero is not a valid positive floating point number at %s." (q srcset_value) (q "srcset") (q element_name) (q srcset_value)))))
695668 end;
696669 false
697670 end else if n < 0.0 then begin
698671 Message_collector.add_typed collector
699699- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number greater than zero but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name num_part srcset_value))));
672672+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected number greater than zero but found %s at %s." (q srcset_value) (q "srcset") (q element_name) (q num_part) (q srcset_value)))));
700673 false
701674 end else if n = neg_infinity || n = infinity then begin
702675 (* Infinity is not a valid float - report as parse error with first char from ORIGINAL desc *)
···704677 let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
705678 let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in
706679 Message_collector.add_typed collector
707707- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad positive floating point number: Expected a digit but saw \xe2\x80\x9c%s\xe2\x80\x9d instead at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name first_char srcset_value))));
680680+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad positive floating point number: Expected a digit but saw %s instead at %s." (q srcset_value) (q "srcset") (q element_name) (q first_char) (q srcset_value)))));
708681 false
709682 end else true
710683 with _ ->
711684 Message_collector.add_typed collector
712712- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Invalid density descriptor." srcset_value element_name))));
685685+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad srcset descriptor: Invalid density descriptor." (q srcset_value) (q "srcset") (q element_name)))));
713686 false)
714687 end
715688 | 'h' ->
···729702 in
730703 if has_sizes then
731704 Message_collector.add_typed collector
732732- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" srcset_value element_name trimmed_desc context))))
705705+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected width descriptor but found %s at %s. (When the %s attribute is present, all image candidate strings must specify a width.)" (q srcset_value) (q "srcset") (q element_name) (q trimmed_desc) (q context) (q "sizes")))))
733706 else
734707 Message_collector.add_typed collector
735735- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad srcset descriptor: Height descriptor \xe2\x80\x9ch\xe2\x80\x9d is not allowed." srcset_value element_name))));
708708+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad srcset descriptor: Height descriptor %s is not allowed." (q srcset_value) (q "srcset") (q element_name) (q "h")))));
736709 false
737710 | _ ->
738711 (* Unknown descriptor - find context in srcset_value *)
···749722 with Not_found -> trimmed_desc ^ ")"
750723 else trimmed_desc
751724 in
752752- (* Try to find the context: show trailing portion ending with descriptor and comma *)
725725+ (* Find context: the entry containing the error with trailing comma *)
753726 let context =
754727 try
755728 let pos = Str.search_forward (Str.regexp_string trimmed_desc) srcset_value 0 in
756729 (* Get the context ending with the descriptor and the comma after *)
757730 let end_pos = min (pos + String.length trimmed_desc + 1) (String.length srcset_value) in
758758- (* Show trailing portion with ellipsis if needed *)
759759- let max_context = 15 in
760760- if end_pos > max_context then
761761- "\xe2\x80\xa6" ^ String.sub srcset_value (end_pos - max_context) max_context
762762- else
763763- String.trim (String.sub srcset_value 0 end_pos)
731731+ String.trim (String.sub srcset_value 0 end_pos)
764732 with Not_found -> srcset_value
765733 in
766734 Message_collector.add_typed collector
767767- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected number followed by \xe2\x80\x9cw\xe2\x80\x9d or \xe2\x80\x9cx\xe2\x80\x9d but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." srcset_value element_name found_desc context))));
735735+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected number followed by %s or %s but found %s at %s." (q srcset_value) (q "srcset") (q element_name) (q "w") (q "x") (q found_desc) (q context)))));
768736 false
769737 end
770738···800768 (* Check for empty srcset *)
801769 if String.trim value = "" then begin
802770 Message_collector.add_typed collector
803803- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Must contain one or more image candidate strings." value element_name))))
771771+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Must contain one or more image candidate strings." (q value) (q "srcset") (q element_name)))))
804772 end;
805773806774 (* Check for leading comma *)
807775 if String.length value > 0 && value.[0] = ',' then begin
808776 Message_collector.add_typed collector
809809- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Starts with empty image-candidate string." value element_name))))
777777+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Starts with empty image-candidate string." (q value) (q "srcset") (q element_name)))))
810778 end;
811779812780 (* Check for trailing comma(s) / empty entries *)
···823791 if trailing_commas > 1 then
824792 (* Multiple trailing commas: "Empty image-candidate string at" *)
825793 Message_collector.add_typed collector
826826- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Empty image-candidate string at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name value))))
794794+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Empty image-candidate string at %s." (q value) (q "srcset") (q element_name) (q value)))))
827795 else
828796 (* Single trailing comma: "Ends with empty image-candidate string." *)
829797 Message_collector.add_typed collector
830830- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Ends with empty image-candidate string." value element_name))))
798798+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Ends with empty image-candidate string." (q value) (q "srcset") (q element_name)))))
831799 end;
832800833801 List.iter (fun entry ->
···845813 let scheme_colon = scheme ^ ":" in
846814 if url_lower = scheme_colon then
847815 Message_collector.add_typed collector
848848- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Bad image-candidate URL: \xe2\x80\x9c%s\xe2\x80\x9d: Expected a slash (\"/\")." value element_name url))))
816816+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Bad image-candidate URL: %s: Expected a slash (\"/\")." (q value) (q "srcset") (q element_name) (q url)))))
849817 ) special_schemes
850818 in
851819 match parts with
···857825 begin match Hashtbl.find_opt seen_descriptors "explicit-1x" with
858826 | Some first_url ->
859827 Message_collector.add_typed collector
860860- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Density for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to density for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name url first_url))))
828828+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Density for image %s is identical to density for image %s." (q value) (q "srcset") (q element_name) (q url) (q first_url)))))
861829 | None ->
862830 Hashtbl.add seen_descriptors "implicit-1x" url
863831 end
···868836 if rest <> [] then begin
869837 let extra_desc = List.hd rest in
870838 Message_collector.add_typed collector
871871- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected single descriptor but found extraneous descriptor \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d." value element_name extra_desc value))))
839839+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected single descriptor but found extraneous descriptor %s at %s." (q value) (q "srcset") (q element_name) (q extra_desc) (q value)))))
872840 end;
873841874842 let desc_lower = String.lowercase_ascii (String.trim desc) in
···907875 value
908876 in
909877 Message_collector.add_typed collector
910910- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Expected width descriptor but found \xe2\x80\x9c%s\xe2\x80\x9d at \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" value element_name trimmed_desc entry_context))))
878878+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Expected width descriptor but found %s at %s. (When the %s attribute is present, all image candidate strings must specify a width.)" (q value) (q "srcset") (q element_name) (q trimmed_desc) (q entry_context) (q "sizes")))))
911879 end
912880 end;
913881···919887 begin match Hashtbl.find_opt seen_descriptors normalized with
920888 | Some first_url ->
921889 Message_collector.add_typed collector
922922- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: %s for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to %s for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name dup_type url (String.lowercase_ascii dup_type) first_url))))
890890+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: %s for image %s is identical to %s for image %s." (q value) (q "srcset") (q element_name) dup_type (q url) (String.lowercase_ascii dup_type) (q first_url)))))
923891 | None ->
924892 begin match (if is_1x then Hashtbl.find_opt seen_descriptors "implicit-1x" else None) with
925893 | Some first_url ->
926894 (* Explicit 1x conflicts with implicit 1x *)
927895 Message_collector.add_typed collector
928928- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: %s for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to %s for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name dup_type url (String.lowercase_ascii dup_type) first_url))))
896896+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: %s for image %s is identical to %s for image %s." (q value) (q "srcset") (q element_name) dup_type (q url) (String.lowercase_ascii dup_type) (q first_url)))))
929897 | None ->
930898 Hashtbl.add seen_descriptors normalized url;
931899 if is_1x then Hashtbl.add seen_descriptors "explicit-1x" url
···946914 (match !no_descriptor_url with
947915 | Some url when has_sizes ->
948916 Message_collector.add_typed collector
949949- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: No width specified for image \xe2\x80\x9c%s\xe2\x80\x9d. (When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width.)" value element_name url))))
917917+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: No width specified for image %s. (When the %s attribute is present, all image candidate strings must specify a width.)" (q value) (q "srcset") (q element_name) (q url) (q "sizes")))))
950918 | _ -> ());
951919952920 (* Check: if sizes is present and srcset uses x descriptors, that's an error.
953921 Only report if we haven't already reported the detailed error. *)
954922 if has_sizes && !has_x_descriptor && not !x_with_sizes_error_reported then
955923 Message_collector.add_typed collector
956956- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: When the \xe2\x80\x9csizes\xe2\x80\x9d attribute is present, all image candidate strings must specify a width." value element_name))));
924924+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: When the %s attribute is present, all image candidate strings must specify a width." (q value) (q "srcset") (q element_name) (q "sizes")))));
957925958926 (* Check for mixing w and x descriptors *)
959927 if !has_w_descriptor && !has_x_descriptor then
960928 Message_collector.add_typed collector
961961- (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Mixing width and density descriptors is not allowed." value element_name))))
929929+ (`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value %s for attribute %s on element %s: Mixing width and density descriptors is not allowed." (q value) (q "srcset") (q element_name)))))
962930963931let start_element _state ~element collector =
964932 match element.Element.tag with
+69
test/expected_message.ml
···4848 require_severity = true;
4949}
50505151+(** Unicode ellipsis character *)
5252+let ellipsis = "\xe2\x80\xa6"
5353+5154(** Normalize Unicode curly quotes to ASCII for comparison *)
5255let normalize_quotes s =
5356 let buf = Buffer.create (String.length s) in
···7073 end
7174 done;
7275 Buffer.contents buf
7676+7777+(** Unicode curly quotes *)
7878+let left_curly_quote = "\xe2\x80\x9c"
7979+let right_curly_quote = "\xe2\x80\x9d"
8080+8181+(** Check if expected message (with potential ellipsis truncation) matches actual.
8282+ When expected has ellipsis followed by text in curly quotes, we check if actual
8383+ has a value that ends with that text.
8484+ This handles Nu validator's message truncation for long attribute values. *)
8585+let truncation_aware_match expected actual =
8686+ (* Look for pattern: left_curly_quote + ellipsis in expected *)
8787+ let quote_ellipsis = left_curly_quote ^ ellipsis in
8888+ try
8989+ let pos = Str.search_forward (Str.regexp_string quote_ellipsis) expected 0 in
9090+ (* Found quote+ellipsis pattern - extract what comes after ellipsis until closing curly quote *)
9191+ let start_after_ellipsis = pos + String.length quote_ellipsis in
9292+ let end_quote_pos =
9393+ try Str.search_forward (Str.regexp_string right_curly_quote) expected start_after_ellipsis
9494+ with Not_found -> String.length expected
9595+ in
9696+ let truncated_suffix = String.sub expected start_after_ellipsis (end_quote_pos - start_after_ellipsis) in
9797+9898+ (* Build expected prefix (everything before the truncated quote) and suffix (everything after) *)
9999+ let prefix = String.sub expected 0 pos in
100100+ let suffix_start = end_quote_pos + String.length right_curly_quote in
101101+ let suffix =
102102+ if suffix_start < String.length expected then
103103+ String.sub expected suffix_start (String.length expected - suffix_start)
104104+ else ""
105105+ in
106106+107107+ (* Check if actual starts with prefix and ends with suffix *)
108108+ let actual_starts_with_prefix =
109109+ String.length actual >= String.length prefix &&
110110+ String.sub actual 0 (String.length prefix) = prefix
111111+ in
112112+ let actual_ends_with_suffix =
113113+ String.length actual >= String.length suffix &&
114114+ String.sub actual (String.length actual - String.length suffix) (String.length suffix) = suffix
115115+ in
116116+117117+ (* If prefix and suffix match, extract the middle (the quoted value in actual) *)
118118+ if actual_starts_with_prefix && actual_ends_with_suffix then begin
119119+ (* Find the quoted value in actual at the same position *)
120120+ let actual_quote_start = String.length prefix in
121121+ try
122122+ (* Check actual has left curly quote at expected position *)
123123+ if String.sub actual actual_quote_start (String.length left_curly_quote) = left_curly_quote then begin
124124+ let actual_value_start = actual_quote_start + String.length left_curly_quote in
125125+ let actual_value_end =
126126+ Str.search_forward (Str.regexp_string right_curly_quote) actual actual_value_start
127127+ in
128128+ let actual_value = String.sub actual actual_value_start (actual_value_end - actual_value_start) in
129129+ (* Check if actual value ends with the truncated suffix from expected *)
130130+ String.length actual_value >= String.length truncated_suffix &&
131131+ String.sub actual_value (String.length actual_value - String.length truncated_suffix) (String.length truncated_suffix) = truncated_suffix
132132+ end else false
133133+ with _ -> false
134134+ end else false
135135+ with Not_found ->
136136+ (* No ellipsis truncation pattern found *)
137137+ false
7313874139(** Pattern matchers for Nu validator messages.
75140 Each returns (error_code option, element option, attribute option) *)
···366431367432 (* Check message text *)
368433 let exact_text_match = actual_norm = expected_norm in
434434+ (* Truncation-aware match: expected may have ellipsis where actual has full value *)
435435+ let truncation_match = truncation_aware_match expected.message actual.Htmlrw_check.text in
369436 let substring_match =
370437 try let _ = Str.search_forward (Str.regexp_string expected_norm) actual_norm 0 in true
371438 with Not_found -> false
···380447 Code_match
381448 else if exact_text_match then
382449 Message_match
450450+ else if truncation_match then
451451+ Message_match (* Treat truncation match same as message match *)
383452 else if substring_match && not strictness.require_exact_message then
384453 Substring_match
385454 else