+54
-86
lib/htmlrw_check/specialized/srcset_sizes_checker.ml
+54
-86
lib/htmlrw_check/specialized/srcset_sizes_checker.ml
···
1
(** Srcset and sizes attribute validation checker. *)
2
3
(** Valid CSS length units for sizes attribute *)
4
let valid_length_units = [
5
"em"; "ex"; "ch"; "rem"; "cap"; "ic";
···
400
(* Empty sizes is invalid *)
401
if String.trim value = "" then begin
402
Message_collector.add_typed collector
403
-
(`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))));
404
false
405
end else begin
406
(* Split on comma and check each entry *)
···
410
(* Check if starts with comma (empty first entry) *)
411
if first_entry = "" then begin
412
Message_collector.add_typed collector
413
-
(`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))));
414
false
415
end else begin
416
(* Check for trailing comma *)
417
let last_entry = String.trim (List.nth entries (List.length entries - 1)) in
418
if List.length entries > 1 && last_entry = "" then begin
419
-
(* Generate abbreviated context - show last ~25 chars with ellipsis if needed *)
420
-
let context =
421
-
if String.length value > 25 then
422
-
"\xe2\x80\xa6" ^ String.sub value (String.length value - 25) 25
423
-
else value
424
-
in
425
Message_collector.add_typed collector
426
-
(`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))));
427
false
428
end else begin
429
let valid = ref true in
···
442
(* Context is the first entry with a comma *)
443
let context = (String.trim first) ^ "," in
444
Message_collector.add_typed collector
445
-
(`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))));
446
valid := false
447
end;
448
(* Check for multiple entries without media conditions.
···
454
(* Multiple defaults - report as "Expected media condition" *)
455
let context = (String.trim first) ^ "," in
456
Message_collector.add_typed collector
457
-
(`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))));
458
valid := false
459
end
460
end
···
468
(* Check for invalid media condition *)
469
(match has_invalid_media_condition trimmed with
470
| Some err_msg ->
471
-
(* Generate context: "entry," with ellipsis if needed *)
472
-
let context = (String.trim entry) ^ "," in
473
-
let context =
474
-
if String.length context > 25 then
475
-
"\xe2\x80\xa6" ^ String.sub context (String.length context - 25) 25
476
-
else context
477
-
in
478
Message_collector.add_typed collector
479
-
(`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))));
480
valid := false
481
| None -> ());
482
···
508
let prev_entries = List.filter (fun e -> String.trim e <> "" && e <> entry) entries in
509
let context =
510
if List.length prev_entries > 0 then
511
-
let prev_value = String.concat ", " (List.map String.trim prev_entries) ^ "," in
512
-
if String.length prev_value > 25 then
513
-
"\xe2\x80\xa6" ^ String.sub prev_value (String.length prev_value - 25) 25
514
-
else prev_value
515
else value
516
in
517
Message_collector.add_typed collector
518
-
(`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))));
519
valid := false
520
end
521
(* If there's extra junk after the size, report BadCssNumber error for it *)
522
else if extra_parts <> [] then begin
523
-
let junk = String.concat " " extra_parts in
524
let last_junk = List.nth extra_parts (List.length extra_parts - 1) in
525
let first_char = if String.length last_junk > 0 then last_junk.[0] else 'x' in
526
-
(* Context depends on whether this is the last entry:
527
-
- For non-last entries: entry with trailing comma, truncated from beginning
528
-
- For last entry: full value truncated from beginning (no trailing comma) *)
529
let is_last_entry = idx = num_entries - 1 in
530
let context =
531
-
if is_last_entry then begin
532
-
(* Last entry: use full value truncated *)
533
-
if String.length value > 25 then
534
-
"\xe2\x80\xa6" ^ String.sub value (String.length value - 25) 25
535
-
else value
536
-
end else begin
537
-
(* Non-last entry: use entry with comma, truncated *)
538
-
let entry_with_comma = trimmed ^ "," in
539
-
if String.length entry_with_comma > 25 then
540
-
"\xe2\x80\xa6" ^ String.sub entry_with_comma (String.length entry_with_comma - 25) 25
541
-
else entry_with_comma
542
-
end
543
in
544
-
let _ = junk in
545
Message_collector.add_typed collector
546
-
(`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))));
547
valid := false
548
end
549
else
···
556
in
557
let _ = full_context in
558
Message_collector.add_typed collector
559
-
(`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))));
560
valid := false
561
| CssCommentAfterSign (found, context) ->
562
(* e.g., +/**/50vw - expected number after sign *)
563
Message_collector.add_typed collector
564
-
(`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))));
565
valid := false
566
| CssCommentBeforeUnit (found, context) ->
567
(* e.g., 50/**/vw - expected units after number *)
568
-
let units_list = List.map (fun u -> Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" u) valid_length_units in
569
let units_str = String.concat ", " units_list in
570
Message_collector.add_typed collector
571
-
(`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))));
572
valid := false
573
| BadScientificNotation ->
574
(* For scientific notation with bad exponent, show what char was expected vs found *)
···
579
(* Find the period in the exponent *)
580
let _ = context in
581
Message_collector.add_typed collector
582
-
(`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))));
583
valid := false
584
| BadCssNumber (first_char, context) ->
585
(* Value doesn't start with a digit or minus sign *)
···
589
in
590
let _ = full_context in
591
Message_collector.add_typed collector
592
-
(`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))));
593
valid := false
594
| InvalidUnit (found_unit, _context) ->
595
(* Generate the full list of expected units *)
596
-
let units_list = List.map (fun u -> Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" u) valid_length_units in
597
let units_str = String.concat ", " units_list in
598
(* Context should be the full entry, with comma only if there are multiple entries *)
599
let full_context =
···
603
(* When found_unit is empty, say "no units" instead of quoting empty string *)
604
let found_str =
605
if found_unit = "" then "no units"
606
-
else Printf.sprintf "\xe2\x80\x9c%s\xe2\x80\x9d" found_unit
607
in
608
Message_collector.add_typed collector
609
-
(`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))));
610
valid := false
611
end
612
end
···
633
(* Show just the number part (without the 'w') *)
634
let num_part_for_msg = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
635
Message_collector.add_typed collector
636
-
(`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))));
637
false
638
end else
639
(try
640
let n = int_of_string num_part in
641
if n <= 0 then begin
642
Message_collector.add_typed collector
643
-
(`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))));
644
false
645
end else begin
646
(* Check for uppercase W - compare original desc with lowercase version *)
647
let original_last = desc.[String.length desc - 1] in
648
if original_last = 'W' then begin
649
Message_collector.add_typed collector
650
-
(`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))));
651
false
652
end else true
653
end
···
655
(* Check for scientific notation, decimal, or other non-integer values *)
656
if String.contains num_part 'e' || String.contains num_part 'E' || String.contains num_part '.' then begin
657
Message_collector.add_typed collector
658
-
(`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))));
659
false
660
end else begin
661
Message_collector.add_typed collector
662
-
(`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))));
663
false
664
end)
665
| 'x' ->
···
669
(* Extract the number part including the plus sign *)
670
let num_with_plus = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
671
Message_collector.add_typed collector
672
-
(`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))));
673
false
674
end else begin
675
(try
···
680
let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
681
let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in
682
Message_collector.add_typed collector
683
-
(`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))));
684
false
685
end else if n = 0.0 then begin
686
(* Check if it's -0 (starts with minus) - report as "greater than zero" error *)
···
688
let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
689
if String.length orig_num_part > 0 && orig_num_part.[0] = '-' then begin
690
Message_collector.add_typed collector
691
-
(`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))))
692
end else begin
693
Message_collector.add_typed collector
694
-
(`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))))
695
end;
696
false
697
end else if n < 0.0 then begin
698
Message_collector.add_typed collector
699
-
(`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))));
700
false
701
end else if n = neg_infinity || n = infinity then begin
702
(* Infinity is not a valid float - report as parse error with first char from ORIGINAL desc *)
···
704
let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
705
let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in
706
Message_collector.add_typed collector
707
-
(`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))));
708
false
709
end else true
710
with _ ->
711
Message_collector.add_typed collector
712
-
(`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))));
713
false)
714
end
715
| 'h' ->
···
729
in
730
if has_sizes then
731
Message_collector.add_typed collector
732
-
(`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))))
733
else
734
Message_collector.add_typed collector
735
-
(`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))));
736
false
737
| _ ->
738
(* Unknown descriptor - find context in srcset_value *)
···
749
with Not_found -> trimmed_desc ^ ")"
750
else trimmed_desc
751
in
752
-
(* Try to find the context: show trailing portion ending with descriptor and comma *)
753
let context =
754
try
755
let pos = Str.search_forward (Str.regexp_string trimmed_desc) srcset_value 0 in
756
(* Get the context ending with the descriptor and the comma after *)
757
let end_pos = min (pos + String.length trimmed_desc + 1) (String.length srcset_value) in
758
-
(* Show trailing portion with ellipsis if needed *)
759
-
let max_context = 15 in
760
-
if end_pos > max_context then
761
-
"\xe2\x80\xa6" ^ String.sub srcset_value (end_pos - max_context) max_context
762
-
else
763
-
String.trim (String.sub srcset_value 0 end_pos)
764
with Not_found -> srcset_value
765
in
766
Message_collector.add_typed collector
767
-
(`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))));
768
false
769
end
770
···
800
(* Check for empty srcset *)
801
if String.trim value = "" then begin
802
Message_collector.add_typed collector
803
-
(`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))))
804
end;
805
806
(* Check for leading comma *)
807
if String.length value > 0 && value.[0] = ',' then begin
808
Message_collector.add_typed collector
809
-
(`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: Starts with empty image-candidate string." value element_name))))
810
end;
811
812
(* Check for trailing comma(s) / empty entries *)
···
823
if trailing_commas > 1 then
824
(* Multiple trailing commas: "Empty image-candidate string at" *)
825
Message_collector.add_typed collector
826
-
(`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))))
827
else
828
(* Single trailing comma: "Ends with empty image-candidate string." *)
829
Message_collector.add_typed collector
830
-
(`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))))
831
end;
832
833
List.iter (fun entry ->
···
845
let scheme_colon = scheme ^ ":" in
846
if url_lower = scheme_colon then
847
Message_collector.add_typed collector
848
-
(`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))))
849
) special_schemes
850
in
851
match parts with
···
857
begin match Hashtbl.find_opt seen_descriptors "explicit-1x" with
858
| Some first_url ->
859
Message_collector.add_typed collector
860
-
(`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))))
861
| None ->
862
Hashtbl.add seen_descriptors "implicit-1x" url
863
end
···
868
if rest <> [] then begin
869
let extra_desc = List.hd rest in
870
Message_collector.add_typed collector
871
-
(`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))))
872
end;
873
874
let desc_lower = String.lowercase_ascii (String.trim desc) in
···
907
value
908
in
909
Message_collector.add_typed collector
910
-
(`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))))
911
end
912
end;
913
···
919
begin match Hashtbl.find_opt seen_descriptors normalized with
920
| Some first_url ->
921
Message_collector.add_typed collector
922
-
(`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))))
923
| None ->
924
begin match (if is_1x then Hashtbl.find_opt seen_descriptors "implicit-1x" else None) with
925
| Some first_url ->
926
(* Explicit 1x conflicts with implicit 1x *)
927
Message_collector.add_typed collector
928
-
(`Attr (`Bad_value_generic (`Message (Printf.sprintf "Bad value \xe2\x80\x9c%s\xe2\x80\x9d for attribute \xe2\x80\x9csrcset\xe2\x80\x9d on element \xe2\x80\x9c%s\xe2\x80\x9d: %s for image \xe2\x80\x9c%s\xe2\x80\x9d is identical to %s for image \xe2\x80\x9c%s\xe2\x80\x9d." value element_name dup_type url (String.lowercase_ascii dup_type) first_url))))
929
| None ->
930
Hashtbl.add seen_descriptors normalized url;
931
if is_1x then Hashtbl.add seen_descriptors "explicit-1x" url
···
946
(match !no_descriptor_url with
947
| Some url when has_sizes ->
948
Message_collector.add_typed collector
949
-
(`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))))
950
| _ -> ());
951
952
(* Check: if sizes is present and srcset uses x descriptors, that's an error.
953
Only report if we haven't already reported the detailed error. *)
954
if has_sizes && !has_x_descriptor && not !x_with_sizes_error_reported then
955
Message_collector.add_typed collector
956
-
(`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))));
957
958
(* Check for mixing w and x descriptors *)
959
if !has_w_descriptor && !has_x_descriptor then
960
Message_collector.add_typed collector
961
-
(`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))))
962
963
let start_element _state ~element collector =
964
match element.Element.tag with
···
1
(** Srcset and sizes attribute validation checker. *)
2
3
+
(** Quote helper for consistent message formatting. *)
4
+
let q = Error_code.q
5
+
6
(** Valid CSS length units for sizes attribute *)
7
let valid_length_units = [
8
"em"; "ex"; "ch"; "rem"; "cap"; "ic";
···
403
(* Empty sizes is invalid *)
404
if String.trim value = "" then begin
405
Message_collector.add_typed collector
406
+
(`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)))));
407
false
408
end else begin
409
(* Split on comma and check each entry *)
···
413
(* Check if starts with comma (empty first entry) *)
414
if first_entry = "" then begin
415
Message_collector.add_typed collector
416
+
(`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)))));
417
false
418
end else begin
419
(* Check for trailing comma *)
420
let last_entry = String.trim (List.nth entries (List.length entries - 1)) in
421
if List.length entries > 1 && last_entry = "" then begin
422
Message_collector.add_typed collector
423
+
(`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)))));
424
false
425
end else begin
426
let valid = ref true in
···
439
(* Context is the first entry with a comma *)
440
let context = (String.trim first) ^ "," in
441
Message_collector.add_typed collector
442
+
(`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)))));
443
valid := false
444
end;
445
(* Check for multiple entries without media conditions.
···
451
(* Multiple defaults - report as "Expected media condition" *)
452
let context = (String.trim first) ^ "," in
453
Message_collector.add_typed collector
454
+
(`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)))));
455
valid := false
456
end
457
end
···
465
(* Check for invalid media condition *)
466
(match has_invalid_media_condition trimmed with
467
| Some err_msg ->
468
+
let context = trimmed ^ "," in
469
Message_collector.add_typed collector
470
+
(`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)))));
471
valid := false
472
| None -> ());
473
···
499
let prev_entries = List.filter (fun e -> String.trim e <> "" && e <> entry) entries in
500
let context =
501
if List.length prev_entries > 0 then
502
+
String.concat ", " (List.map String.trim prev_entries) ^ ","
503
else value
504
in
505
Message_collector.add_typed collector
506
+
(`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)))));
507
valid := false
508
end
509
(* If there's extra junk after the size, report BadCssNumber error for it *)
510
else if extra_parts <> [] then begin
511
let last_junk = List.nth extra_parts (List.length extra_parts - 1) in
512
let first_char = if String.length last_junk > 0 then last_junk.[0] else 'x' in
513
let is_last_entry = idx = num_entries - 1 in
514
let context =
515
+
if is_last_entry then value
516
+
else trimmed ^ ","
517
in
518
Message_collector.add_typed collector
519
+
(`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)))));
520
valid := false
521
end
522
else
···
529
in
530
let _ = full_context in
531
Message_collector.add_typed collector
532
+
(`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)))));
533
valid := false
534
| CssCommentAfterSign (found, context) ->
535
(* e.g., +/**/50vw - expected number after sign *)
536
Message_collector.add_typed collector
537
+
(`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)))));
538
valid := false
539
| CssCommentBeforeUnit (found, context) ->
540
(* e.g., 50/**/vw - expected units after number *)
541
+
let units_list = List.map q valid_length_units in
542
let units_str = String.concat ", " units_list in
543
Message_collector.add_typed collector
544
+
(`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)))));
545
valid := false
546
| BadScientificNotation ->
547
(* For scientific notation with bad exponent, show what char was expected vs found *)
···
552
(* Find the period in the exponent *)
553
let _ = context in
554
Message_collector.add_typed collector
555
+
(`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)))));
556
valid := false
557
| BadCssNumber (first_char, context) ->
558
(* Value doesn't start with a digit or minus sign *)
···
562
in
563
let _ = full_context in
564
Message_collector.add_typed collector
565
+
(`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)))));
566
valid := false
567
| InvalidUnit (found_unit, _context) ->
568
(* Generate the full list of expected units *)
569
+
let units_list = List.map q valid_length_units in
570
let units_str = String.concat ", " units_list in
571
(* Context should be the full entry, with comma only if there are multiple entries *)
572
let full_context =
···
576
(* When found_unit is empty, say "no units" instead of quoting empty string *)
577
let found_str =
578
if found_unit = "" then "no units"
579
+
else q found_unit
580
in
581
Message_collector.add_typed collector
582
+
(`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)))));
583
valid := false
584
end
585
end
···
606
(* Show just the number part (without the 'w') *)
607
let num_part_for_msg = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
608
Message_collector.add_typed collector
609
+
(`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)))));
610
false
611
end else
612
(try
613
let n = int_of_string num_part in
614
if n <= 0 then begin
615
Message_collector.add_typed collector
616
+
(`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)))));
617
false
618
end else begin
619
(* Check for uppercase W - compare original desc with lowercase version *)
620
let original_last = desc.[String.length desc - 1] in
621
if original_last = 'W' then begin
622
Message_collector.add_typed collector
623
+
(`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")))));
624
false
625
end else true
626
end
···
628
(* Check for scientific notation, decimal, or other non-integer values *)
629
if String.contains num_part 'e' || String.contains num_part 'E' || String.contains num_part '.' then begin
630
Message_collector.add_typed collector
631
+
(`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)))));
632
false
633
end else begin
634
Message_collector.add_typed collector
635
+
(`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)))));
636
false
637
end)
638
| 'x' ->
···
642
(* Extract the number part including the plus sign *)
643
let num_with_plus = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
644
Message_collector.add_typed collector
645
+
(`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)))));
646
false
647
end else begin
648
(try
···
653
let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
654
let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in
655
Message_collector.add_typed collector
656
+
(`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)))));
657
false
658
end else if n = 0.0 then begin
659
(* Check if it's -0 (starts with minus) - report as "greater than zero" error *)
···
661
let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
662
if String.length orig_num_part > 0 && orig_num_part.[0] = '-' then begin
663
Message_collector.add_typed collector
664
+
(`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)))))
665
end else begin
666
Message_collector.add_typed collector
667
+
(`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)))))
668
end;
669
false
670
end else if n < 0.0 then begin
671
Message_collector.add_typed collector
672
+
(`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)))));
673
false
674
end else if n = neg_infinity || n = infinity then begin
675
(* Infinity is not a valid float - report as parse error with first char from ORIGINAL desc *)
···
677
let orig_num_part = String.sub trimmed_desc 0 (String.length trimmed_desc - 1) in
678
let first_char = if String.length orig_num_part > 0 then String.make 1 orig_num_part.[0] else "" in
679
Message_collector.add_typed collector
680
+
(`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)))));
681
false
682
end else true
683
with _ ->
684
Message_collector.add_typed collector
685
+
(`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)))));
686
false)
687
end
688
| 'h' ->
···
702
in
703
if has_sizes then
704
Message_collector.add_typed collector
705
+
(`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")))))
706
else
707
Message_collector.add_typed collector
708
+
(`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")))));
709
false
710
| _ ->
711
(* Unknown descriptor - find context in srcset_value *)
···
722
with Not_found -> trimmed_desc ^ ")"
723
else trimmed_desc
724
in
725
+
(* Find context: the entry containing the error with trailing comma *)
726
let context =
727
try
728
let pos = Str.search_forward (Str.regexp_string trimmed_desc) srcset_value 0 in
729
(* Get the context ending with the descriptor and the comma after *)
730
let end_pos = min (pos + String.length trimmed_desc + 1) (String.length srcset_value) in
731
+
String.trim (String.sub srcset_value 0 end_pos)
732
with Not_found -> srcset_value
733
in
734
Message_collector.add_typed collector
735
+
(`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)))));
736
false
737
end
738
···
768
(* Check for empty srcset *)
769
if String.trim value = "" then begin
770
Message_collector.add_typed collector
771
+
(`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)))))
772
end;
773
774
(* Check for leading comma *)
775
if String.length value > 0 && value.[0] = ',' then begin
776
Message_collector.add_typed collector
777
+
(`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)))))
778
end;
779
780
(* Check for trailing comma(s) / empty entries *)
···
791
if trailing_commas > 1 then
792
(* Multiple trailing commas: "Empty image-candidate string at" *)
793
Message_collector.add_typed collector
794
+
(`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)))))
795
else
796
(* Single trailing comma: "Ends with empty image-candidate string." *)
797
Message_collector.add_typed collector
798
+
(`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)))))
799
end;
800
801
List.iter (fun entry ->
···
813
let scheme_colon = scheme ^ ":" in
814
if url_lower = scheme_colon then
815
Message_collector.add_typed collector
816
+
(`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)))))
817
) special_schemes
818
in
819
match parts with
···
825
begin match Hashtbl.find_opt seen_descriptors "explicit-1x" with
826
| Some first_url ->
827
Message_collector.add_typed collector
828
+
(`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)))))
829
| None ->
830
Hashtbl.add seen_descriptors "implicit-1x" url
831
end
···
836
if rest <> [] then begin
837
let extra_desc = List.hd rest in
838
Message_collector.add_typed collector
839
+
(`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)))))
840
end;
841
842
let desc_lower = String.lowercase_ascii (String.trim desc) in
···
875
value
876
in
877
Message_collector.add_typed collector
878
+
(`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")))))
879
end
880
end;
881
···
887
begin match Hashtbl.find_opt seen_descriptors normalized with
888
| Some first_url ->
889
Message_collector.add_typed collector
890
+
(`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)))))
891
| None ->
892
begin match (if is_1x then Hashtbl.find_opt seen_descriptors "implicit-1x" else None) with
893
| Some first_url ->
894
(* Explicit 1x conflicts with implicit 1x *)
895
Message_collector.add_typed collector
896
+
(`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)))))
897
| None ->
898
Hashtbl.add seen_descriptors normalized url;
899
if is_1x then Hashtbl.add seen_descriptors "explicit-1x" url
···
914
(match !no_descriptor_url with
915
| Some url when has_sizes ->
916
Message_collector.add_typed collector
917
+
(`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")))))
918
| _ -> ());
919
920
(* Check: if sizes is present and srcset uses x descriptors, that's an error.
921
Only report if we haven't already reported the detailed error. *)
922
if has_sizes && !has_x_descriptor && not !x_with_sizes_error_reported then
923
Message_collector.add_typed collector
924
+
(`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")))));
925
926
(* Check for mixing w and x descriptors *)
927
if !has_w_descriptor && !has_x_descriptor then
928
Message_collector.add_typed collector
929
+
(`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)))))
930
931
let start_element _state ~element collector =
932
match element.Element.tag with
+69
test/expected_message.ml
+69
test/expected_message.ml
···
48
require_severity = true;
49
}
50
51
(** Normalize Unicode curly quotes to ASCII for comparison *)
52
let normalize_quotes s =
53
let buf = Buffer.create (String.length s) in
···
70
end
71
done;
72
Buffer.contents buf
73
74
(** Pattern matchers for Nu validator messages.
75
Each returns (error_code option, element option, attribute option) *)
···
366
367
(* Check message text *)
368
let exact_text_match = actual_norm = expected_norm in
369
let substring_match =
370
try let _ = Str.search_forward (Str.regexp_string expected_norm) actual_norm 0 in true
371
with Not_found -> false
···
380
Code_match
381
else if exact_text_match then
382
Message_match
383
else if substring_match && not strictness.require_exact_message then
384
Substring_match
385
else
···
48
require_severity = true;
49
}
50
51
+
(** Unicode ellipsis character *)
52
+
let ellipsis = "\xe2\x80\xa6"
53
+
54
(** Normalize Unicode curly quotes to ASCII for comparison *)
55
let normalize_quotes s =
56
let buf = Buffer.create (String.length s) in
···
73
end
74
done;
75
Buffer.contents buf
76
+
77
+
(** Unicode curly quotes *)
78
+
let left_curly_quote = "\xe2\x80\x9c"
79
+
let right_curly_quote = "\xe2\x80\x9d"
80
+
81
+
(** Check if expected message (with potential ellipsis truncation) matches actual.
82
+
When expected has ellipsis followed by text in curly quotes, we check if actual
83
+
has a value that ends with that text.
84
+
This handles Nu validator's message truncation for long attribute values. *)
85
+
let truncation_aware_match expected actual =
86
+
(* Look for pattern: left_curly_quote + ellipsis in expected *)
87
+
let quote_ellipsis = left_curly_quote ^ ellipsis in
88
+
try
89
+
let pos = Str.search_forward (Str.regexp_string quote_ellipsis) expected 0 in
90
+
(* Found quote+ellipsis pattern - extract what comes after ellipsis until closing curly quote *)
91
+
let start_after_ellipsis = pos + String.length quote_ellipsis in
92
+
let end_quote_pos =
93
+
try Str.search_forward (Str.regexp_string right_curly_quote) expected start_after_ellipsis
94
+
with Not_found -> String.length expected
95
+
in
96
+
let truncated_suffix = String.sub expected start_after_ellipsis (end_quote_pos - start_after_ellipsis) in
97
+
98
+
(* Build expected prefix (everything before the truncated quote) and suffix (everything after) *)
99
+
let prefix = String.sub expected 0 pos in
100
+
let suffix_start = end_quote_pos + String.length right_curly_quote in
101
+
let suffix =
102
+
if suffix_start < String.length expected then
103
+
String.sub expected suffix_start (String.length expected - suffix_start)
104
+
else ""
105
+
in
106
+
107
+
(* Check if actual starts with prefix and ends with suffix *)
108
+
let actual_starts_with_prefix =
109
+
String.length actual >= String.length prefix &&
110
+
String.sub actual 0 (String.length prefix) = prefix
111
+
in
112
+
let actual_ends_with_suffix =
113
+
String.length actual >= String.length suffix &&
114
+
String.sub actual (String.length actual - String.length suffix) (String.length suffix) = suffix
115
+
in
116
+
117
+
(* If prefix and suffix match, extract the middle (the quoted value in actual) *)
118
+
if actual_starts_with_prefix && actual_ends_with_suffix then begin
119
+
(* Find the quoted value in actual at the same position *)
120
+
let actual_quote_start = String.length prefix in
121
+
try
122
+
(* Check actual has left curly quote at expected position *)
123
+
if String.sub actual actual_quote_start (String.length left_curly_quote) = left_curly_quote then begin
124
+
let actual_value_start = actual_quote_start + String.length left_curly_quote in
125
+
let actual_value_end =
126
+
Str.search_forward (Str.regexp_string right_curly_quote) actual actual_value_start
127
+
in
128
+
let actual_value = String.sub actual actual_value_start (actual_value_end - actual_value_start) in
129
+
(* Check if actual value ends with the truncated suffix from expected *)
130
+
String.length actual_value >= String.length truncated_suffix &&
131
+
String.sub actual_value (String.length actual_value - String.length truncated_suffix) (String.length truncated_suffix) = truncated_suffix
132
+
end else false
133
+
with _ -> false
134
+
end else false
135
+
with Not_found ->
136
+
(* No ellipsis truncation pattern found *)
137
+
false
138
139
(** Pattern matchers for Nu validator messages.
140
Each returns (error_code option, element option, attribute option) *)
···
431
432
(* Check message text *)
433
let exact_text_match = actual_norm = expected_norm in
434
+
(* Truncation-aware match: expected may have ellipsis where actual has full value *)
435
+
let truncation_match = truncation_aware_match expected.message actual.Htmlrw_check.text in
436
let substring_match =
437
try let _ = Str.search_forward (Str.regexp_string expected_norm) actual_norm 0 in true
438
with Not_found -> false
···
447
Code_match
448
else if exact_text_match then
449
Message_match
450
+
else if truncation_match then
451
+
Message_match (* Treat truncation match same as message match *)
452
else if substring_match && not strictness.require_exact_message then
453
Substring_match
454
else