Repository: gohugoio/hugo (language: go)

Issue:
Improve .Page.Fragments.ToHTML
The [`Fragments.ToHTML`](https://github.com/gohugoio/hugo/blob/487bb96474363070a9b5b22ce4640f80329e91e4/markup/tableofcontents/tableofcontents.go#L136-L149) method on `Page` takes three arguments:

```text
func (toc *Fragments) ToHTML(startLevel, stopLevel int, ordered bool) template.HTML {
  ...
}
```

It would be convenient to pass site and/or page parameters to this method, something like:

```text
{{ .Fragments.ToHTML (.Param "toc.startLevel") (.Param "toc.endLevel") true }}
```

The above throws an error with JSON and TOML site/page parameters because the values are unmarshaled to `float64` (JSON) or `int64` (TOML). You can work around this with:

```text
{{ .Fragments.ToHTML (.Param "toc.startLevel" | int) (.Param "toc.endLevel" | int) true }}
```

But it would easier if the method signature were...

```text
func (toc *Fragments) ToHTML(startLevel, stopLevel any, ordered bool) template.HTML {
  ...
}
```

... with `cast.ToIntE`, etc.







Discussion / hints:

Fix patch:
diff --git a/hugolib/page__content.go b/hugolib/page__content.go
index b5527a28103..f7579f182f8 100644
--- a/hugolib/page__content.go
+++ b/hugolib/page__content.go
@@ -730,16 +730,15 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
 		isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
 
 		if !isHTML {
-			createAndSetToC := func(tocProvider converter.TableOfContentsProvider) {
+			createAndSetToC := func(tocProvider converter.TableOfContentsProvider) error {
 				cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
 				ct.tableOfContents = tocProvider.TableOfContents()
-				ct.tableOfContentsHTML = template.HTML(
-					ct.tableOfContents.ToHTML(
-						cfg.TableOfContents.StartLevel,
-						cfg.TableOfContents.EndLevel,
-						cfg.TableOfContents.Ordered,
-					),
+				ct.tableOfContentsHTML, err = ct.tableOfContents.ToHTML(
+					cfg.TableOfContents.StartLevel,
+					cfg.TableOfContents.EndLevel,
+					cfg.TableOfContents.Ordered,
 				)
+				return err
 			}
 
 			// If the converter supports doing the parsing separately, we do that.
diff --git a/markup/tableofcontents/tableofcontents.go b/markup/tableofcontents/tableofcontents.go
index 49a9cdeb79c..560e421b7e8 100644
--- a/markup/tableofcontents/tableofcontents.go
+++ b/markup/tableofcontents/tableofcontents.go
@@ -14,11 +14,13 @@
 package tableofcontents
 
 import (
+	"fmt"
 	"html/template"
 	"sort"
 	"strings"
 
 	"github.com/gohugoio/hugo/common/collections"
+	"github.com/spf13/cast"
 )
 
 // Empty is an empty ToC.
@@ -133,19 +135,30 @@ func (toc *Fragments) addAt(h *Heading, row, level int) {
 }
 
 // ToHTML renders the ToC as HTML.
-func (toc *Fragments) ToHTML(startLevel, stopLevel int, ordered bool) template.HTML {
+func (toc *Fragments) ToHTML(startLevel, stopLevel any, ordered bool) (template.HTML, error) {
 	if toc == nil {
-		return ""
+		return "", nil
 	}
+
+	iStartLevel, err := cast.ToIntE(startLevel)
+	if err != nil {
+		return "", fmt.Errorf("startLevel: %w", err)
+	}
+
+	iStopLevel, err := cast.ToIntE(stopLevel)
+	if err != nil {
+		return "", fmt.Errorf("stopLevel: %w", err)
+	}
+
 	b := &tocBuilder{
 		s:          strings.Builder{},
 		h:          toc.Headings,
-		startLevel: startLevel,
-		stopLevel:  stopLevel,
+		startLevel: iStartLevel,
+		stopLevel:  iStopLevel,
 		ordered:    ordered,
 	}
 	b.Build()
-	return template.HTML(b.s.String())
+	return template.HTML(b.s.String()), nil
 }
 
 func (toc Fragments) walk(fn func(*Heading)) {


Test patch:
diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go
index 6048bce397a..a35441affe1 100644
--- a/markup/goldmark/convert_test.go
+++ b/markup/goldmark/convert_test.go
@@ -208,8 +208,8 @@ unsafe = true
 
 	toc, ok := b.(converter.TableOfContentsProvider)
 	c.Assert(ok, qt.Equals, true)
-	tocString := string(toc.TableOfContents().ToHTML(1, 2, false))
-	c.Assert(tocString, qt.Contains, "TableOfContents")
+	tocHTML, _ := toc.TableOfContents().ToHTML(1, 2, false)
+	c.Assert(string(tocHTML), qt.Contains, "TableOfContents")
 }
 
 func TestConvertAutoIDAsciiOnly(t *testing.T) {
diff --git a/markup/tableofcontents/tableofcontents_integration_test.go b/markup/tableofcontents/tableofcontents_integration_test.go
index 87a7c010854..e6ae03ce2d3 100644
--- a/markup/tableofcontents/tableofcontents_integration_test.go
+++ b/markup/tableofcontents/tableofcontents_integration_test.go
@@ -14,6 +14,7 @@
 package tableofcontents_test
 
 import (
+	"strings"
 	"testing"
 
 	"github.com/gohugoio/hugo/hugolib"
@@ -43,3 +44,80 @@ disableKinds = ['page','rss','section','sitemap','taxonomy','term']
 		"heading-l5|5|Heading L5",
 	)
 }
+
+// Issue #13107
+func TestToHTMLArgTypes(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ['home','section','rss','sitemap','taxonomy','term']
+-- layouts/_default/single.html --
+{{ .Fragments.ToHTML .Params.toc.startLevel .Params.toc.endLevel false }}
+-- content/json.md --
+{
+  "title": "json",
+  "params": {
+    "toc": {
+      "startLevel": 2,
+      "endLevel": 4
+    }
+  }
+}
+CONTENT
+-- content/toml.md --
++++
+title = 'toml'
+[params.toc]
+startLevel = 2
+endLevel = 4
++++
+CONTENT
+-- content/yaml.md --
+---
+title: yaml
+params:
+  toc:
+    startLevel: 2
+    endLevel: 4
+---
+CONTENT
+`
+
+	content := `
+# Level One
+## Level Two
+### Level Three
+#### Level Four
+##### Level Five
+###### Level Six
+	`
+
+	want := `
+<nav id="TableOfContents">
+  <ul>
+    <li><a href="#level-two">Level Two</a>
+      <ul>
+        <li><a href="#level-three">Level Three</a>
+          <ul>
+            <li><a href="#level-four">Level Four</a></li>
+          </ul>
+        </li>
+      </ul>
+    </li>
+  </ul>
+</nav>
+`
+
+	files = strings.ReplaceAll(files, "CONTENT", content)
+
+	b := hugolib.Test(t, files)
+	b.AssertFileContentEquals("public/json/index.html", strings.TrimSpace(want))
+	b.AssertFileContentEquals("public/toml/index.html", strings.TrimSpace(want))
+	b.AssertFileContentEquals("public/yaml/index.html", strings.TrimSpace(want))
+
+	files = strings.ReplaceAll(files, `2`, `"x"`)
+
+	b, _ = hugolib.TestE(t, files)
+	b.AssertLogMatches(`error calling ToHTML: startLevel: unable to cast "x" of type string`)
+}
diff --git a/markup/tableofcontents/tableofcontents_test.go b/markup/tableofcontents/tableofcontents_test.go
index 3af9c4eb664..9ec7ec293ae 100644
--- a/markup/tableofcontents/tableofcontents_test.go
+++ b/markup/tableofcontents/tableofcontents_test.go
@@ -45,7 +45,8 @@ func TestToc(t *testing.T) {
 	toc.addAt(&Heading{Title: "1-H3-1", ID: "1-h2-2"}, 0, 2)
 	toc.addAt(&Heading{Title: "Heading 2", ID: "h1-2"}, 1, 0)
 
-	got := string(toc.ToHTML(1, -1, false))
+	tocHTML, _ := toc.ToHTML(1, -1, false)
+	got := string(tocHTML)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
   <ul>
     <li><a href="#h1-1">Heading 1</a>
@@ -62,7 +63,8 @@ func TestToc(t *testing.T) {
   </ul>
 </nav>`, qt.Commentf(got))
 
-	got = string(toc.ToHTML(1, 1, false))
+	tocHTML, _ = toc.ToHTML(1, 1, false)
+	got = string(tocHTML)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
   <ul>
     <li><a href="#h1-1">Heading 1</a></li>
@@ -70,7 +72,8 @@ func TestToc(t *testing.T) {
   </ul>
 </nav>`, qt.Commentf(got))
 
-	got = string(toc.ToHTML(1, 2, false))
+	tocHTML, _ = toc.ToHTML(1, 2, false)
+	got = string(tocHTML)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
   <ul>
     <li><a href="#h1-1">Heading 1</a>
@@ -83,7 +86,8 @@ func TestToc(t *testing.T) {
   </ul>
 </nav>`, qt.Commentf(got))
 
-	got = string(toc.ToHTML(2, 2, false))
+	tocHTML, _ = toc.ToHTML(2, 2, false)
+	got = string(tocHTML)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
   <ul>
     <li><a href="#1-h2-1">1-H2-1</a></li>
@@ -91,7 +95,8 @@ func TestToc(t *testing.T) {
   </ul>
 </nav>`, qt.Commentf(got))
 
-	got = string(toc.ToHTML(1, -1, true))
+	tocHTML, _ = toc.ToHTML(1, -1, true)
+	got = string(tocHTML)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
   <ol>
     <li><a href="#h1-1">Heading 1</a>
@@ -118,7 +123,8 @@ func TestTocMissingParent(t *testing.T) {
 	toc.addAt(&Heading{Title: "H3", ID: "h3"}, 1, 2)
 	toc.addAt(&Heading{Title: "H3", ID: "h3"}, 1, 2)
 
-	got := string(toc.ToHTML(1, -1, false))
+	tocHTML, _ := toc.ToHTML(1, -1, false)
+	got := string(tocHTML)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
   <ul>
     <li>
@@ -139,7 +145,8 @@ func TestTocMissingParent(t *testing.T) {
   </ul>
 </nav>`, qt.Commentf(got))
 
-	got = string(toc.ToHTML(3, 3, false))
+	tocHTML, _ = toc.ToHTML(3, 3, false)
+	got = string(tocHTML)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
   <ul>
     <li><a href="#h3">H3</a></li>
@@ -147,7 +154,8 @@ func TestTocMissingParent(t *testing.T) {
   </ul>
 </nav>`, qt.Commentf(got))
 
-	got = string(toc.ToHTML(1, -1, true))
+	tocHTML, _ = toc.ToHTML(1, -1, true)
+	got = string(tocHTML)
 	c.Assert(got, qt.Equals, `<nav id="TableOfContents">
   <ol>
     <li>