package toc

import "go.abhg.dev/goldmark/toc"

Package toc provides support for building a Table of Contents from a goldmark Markdown document.

The package operates in two stages: inspection and rendering. During inspection, the package analyzes an existing Markdown document, and builds a Table of Contents from it.

markdown := goldmark.New(...)

parser := markdown.Parser()
doc := parser.Parse(text.NewReader(src))
tocTree, err := toc.Inspect(doc, src)

During rendering, it converts the Table of Contents into a list of headings with nested items under each as a goldmark Markdown document. You may manipulate the TOC, removing items from it or simplifying it, before rendering.

if len(tocTree.Items) == 0 {
	// No headings in the document.
	return
}
tocList := toc.RenderList(tocTree)

You can render that Markdown document using goldmark into whatever form you prefer.

renderer := markdown.Renderer()
renderer.Render(out, src, tocList)

The following diagram summarizes the flow of information with goldmark-toc.

   src
+--------+                           +-------------------+
|        |   goldmark/Parser.Parse   |                   |
| []byte :---------------------------> goldmark/ast.Node |
|        |                           |                   |
+---.----+                           +-------.-----.-----+
    |                                        |     |
    '----------------.     .-----------------'     |
                      \   /                        |
                       \ /                         |
                        |                          |
                        | toc.Inspect              |
                        |                          |
                   +----v----+                     |
                   |         |                     |
                   | toc.TOC |                     |
                   |         |                     |
                   +----.----+                     |
                        |                          |
                        | toc/Renderer.Render      |
                        |                          |
              +---------v---------+                |
              |                   |                |
              | goldmark/ast.Node |                |
              |                   |                |
              +---------.---------+                |
                        |                          |
                        '-------.   .--------------'
                                 \ /
                                  |
         goldmark/Renderer.Render |
                                  |
                                  v
                              +------+
                              | HTML |
                              +------+
Example
package main

import (
	"os"

	"github.com/yuin/goldmark"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/text"
	"go.abhg.dev/goldmark/toc"
)

func main() {
	src := []byte(`
# A section

Hello

# Another section

## A sub-section

### A sub-sub-section

Bye
`)

	markdown := goldmark.New()

	// Request that IDs are automatically assigned to headers.
	markdown.Parser().AddOptions(parser.WithAutoHeadingID())
	// Alternatively, we can provide our own implementation of parser.IDs
	// and use,
	//
	//   pctx := parser.NewContext(parser.WithIDs(ids))
	//   doc := parser.Parse(text.NewReader(src), parser.WithContext(pctx))

	doc := markdown.Parser().Parse(text.NewReader(src))

	// Inspect the parsed Markdown document to find headers and build a
	// tree for the table of contents.
	tree, err := toc.Inspect(doc, src)
	if err != nil {
		panic(err)
	}

	if len(tree.Items) == 0 {
		return
		// No table of contents because there are no headers.
	}

	// Render the tree as-is into a Markdown list.
	treeList := toc.RenderList(tree)

	// Render the Markdown list into HTML.
	if err := markdown.Renderer().Render(os.Stdout, src, treeList); err != nil {
		panic(err)
	}

}

Output:

<ul>
<li>
<a href="#a-section">A section</a></li>
<li>
<a href="#another-section">Another section</a><ul>
<li>
<a href="#a-sub-section">A sub-section</a><ul>
<li>
<a href="#a-sub-sub-section">A sub-sub-section</a></li>
</ul>
</li>
</ul>
</li>
</ul>

Index

Examples

Functions

func RenderList

func RenderList(toc *TOC) ast.Node

RenderList renders a table of contents as a nested list with a sane, default configuration for the ListRenderer.

If the TOC is nil or empty, nil is returned. Do not call Goldmark's renderer if the returned node is nil.

Types

type Extender

type Extender struct {
	// Title is the title of the table of contents section.
	// Defaults to "Table of Contents" if unspecified.
	Title string

	// TitleDepth is the heading depth for the Title.
	// Defaults to 1 (<h1>) if unspecified.
	TitleDepth int

	// MinDepth is the minimum depth of the table of contents.
	// Headings with a level lower than the specified depth will be ignored.
	// See the documentation for MinDepth for more information.
	//
	// Defaults to 0 (no limit) if unspecified.
	MinDepth int

	// MaxDepth is the maximum depth of the table of contents.
	// Headings with a level greater than the specified depth will be ignored.
	// See the documentation for MaxDepth for more information.
	//
	// Defaults to 0 (no limit) if unspecified.
	MaxDepth int

	// ListID is the id for the list of TOC items rendered in the HTML.
	//
	// See the documentation for Transformer.ListID for more information.
	ListID string

	// TitleID is the id for the Title heading rendered in the HTML.
	//
	// See the documentation for Transformer.TitleID for more information.
	TitleID string

	// Compact controls whether empty items should be removed
	// from the table of contents.
	//
	// See the documentation for Compact for more information.
	Compact bool
}

