@@ -132,16 +132,16 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &
132132 match event {
133133 Event :: Start ( Tag :: CodeBlock ( _) ) => in_code_block = true ,
134134 Event :: Html ( text) | Event :: InlineHtml ( text) if !in_code_block => {
135- extract_tags ( & mut tags, & text, range, & mut is_in_comment, & report_diag)
135+ extract_tags ( & mut tags, & text, range, dox , & mut is_in_comment, & report_diag)
136136 }
137137 Event :: End ( TagEnd :: CodeBlock ) => in_code_block = false ,
138138 _ => { }
139139 }
140140 }
141141
142- for ( tag, range) in tags. iter ( ) . filter ( |( t, _ ) | {
142+ for ( tag, range) in tags. iter ( ) . filter ( |( t, range ) | {
143143 let t = t. to_lowercase ( ) ;
144- !ALLOWED_UNCLOSED . contains ( & t. as_str ( ) )
144+ !is_implicitly_self_closing ( & t, range . clone ( ) , dox )
145145 } ) {
146146 report_diag ( format ! ( "unclosed HTML tag `{tag}`" ) , range, true ) ;
147147 }
@@ -151,15 +151,24 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &
151151 }
152152}
153153
154+ /// These tags are interpreted as self-closing if they lack an explict closing tag.
154155const ALLOWED_UNCLOSED : & [ & str ] = & [
155156 "area" , "base" , "br" , "col" , "embed" , "hr" , "img" , "input" , "keygen" , "link" , "meta" , "param" ,
156157 "source" , "track" , "wbr" ,
157158] ;
158159
160+ /// Allows constructs like `<img>`, but not `<img`.
161+ fn is_implicitly_self_closing ( tag_name : & str , range : Range < usize > , dox : & str ) -> bool {
162+ ALLOWED_UNCLOSED . contains ( & tag_name) &&
163+ // search in reverse order to try to exit early, since `>` appears near the end in the happy case
164+ dox[ range] . bytes ( ) . rev ( ) . any ( |c| c == b'>' )
165+ }
166+
159167fn drop_tag (
160168 tags : & mut Vec < ( String , Range < usize > ) > ,
161169 tag_name : String ,
162170 range : Range < usize > ,
171+ dox : & str ,
163172 f : & impl Fn ( String , & Range < usize > , bool ) ,
164173) {
165174 let tag_name_low = tag_name. to_lowercase ( ) ;
@@ -175,7 +184,7 @@ fn drop_tag(
175184 continue ;
176185 }
177186 let last_tag_name_low = last_tag_name. to_lowercase ( ) ;
178- if ALLOWED_UNCLOSED . contains ( & last_tag_name_low. as_str ( ) ) {
187+ if is_implicitly_self_closing ( & last_tag_name_low, last_tag_span . clone ( ) , dox ) {
179188 continue ;
180189 }
181190 // `tags` is used as a queue, meaning that everything after `pos` is included inside it.
@@ -256,6 +265,7 @@ fn extract_html_tag(
256265 tags : & mut Vec < ( String , Range < usize > ) > ,
257266 text : & str ,
258267 range : & Range < usize > ,
268+ dox : & str ,
259269 start_pos : usize ,
260270 iter : & mut Peekable < CharIndices < ' _ > > ,
261271 f : & impl Fn ( String , & Range < usize > , bool ) ,
@@ -306,7 +316,7 @@ fn extract_html_tag(
306316 break ;
307317 }
308318 }
309- drop_tag ( tags, tag_name, r, f) ;
319+ drop_tag ( tags, tag_name, r, dox , f) ;
310320 } else {
311321 let mut is_self_closing = false ;
312322 let mut quote_pos = None ;
@@ -374,6 +384,7 @@ fn extract_tags(
374384 tags : & mut Vec < ( String , Range < usize > ) > ,
375385 text : & str ,
376386 range : Range < usize > ,
387+ dox : & str ,
377388 is_in_comment : & mut Option < Range < usize > > ,
378389 f : & impl Fn ( String , & Range < usize > , bool ) ,
379390) {
@@ -395,7 +406,7 @@ fn extract_tags(
395406 end : range. start + start_pos + 3 ,
396407 } ) ;
397408 } else {
398- extract_html_tag ( tags, text, & range, start_pos, & mut iter, f) ;
409+ extract_html_tag ( tags, text, & range, dox , start_pos, & mut iter, f) ;
399410 }
400411 }
401412 }
0 commit comments