tableofcontents.go (3742B)
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 "strings"
18 )
19
20 // Headings holds the top level headings.
21 type Headings []Heading
22
23 // Heading holds the data about a heading and its children.
24 type Heading struct {
25 ID string
26 Text string
27
28 Headings Headings
29 }
30
31 // IsZero is true when no ID or Text is set.
32 func (h Heading) IsZero() bool {
33 return h.ID == "" && h.Text == ""
34 }
35
36 // Root implements AddAt, which can be used to build the
37 // data structure for the ToC.
38 type Root struct {
39 Headings Headings
40 }
41
42 // AddAt adds the heading into the given location.
43 func (toc *Root) AddAt(h Heading, row, level int) {
44 for i := len(toc.Headings); i <= row; i++ {
45 toc.Headings = append(toc.Headings, Heading{})
46 }
47
48 if level == 0 {
49 toc.Headings[row] = h
50 return
51 }
52
53 heading := &toc.Headings[row]
54
55 for i := 1; i < level; i++ {
56 if len(heading.Headings) == 0 {
57 heading.Headings = append(heading.Headings, Heading{})
58 }
59 heading = &heading.Headings[len(heading.Headings)-1]
60 }
61 heading.Headings = append(heading.Headings, h)
62 }
63
64 // ToHTML renders the ToC as HTML.
65 func (toc Root) ToHTML(startLevel, stopLevel int, ordered bool) string {
66 b := &tocBuilder{
67 s: strings.Builder{},
68 h: toc.Headings,
69 startLevel: startLevel,
70 stopLevel: stopLevel,
71 ordered: ordered,
72 }
73 b.Build()
74 return b.s.String()
75 }
76
77 type tocBuilder struct {
78 s strings.Builder
79 h Headings
80
81 startLevel int
82 stopLevel int
83 ordered bool
84 }
85
86 func (b *tocBuilder) Build() {
87 b.writeNav(b.h)
88 }
89
90 func (b *tocBuilder) writeNav(h Headings) {
91 b.s.WriteString("<nav id=\"TableOfContents\">")
92 b.writeHeadings(1, 0, b.h)
93 b.s.WriteString("</nav>")
94 }
95
96 func (b *tocBuilder) writeHeadings(level, indent int, h Headings) {
97 if level < b.startLevel {
98 for _, h := range h {
99 b.writeHeadings(level+1, indent, h.Headings)
100 }
101 return
102 }
103
104 if b.stopLevel != -1 && level > b.stopLevel {
105 return
106 }
107
108 hasChildren := len(h) > 0
109
110 if hasChildren {
111 b.s.WriteString("\n")
112 b.indent(indent + 1)
113 if b.ordered {
114 b.s.WriteString("<ol>\n")
115 } else {
116 b.s.WriteString("<ul>\n")
117 }
118 }
119
120 for _, h := range h {
121 b.writeHeading(level+1, indent+2, h)
122 }
123
124 if hasChildren {
125 b.indent(indent + 1)
126 if b.ordered {
127 b.s.WriteString("</ol>")
128 } else {
129 b.s.WriteString("</ul>")
130 }
131 b.s.WriteString("\n")
132 b.indent(indent)
133 }
134 }
135
136 func (b *tocBuilder) writeHeading(level, indent int, h Heading) {
137 b.indent(indent)
138 b.s.WriteString("<li>")
139 if !h.IsZero() {
140 b.s.WriteString("<a href=\"#" + h.ID + "\">" + h.Text + "</a>")
141 }
142 b.writeHeadings(level, indent, h.Headings)
143 b.s.WriteString("</li>\n")
144 }
145
146 func (b *tocBuilder) indent(n int) {
147 for i := 0; i < n; i++ {
148 b.s.WriteString(" ")
149 }
150 }
151
152 // DefaultConfig is the default ToC configuration.
153 var DefaultConfig = Config{
154 StartLevel: 2,
155 EndLevel: 3,
156 Ordered: false,
157 }
158
159 type Config struct {
160 // Heading start level to include in the table of contents, starting
161 // at h1 (inclusive).
162 StartLevel int
163
164 // Heading end level, inclusive, to include in the table of contents.
165 // Default is 3, a value of -1 will include everything.
166 EndLevel int
167
168 // Whether to produce a ordered list or not.
169 Ordered bool
170 }