Extender extends a Goldmark Markdown parser and renderer to always include a table of contents in the output.

To use this, install it into your Goldmark Markdown object.

md := goldmark.New(
  // ...
  goldmark.WithParserOptions(parser.WithAutoHeadingID()),
  goldmark.WithExtensions(
    // ...
    &toc.Extender{
    },
  ),
)

This will install the default Transformer. For more control, install the Transformer directly on the Markdown Parser.

NOTE: Unless you've supplied your own parser.IDs implementation, you'll need to enable the WithAutoHeadingID option on the parser to generate IDs and links for headings.

func (*Extender) Extend

func (e *Extender) Extend(md goldmark.Markdown)

Extend adds support for rendering a table of contents to the provided Markdown parser/renderer.

type InspectOption

type InspectOption interface {
	// contains filtered or unexported methods
}

InspectOption customizes the behavior of Inspect.

func Compact

func Compact(compact bool) InspectOption

Compact instructs Inspect to remove empty items from the table of contents. Children of removed items will be promoted to the parent item.

For example, given the following:

# A
### B
#### C
# D
#### E

Compact(false), which is the default, will result in the following:

TOC{Items: ...}
 |
 +--- &Item{Title: "A", ...}
 |     |
 |     +--- &Item{Title: "", ...}
 |           |
 |           +--- &Item{Title: "B", ...}
 |                 |
 |                 +--- &Item{Title: "C"}
 |
 +--- &Item{Title: "D", ...}
       |
       +--- &Item{Title: "", ...}
             |
             +--- &Item{Title: "", ...}
                   |
                   +--- &Item{Title: "E", ...}

Whereas, Compact(true) will result in the following:

TOC{Items: ...}
 |
 +--- &Item{Title: "A", ...}
 |     |
 |     +--- &Item{Title: "B", ...}
 |           |
 |           +--- &Item{Title: "C"}
 |
 +--- &Item{Title: "D", ...}
       |
       +--- &Item{Title: "E", ...}

Notice that the empty items have been removed and the generated TOC is more compact.

func MaxDepth

func MaxDepth(depth int) InspectOption

MaxDepth limits the depth of the table of contents. Headings with a level greater than the specified depth will be ignored.

For example, given the following:

# Foo
## Bar
### Baz
# Quux
## Qux

MaxDepth(1) will result in the following:

TOC{Items: ...}
 |
 +--- &Item{Title: "Foo", ID: "foo"}
 |
 +--- &Item{Title: "Quux", ID: "quux", Items: ...}

Whereas, MaxDepth(2) will result in the following:

TOC{Items: ...}
 |
 +--- &Item{Title: "Foo", ID: "foo", Items: ...}
 |     |
 |     +--- &Item{Title: "Bar", ID: "bar"}
 |
 +--- &Item{Title: "Quux", ID: "quux", Items: ...}
       |
       +--- &Item{Title: "Qux", ID: "qux"}

A value of 0 or less will result in no limit.

The default is no limit.

func MinDepth

func MinDepth(depth int) InspectOption

MinDepth limits the depth of the table of contents. Headings with a level lower than the specified depth will be ignored.

For example, given the following:

# Foo
## Bar
### Baz
# Quux
## Qux

MinDepth(3) will result in the following:

TOC{Items: ...}
 |
 +--- &Item{Title: "Baz", ID: "baz"}

Whereas, MinDepth(2) will result in the following:

TOC{Items: ...}
 |
 +--- &Item{Title: "Bar", ID: "bar", Items: ...}
 |     |
 |     +--- &Item{Title: "Baz", ID: "baz"}
 |
 +--- &Item{Title: "Qux", ID: "qux"}

A value of 0 or less will result in no limit.

The default is no limit.

type Item

