github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/markup/tableofcontents/tableofcontents.go (about)

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