github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/markup/tableofcontents/tableofcontents.go (about) 1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package tableofcontents 15 16 import ( 17 "sort" 18 "strings" 19 20 "github.com/gohugoio/hugo/common/collections" 21 ) 22 23 // Empty is an empty ToC. 24 var Empty = &Fragments{ 25 Headings: Headings{}, 26 HeadingsMap: map[string]*Heading{}, 27 } 28 29 // Builder is used to build the ToC data structure. 30 type Builder struct { 31 toc *Fragments 32 } 33 34 // Add adds the heading to the ToC. 35 func (b *Builder) AddAt(h *Heading, row, level int) { 36 if b.toc == nil { 37 b.toc = &Fragments{} 38 } 39 b.toc.addAt(h, row, level) 40 } 41 42 // Build returns the ToC. 43 func (b Builder) Build() *Fragments { 44 if b.toc == nil { 45 return Empty 46 } 47 b.toc.HeadingsMap = make(map[string]*Heading) 48 b.toc.walk(func(h *Heading) { 49 if h.ID != "" { 50 b.toc.HeadingsMap[h.ID] = h 51 b.toc.Identifiers = append(b.toc.Identifiers, h.ID) 52 } 53 }) 54 sort.Strings(b.toc.Identifiers) 55 return b.toc 56 } 57 58 // Headings holds the top level headings. 59 type Headings []*Heading 60 61 // FilterBy returns a new Headings slice with all headings that matches the given predicate. 62 // For internal use only. 63 func (h Headings) FilterBy(fn func(*Heading) bool) Headings { 64 var out Headings 65 66 for _, h := range h { 67 h.walk(func(h *Heading) { 68 if fn(h) { 69 out = append(out, h) 70 } 71 }) 72 } 73 return out 74 } 75 76 // Heading holds the data about a heading and its children. 77 type Heading struct { 78 ID string 79 Title string 80 81 Headings Headings 82 } 83 84 // IsZero is true when no ID or Text is set. 85 func (h Heading) IsZero() bool { 86 return h.ID == "" && h.Title == "" 87 } 88 89 func (h *Heading) walk(fn func(*Heading)) { 90 fn(h) 91 for _, h := range h.Headings { 92 h.walk(fn) 93 } 94 } 95 96 // Fragments holds the table of contents for a page. 97 type Fragments struct { 98 // Headings holds the top level headings. 99 Headings Headings 100 101 // Identifiers holds all the identifiers in the ToC as a sorted slice. 102 // Note that collections.SortedStringSlice has both a Contains and Count method 103 // that can be used to identify missing and duplicate IDs. 104 Identifiers collections.SortedStringSlice 105 106 // HeadingsMap holds all the headings in the ToC as a map. 107 // Note that with duplicate IDs, the last one will win. 108 HeadingsMap map[string]*Heading 109 } 110 111 // addAt adds the heading into the given location. 112 func (toc *Fragments) addAt(h *Heading, row, level int) { 113 for i := len(toc.Headings); i <= row; i++ { 114 toc.Headings = append(toc.Headings, &Heading{}) 115 } 116 117 if level == 0 { 118 toc.Headings[row] = h 119 return 120 } 121 122 heading := toc.Headings[row] 123 124 for i := 1; i < level; i++ { 125 if len(heading.Headings) == 0 { 126 heading.Headings = append(heading.Headings, &Heading{}) 127 } 128 heading = heading.Headings[len(heading.Headings)-1] 129 } 130 heading.Headings = append(heading.Headings, h) 131 } 132 133 // ToHTML renders the ToC as HTML. 134 func (toc *Fragments) ToHTML(startLevel, stopLevel int, ordered bool) string { 135 if toc == nil { 136 return "" 137 } 138 b := &tocBuilder{ 139 s: strings.Builder{}, 140 h: toc.Headings, 141 startLevel: startLevel, 142 stopLevel: stopLevel, 143 ordered: ordered, 144 } 145 b.Build() 146 return b.s.String() 147 } 148 149 func (toc Fragments) walk(fn func(*Heading)) { 150 for _, h := range toc.Headings { 151 h.walk(fn) 152 } 153 } 154 155 type tocBuilder struct { 156 s strings.Builder 157 h Headings 158 159 startLevel int 160 stopLevel int 161 ordered bool 162 } 163 164 func (b *tocBuilder) Build() { 165 b.writeNav(b.h) 166 } 167 168 func (b *tocBuilder) writeNav(h Headings) { 169 b.s.WriteString("<nav id=\"TableOfContents\">") 170 b.writeHeadings(1, 0, b.h) 171 b.s.WriteString("</nav>") 172 } 173 174 func (b *tocBuilder) writeHeadings(level, indent int, h Headings) { 175 if level < b.startLevel { 176 for _, h := range h { 177 b.writeHeadings(level+1, indent, h.Headings) 178 } 179 return 180 } 181 182 if b.stopLevel != -1 && level > b.stopLevel { 183 return 184 } 185 186 hasChildren := len(h) > 0 187 188 if hasChildren { 189 b.s.WriteString("\n") 190 b.indent(indent + 1) 191 if b.ordered { 192 b.s.WriteString("<ol>\n") 193 } else { 194 b.s.WriteString("<ul>\n") 195 } 196 } 197 198 for _, h := range h { 199 b.writeHeading(level+1, indent+2, h) 200 } 201 202 if hasChildren { 203 b.indent(indent + 1) 204 if b.ordered { 205 b.s.WriteString("</ol>") 206 } else { 207 b.s.WriteString("</ul>") 208 } 209 b.s.WriteString("\n") 210 b.indent(indent) 211 } 212 } 213 214 func (b *tocBuilder) writeHeading(level, indent int, h *Heading) { 215 b.indent(indent) 216 b.s.WriteString("<li>") 217 if !h.IsZero() { 218 b.s.WriteString("<a href=\"#" + h.ID + "\">" + h.Title + "</a>") 219 } 220 b.writeHeadings(level, indent, h.Headings) 221 b.s.WriteString("</li>\n") 222 } 223 224 func (b *tocBuilder) indent(n int) { 225 for i := 0; i < n; i++ { 226 b.s.WriteString(" ") 227 } 228 } 229 230 // DefaultConfig is the default ToC configuration. 231 var DefaultConfig = Config{ 232 StartLevel: 2, 233 EndLevel: 3, 234 Ordered: false, 235 } 236 237 type Config struct { 238 // Heading start level to include in the table of contents, starting 239 // at h1 (inclusive). 240 StartLevel int 241 242 // Heading end level, inclusive, to include in the table of contents. 243 // Default is 3, a value of -1 will include everything. 244 EndLevel int 245 246 // Whether to produce a ordered list or not. 247 Ordered bool 248 }