Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7d4af7a

Browse files
authored
markup/tableofcontents: Skip empty TOC levels
Fixes #7128
1 parent 28147cb commit 7d4af7a

3 files changed

Lines changed: 278 additions & 48 deletions

File tree

markup/goldmark/toc_integration_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,101 @@ title: home
305305
b.AssertFileExists("public/index.html", true)
306306
}
307307

308+
func TestTableOfContentsSkippedHeadingLevelsIssue7128(t *testing.T) {
309+
t.Parallel()
310+
311+
filesTemplate := `
312+
-- hugo.toml --
313+
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
314+
TOC_CONFIG
315+
-- layouts/page.html --
316+
{{ .TableOfContents }}
317+
-- content/p1.md --
318+
---
319+
title: p1
320+
---
321+
## Extra-Curriculars
322+
323+
#### Heading Four Here without Heading Three Before It
324+
325+
## Technology
326+
327+
#### Heading Four Here without Heading Three Before It
328+
`
329+
330+
t.Run("default end level", func(t *testing.T) {
331+
t.Parallel()
332+
333+
files := strings.ReplaceAll(filesTemplate, "TOC_CONFIG", "")
334+
b := hugolib.Test(t, files)
335+
336+
b.AssertFileContentExact("public/p1/index.html", `<nav id="TableOfContents">
337+
<ul>
338+
<li><a href="#extra-curriculars">Extra-Curriculars</a></li>
339+
<li><a href="#technology">Technology</a></li>
340+
</ul>
341+
</nav>`)
342+
})
343+
344+
t.Run("include skipped level", func(t *testing.T) {
345+
t.Parallel()
346+
347+
files := strings.ReplaceAll(filesTemplate, "TOC_CONFIG", `
348+
[markup.tableOfContents]
349+
startLevel = 2
350+
endLevel = -1`)
351+
b := hugolib.Test(t, files)
352+
353+
b.AssertFileContentExact("public/p1/index.html", `<nav id="TableOfContents">
354+
<ul>
355+
<li><a href="#extra-curriculars">Extra-Curriculars</a>
356+
<ul>
357+
<li><a href="#heading-four-here-without-heading-three-before-it">Heading Four Here without Heading Three Before It</a></li>
358+
</ul>
359+
</li>
360+
<li><a href="#technology">Technology</a>
361+
<ul>
362+
<li><a href="#heading-four-here-without-heading-three-before-it-1">Heading Four Here without Heading Three Before It</a></li>
363+
</ul>
364+
</li>
365+
</ul>
366+
</nav>`)
367+
})
368+
369+
t.Run("starts at h4", func(t *testing.T) {
370+
t.Parallel()
371+
372+
files := `
373+
-- hugo.toml --
374+
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
375+
[markup.tableOfContents]
376+
startLevel = 2
377+
endLevel = -1
378+
-- layouts/page.html --
379+
{{ .TableOfContents }}
380+
-- content/p1.md --
381+
---
382+
title: p1
383+
---
384+
#### Heading Four First
385+
386+
###### Heading Six After Heading Four
387+
`
388+
389+
b := hugolib.Test(t, files)
390+
391+
b.AssertFileContentExact("public/p1/index.html", `<nav id="TableOfContents">
392+
<ul>
393+
<li><a href="#heading-four-first">Heading Four First</a>
394+
<ul>
395+
<li><a href="#heading-six-after-heading-four">Heading Six After Heading Four</a></li>
396+
</ul>
397+
</li>
398+
</ul>
399+
</nav>`)
400+
})
401+
}
402+
308403
// Issue 12605
309404
func TestTableOfContentsWithGoldmarkExtras(t *testing.T) {
310405
t.Parallel()

markup/tableofcontents/tableofcontents.go

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -195,23 +195,13 @@ func (b *tocBuilder) Build() {
195195

196196
func (b *tocBuilder) writeNav(h Headings) {
197197
b.s.WriteString("<nav id=\"TableOfContents\">")
198-
b.writeHeadings(1, 0, b.h)
198+
b.writeHeadings(1, 0, h)
199199
b.s.WriteString("</nav>")
200200
}
201201

202202
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
203+
headings := b.collectHeadings(level, h)
204+
hasChildren := len(headings) > 0
215205

216206
if hasChildren {
217207
b.s.WriteString("\n")
@@ -223,8 +213,8 @@ func (b *tocBuilder) writeHeadings(level, indent int, h Headings) {
223213
}
224214
}
225215

226-
for _, h := range h {
227-
b.writeHeading(level+1, indent+2, h)
216+
for _, h := range headings {
217+
b.writeHeading(h.level, indent+2, h.h)
228218
}
229219

230220
if hasChildren {
@@ -239,13 +229,35 @@ func (b *tocBuilder) writeHeadings(level, indent int, h Headings) {
239229
}
240230
}
241231

232+
type tocHeading struct {
233+
h *Heading
234+
level int
235+
}
236+
237+
func (b *tocBuilder) collectHeadings(level int, h Headings) []tocHeading {
238+
var out []tocHeading
239+
240+
for _, h := range h {
241+
if h.IsZero() || level < b.startLevel {
242+
out = append(out, b.collectHeadings(level+1, h.Headings)...)
243+
continue
244+
}
245+
246+
if b.stopLevel != -1 && level > b.stopLevel {
247+
continue
248+
}
249+
250+
out = append(out, tocHeading{h: h, level: level})
251+
}
252+
253+
return out
254+
}
255+
242256
func (b *tocBuilder) writeHeading(level, indent int, h *Heading) {
243257
b.indent(indent)
244258
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)
259+
b.s.WriteString("<a href=\"#" + h.ID + "\">" + h.Title + "</a>")
260+
b.writeHeadings(level+1, indent, h.Headings)
249261
b.s.WriteString("</li>\n")
250262
}
251263

markup/tableofcontents/tableofcontents_test.go

Lines changed: 152 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package tableofcontents
1515

1616
import (
17+
"strings"
1718
"testing"
1819

1920
qt "github.com/frankban/quicktest"
@@ -127,21 +128,9 @@ func TestTocMissingParent(t *testing.T) {
127128
got := string(tocHTML)
128129
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
129130
<ul>
130-
<li>
131-
<ul>
132-
<li><a href="#h2">H2</a></li>
133-
</ul>
134-
</li>
135-
<li>
136-
<ul>
137-
<li>
138-
<ul>
139-
<li><a href="#h3">H3</a></li>
140-
<li><a href="#h3">H3</a></li>
141-
</ul>
142-
</li>
143-
</ul>
144-
</li>
131+
<li><a href="#h2">H2</a></li>
132+
<li><a href="#h3">H3</a></li>
133+
<li><a href="#h3">H3</a></li>
145134
</ul>
146135
</nav>`, qt.Commentf(got))
147136

@@ -158,23 +147,157 @@ func TestTocMissingParent(t *testing.T) {
158147
got = string(tocHTML)
159148
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
160149
<ol>
161-
<li>
162-
<ol>
163-
<li><a href="#h2">H2</a></li>
164-
</ol>
150+
<li><a href="#h2">H2</a></li>
151+
<li><a href="#h3">H3</a></li>
152+
<li><a href="#h3">H3</a></li>
153+
</ol>
154+
</nav>`, qt.Commentf(got))
155+
}
156+
157+
func TestTocMissingIntermediateLevels(t *testing.T) {
158+
c := qt.New(t)
159+
160+
type item struct {
161+
title string
162+
id string
163+
row int
164+
level int
165+
}
166+
167+
for _, test := range []struct {
168+
name string
169+
items []item
170+
expected string
171+
}{
172+
{
173+
name: "h2 to h4",
174+
items: []item{
175+
{title: "H2", id: "h2", level: 1},
176+
{title: "H4", id: "h4", level: 3},
177+
},
178+
expected: `<nav id="TableOfContents">
179+
<ul>
180+
<li><a href="#h2">H2</a>
181+
<ul>
182+
<li><a href="#h4">H4</a></li>
183+
</ul>
165184
</li>
166-
<li>
167-
<ol>
168-
<li>
169-
<ol>
170-
<li><a href="#h3">H3</a></li>
171-
<li><a href="#h3">H3</a></li>
172-
</ol>
173-
</li>
174-
</ol>
185+
</ul>
186+
</nav>`,
187+
},
188+
{
189+
name: "h2 to h5",
190+
items: []item{
191+
{title: "H2", id: "h2", level: 1},
192+
{title: "H5", id: "h5", level: 4},
193+
},
194+
expected: `<nav id="TableOfContents">
195+
<ul>
196+
<li><a href="#h2">H2</a>
197+
<ul>
198+
<li><a href="#h5">H5</a></li>
199+
</ul>
175200
</li>
176-
</ol>
201+
</ul>
202+
</nav>`,
203+
},
204+
{
205+
name: "h2 to h6",
206+
items: []item{
207+
{title: "H2", id: "h2", level: 1},
208+
{title: "H6", id: "h6", level: 5},
209+
},
210+
expected: `<nav id="TableOfContents">
211+
<ul>
212+
<li><a href="#h2">H2</a>
213+
<ul>
214+
<li><a href="#h6">H6</a></li>
215+
</ul>
216+
</li>
217+
</ul>
218+
</nav>`,
219+
},
220+
{
221+
name: "h3 to h5",
222+
items: []item{
223+
{title: "H3", id: "h3", level: 2},
224+
{title: "H5", id: "h5", level: 4},
225+
},
226+
expected: `<nav id="TableOfContents">
227+
<ul>
228+
<li><a href="#h3">H3</a>
229+
<ul>
230+
<li><a href="#h5">H5</a></li>
231+
</ul>
232+
</li>
233+
</ul>
234+
</nav>`,
235+
},
236+
{
237+
name: "starts at h4",
238+
items: []item{
239+
{title: "H4", id: "h4", level: 3},
240+
},
241+
expected: `<nav id="TableOfContents">
242+
<ul>
243+
<li><a href="#h4">H4</a></li>
244+
</ul>
245+
</nav>`,
246+
},
247+
{
248+
name: "starts at h6",
249+
items: []item{
250+
{title: "H6", id: "h6", level: 5},
251+
},
252+
expected: `<nav id="TableOfContents">
253+
<ul>
254+
<li><a href="#h6">H6</a></li>
255+
</ul>
256+
</nav>`,
257+
},
258+
} {
259+
c.Run(test.name, func(c *qt.C) {
260+
toc := &Fragments{}
261+
for _, item := range test.items {
262+
toc.addAt(&Heading{Title: item.title, ID: item.id}, item.row, item.level)
263+
}
264+
265+
tocHTML, err := toc.ToHTML(2, -1, false)
266+
c.Assert(err, qt.IsNil)
267+
got := string(tocHTML)
268+
c.Assert(got, qt.Equals, test.expected, qt.Commentf(got))
269+
c.Assert(got, qt.Not(qt.Contains), "<li>\n")
270+
c.Assert(hasListItemWithoutAnchor(got), qt.Equals, false)
271+
})
272+
}
273+
274+
toc := &Fragments{}
275+
toc.addAt(&Heading{Title: "H2", ID: "h2"}, 0, 1)
276+
toc.addAt(&Heading{Title: "H4", ID: "h4"}, 0, 3)
277+
278+
tocHTML, err := toc.ToHTML(2, 3, false)
279+
c.Assert(err, qt.IsNil)
280+
got := string(tocHTML)
281+
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
282+
<ul>
283+
<li><a href="#h2">H2</a></li>
284+
</ul>
177285
</nav>`, qt.Commentf(got))
286+
c.Assert(got, qt.Not(qt.Contains), "<li>\n")
287+
c.Assert(hasListItemWithoutAnchor(got), qt.Equals, false)
288+
}
289+
290+
func hasListItemWithoutAnchor(s string) bool {
291+
for {
292+
i := strings.Index(s, "<li>")
293+
if i == -1 {
294+
return false
295+
}
296+
s = s[i+len("<li>"):]
297+
if !strings.HasPrefix(strings.TrimLeft(s, " \n\t\r"), "<a ") {
298+
return true
299+
}
300+
}
178301
}
179302

180303
func TestTocMisc(t *testing.T) {

0 commit comments

Comments
 (0)