Skip to content

Commit

Permalink
feat: Add MinDepth option (#45)
Browse files Browse the repository at this point in the history
Adds a new 'MinDepth' option.
This option provides the ability to restrict the minimum header depth to
include in the toc.

Adds entries to the README and an option to the demo playground for the
new feature.

Resolves #40
  • Loading branch information
meblum authored Sep 12, 2023
1 parent 44ab888 commit c37fb31
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20230911-213518.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: The new MinDepth option allows ignoring headings below the specified level.
time: 2023-09-11T21:35:18.694466278-07:00
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,16 @@ This will render:

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 `MaxDepth` field.
use the `MinDepth` and `MaxDepth` field.

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

Headers with a level higher than the specified value
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
Expand Down Expand Up @@ -210,10 +211,10 @@ if err != nil {
```

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

```go
tree, err := toc.Inspect(doc, src, toc.MaxDepth(3))
tree, err := toc.Inspect(doc, src, toc.MinDepth(2), toc.MaxDepth(3))
```

#### Generate a Markdown list
Expand Down
3 changes: 3 additions & 0 deletions demo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ func main() {
type request struct {
Markdown string
Title string
MinDepth int
MaxDepth int
Compact bool
}

func (r *request) Decode(v js.Value) {
r.Markdown = v.Get("markdown").String()
r.Title = v.Get("title").String()
r.MinDepth = v.Get("minDepth").Int()
r.MaxDepth = v.Get("maxDepth").Int()
r.Compact = v.Get("compact").Bool()
}
Expand All @@ -42,6 +44,7 @@ func formatMarkdown(req request) string {
goldmark.WithExtensions(
&toc.Extender{
Title: req.Title,
MinDepth: req.MinDepth,
MaxDepth: req.MaxDepth,
Compact: req.Compact,
},
Expand Down
25 changes: 22 additions & 3 deletions demo/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,25 @@ <h2>Input</h2>
<label for="title">Title</label>
<input id="title" type="text" value="" />

<label for="maxDepth">Maximum Depth</label>
<label for="compact">Compact</label>
<input type="checkbox" id="compact" name="compact" checked />

<br/>

Depth limit:

<label for="minDepth">Minimum</label>
<select id="minDepth" active="after">
<option value="0" selected>No limit</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>

<label for="maxDepth">Maximum</label>
<select id="maxDepth" active="after">
<option value="0" selected>No limit</option>
<option value="1">1</option>
Expand All @@ -69,8 +87,6 @@ <h2>Input</h2>
<option value="6">6</option>
</select>

<label for="compact">Compact</label>
<input type="checkbox" id="compact" name="compact" checked />
</div>

<div class="output-container">
Expand All @@ -82,19 +98,22 @@ <h2>Output</h2>

<script>
const input = document.getElementById("input");
const minDepth = document.getElementById("minDepth");
const maxDepth = document.getElementById("maxDepth");
const compact = document.getElementById("compact");
const title = document.getElementById("title");
const output = document.getElementById("output");

input.addEventListener("input", refresh);
minDepth.addEventListener("change", refresh);
maxDepth.addEventListener("change", refresh);
title.addEventListener("input", refresh);
compact.addEventListener("change", refresh);

function refresh() {
output.innerHTML = formatMarkdown({
markdown: input.value,
minDepth: parseInt(minDepth.value),
maxDepth: parseInt(maxDepth.value),
title: title.value,
compact: compact.checked,
Expand Down
8 changes: 8 additions & 0 deletions extend.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ type Extender struct {
// Defaults to "Table of Contents" if unspecified.
Title string

// 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.
Expand All @@ -58,6 +65,7 @@ func (e *Extender) Extend(md goldmark.Markdown) {
parser.WithASTTransformers(
util.Prioritized(&Transformer{
Title: e.Title,
MinDepth: e.MinDepth,
MaxDepth: e.MaxDepth,
ListID: e.ListID,
Compact: e.Compact,
Expand Down
48 changes: 48 additions & 0 deletions inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,55 @@ type InspectOption interface {
}

type inspectOptions struct {
minDepth int
maxDepth int
compact bool
}

// 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.
func MinDepth(depth int) InspectOption {
return minDepthOption(depth)
}

type minDepthOption int

func (d minDepthOption) apply(opts *inspectOptions) {
opts.minDepth = int(d)
}

func (d minDepthOption) String() string {
return fmt.Sprintf("MinDepth(%d)", int(d))
}

// MaxDepth limits the depth of the table of contents.
// Headings with a level greater than the specified depth will be ignored.
//
Expand Down Expand Up @@ -193,6 +238,9 @@ func Inspect(n ast.Node, src []byte, options ...InspectOption) (*TOC, error) {
if !ok {
return ast.WalkContinue, nil
}
if opts.minDepth > 0 && heading.Level < opts.minDepth {
return ast.WalkSkipChildren, nil
}

if opts.maxDepth > 0 && heading.Level > opts.maxDepth {
return ast.WalkSkipChildren, nil
Expand Down
47 changes: 46 additions & 1 deletion inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,49 @@ func TestInspect(t *testing.T) {
},
},
{
desc: "depth",
desc: "minDepth",
give: []string{
"# A",
"###### B",
"### C",
"##### D",
"## E",
"# F",
"# G",
},
opts: []InspectOption{MinDepth(3)},
want: Items{
item("", "",
item("", "",
item("", "",
item("", "",
item("", "",
item("B", "b")))),
item("C", "c",
item("", "",
item("D", "d"))))),
},
},
{
desc: "minDepth/compact",
give: []string{
"# A",
"###### B",
"### C",
"##### D",
"## E",
"# F",
"# G",
},
opts: []InspectOption{MinDepth(3), Compact(true)},
want: Items{
item("B", "b"),
item("C", "c",
item("D", "d")),
},
},
{
desc: "maxDepth",
give: []string{
"# A",
"###### B",
Expand Down Expand Up @@ -252,6 +294,9 @@ func TestInspectOption_String(t *testing.T) {
give InspectOption
want string
}{
{give: MinDepth(3), want: "MinDepth(3)"},
{give: MinDepth(0), want: "MinDepth(0)"},
{give: MinDepth(-1), want: "MinDepth(-1)"},
{give: MaxDepth(3), want: "MaxDepth(3)"},
{give: MaxDepth(0), want: "MaxDepth(0)"},
{give: MaxDepth(-1), want: "MaxDepth(-1)"},
Expand Down
2 changes: 2 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func TestIntegration(t *testing.T) {
Title string `yaml:"title"`
ListID string `yaml:"listID"`

MinDepth int `yaml:"minDepth"`
MaxDepth int `yaml:"maxDepth"`
Compact bool `yaml:"compact"`
}
Expand All @@ -38,6 +39,7 @@ func TestIntegration(t *testing.T) {
md := goldmark.New(
goldmark.WithExtensions(&toc.Extender{
Title: tt.Title,
MinDepth: tt.MinDepth,
MaxDepth: tt.MaxDepth,
Compact: tt.Compact,
ListID: tt.ListID,
Expand Down
28 changes: 28 additions & 0 deletions testdata/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,34 @@
<h2 id="bar">Bar</h2>
<h1 id="baz">Baz</h1>
<h3 id="qux">Qux</h3>
- desc: minDepth
minDepth: 3
give: |
# Foo
## Bar
# Baz
### Qux
want: |
<h1>Table of Contents</h1>
<ul>
<li>
<ul>
<li>
<ul>
<li>
<a href="#qux">Qux</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="foo">Foo</h1>
<h2 id="bar">Bar</h2>
<h1 id="baz">Baz</h1>
<h3 id="qux">Qux</h3>
- desc: list id
listID: my-toc
Expand Down
6 changes: 5 additions & 1 deletion transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ type Transformer struct {
// Defaults to "Table of Contents" if unspecified.
Title string

// 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
Expand Down Expand Up @@ -59,7 +63,7 @@ var _ parser.ASTTransformer = (*Transformer)(nil) // interface compliance
// Errors encountered while transforming are ignored. For more fine-grained
// control, use Inspect and transform the document manually.
func (t *Transformer) Transform(doc *ast.Document, reader text.Reader, _ parser.Context) {
toc, err := Inspect(doc, reader.Source(), MaxDepth(t.MaxDepth), Compact(t.Compact))
toc, err := Inspect(doc, reader.Source(), MinDepth(t.MinDepth), MaxDepth(t.MaxDepth), Compact(t.Compact))
if err != nil {
// There are currently no scenarios under which Inspect
// returns an error but we have to account for it anyway.
Expand Down

0 comments on commit c37fb31

Please sign in to comment.