Improve rendering of TOC items

The table of contents now support headings generated by shortcodes. Be sure to include an `id` if your custom partials or shortcodes include HTML headings (h1-h6). To exclude these headings from the TOC, use the attribute `fs-` to define a font size.
This commit is contained in:
Mark Dumay
2025-01-16 14:42:23 +01:00
parent 2e8b087c48
commit d28b51aa63
10 changed files with 278 additions and 41 deletions

View File

@@ -75,6 +75,38 @@
}
}
a.toc-item {
display: block;
}
a.toc-level-1 {
margin-left: 0em;
}
a.toc-level-2 {
margin-left: 1em;
}
a.toc-level-3 {
margin-left: 2em;
}
a.toc-level-4 {
margin-left: 3em;
}
a.toc-level-5 {
margin-left: 4em;
}
a.toc-level-6 {
margin-left: 5em;
}
.toc nav {
margin-bottom: 0;
}
@if $enable-dark-mode {
[data-bs-theme="dark"] {
.toc-button {

View File

@@ -14,7 +14,6 @@
"footer",
"form",
"h2",
"h3",
"head",
"hr",
"html",
@@ -252,6 +251,7 @@
"font-monospace",
"footer",
"form-control",
"fs-2",
"fs-3",
"fs-5",
"fs-6",
@@ -357,7 +357,6 @@
"p-2",
"p-3",
"p-4",
"pb-2",
"pb-3",
"pb-4",
"pb-5",
@@ -373,6 +372,7 @@
"ps-1",
"ps-3",
"pt-1",
"pt-4",
"pt-5",
"pt-md-3",
"ptw-3",
@@ -469,6 +469,8 @@
"toast-header",
"toc",
"toc-button",
"toc-item",
"toc-level-1",
"toc-panel",
"toc-sidebar",
"toggler-icon",
@@ -485,7 +487,6 @@
"w-50"
],
"ids": [
"TableOfContents",
"abbr",
"accordion",
"accordion-0",

View File

@@ -53,7 +53,7 @@
{{- $title := .title -}}
{{- $content := .content -}}
<h3>{{ $title }}</h3>
<div class="fs-3 mt-4">{{ $title }}</div>
{{ with $content }}<p>{{ . }}</p>{{ end -}}
{{- end -}}

View File

@@ -4,36 +4,60 @@
Visit gethinode.com/license for more details.
-->
<!-- Initialize arguments -->
{{ $error := false }}
<!-- Validate arguments -->
{{ if partial "utilities/IsInvalidArgs.html" (dict "structure" "toc" "args" . "group" "partial") }}
{{- errorf "partial [assets/toc-dropdown.html] - Invalid arguments" -}}
{{ $args := partial "utilities/InitArgs.html" (dict "structure" "toc" "args" . "group" "partial") }}
{{ if $args.err }}
{{ partial "utilities/LogErr.html" (dict
"partial" "assets/schema.html"
"msg" "Invalid arguments"
"details" $args.errmsg
"file" page.File
)}}
{{ $error = true }}
{{ end }}
<!-- Initialize arguments -->
{{- $page := .page -}}
{{ $items := len (findRE "<li.*?>(.|\n)*?</li>" $page.TableOfContents) -}}
<!-- Initialize local arguments -->
{{- $startLevel := or (.Site.Params.navigation.startLevel | int) 2 }}
{{- $endLevel := or (.Site.Params.navigation.endLevel | int) 3 }}
{{- $minNumHeadings := or (.Site.Params.navigation.minNumHeadings | int) 2 }}
{{ if and (not $error) (gt $items 1) -}}
<div class="d-grid gap-2 mx-auto">
{{ partial "assets/button.html" (dict
"title" (T "toc")
"color" "secondary"
"outline" "true"
"class" "toc-button"
"icon" "fas sort"
"justify" "between"
"collapse" "toc-collapse"
"order" "last"
"spacing" false
) -}}
</div>
<div class="collapse border bg-body-tertiary rounded p-1 navbar-nav-scroll" id="toc-collapse">
<div class="toc toc-panel text-body p-2">
<small>{{ $page.TableOfContents }}</small>
<!-- Main code -->
{{ if not $error }}
{{ $headings := partial "assets/toc-headings.html" $args.page }}
{{- with $headings }}
{{- if and (not $error) (ge (len .) $minNumHeadings) }}
<div class="d-grid gap-2 mx-auto">
{{ partial "assets/button.html" (dict
"title" (T "toc")
"color" "secondary"
"outline" "true"
"class" "toc-button"
"icon" "fas sort"
"justify" "between"
"collapse" "toc-collapse"
"order" "last"
"spacing" false
) -}}
</div>
</div>
{{ end -}}
<div class="collapse border bg-body-tertiary rounded p-1 navbar-nav-scroll" id="toc-collapse">
<small>
<div class="toc toc-panel text-body p-2">
{{- range . }}
{{- $attrs := dict "class" (printf "toc-item toc-level-%d" (add 1 (sub .level $startLevel))) }}
{{- with .id }}
{{- $attrs = merge $attrs (dict "href" (printf "%s#%s" $args.page.RelPermalink .)) }}
{{- end }}
<a
{{- range $k, $v := $attrs }}
{{- printf " %s=%q" $k $v | safeHTMLAttr }}
{{- end -}}
>{{ .text }}</a>
{{- end }}
</div>
</small>
</div>
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,75 @@
{{- /*
Copyright 2023 Veriphor, LLC
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
https://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.
*/}}
{{- /* Initialize. */}}
{{- $partialName := "toc-headings" }}
{{- /* Verify minimum required version. */}}
{{- $minHugoVersion := "0.125.6" }}
{{- if lt hugo.Version $minHugoVersion }}
{{- errorf "The %q partial requires Hugo v%s or later." $partialName $minHugoVersion }}
{{- end }}
{{- /* Determine content path for warning and error messages. */}}
{{- $contentPath := "" }}
{{- with .File }}
{{- $contentPath = .Path }}
{{- else }}
{{- $contentPath = .Path }}
{{- end }}
{{- /* Get configuration. */}}
{{- $startLevel := or (.Site.Params.navigation.startLevel | int) 2 }}
{{- $endLevel := or (.Site.Params.navigation.endLevel | int) 3 }}
{{- /* Get headings. */}}
{{- $headings := slice }}
{{- $ids := slice }}
{{- range findRE `(?is)<h\d.*?</h\d>` .Content }}
{{- $level := substr . 2 1 | int }}
{{- if and (ge $level $startLevel) (le $level $endLevel) }}
{{- $text := replaceRE `(?is)<h\d.*?>(.+?)</h\d>` "$1" . }}
{{- $text = trim $text " " | plainify | safeHTML }}
{{- $id := "" }}
{{- if findRE `\s+id=` . }}
{{- $id = replaceRE `(?is).+?\s+id=(?:\x22|\x27)?(.*?)(?:\x22|\x27)?[\s>].+` "$1" . }}
{{- $ids = $ids | append $id }}
{{- if not $id }}
{{- errorf "The %q partial detected that the %q heading has an empty ID attribute. See %s" $partialName $text $contentPath }}
{{- end }}
{{- else }}
{{- errorf "The %q partial detected that the %q heading does not have an ID attribute. See %s" $partialName $text $contentPath }}
{{- end }}
{{- $headings = $headings | append (dict "id" $id "level" $level "text" $text) }}
{{- end }}
{{- end }}
{{- /* Check for duplicate heading IDs. */}}
{{- $unique := slice }}
{{- $duplicates := slice }}
{{- range $ids }}
{{- if in $unique . }}
{{- $duplicates = $duplicates | append . }}
{{- else }}
{{- $unique = $unique | append . }}
{{- end }}
{{- end }}
{{- with $duplicates }}
{{- errorf "The %q partial detected duplicate heading IDs (%s) in %s" $partialName (delimit . ", ") $contentPath }}
{{- end }}
{{ return $headings }}

View File

@@ -0,0 +1,107 @@
{{- /*
Copyright 2023 Veriphor, LLC
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
https://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.
*/}}
{{- /*
Renders a table of contents by parsing rendered content.
In site configuration, set the default start level, end level, and the minimum
number of headings required to show the table of contents:
[params.toc]
startLevel = 2 # default is 2
endLevel = 3 # default is 3
minNumHeadings = 2 # default is 2
To display the table of contents on a page:
+++
title = 'Post 1'
toc = true
+++
To display the table of contents on a page, and override one or more of the
default settings:
+++
title = 'Post 1'
[toc]
startLevel = 2 # default is 2
endLevel = 3 # default is 3
minNumHeadings = 2 # default is 2
+++
Change or localize the title with a "toc_title" key in your i18n file(s).
Start with these basic CSS rules to style the table of contents:
a.toc-item {
display: block;
}
a.toc-level-1 {
margin-left: 0em;
}
a.toc-level-2 {
margin-left: 1em;
}
a.toc-level-3 {
margin-left: 2em;
}
a.toc-level-4 {
margin-left: 3em;
}
a.toc-level-5 {
margin-left: 4em;
}
a.toc-level-6 {
margin-left: 5em;
}
@context {page} .
@returns {template.HTML}
@example {{ partial "toc-parse-content.html" . }}
*/}}
{{- /* Get configuration. */}}
{{- $startLevel := or (.Site.Params.navigation.startLevel | int) 2 }}
{{- $endLevel := or (.Site.Params.navigation.endLevel | int) 3 }}
{{- $minNumHeadings := or (.Site.Params.navigation.minNumHeadings | int) 2 }}
{{- /* Initialize. */}}
{{ $headings := partial "assets/toc-headings.html" . }}
{{- /* Render */}}
{{- if .Site.Params.navigation.toc }}
{{- with $headings }}
{{- if ge (len .) $minNumHeadings }}
<strong class="d-block h6 my-2 pt-4">{{ T "toc" }}:</strong>
<nav class="toc">
{{- range . }}
{{- $attrs := dict "class" (printf "toc-item toc-level-%d" (add 1 (sub .level $startLevel))) }}
{{- with .id }}
{{- $attrs = merge $attrs (dict "href" (printf "%s#%s" $.RelPermalink .)) }}
{{- end }}
<a
{{- range $k, $v := $attrs }}
{{- printf " %s=%q" $k $v | safeHTMLAttr }}
{{- end -}}
>{{ .text }}</a>
{{- end }}
</nav>
{{- end }}
{{- end }}
{{- end }}

View File

@@ -1,5 +1,5 @@
<!--
Copyright © 2022 - 2025 The Hinode Team / Mark Dumay. All rights reserved.
Copyright © 2024 The Hinode Team / Mark Dumay. All rights reserved.
Use of this source code is governed by The MIT License (MIT) that can be found in the LICENSE file.
Visit gethinode.com/license for more details.
-->
@@ -14,10 +14,8 @@
<!-- Initialize arguments -->
{{- $page := .page -}}
{{ $items := len (findRE "<li.*?>(.|\n)*?</li>" $page.TableOfContents) -}}
<!-- Main code -->
{{ if and (not $error) (gt $items 1) -}}
<strong class="d-block h6 my-2 pb-2">{{ T "toc" }}:</strong>
{{ $page.TableOfContents }}
{{ if not $error -}}
{{ partial "assets/toc-parse-content.html" $page }}
{{ end -}}

View File

@@ -53,11 +53,11 @@
<div class="accordion-item">
{{- with $header -}}
<h2 class="accordion-header m-0" id="{{ $parent }}-heading-{{ $id }}">
<div class="accordion-header m-0 fs-2" id="{{ $parent }}-heading-{{ $id }}">
<button class="accordion-button{{ if not $show }} collapsed{{ end }}" type="button" data-bs-toggle="collapse" data-bs-target="#{{ $parent }}-item-{{ $id }}" aria-expanded="false" aria-controls="{{ $parent }}-item-{{ $id }}">
{{ . }}
</button>
</h2>
</div>
{{- end -}}
<div id="{{ $parent }}-item-{{ $id }}" class="accordion-collapse collapse{{ if $show }} show{{ end }}{{ with $class }} {{ . }}{{ end }}" aria-labelledby="{{ $parent }}-heading-{{ $id }}" data-bs-parent="#{{ $parent }}">
<div class="accordion-body">{{ $body | .Page.RenderString | safeHTML }}</div>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@gethinode/hinode",
"version": "0.28.0",
"version": "0.28.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@gethinode/hinode",
"version": "0.28.0",
"version": "0.28.1",
"license": "MIT",
"dependencies": {
"@fullhuman/postcss-purgecss": "^7.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@gethinode/hinode",
"version": "0.28.0",
"version": "0.28.1",
"description": "Hinode is a clean documentation and blog theme for Hugo, an open-source static site generator",
"keywords": [
"hugo",