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