type Item struct {
	// Title of this item in the table of contents.
	//
	// This may be blank for items that don't refer to a heading, and only
	// have sub-items.
	Title []byte

	// ID is the identifier for the heading that this item refers to. This
	// is the fragment portion of the link without the "#".
	//
	// This may be blank if the item doesn't have an id assigned to it, or
	// if it doesn't have a title.
	//
	// Enable AutoHeadingID in your parser if you expected these to be set
	// but they weren't.
	ID []byte

	// Items references children of this item.
	//
	// For a heading at level 3, Items, contains the headings at level 4
	// under that section.
	Items Items
}

Item is a single item in the table of contents.

type Items

type Items []*Item

Items is a list of items in a table of contents.

type ListRenderer

type ListRenderer struct {
	// Marker for elements of the list, e.g. '-', '*', etc.
	//
	// Defaults to '*'.
	Marker byte
}

ListRenderer builds a nested list from a table of contents.

For example,

# Foo
## Bar
## Baz
# Qux

// becomes

- Foo
  - Bar
  - Baz
- Qux

func (*ListRenderer) Render

func (r *ListRenderer) Render(toc *TOC) ast.Node

Render renders the table of contents into Markdown.

If the TOC is nil or empty, nil is returned. Do not call Goldmark's renderer if the returned node is nil.

type TOC

type TOC struct {
	// Items holds the top-level headings under the table of contents.
	//
	// Items is empty if there are no headings in the document.
	Items Items
}

TOC is the table of contents. It's the top-level object under which the rest of the table of contents resides.

func Inspect

func Inspect(n ast.Node, src []byte, options ...InspectOption) (*TOC, error)

Inspect builds a table of contents by inspecting the provided document.

The table of contents is represents as a tree where each item represents a heading or a heading level with zero or more children. The returned TOC will be empty if there are no headings in the document.

For example,

# Section 1
## Subsection 1.1
## Subsection 1.2
# Section 2
## Subsection 2.1
# Section 3

Will result in the following items.

TOC{Items: ...}
 |
 +--- &Item{Title: "Section 1", ID: "section-1", Items: ...}
 |     |
 |     +--- &Item{Title: "Subsection 1.1", ID: "subsection-1-1"}
 |     |
 |     +--- &Item{Title: "Subsection 1.2", ID: "subsection-1-2"}
 |
 +--- &Item{Title: "Section 2", ID: "section-2", Items: ...}
 |     |
 |     +--- &Item{Title: "Subsection 2.1", ID: "subsection-2-1"}
 |
 +--- &Item{Title: "Section 3", ID: "section-3"}

You may analyze or manipulate the table of contents before rendering it.

type Transformer

type Transformer struct {
	// Title is the title of the table of contents section.
	// Defaults to "Table of Contents" if unspecified.
	Title string

	// TitleDepth is the heading depth for the Title.
	// Defaults to 1 (<h1>) if unspecified.
	TitleDepth int

	// MinDepth is the minimum depth of the table of contents.
	// See the documentation for MinDepth for more information.
	MinDepth int

	// MaxDepth is the maximum depth of the table of contents.
	// See the documentation for MaxDepth for more information.
	MaxDepth int

	// ListID is the id for the list of TOC items rendered in the HTML.
	//
	// For example, if ListID is "toc", the table of contents will be
	// rendered as:
	//
	//	<ul id="toc">
	//	  ...
	//	</ul>
	//
	// The HTML element does not have an ID if ListID is empty.
	ListID string

	// TitleID is the id for the Title heading rendered in the HTML.
	//
	// For example, if TitleID is "toc-title",
	// the title will be rendered as:
	//
	//	<h1 id="toc-title">Table of Contents</h1>
	//
	// If TitleID is empty, a value will be requested
	// from the Goldmark Parser.
	TitleID string

	// Compact controls whether empty items should be removed
	// from the table of contents.
	// See the documentation for Compact for more information.
	Compact bool
}

Transformer is a Goldmark AST transformer adds a TOC to the top of a Markdown document.

To use this, either install the Extender on the goldmark.Markdown object, or install the AST transformer on the Markdown parser like so.

markdown := goldmark.New(...)
markdown.Parser().AddOptions(
  parser.WithAutoHeadingID(),
  parser.WithASTTransformers(
    util.Prioritized(&toc.Transformer{}, 100),
  ),
)

NOTE: Unless you've supplied your own parser.IDs implementation, you'll need to enable the WithAutoHeadingID option on the parser to generate IDs and links for headings.

func (*Transformer) Transform

func (t *Transformer) Transform(doc *ast.Document, reader text.Reader, ctx parser.Context)

Transform adds a table of contents to the provided Markdown document.

Errors encountered while transforming are ignored. For more fine-grained control, use Inspect and transform the document manually.