toc

package module
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 27, 2024 License: BSD-3-Clause Imports: 6 Imported by: 10

README

goldmark-toc

Go Reference CI codecov

goldmark-toc is an add-on for the goldmark Markdown parser that adds support for rendering a table-of-contents.

Demo: A web-based demonstration of the extension is available at https://abhinav.github.io/goldmark-toc/demo/.

Installation

go get go.abhg.dev/goldmark/toc@latest

Usage

To use goldmark-toc, import the toc package.

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

Following that, you have three options for using this package:

  • Extension: This is the easiest way to get a table of contents into your document and provides very little control over the output.

  • Transformer: This is the next easiest option and provides more control over the output.

  • Manual: This option requires the most work but also provides the most control.

Extension

To use this package as a simple Goldmark extension, install the Extender when constructing the goldmark.Markdown object.

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

This will add a "Table of Contents" section to the top of every Markdown document parsed by this Markdown object.

NOTE: The example above enables parser.WithAutoHeadingID. Without this or a custom implementation of parser.IDs, none of the headings in the document will have links generated for them.

Changing the title

If you want to use a title other than "Table of Contents", set the Title field of Extender.

&toc.Extender{
  Title: "Contents",
}

You can specify an ID for the title heading with the TitleID option.

&toc.Extender{
  Title:   "Contents",
  TitleID: "toc-header",
}
Adding an ID

If you want the rendered HTML list to include an id, set the ListID field of Extender.

&toc.Extender{
  ListID: "toc",
}

This will render:

<ul id="toc">
  <!-- ... -->
</ul>
Limiting the Table of Contents

By default, goldmark-toc will include all headers in the table of contents. If you want to limit the depth of the table of contents, use the MinDepth and MaxDepth field.

&toc.Extender{
  MinDepth: 2,
  MaxDepth: 3,
}

Headers with a level lower or higher than the specified values will not be included in the table of contents.

Compacting the Table of Contents

The Table of Contents generated by goldmark-toc matches your heading hierarchy exactly. This can be a problem if you have multiple levels of difference between items. For example, if you have the document:

# h1
### h3

goldmark-toc will generate a TOC with the equivalent of the following, resulting in an empty entry between h1 and h3.

- h1
  - <blank>
    - h3

You can use the Compact option to collapse away these intermediate items.

&toc.Extender{
  Compact: true,
}

With this option enabled, the hierarchy above will render as the equivalent of the following.

- h1
  - h3
Transformer

Installing this package as an AST Transformer provides slightly more control over the output. To use it, install the AST transformer on the Goldmark Markdown parser.

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

This will generate a "Contents" section at the top of all Markdown documents parsed by this parser.

As with the previous example, this enables parser.WithAutoHeadingID to get auto-generated heading IDs.

Manual

If you use this package manually to generate Tables of Contents, you have a lot more control over the behavior. This requires a few steps.

Parse Markdown

Parse a Markdown document with goldmark.

markdown := goldmark.New(...)
markdown.Parser().AddOptions(parser.WithAutoHeadingID())
doc := markdown.Parser().Parse(text.NewReader(src))

Note that the parser must be configured to generate IDs for headers or the headers in the table of contents won't have anything to point to. This can be accomplished by adding the parser.WithAutoHeadingID option as in the example above, or with a custom implementation of goldmark/parser.IDs by using the snippet below.

markdown := goldmark.New(...)
pctx := parser.NewContext(parser.WithIDs(ids))
doc := parser.Parse(text.NewReader(src), parser.WithContext(pctx))
Build a table of contents

After parsing a Markdown document, inspect it with toc.

tree, err := toc.Inspect(doc, src)
if err != nil {
  // handle the error
}

If you need to limit the depth of the table of contents, use the MinDepth and MaxDepth option.

tree, err := toc.Inspect(doc, src, toc.MinDepth(2), toc.MaxDepth(3))
Generate a Markdown list

You can render the table of contents into a Markdown list with toc.RenderList.

list := toc.RenderList(tree)

This builds a list representation of the table of contents to be rendered as Markdown or HTML.

You may manipulate the tree before rendering the list.

Render HTML

Finally, render this table of contents along with your Markdown document:

// Render the table of contents.
if list != nil {
    // list will be nil if the table of contents is empty
    // because there were no headings in the document.
    markdown.Renderer().Render(output, src, list)
}

// Render the document.
markdown.Renderer().Render(output, src, doc)

Alternatively, include the table of contents into your Markdown document in your desired position and render it using your Markdown renderer.

// Prepend table of contents to the front of the document.
if list != nil {
    doc.InsertBefore(doc, doc.FirstChild(), list)
}

// Render the document.
markdown.Renderer().Render(output, src, doc)
Customize TOC attributes

If you want the rendered TOC to have an id or other attributes, use Node.SetAttribute on the ast.Node returned by toc.RenderList.

For example, with the following:

list := toc.RenderList(tree)
list.SetAttribute([]byte("id"), []byte("toc"))

The output will take the form:

<ul id="toc">
  <!-- ... -->
</ul>

Documentation

Overview

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

Constants

This section is empty.

Variables

This section is empty.

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 added in v0.6.0

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 added in v0.4.0

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 added in v0.7.0

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL