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