Repository: gohugoio/hugo (language: go)

Issue:
Add Level to the Heading struct
#### Use Case 1

Building a TOC by walking `.Page.Fragments.Headings` works great, and it's easy to control the start and end levels from site and/or page params.

```text
[params.toc]
startLevel = 2
endLevel = 4
```

However, to do this, the partial needs to keep track of the current Level while walking the map, adding 1 each time it goes a level deeper. This is not difficult to do, but would be a bit easier if we could access `.Level` for each heading.

#### Use Case 2

Control whether or not a TOC is displayed based on the number of headings _of a certain level or levels_.

```text
[params.toc]
startLevel = 2
endLevel = 4
minNumHeadings = 3
```

If there are 3 or more headings of Level 2, 3, or 4 (combined), then display the TOC, otherwise don't.

I could walk the map once to get the counts, then walk it again to render, but it would be easier if I could get the counts up front. While some sort of count method would be ideal, adding `Level` would be sufficient.

```text
{{ $count := where (sort .Fragments.HeadingsMap) "Level" "in" (slice 2 3 4) | len }}
```

The example above works because `sort` [removes the first level keys](https://gohugo.io/functions/collections/sort/#first-level-key-removal).




Discussion / hints:
Yea, we can add that.

But I think what you ask for gets less obvious if you start from an arbitrary level, e.g.

```
{{ $startHeading := index .Fragments.HeadingMap "heading-on-level3" }}
// How do I  walk 2 level down from where I am now?
```

Building a ToC with this new structure would always need a recursive template, so passing `add $currentLevel 1` to the template shouldn't be too hard.
> Yea, we can add that.

I suggest waiting until a use case with more value comes along.

> add $currentLevel 1 to the template shouldn't be too hard

Agreed. 
> {{ range .Fragments.HeadingsMap }}
>   {{ $headings = $headings | append . }}
> {{ end }}

Could you show a complete example please?


```
{{ range .Fragments.HeadingsMap }}
  {{ $headings = $headings | append . }}
{{ end }}
```

```
plate failed: template: posts/single.html:19:17: executing "main" at <$headings>: undefined variable: $headings
Error: Error building site: failed to render pages: render of "page" failed: "layouts/posts/single.html:19:17": execute of template failed: template: posts/single.html:19:17: executing "main" at <$headings>: undefined variable: $headings
```

Thanks in advance!

@l1x This kind of question is better suited for https://discourse.gohugo.io/ so we can focus the discussion on @jmooring 's feature request.

However, the error you're encountering is because there's no `$headings` variable initialized, so before the `range` loop, you can just initialize it as an empty slice:

```text
{{ $headings := slice }}
{{ range .Fragments.HeadingsMap }}
  {{ $headings = $headings | append . }}
{{ end }}
```

But best to ask these question on discourse; something like "Collect headings from .Page"

Fix patch:
diff --git a/markup/goldmark/toc.go b/markup/goldmark/toc.go
index ac5040e8537..bc209adf233 100644
--- a/markup/goldmark/toc.go
+++ b/markup/goldmark/toc.go
@@ -80,6 +80,7 @@ func (t *tocTransformer) Transform(n *ast.Document, reader text.Reader, pc parse
 			id, found := heading.AttributeString("id")
 			if found {
 				tocHeading.ID = string(id.([]byte))
+				tocHeading.Level = level
 			}
 		case
 			ast.KindCodeSpan,
diff --git a/markup/tableofcontents/tableofcontents.go b/markup/tableofcontents/tableofcontents.go
index 9109ba87cc4..49a9cdeb79c 100644
--- a/markup/tableofcontents/tableofcontents.go
+++ b/markup/tableofcontents/tableofcontents.go
@@ -77,6 +77,7 @@ func (h Headings) FilterBy(fn func(*Heading) bool) Headings {
 // Heading holds the data about a heading and its children.
 type Heading struct {
 	ID    string
+	Level int
 	Title string
 
 	Headings Headings


Test patch:
diff --git a/markup/tableofcontents/integration_test.go b/markup/tableofcontents/integration_test.go
new file mode 100644
index 00000000000..a51ad3d459f
--- /dev/null
+++ b/markup/tableofcontents/integration_test.go
@@ -0,0 +1,45 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tableofcontents_test
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/hugolib"
+)
+
+// Issue #10776
+func TestHeadingsLevel(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ range .Fragments.HeadingsMap }}
+	{{ printf "%s|%d|%s" .ID .Level .Title }}
+{{ end }}
+-- content/_index.md --
+## Heading L2
+### Heading L3
+##### Heading L5
+`
+
+	b := hugolib.Test(t, files)
+	b.AssertFileContent("public/index.html",
+		"heading-l2|2|Heading L2",
+		"heading-l3|3|Heading L3",
+		"heading-l5|5|Heading L5",
+	)
+}