···2233type state = {
44 mutable in_head : bool;
55+ mutable head_had_children : bool; (* true if head contained any child elements *)
56 mutable has_title : bool;
67 mutable in_title : bool;
78 mutable title_has_content : bool;
···10111112let create () = {
1213 in_head = false;
1414+ head_had_children = false;
1315 has_title = false;
1416 in_title = false;
1517 title_has_content = false;
···18201921let reset state =
2022 state.in_head <- false;
2323+ state.head_had_children <- false;
2124 state.has_title <- false;
2225 state.in_title <- false;
2326 state.title_has_content <- false;
···2730 (match element.Element.tag with
2831 | Tag.Html `Html -> ()
2932 | Tag.Html `Head ->
3030- state.in_head <- true
3333+ state.in_head <- true;
3434+ state.head_had_children <- false
3135 | Tag.Html `Title when state.in_head ->
3636+ state.head_had_children <- true;
3237 state.has_title <- true;
3338 state.in_title <- true;
3439 state.title_has_content <- false;
3540 state.title_depth <- 0
4141+ | _ when state.in_head ->
4242+ (* Any element inside head means head had children *)
4343+ state.head_had_children <- true
3644 | _ -> ());
3745 if state.in_title then
3846 state.title_depth <- state.title_depth + 1
···4755 (`Element (`Must_not_be_empty (`Elem "title")));
4856 state.in_title <- false
4957 | Tag.Html `Head ->
5050- if state.in_head && not state.has_title then
5858+ (* Only report missing title if head had children (was explicit with content).
5959+ An empty head was likely implicit (fragment validation from body). *)
6060+ if state.in_head && not state.has_title && state.head_had_children then
5161 Message_collector.add_typed collector
5262 (`Element (`Missing_child (`Parent "head", `Child "title")));
5363 state.in_head <- false
+16-4
lib/js/htmlrw_js_dom.ml
···81818282 (* Build the location map by matching elements *)
8383 let loc_to_el =
8484+ (* Find the starting point in parsed elements that matches the root tag *)
8585+ let root_tag = String.lowercase_ascii (Jstr.to_string (El.tag_name root)) in
8686+ let rec find_start = function
8787+ | [] -> []
8888+ | h_el :: rest ->
8989+ if String.lowercase_ascii h_el.Html5rw.Dom.name = root_tag then
9090+ h_el :: rest
9191+ else
9292+ find_start rest
9393+ in
9494+ let html5rw_elements_aligned = find_start html5rw_elements in
9595+8496 let rec match_elements loc_map browser_els html5rw_els =
8597 match browser_els, html5rw_els with
8698 | [], _ | _, [] -> loc_map
···96108 in
97109 match_elements loc_map b_rest h_rest
98110 else
9999- (* Tags don't match - try to resync by skipping one side *)
100100- (* This handles cases where browser might have implicit elements *)
101101- match_elements loc_map b_rest html5rw_els
111111+ (* Tags don't match - try skipping the parsed element first *)
112112+ (* This handles cases where parser creates implicit elements *)
113113+ match_elements loc_map browser_els h_rest
102114 in
103103- match_elements LocMap.empty browser_elements html5rw_elements
115115+ match_elements LocMap.empty browser_elements html5rw_elements_aligned
104116 in
105117106118 { root; html_source = html; loc_to_el }, html