1- // A line the renderer could read as an ordered-list item, including one nested
2- // after quote or list markers. Group 1 is everything before the number, so its
3- // length tells nested lists apart. Indented code also matches; that only widens.
4- // Each repeat starts with a marker or `>`, which keeps matching linear.
5- const ORDERED_MARKER_LINE =
6- / ^ ( [ \t > ] * (?: (?: [ - + * ] | \d { 1 , 9 } [ . ) ] ) [ \t ] + (?: > [ \t > ] * ) ? ) * ) ( \d { 1 , 9 } ) [ . ) ] (?: [ \t \r ] | $ ) / ;
7- const CODE_FENCE = / ^ { 0 , 3 } ( ` { 3 , } | ~ { 3 , } ) / ;
1+ interface ListMarker {
2+ readonly column : number ;
3+ readonly contentColumn : number ;
4+ readonly number : number ;
5+ }
6+
7+ interface LinePrefix {
8+ readonly quoteDepth : number ;
9+ /** Whether any bullet or ordered-list marker opens the line. */
10+ readonly hasListMarker : boolean ;
11+ readonly orderedMarkers : ReadonlyArray < ListMarker > ;
12+ /** Column where the innermost container's content starts. */
13+ readonly contentColumn : number ;
14+ readonly restColumn : number ;
15+ readonly rest : string ;
16+ }
17+
18+ const isBlank = ( char : string | undefined ) => char === " " || char === "\t" ;
19+ const isDigit = ( char : string | undefined ) => char !== undefined && char >= "0" && char <= "9" ;
20+
21+ // Walks the quote, bullet, and ordered-list markers at the start of a line,
22+ // with tab stops of 4. Content starts 1-4 columns after a marker, as in CommonMark.
23+ function readLinePrefix ( line : string ) : LinePrefix {
24+ const orderedMarkers : ListMarker [ ] = [ ] ;
25+ let quoteDepth = 0 ;
26+ let hasListMarker = false ;
27+ let index = 0 ;
28+ let column = 0 ;
29+ let contentColumn = 0 ;
30+ // Set after a list marker until the following spaces fix its content column.
31+ let pending : { markerEnd : number ; number : number | null ; column : number } | null = null ;
32+ for ( ; ; ) {
33+ while ( isBlank ( line [ index ] ) ) {
34+ column = line [ index ] === "\t" ? column + 4 - ( column % 4 ) : column + 1 ;
35+ index += 1 ;
36+ }
37+ if ( pending ) {
38+ const gap = column - pending . markerEnd ;
39+ contentColumn = gap >= 1 && gap <= 4 && index < line . length ? column : pending . markerEnd + 1 ;
40+ if ( pending . number !== null ) {
41+ orderedMarkers . push ( { column : pending . column , contentColumn, number : pending . number } ) ;
42+ }
43+ pending = null ;
44+ }
45+ const markerColumn = column ;
46+ const char = line [ index ] ;
47+ if ( char === ">" ) {
48+ quoteDepth += 1 ;
49+ index += 1 ;
50+ column += 1 ;
51+ contentColumn = column + ( isBlank ( line [ index ] ) ? 1 : 0 ) ;
52+ continue ;
53+ }
54+ if ( ( char === "-" || char === "+" || char === "*" ) && isBlank ( line [ index + 1 ] ) ) {
55+ hasListMarker = true ;
56+ index += 1 ;
57+ column += 1 ;
58+ pending = { markerEnd : column , number : null , column : markerColumn } ;
59+ continue ;
60+ }
61+ let digitsEnd = index ;
62+ while ( digitsEnd - index < 9 && isDigit ( line [ digitsEnd ] ) ) digitsEnd += 1 ;
63+ const delimiter = line [ digitsEnd ] ;
64+ const after = line [ digitsEnd + 1 ] ;
65+ if (
66+ digitsEnd > index &&
67+ ( delimiter === "." || delimiter === ")" ) &&
68+ ( isBlank ( after ) || after === undefined || after === "\r" )
69+ ) {
70+ hasListMarker = true ;
71+ column += digitsEnd + 1 - index ;
72+ const number = Number ( line . slice ( index , digitsEnd ) ) ;
73+ index = digitsEnd + 1 ;
74+ pending = { markerEnd : column , number, column : markerColumn } ;
75+ continue ;
76+ }
77+ return {
78+ quoteDepth,
79+ hasListMarker,
80+ orderedMarkers,
81+ contentColumn,
82+ restColumn : column ,
83+ rest : line . slice ( index ) ,
84+ } ;
85+ }
86+ }
87+
88+ const FENCE_OPENER = / ^ ( ` { 3 , } | ~ { 3 , } ) ( .* ) $ / ;
89+ const FENCE_CLOSER = / ^ ( ` { 3 , } | ~ { 3 , } ) [ \t \r ] * $ / ;
890
991/**
1092 * Digit count of the widest number the renderer could draw for an ordered list
@@ -13,34 +95,61 @@ const CODE_FENCE = /^ {0,3}(`{3,}|~{3,})/;
1395 * Lists number their items up from the first marker, so each item counts as at
1496 * least one more than the previous item of its list. This is one cheap pass
1597 * while a reply streams. When a line is ambiguous it keeps counting, so it can
16- * overestimate (a slightly wider gutter) but never clip a number.
98+ * overestimate (a slightly wider gutter) but should not clip a number.
1799 */
18100export function wideOrderedListMarkerDigits ( markdown : string ) : number {
19- // Last number per open list, keyed by the marker's column .
20- const lists = new Map < number , number > ( ) ;
21- let openFence : string | null = null ;
101+ // Open ordered lists, outermost first .
102+ let lists : Array < { contentColumn : number ; number : number } > = [ ] ;
103+ let fence : { char : string ; length : number ; quoteDepth : number ; column : number } | null = null ;
22104 let afterBlankLine = false ;
23105 let widest = 0 ;
24106 for ( const line of markdown . split ( "\n" ) ) {
25- const fence = CODE_FENCE . exec ( line ) ?. [ 1 ] ;
26- if ( openFence !== null ) {
27- if ( fence && fence [ 0 ] === openFence [ 0 ] && fence . length >= openFence . length ) openFence = null ;
28- continue ;
107+ const prefix = readLinePrefix ( line ) ;
108+ if ( fence !== null ) {
109+ // A fence ends at its closer, or when the quote holding it ends.
110+ if ( prefix . quoteDepth >= fence . quoteDepth ) {
111+ const closer = FENCE_CLOSER . exec ( prefix . rest ) ?. [ 1 ] ;
112+ if (
113+ ! prefix . hasListMarker &&
114+ prefix . restColumn - fence . column <= 3 &&
115+ closer ?. [ 0 ] === fence . char &&
116+ closer . length >= fence . length
117+ ) {
118+ fence = null ;
119+ }
120+ continue ;
121+ }
122+ fence = null ;
29123 }
30- if ( fence ) {
31- openFence = fence ;
32- continue ;
124+ for ( const marker of prefix . orderedMarkers ) {
125+ // Leave lists whose items this marker sits outside of.
126+ while ( lists . length >= 2 && marker . column < lists [ lists . length - 2 ] ! . contentColumn ) {
127+ lists . pop ( ) ;
128+ }
129+ const list = lists . at ( - 1 ) ;
130+ if ( list && marker . column < list . contentColumn ) {
131+ list . number = Math . max ( marker . number , list . number + 1 ) ;
132+ list . contentColumn = marker . contentColumn ;
133+ } else {
134+ lists . push ( { contentColumn : marker . contentColumn , number : marker . number } ) ;
135+ }
136+ widest = Math . max ( widest , lists . at ( - 1 ) ! . number ) ;
33137 }
34- const marker = ORDERED_MARKER_LINE . exec ( line ) ;
35- if ( marker ) {
36- const column = marker [ 1 ] ! . length ;
37- for ( const key of lists . keys ( ) ) if ( key > column ) lists . delete ( key ) ;
38- const number = Math . max ( Number ( marker [ 2 ] ) , ( lists . get ( column ) ?? - 1 ) + 1 ) ;
39- lists . set ( column , number ) ;
40- widest = Math . max ( widest , number ) ;
41- } else if ( afterBlankLine && / ^ [ ^ \t \r ] / . test ( line ) ) {
138+ const opener = FENCE_OPENER . exec ( prefix . rest ) ;
139+ if (
140+ opener &&
141+ prefix . restColumn - prefix . contentColumn <= 3 &&
142+ ! ( opener [ 1 ] ! . startsWith ( "`" ) && opener [ 2 ] ! . includes ( "`" ) )
143+ ) {
144+ fence = {
145+ char : opener [ 1 ] ! [ 0 ] ! ,
146+ length : opener [ 1 ] ! . length ,
147+ quoteDepth : prefix . quoteDepth ,
148+ column : prefix . restColumn ,
149+ } ;
150+ } else if ( afterBlankLine && ! prefix . hasListMarker && / ^ [ ^ \t \r ] / . test ( line ) ) {
42151 // Unindented text after a blank line cannot belong to any list item.
43- lists . clear ( ) ;
152+ lists = [ ] ;
44153 }
45154 afterBlankLine = line . trim ( ) === "" ;
46155 }
0 commit comments