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 }