33 * entry bodies (tables, fenced code, blockquotes, numbered lists, ### headings),
44 * so they get their own renderer; renderBlocks stays strict for entries.
55 * Inline markup and links go through renderInline and the caller's resolver.
6- * Images and raw HTML still fail the build, as on entry pages. Pure.
6+ * Images and raw HTML still fail the build, as on entry pages, and so do the
7+ * constructs it would otherwise get wrong: setext headings and code fences
8+ * indented under a list item. Pure.
79 */
810import { esc } from "./shared.mjs" ;
9- import { MarkdownError , renderInline } from "./markdown.mjs" ;
11+ import { MarkdownError , renderInline , RESERVED_IDS } from "./markdown.mjs" ;
1012
1113/** GitHub's heading anchor, so links written for GitHub (licenses.md#cc-by) land here too. */
1214export function headingSlug ( text ) {
@@ -41,6 +43,12 @@ function cells(line) {
4143 return out ;
4244}
4345
46+ /** A thematic break: three or more of the same -, * or _ (spaces allowed between). */
47+ const isBreak = ( line ) => / ^ \s { 0 , 3 } ( [ - * _ ] ) ( \s * \1) { 2 , } \s * $ / . test ( line ) ;
48+ /** A setext underline (=== or ---) under a paragraph line. */
49+ const isSetext = ( line ) => / ^ \s { 0 , 3 } ( = + | - + ) \s * $ / . test ( line ) ;
50+ const FENCE_RE = / ^ \s * ( ` { 3 , } | ~ { 3 , } ) / ;
51+
4452const isSeparator = ( line ) => / ^ \s * \| ? \s * : ? - { 3 , } : ? \s * ( \| \s * : ? - { 3 , } : ? \s * ) * \| ? \s * $ / . test ( line ) ;
4553
4654/**
@@ -59,7 +67,7 @@ export function renderGuide(md, { file, resolveLink }) {
5967 const fail = ( n , what ) => {
6068 throw new MarkdownError ( `${ file } :${ n + 1 } : ${ what } is not supported on guide pages` ) ;
6169 } ;
62- const isBlockStart = ( l ) => / ^ \s * ( ` ` ` | > | \| | # { 1 , 6 } \s | [ - * ] \s | \d + [ . ) ] \s ) / . test ( l ) ;
70+ const isBlockStart = ( l ) => / ^ \s * ( ` ` ` | ~ ~ ~ | > | \| | # { 1 , 6 } \s | [ - * ] \s | \d + [ . ) ] \s ) / . test ( l ) || isBreak ( l ) ;
6371
6472 while ( i < ls . length ) {
6573 const line = ls [ i ] ;
@@ -70,18 +78,29 @@ export function renderGuide(md, { file, resolveLink }) {
7078 if ( / ^ \s * ! \[ / . test ( line ) ) fail ( i , "an image" ) ;
7179 if ( / ^ \s * < (? ! h t t p s ? : \/ \/ ) [ A - Z a - z ! / ] / . test ( line ) ) fail ( i , "raw HTML" ) ;
7280
73- // Fenced code: literal, escaped.
74- if ( / ^ \s * ` ` ` / . test ( line ) ) {
75- const lang = line . trim ( ) . slice ( 3 ) . trim ( ) ;
81+ // Fenced code (``` or ~~~): literal, escaped. It closes on a fence of
82+ // the same character at least as long as the opening one.
83+ const fence = line . match ( FENCE_RE ) ;
84+ if ( fence ) {
85+ if ( / ^ \s + / . test ( line ) ) fail ( i , "an indented code fence (inside a list item)" ) ;
86+ const mark = fence [ 1 ] ;
87+ const lang = line . trim ( ) . slice ( mark . length ) . trim ( ) ;
88+ const closes = new RegExp ( `^\\s*\\${ mark [ 0 ] } {${ mark . length } ,}\\s*$` ) ;
7689 const body = [ ] ;
7790 i += 1 ;
78- while ( i < ls . length && ! / ^ \s * ` ` ` / . test ( ls [ i ] ) ) body . push ( ls [ i ++ ] ) ;
91+ while ( i < ls . length && ! closes . test ( ls [ i ] ) ) body . push ( ls [ i ++ ] ) ;
7992 if ( i >= ls . length ) fail ( i - 1 , "an unclosed code fence" ) ;
8093 i += 1 ;
8194 html . push ( `<pre class="guide-code"${ lang ? ` data-lang="${ esc ( lang ) } "` : "" } ><code>${ esc ( body . join ( "\n" ) ) } </code></pre>` ) ;
8295 continue ;
8396 }
8497
98+ if ( isBreak ( line ) ) {
99+ html . push ( "<hr />" ) ;
100+ i += 1 ;
101+ continue ;
102+ }
103+
85104 const h = line . match ( / ^ ( # { 1 , 6 } ) \s + ( .* ?) \s * # * \s * $ / ) ;
86105 if ( h ) {
87106 const level = h [ 1 ] . length ;
@@ -96,9 +115,14 @@ export function renderGuide(md, { file, resolveLink }) {
96115 continue ;
97116 }
98117 const base = headingSlug ( h [ 2 ] ) ;
99- const n = seen . get ( base ) || 0 ;
118+ let n = seen . get ( base ) || 0 ;
119+ let id = n ? `${ base } -${ n } ` : base ;
120+ // The page template owns some ids (<main id="content">): take the next suffix.
121+ while ( RESERVED_IDS . has ( id ) ) {
122+ n += 1 ;
123+ id = `${ base } -${ n } ` ;
124+ }
100125 seen . set ( base , n + 1 ) ;
101- const id = n ? `${ base } -${ n } ` : base ;
102126 const tag = `h${ Math . min ( level , 4 ) } ` ;
103127 html . push ( `<${ tag } id="${ esc ( id ) } ">${ inline ( h [ 2 ] ) } </${ tag } >` ) ;
104128 i += 1 ;
@@ -131,6 +155,7 @@ export function renderGuide(md, { file, resolveLink }) {
131155 const li = line . match ( listRe ) ;
132156 if ( li ) {
133157 const ordered = / \d / . test ( li [ 1 ] ) ;
158+ const start = ordered ? parseInt ( li [ 1 ] , 10 ) : 1 ;
134159 const items = [ ] ;
135160 while ( i < ls . length ) {
136161 const m = ls [ i ] . match ( listRe ) ;
@@ -140,15 +165,34 @@ export function renderGuide(md, { file, resolveLink }) {
140165 } else if ( items . length && ls [ i ] . trim ( ) && / ^ \s + / . test ( ls [ i ] ) && ! isBlockStart ( ls [ i ] ) ) {
141166 items [ items . length - 1 ] += ` ${ ls [ i ] . trim ( ) } ` ;
142167 i += 1 ;
168+ } else if ( items . length && FENCE_RE . test ( ls [ i ] ) && / ^ \s + / . test ( ls [ i ] ) ) {
169+ fail ( i , "an indented code fence (inside a list item)" ) ;
170+ } else if ( items . length && ! ls [ i ] . trim ( ) ) {
171+ // Blank lines between items of the same list keep one list
172+ // (a "loose" list), so 1. a / blank / 2. b stays numbered 1, 2.
173+ let j = i ;
174+ while ( j < ls . length && ! ls [ j ] . trim ( ) ) j += 1 ;
175+ const next = j < ls . length ? ls [ j ] . match ( listRe ) : null ;
176+ if ( next && / \d / . test ( next [ 1 ] ) === ordered && ! / ^ \s + / . test ( ls [ j ] ) ) i = j ;
177+ else if ( j < ls . length && FENCE_RE . test ( ls [ j ] ) && / ^ \s + / . test ( ls [ j ] ) ) fail ( j , "an indented code fence (inside a list item)" ) ;
178+ else break ;
143179 } else break ;
144180 }
145181 const tag = ordered ? "ol" : "ul" ;
146- html . push ( `<${ tag } >${ items . map ( ( t ) => `<li>${ inline ( t ) } </li>` ) . join ( "" ) } </${ tag } >` ) ;
182+ const startAttr = ordered && start !== 1 ? ` start="${ start } "` : "" ;
183+ html . push ( `<${ tag } ${ startAttr } >${ items . map ( ( t ) => `<li>${ inline ( t ) } </li>` ) . join ( "" ) } </${ tag } >` ) ;
147184 continue ;
148185 }
149186
150187 const para = [ ] ;
151- while ( i < ls . length && ls [ i ] . trim ( ) && ! isBlockStart ( ls [ i ] ) ) para . push ( ls [ i ++ ] . trim ( ) ) ;
188+ while ( i < ls . length && ls [ i ] . trim ( ) && ! isBlockStart ( ls [ i ] ) ) {
189+ // A === or --- under paragraph text is a setext heading on GitHub.
190+ if ( para . length && isSetext ( ls [ i ] ) ) fail ( i , "a setext heading underline (use ## instead)" ) ;
191+ para . push ( ls [ i ++ ] . trim ( ) ) ;
192+ }
193+ if ( para . length && i < ls . length && isBreak ( ls [ i ] ) && / ^ \s { 0 , 3 } - / . test ( ls [ i ] ) ) {
194+ fail ( i , "a setext heading underline (use ## instead)" ) ;
195+ }
152196 if ( ! para . length ) {
153197 // A line that looks like a block start but matched nothing above.
154198 para . push ( ls [ i ++ ] . trim ( ) ) ;
0 commit comments