@@ -14,7 +14,13 @@ interface Options {
1414 list_number ?: boolean ;
1515}
1616
17- function tocHelper ( str : string , options : Options = { } ) {
17+ /**
18+ * Hexo TOC helper: generates a nested <ol> list from markdown headings
19+ * @param {string } str Raw markdown/html string
20+ * @param {Options } options Configuration options
21+ */
22+ function tocHelper ( str , options : Options = { } ) {
23+ // Default options
1824 options = Object . assign ( {
1925 min_depth : 1 ,
2026 max_depth : 6 ,
@@ -29,10 +35,15 @@ function tocHelper(str: string, options: Options = {}) {
2935 list_number : true
3036 } , options ) ;
3137
32- const data = getAndTruncateTocObj ( str , { min_depth : options . min_depth , max_depth : options . max_depth } , options . max_items ) ;
33-
34- if ( ! data . length ) return '' ;
38+ // Extract and truncate flat TOC data
39+ const flat = getAndTruncateTocObj (
40+ str ,
41+ { min_depth : options . min_depth , max_depth : options . max_depth } ,
42+ options . max_items
43+ ) ;
44+ if ( ! flat . length ) return '' ;
3545
46+ // Prepare class names
3647 const className = escapeHTML ( options . class ) ;
3748 const itemClassName = escapeHTML ( options . class_item || options . class + '-item' ) ;
3849 const linkClassName = escapeHTML ( options . class_link || options . class + '-link' ) ;
@@ -42,89 +53,89 @@ function tocHelper(str: string, options: Options = {}) {
4253 const levelClassName = escapeHTML ( options . class_level || options . class + '-level' ) ;
4354 const listNumber = options . list_number ;
4455
45- let result = `<ol class="${ className } ">` ;
46-
47- const lastNumber = [ 0 , 0 , 0 , 0 , 0 , 0 ] ;
48- let firstLevel = 0 ;
49- let lastLevel = 0 ;
50-
51- for ( let i = 0 , len = data . length ; i < len ; i ++ ) {
52- const el = data [ i ] ;
53- const { level, id, text } = el ;
54- const href = id ? `#${ encodeURI ( id ) } ` : null ;
55-
56- if ( ! el . unnumbered ) {
57- lastNumber [ level - 1 ] ++ ;
58- }
59-
60- for ( let i = level ; i <= 5 ; i ++ ) {
61- lastNumber [ i ] = 0 ;
62- }
63-
64- if ( firstLevel ) {
65- for ( let i = level ; i < lastLevel ; i ++ ) {
66- result += '</li></ol>' ;
56+ // Build tree, assign numbers, render HTML
57+ const tree = buildTree ( flat ) ;
58+ if ( listNumber ) assignNumbers ( tree ) ;
59+
60+ function render ( list , depth = 0 ) {
61+ if ( ! list . length ) return '' ;
62+ const olCls = depth === 0 ? className : childClassName ;
63+ let out = `<ol class="${ olCls } ">` ;
64+
65+ list . forEach ( node => {
66+ const lvl = node . level ;
67+ out += `<li class="${ itemClassName } ${ levelClassName } -${ lvl } ">` ;
68+ out += `<a class="${ linkClassName } "${ node . id ? ` href="#${ encodeURI ( node . id ) } "` : '' } >` ;
69+ if ( listNumber && ! node . unnumbered ) {
70+ out += `<span class="${ numberClassName } ">${ node . number } </span> ` ;
6771 }
72+ out += `<span class="${ textClassName } ">${ node . text } </span></a>` ;
73+ out += render ( node . children , depth + 1 ) ;
74+ out += '</li>' ;
75+ } ) ;
6876
69- if ( level > lastLevel ) {
70- result += `<ol class="${ childClassName } ">` ;
71- } else {
72- result += '</li>' ;
73- }
74- } else {
75- firstLevel = level ;
76- }
77-
78- result += `<li class="${ itemClassName } ${ levelClassName } -${ level } ">` ;
79- if ( href ) {
80- result += `<a class="${ linkClassName } " href="${ href } ">` ;
81- } else {
82- result += `<a class="${ linkClassName } ">` ;
83- }
84-
85- if ( listNumber && ! el . unnumbered ) {
86- result += `<span class="${ numberClassName } ">` ;
77+ out += '</ol>' ;
78+ return out ;
79+ }
8780
88- for ( let i = firstLevel - 1 ; i < level ; i ++ ) {
89- result += `${ lastNumber [ i ] } .` ;
90- }
81+ return render ( tree ) ;
82+ }
9183
92- result += '</span> ' ;
84+ /**
85+ * Extract flat TOC data and enforce max_items
86+ */
87+ function getAndTruncateTocObj ( str , { min_depth, max_depth } , max_items ) {
88+ let data = tocObj ( str , { min_depth, max_depth } ) ;
89+ if ( max_items < Infinity && data . length > max_items ) {
90+ const levels = data . map ( i => i . level ) ;
91+ const min = Math . min ( ...levels ) ;
92+ let curMax = Math . max ( ...levels ) ;
93+ // remove deeper headings until within limit
94+ while ( data . length > max_items && curMax > min ) {
95+ // eslint-disable-next-line no-loop-func
96+ data = data . filter ( i => i . level < curMax ) ;
97+ curMax -- ;
9398 }
94-
95- result += `<span class="${ textClassName } ">${ text } </span></a>` ;
96-
97- lastLevel = level ;
98- }
99-
100- for ( let i = firstLevel - 1 ; i < lastLevel ; i ++ ) {
101- result += '</li></ol>' ;
99+ data = data . slice ( 0 , max_items ) ;
102100 }
103-
104- return result ;
101+ return data ;
105102}
106103
107- function getAndTruncateTocObj ( str : string , options : { min_depth : number , max_depth : number } , max_items : number ) {
108- let data = tocObj ( str , { min_depth : options . min_depth , max_depth : options . max_depth } ) ;
109-
110- if ( data . length === 0 ) {
111- return data ;
112- }
113- if ( max_items < 1 || max_items === Infinity ) {
114- return data ;
115- }
104+ /**
105+ * Build nested tree from flat heading list
106+ */
107+ function buildTree ( headings ) {
108+ const root = { level : 0 , children : [ ] } ;
109+ const stack = [ root ] ;
110+
111+ headings . forEach ( h => {
112+ // pop until parent.level < h.level
113+ while ( stack [ stack . length - 1 ] . level >= h . level ) {
114+ stack . pop ( ) ;
115+ }
116+ const parent = stack [ stack . length - 1 ] ;
117+ const node = { ...h , children : [ ] } ;
118+ parent . children . push ( node ) ;
119+ stack . push ( node ) ;
120+ } ) ;
116121
117- const levels = data . map ( item => item . level ) ;
118- const min = Math . min ( ...levels ) ;
119- const max = Math . max ( ...levels ) ;
122+ return root . children ;
123+ }
120124
121- for ( let currentLevel = max ; data . length > max_items && currentLevel > min ; currentLevel -- ) {
122- data = data . filter ( item => item . level < currentLevel ) ;
125+ /**
126+ * Assign hierarchical numbering to each node
127+ */
128+ function assignNumbers ( nodes ) {
129+ const counters = [ ] ;
130+ function dfs ( list , depth ) {
131+ counters [ depth ] = 0 ;
132+ list . forEach ( node => {
133+ counters [ depth ] ++ ;
134+ node . number = counters . slice ( 0 , depth + 1 ) . join ( '.' ) + '.' ;
135+ if ( node . children . length ) dfs ( node . children , depth + 1 ) ;
136+ } ) ;
123137 }
124-
125- data = data . slice ( 0 , max_items ) ;
126-
127- return data ;
138+ dfs ( nodes , 0 ) ;
128139}
129140
130141export = tocHelper ;
0 commit comments