diff --git a/markup/goldmark/toc_integration_test.go b/markup/goldmark/toc_integration_test.go index 105e1a744..f171af600 100644 --- a/markup/goldmark/toc_integration_test.go +++ b/markup/goldmark/toc_integration_test.go @@ -305,6 +305,101 @@ title: home b.AssertFileExists("public/index.html", true) } +func TestTableOfContentsSkippedHeadingLevelsIssue7128(t *testing.T) { + t.Parallel() + + filesTemplate := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +TOC_CONFIG +-- layouts/page.html -- +{{ .TableOfContents }} +-- content/p1.md -- +--- +title: p1 +--- +## Extra-Curriculars + +#### Heading Four Here without Heading Three Before It + +## Technology + +#### Heading Four Here without Heading Three Before It +` + + t.Run("default end level", func(t *testing.T) { + t.Parallel() + + files := strings.ReplaceAll(filesTemplate, "TOC_CONFIG", "") + b := hugolib.Test(t, files) + + b.AssertFileContentExact("public/p1/index.html", ``) + }) + + t.Run("include skipped level", func(t *testing.T) { + t.Parallel() + + files := strings.ReplaceAll(filesTemplate, "TOC_CONFIG", ` +[markup.tableOfContents] +startLevel = 2 +endLevel = -1`) + b := hugolib.Test(t, files) + + b.AssertFileContentExact("public/p1/index.html", ``) + }) + + t.Run("starts at h4", func(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +[markup.tableOfContents] +startLevel = 2 +endLevel = -1 +-- layouts/page.html -- +{{ .TableOfContents }} +-- content/p1.md -- +--- +title: p1 +--- +#### Heading Four First + +###### Heading Six After Heading Four +` + + b := hugolib.Test(t, files) + + b.AssertFileContentExact("public/p1/index.html", ``) + }) +} + // Issue 12605 func TestTableOfContentsWithGoldmarkExtras(t *testing.T) { t.Parallel() diff --git a/markup/tableofcontents/tableofcontents.go b/markup/tableofcontents/tableofcontents.go index 6c40c9a59..271c44e67 100644 --- a/markup/tableofcontents/tableofcontents.go +++ b/markup/tableofcontents/tableofcontents.go @@ -195,23 +195,13 @@ func (b *tocBuilder) Build() { func (b *tocBuilder) writeNav(h Headings) { b.s.WriteString("") } func (b *tocBuilder) writeHeadings(level, indent int, h Headings) { - if level < b.startLevel { - for _, h := range h { - b.writeHeadings(level+1, indent, h.Headings) - } - return - } - - if b.stopLevel != -1 && level > b.stopLevel { - return - } - - hasChildren := len(h) > 0 + headings := b.collectHeadings(level, h) + hasChildren := len(headings) > 0 if hasChildren { b.s.WriteString("\n") @@ -223,8 +213,8 @@ func (b *tocBuilder) writeHeadings(level, indent int, h Headings) { } } - for _, h := range h { - b.writeHeading(level+1, indent+2, h) + for _, h := range headings { + b.writeHeading(h.level, indent+2, h.h) } if hasChildren { @@ -239,13 +229,35 @@ func (b *tocBuilder) writeHeadings(level, indent int, h Headings) { } } +type tocHeading struct { + h *Heading + level int +} + +func (b *tocBuilder) collectHeadings(level int, h Headings) []tocHeading { + var out []tocHeading + + for _, h := range h { + if h.IsZero() || level < b.startLevel { + out = append(out, b.collectHeadings(level+1, h.Headings)...) + continue + } + + if b.stopLevel != -1 && level > b.stopLevel { + continue + } + + out = append(out, tocHeading{h: h, level: level}) + } + + return out +} + func (b *tocBuilder) writeHeading(level, indent int, h *Heading) { b.indent(indent) b.s.WriteString("
  • ") - if !h.IsZero() { - b.s.WriteString("" + h.Title + "") - } - b.writeHeadings(level, indent, h.Headings) + b.s.WriteString("" + h.Title + "") + b.writeHeadings(level+1, indent, h.Headings) b.s.WriteString("
  • \n") } diff --git a/markup/tableofcontents/tableofcontents_test.go b/markup/tableofcontents/tableofcontents_test.go index db50c91ab..dfbb21a53 100644 --- a/markup/tableofcontents/tableofcontents_test.go +++ b/markup/tableofcontents/tableofcontents_test.go @@ -14,6 +14,7 @@ package tableofcontents import ( + "strings" "testing" qt "github.com/frankban/quicktest" @@ -127,21 +128,9 @@ func TestTocMissingParent(t *testing.T) { got := string(tocHTML) c.Assert(got, qt.Equals, ``, qt.Commentf(got)) @@ -158,25 +147,159 @@ func TestTocMissingParent(t *testing.T) { got = string(tocHTML) c.Assert(got, qt.Equals, ``, qt.Commentf(got)) } +func TestTocMissingIntermediateLevels(t *testing.T) { + c := qt.New(t) + + type item struct { + title string + id string + row int + level int + } + + for _, test := range []struct { + name string + items []item + expected string + }{ + { + name: "h2 to h4", + items: []item{ + {title: "H2", id: "h2", level: 1}, + {title: "H4", id: "h4", level: 3}, + }, + expected: ``, + }, + { + name: "h2 to h5", + items: []item{ + {title: "H2", id: "h2", level: 1}, + {title: "H5", id: "h5", level: 4}, + }, + expected: ``, + }, + { + name: "h2 to h6", + items: []item{ + {title: "H2", id: "h2", level: 1}, + {title: "H6", id: "h6", level: 5}, + }, + expected: ``, + }, + { + name: "h3 to h5", + items: []item{ + {title: "H3", id: "h3", level: 2}, + {title: "H5", id: "h5", level: 4}, + }, + expected: ``, + }, + { + name: "starts at h4", + items: []item{ + {title: "H4", id: "h4", level: 3}, + }, + expected: ``, + }, + { + name: "starts at h6", + items: []item{ + {title: "H6", id: "h6", level: 5}, + }, + expected: ``, + }, + } { + c.Run(test.name, func(c *qt.C) { + toc := &Fragments{} + for _, item := range test.items { + toc.addAt(&Heading{Title: item.title, ID: item.id}, item.row, item.level) + } + + tocHTML, err := toc.ToHTML(2, -1, false) + c.Assert(err, qt.IsNil) + got := string(tocHTML) + c.Assert(got, qt.Equals, test.expected, qt.Commentf(got)) + c.Assert(got, qt.Not(qt.Contains), "
  • \n") + c.Assert(hasListItemWithoutAnchor(got), qt.Equals, false) + }) + } + + toc := &Fragments{} + toc.addAt(&Heading{Title: "H2", ID: "h2"}, 0, 1) + toc.addAt(&Heading{Title: "H4", ID: "h4"}, 0, 3) + + tocHTML, err := toc.ToHTML(2, 3, false) + c.Assert(err, qt.IsNil) + got := string(tocHTML) + c.Assert(got, qt.Equals, ``, qt.Commentf(got)) + c.Assert(got, qt.Not(qt.Contains), "
  • \n") + c.Assert(hasListItemWithoutAnchor(got), qt.Equals, false) +} + +func hasListItemWithoutAnchor(s string) bool { + for { + i := strings.Index(s, "
  • ") + if i == -1 { + return false + } + s = s[i+len("
  • "):] + if !strings.HasPrefix(strings.TrimLeft(s, " \n\t\r"), "