@@ -4,16 +4,16 @@ import { addError, addErrorDetailIf } from "../helpers/helpers.cjs";
44import { getHeadingLevel , getHeadingText } from "../helpers/micromark-helpers.cjs" ;
55import { filterByTypesCached } from "./cache.mjs" ;
66
7- export const headingsItemsRe = / ^ ( [ * + ? ] | \^ + | [ # % ] { 1 , 6 } \s + \S .* ) $ / u;
8- const headingPartsRe = / ^ ( [ # % ] { 1 , 6 } ) \s + ( \S .* ) $ / u;
7+ export const headingsItemsRe = / ^ (?: [ * + ? ] | \^ + | [ # % ] { 1 , 6 } \s + \S .* ) $ / u;
8+ const prefixedHeadingPartsRe = / ^ ( [ # % ] { 1 , 6 } ) \s + ( \S .* ) $ / u;
99const loopHeadingRe = / ^ \^ + $ / u;
10- const validExtraHeadingsRe = / ^ (?: \* | \^ + ) $ / u;
10+ const canMatchNothingHeadingsRe = / ^ (?: \* | \^ + ) $ / u;
1111
1212const headingIsExpected = ( /** @type {string } */ actual , /** @type {string } */ expected , /** @type {boolean } */ matchCase ) => {
1313 if ( expected . startsWith ( "%" ) ) {
1414 // Regular expression comparison
15- const [ , actualLevel , actualText ] = headingPartsRe . exec ( actual ) || [ ] ;
16- const [ , expectedLevel , expectedText ] = headingPartsRe . exec ( expected ) || [ ] ;
15+ const [ , actualLevel , actualText ] = prefixedHeadingPartsRe . exec ( actual ) || [ ] ;
16+ const [ , expectedLevel , expectedText ] = prefixedHeadingPartsRe . exec ( expected ) || [ ] ;
1717 return (
1818 ( actualLevel . length === expectedLevel . length ) &&
1919 // eslint-disable-next-line unicorn/no-unreadable-new-expression
@@ -36,14 +36,13 @@ export default {
3636 /** @type {string[] | null | undefined } */
3737 const headings = params . config . headings ;
3838 if ( ! headings ) {
39- // Nothing to check
4039 return ;
4140 }
4241 const requiredHeadings = headings . map ( ( heading ) => headingsItemsRe . test ( heading ) ? heading : "[Invalid syntax]" ) ;
43- const matchCase = params . config . match_case || false ;
42+ const matchCase = Boolean ( params . config . match_case ) ;
4443 let i = 0 ;
45- let loopsRemaining = 1000 ;
46- let matchAny = false ;
44+ let loopsAllowed = 1000 ;
45+ let allowAnyMatchFallback = false ;
4746 let hasError = false ;
4847 const getExpected = ( /** @type {boolean } */ skipLoopHeadings ) => {
4948 let expected = null ;
@@ -54,72 +53,70 @@ export default {
5453 return expected ;
5554 } ;
5655 for ( const heading of filterByTypesCached ( [ "atxHeading" , "setextHeading" ] ) ) {
57- if ( ! hasError ) {
58- const headingText = getHeadingText ( heading ) ;
59- const headingLevel = getHeadingLevel ( heading ) ;
60- const actual = `${ "" . padEnd ( headingLevel , "#" ) } ${ headingText } ` ;
61- let expected = getExpected ( false ) ;
62-
63- if ( expected . startsWith ( "^" ) ) {
64- const save = i ;
65- const nextExpected = getExpected ( true ) ;
66- if ( headingIsExpected ( actual , nextExpected , matchCase ) ) {
67- expected = nextExpected ;
68- } else {
69- for ( let j = save - 1 ; j -- ; j >= 0 ) {
70- if ( requiredHeadings [ j ] === expected ) {
71- i = j + 1 ;
72- expected = getExpected ( false ) ;
73- loopsRemaining -- ;
74- break ;
75- }
76- }
77- if ( i < 0 ) {
78- // Nothing to skip back to; continue from current location
79- i = save ;
56+ const headingText = getHeadingText ( heading ) ;
57+ const headingLevel = getHeadingLevel ( heading ) ;
58+ const actual = `${ "" . padEnd ( headingLevel , "#" ) } ${ headingText } ` ;
59+ let expected = getExpected ( false ) ;
60+ if ( expected . startsWith ( "^" ) ) {
61+ // Handle loop
62+ const current = i ;
63+ const nextExpected = getExpected ( true ) ;
64+ if ( headingIsExpected ( actual , nextExpected , matchCase ) ) {
65+ // No need to loop (again)
66+ expected = nextExpected ;
67+ } else {
68+ // Loop to previous matching marker
69+ let j = current - 1 ;
70+ for ( ; j -- ; j >= 0 ) {
71+ if ( requiredHeadings [ j ] === expected ) {
72+ i = j + 1 ;
73+ expected = getExpected ( false ) ;
74+ loopsAllowed -- ;
75+ break ;
8076 }
8177 }
82- }
83-
84- if ( loopsRemaining < 0 ) {
85- addError ( onError , heading . startLine , "Too many loops" ) ;
86- hasError = true ;
87- } else if ( expected === "*" ) {
88- const nextExpected = getExpected ( false ) ;
89- if ( ! headingIsExpected ( actual , nextExpected , matchCase ) ) {
90- matchAny = true ;
91- i -- ;
78+ if ( j < 0 ) {
79+ expected = nextExpected ;
9280 }
93- } else if ( expected === "+" ) {
94- matchAny = true ;
95- } else if ( expected === "?" ) {
96- // Allow current, match next
97- } else if ( headingIsExpected ( actual , expected , matchCase ) ) {
98- matchAny = false ;
99- } else if ( matchAny ) {
81+ }
82+ }
83+ if ( loopsAllowed < 0 ) {
84+ // Too many loops
85+ addError ( onError , heading . startLine , "Too many loops" ) ;
86+ hasError = true ;
87+ break ;
88+ } else if ( expected === "*" ) {
89+ // Handle "zero or more of anything"
90+ const nextExpected = getExpected ( true ) ;
91+ if ( ! headingIsExpected ( actual , nextExpected , matchCase ) ) {
92+ allowAnyMatchFallback = true ;
10093 i -- ;
101- } else {
102- addErrorDetailIf (
103- onError ,
104- heading . startLine ,
105- expected ,
106- actual
107- ) ;
108- hasError = true ;
10994 }
95+ } else if ( expected === "+" ) {
96+ // Handle "one or more of anything"
97+ allowAnyMatchFallback = true ;
98+ } else if ( expected === "?" ) {
99+ // Handle "exactly one of anything"
100+ } else if ( headingIsExpected ( actual , expected , matchCase ) ) {
101+ // Handle successful match
102+ allowAnyMatchFallback = false ;
103+ } else if ( allowAnyMatchFallback ) {
104+ // Handle "anything" match by restoring index
105+ i -- ;
106+ } else {
107+ // Report heading mismatch
108+ addErrorDetailIf ( onError , heading . startLine , expected , actual ) ;
109+ hasError = true ;
110+ break ;
110111 }
111112 }
112- const extraHeadings = requiredHeadings . slice ( i ) ;
113+ const remainingRequiredHeadings = requiredHeadings . slice ( i ) . filter ( ( heading ) => ! canMatchNothingHeadingsRe . test ( heading ) ) ;
113114 if (
114115 ! hasError &&
115- ! extraHeadings . every ( ( heading ) => validExtraHeadingsRe . test ( heading ) )
116+ ( remainingRequiredHeadings . length > 0 )
116117 ) {
117- addErrorDetailIf (
118- onError ,
119- params . lines . length ,
120- requiredHeadings [ i ] ,
121- "[End of file]"
122- ) ;
118+ // Report unmatched heading(s)
119+ addErrorDetailIf ( onError , params . lines . length , remainingRequiredHeadings [ 0 ] , "[End of file]" ) ;
123120 }
124121 }
125122} ;
0 commit comments