@@ -279,36 +279,40 @@ fn extract_normalized_headings(readme_lower: &str) -> Vec<String> {
279279 let mut headings = Vec :: new ( ) ;
280280 let mut fence: Option < FenceSpec > = None ;
281281 let mut front_matter_possible = true ;
282- let mut in_front_matter = false ;
282+ let mut front_matter_end_index : Option < usize > = None ;
283283 let lines: Vec < & str > = readme_lower. lines ( ) . collect ( ) ;
284284 let mut index = 0usize ;
285285
286286 while index < lines. len ( ) {
287287 let trimmed = lines[ index] . trim_start ( ) ;
288288 let trimmed_both = lines[ index] . trim ( ) ;
289289
290+ if let Some ( end_index) = front_matter_end_index
291+ && index <= end_index
292+ {
293+ if index == end_index {
294+ front_matter_end_index = None ;
295+ }
296+ index += 1 ;
297+ continue ;
298+ }
299+
290300 if front_matter_possible && trimmed_both. is_empty ( ) {
291301 index += 1 ;
292302 continue ;
293303 }
294304
295305 if front_matter_possible {
296306 front_matter_possible = false ;
297- if trimmed_both == "---" || trimmed_both == "+++" {
298- in_front_matter = true ;
307+ if ( trimmed_both == "---" || trimmed_both == "+++" )
308+ && let Some ( end_index) = find_front_matter_end ( & lines, index, trimmed_both)
309+ {
310+ front_matter_end_index = Some ( end_index) ;
299311 index += 1 ;
300312 continue ;
301313 }
302314 }
303315
304- if in_front_matter {
305- if trimmed_both == "---" || trimmed_both == "+++" || trimmed_both == "..." {
306- in_front_matter = false ;
307- }
308- index += 1 ;
309- continue ;
310- }
311-
312316 if let Some ( current_fence) = fence {
313317 if is_closing_fence ( trimmed, current_fence) {
314318 fence = None ;
@@ -355,6 +359,25 @@ fn extract_normalized_headings(readme_lower: &str) -> Vec<String> {
355359 headings
356360}
357361
362+ fn find_front_matter_end ( lines : & [ & str ] , opening_index : usize , delimiter : & str ) -> Option < usize > {
363+ let mut has_metadata_like_line = false ;
364+
365+ for ( index, line) in lines. iter ( ) . enumerate ( ) . skip ( opening_index + 1 ) {
366+ let trimmed = line. trim ( ) ;
367+
368+ let is_closing_delimiter = trimmed == delimiter || ( delimiter == "---" && trimmed == "..." ) ;
369+ if is_closing_delimiter {
370+ return has_metadata_like_line. then_some ( index) ;
371+ }
372+
373+ if !trimmed. is_empty ( ) && trimmed. contains ( ':' ) {
374+ has_metadata_like_line = true ;
375+ }
376+ }
377+
378+ None
379+ }
380+
358381fn parse_opening_fence ( trimmed_line : & str ) -> Option < FenceSpec > {
359382 let mut chars = trimmed_line. chars ( ) ;
360383 let marker = chars. next ( ) ?;
@@ -626,4 +649,18 @@ features: false
626649 let audit = audit_repo ( & example_repo ( ) , Some ( readme) , 70 , false ) ;
627650 assert ! ( audit. missing_required. contains( & "Features" ) ) ;
628651 }
652+
653+ #[ test]
654+ fn does_not_treat_unclosed_horizontal_rule_as_front_matter ( ) {
655+ let readme = "
656+ ---
657+ ## Features
658+ ## Quick Start
659+ ## Architecture
660+ ## License
661+ " ;
662+
663+ let audit = audit_repo ( & example_repo ( ) , Some ( readme) , 70 , false ) ;
664+ assert ! ( audit. missing_required. is_empty( ) ) ;
665+ }
629666}
0 commit comments