Generate ToC with Hugo rather than JavaScript (#1851)

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
pull/1506/merge
Kévin Commaille 5 months ago committed by GitHub
parent a7a7eadf2c
commit 784b8984f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -193,7 +193,7 @@ jobs:
- name: " Setup Hugo" - name: " Setup Hugo"
uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3.0.0 uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3.0.0
with: with:
hugo-version: '0.113.0' hugo-version: '0.117.0'
extended: true extended: true
- name: "📥 Source checkout" - name: "📥 Source checkout"
uses: actions/checkout@v4 uses: actions/checkout@v4

@ -4,3 +4,4 @@
IgnoreDirectoryMissingTrailingSlash: true IgnoreDirectoryMissingTrailingSlash: true
DirectoryPath: spec DirectoryPath: spec
CheckExternal: false CheckExternal: false
IgnoreInternalEmptyHash: true

@ -61,7 +61,7 @@ place after an MSC has been accepted, not as part of a proposal itself.
1. Install the extended version (often the OS default) of Hugo: 1. Install the extended version (often the OS default) of Hugo:
<https://gohugo.io/getting-started/installing>. Note that at least Hugo <https://gohugo.io/getting-started/installing>. Note that at least Hugo
v0.113.0 is required. v0.117.0 is required.
Alternatively, use the Docker image at Alternatively, use the Docker image at
https://hub.docker.com/r/klakegg/hugo/. (The "extended edition" is required https://hub.docker.com/r/klakegg/hugo/. (The "extended edition" is required

@ -0,0 +1 @@
Generate ToC with Hugo rather than JavaScript.

@ -37,6 +37,10 @@ description = "Home of the Matrix specification for decentralised communication"
weight = 30 weight = 30
[markup] [markup]
[markup.tableOfContents]
startLevel = 2
endLevel = 6
ordered = true
[markup.goldmark] [markup.goldmark]
[markup.goldmark.renderer] [markup.goldmark.renderer]
# Enables us to render raw HTML # Enables us to render raw HTML
@ -130,7 +134,7 @@ sidebar_menu_compact = true
[module] [module]
[module.hugoVersion] [module.hugoVersion]
extended = true extended = true
min = "0.113.0" min = "0.117.0"
[[module.imports]] [[module.imports]]
path = "github.com/matrix-org/docsy" path = "github.com/matrix-org/docsy"
disable = false disable = false

@ -2812,42 +2812,42 @@ operations and run in a resource constrained environment. Like embedded
applications, they are not intended to be fully-fledged communication applications, they are not intended to be fully-fledged communication
systems. systems.
{{< cs-module name="instant_messaging" >}} {{% cs-module name="instant_messaging" %}}
{{< cs-module name="rich_replies" >}} {{% cs-module name="rich_replies" %}}
{{< cs-module name="voip_events" >}} {{% cs-module name="voip_events" %}}
{{< cs-module name="typing_notifications" >}} {{% cs-module name="typing_notifications" %}}
{{< cs-module name="receipts" >}} {{% cs-module name="receipts" %}}
{{< cs-module name="read_markers" >}} {{% cs-module name="read_markers" %}}
{{< cs-module name="presence" >}} {{% cs-module name="presence" %}}
{{< cs-module name="content_repo" >}} {{% cs-module name="content_repo" %}}
{{< cs-module name="send_to_device" >}} {{% cs-module name="send_to_device" %}}
{{< cs-module name="device_management" >}} {{% cs-module name="device_management" %}}
{{< cs-module name="end_to_end_encryption" >}} {{% cs-module name="end_to_end_encryption" %}}
{{< cs-module name="secrets" >}} {{% cs-module name="secrets" %}}
{{< cs-module name="history_visibility" >}} {{% cs-module name="history_visibility" %}}
{{< cs-module name="push" >}} {{% cs-module name="push" %}}
{{< cs-module name="third_party_invites" >}} {{% cs-module name="third_party_invites" %}}
{{< cs-module name="search" >}} {{% cs-module name="search" %}}
{{< cs-module name="guest_access" >}} {{% cs-module name="guest_access" %}}
{{< cs-module name="room_previews" >}} {{% cs-module name="room_previews" %}}
{{< cs-module name="tags" >}} {{% cs-module name="tags" %}}
{{< cs-module name="account_data" >}} {{% cs-module name="account_data" %}}
{{< cs-module name="admin" >}} {{% cs-module name="admin" %}}
{{< cs-module name="event_context" >}} {{% cs-module name="event_context" %}}
{{< cs-module name="sso_login" >}} {{% cs-module name="sso_login" %}}
{{< cs-module name="dm" >}} {{% cs-module name="dm" %}}
{{< cs-module name="ignore_users" >}} {{% cs-module name="ignore_users" %}}
{{< cs-module name="stickers" >}} {{% cs-module name="stickers" %}}
{{< cs-module name="report_content" >}} {{% cs-module name="report_content" %}}
{{< cs-module name="third_party_networks" >}} {{% cs-module name="third_party_networks" %}}
{{< cs-module name="openid" >}} {{% cs-module name="openid" %}}
{{< cs-module name="server_acls" >}} {{% cs-module name="server_acls" %}}
{{< cs-module name="mentions" >}} {{% cs-module name="mentions" %}}
{{< cs-module name="room_upgrades" >}} {{% cs-module name="room_upgrades" %}}
{{< cs-module name="server_notices" >}} {{% cs-module name="server_notices" %}}
{{< cs-module name="moderation_policies" >}} {{% cs-module name="moderation_policies" %}}
{{< cs-module name="spaces" >}} {{% cs-module name="spaces" %}}
{{< cs-module name="event_replacements" >}} {{% cs-module name="event_replacements" %}}
{{< cs-module name="event_annotations" >}} {{% cs-module name="event_annotations" %}}
{{< cs-module name="threading" >}} {{% cs-module name="threading" %}}
{{< cs-module name="reference_relations" >}} {{% cs-module name="reference_relations" %}}

@ -0,0 +1,83 @@
{{/*
A modified version of the siderbar-tree.html partial in Docsy, adding:
* The "toc.html" partial at L45.
*/}}
{{/* We cache this partial for bigger sites and set the active class client side. */ -}}
{{ $sidebarCacheLimit := .Site.Params.ui.sidebar_cache_limit | default 2000 -}}
{{ $shouldDelayActive := ge (len .Site.Pages) $sidebarCacheLimit -}}
<div id="td-sidebar-menu" class="td-sidebar__inner{{ if $shouldDelayActive }} d-none{{ end }}">
{{ if not .Site.Params.ui.sidebar_search_disable -}}
<form class="td-sidebar__search d-flex align-items-center">
{{ partial "search-input.html" . }}
<button class="btn btn-link td-sidebar__toggle d-md-none p-0 ms-3 fas fa-bars" type="button" data-bs-toggle="collapse" data-bs-target="#td-section-nav" aria-controls="td-section-nav" aria-expanded="false" aria-label="Toggle section navigation">
</button>
</form>
{{ else -}}
<div id="content-mobile">
<form class="td-sidebar__search d-flex align-items-center">
{{ partial "search-input.html" . }}
<button class="btn btn-link td-sidebar__toggle d-md-none p-0 ms-3 fas fa-bars" type="button" data-bs-toggle="collapse" data-bs-target="#td-section-nav" aria-controls="td-section-nav" aria-expanded="false" aria-label="Toggle section navigation">
</button>
</form>
</div>
<div id="content-desktop"></div>
{{ end -}}
<nav class="td-sidebar-nav collapse
{{- if .Site.Params.ui.sidebar_search_disable }} td-sidebar-nav--search-disabled{{ end -}}
{{- if .Site.Params.ui.sidebar_menu_foldable }} foldable-nav{{ end -}}
" id="td-section-nav">
{{ if (gt (len .Site.Home.Translations) 0) -}}
<div class="td-sidebar-nav__section nav-item dropdown d-block d-lg-none">
{{ partial "navbar-lang-selector.html" . }}
</div>
{{ end -}}
{{ $navRoot := cond (and (ne .Params.toc_root true) (eq .Site.Home.Type "docs")) .Site.Home .FirstSection -}}
{{ $ulNr := 0 -}}
{{ $ulShow := .Site.Params.ui.ul_show | default 1 -}}
{{ $sidebarMenuTruncate := .Site.Params.ui.sidebar_menu_truncate | default 50 -}}
<ul class="td-sidebar-nav__section pe-md-3 ul-{{ $ulNr }}">
{{ template "section-tree-nav-section" (dict "page" . "section" $navRoot "shouldDelayActive" $shouldDelayActive "sidebarMenuTruncate" $sidebarMenuTruncate "ulNr" $ulNr "ulShow" (add $ulShow 1)) }}
</ul>
{{ partial "toc.html" . }}
</nav>
</div>
{{ define "section-tree-nav-section" -}}
{{ $s := .section -}}
{{ $p := .page -}}
{{ $shouldDelayActive := .shouldDelayActive -}}
{{ $sidebarMenuTruncate := .sidebarMenuTruncate -}}
{{ $treeRoot := cond (eq .ulNr 0) true false -}}
{{ $ulNr := .ulNr -}}
{{ $ulShow := .ulShow -}}
{{ $active := and (not $shouldDelayActive) (eq $s $p) -}}
{{ $activePath := and (not $shouldDelayActive) (or (eq $p $s) ($p.IsDescendant $s)) -}}
{{ $show := cond (or (lt $ulNr $ulShow) $activePath (and (not $shouldDelayActive) (eq $s.Parent $p.Parent)) (and (not $shouldDelayActive) (eq $s.Parent $p)) (not $p.Site.Params.ui.sidebar_menu_compact) (and (not $shouldDelayActive) ($p.IsDescendant $s.Parent))) true false -}}
{{ $mid := printf "m-%s" ($s.RelPermalink | anchorize) -}}
{{ $pages_tmp := where (union $s.Pages $s.Sections).ByWeight ".Params.toc_hide" "!=" true -}}
{{ $pages := $pages_tmp | first $sidebarMenuTruncate -}}
{{ $withChild := gt (len $pages) 0 -}}
{{ $manualLink := cond (isset $s.Params "manuallink") $s.Params.manualLink ( cond (isset $s.Params "manuallinkrelref") (relref $s $s.Params.manualLinkRelref) $s.RelPermalink) -}}
{{ $manualLinkTitle := cond (isset $s.Params "manuallinktitle") $s.Params.manualLinkTitle $s.Title -}}
<li class="td-sidebar-nav__section-title td-sidebar-nav__section{{ if $withChild }} with-child{{ else }} without-child{{ end }}{{ if $activePath }} active-path{{ end }}{{ if (not (or $show $p.Site.Params.ui.sidebar_menu_foldable )) }} collapse{{ end }}" id="{{ $mid }}-li">
{{ if (and $p.Site.Params.ui.sidebar_menu_foldable (ge $ulNr 1)) -}}
<input type="checkbox" id="{{ $mid }}-check"{{ if $activePath}} checked{{ end }}/>
<label for="{{ $mid }}-check"><a href="{{ $manualLink }}"{{ if ne $s.LinkTitle $manualLinkTitle }} title="{{ $manualLinkTitle }}"{{ end }}{{ with $s.Params.manualLinkTarget }} target="{{ . }}"{{ if eq . "_blank" }} rel="noopener"{{ end }}{{ end }} class="align-left ps-0 {{ if $active}} active{{ end }} td-sidebar-link{{ if $s.IsPage }} td-sidebar-link__page{{ else }} td-sidebar-link__section{{ end }}{{ if $treeRoot }} tree-root{{ end }}" id="{{ $mid }}">{{ with $s.Params.Icon}}<i class="{{ . }}"></i>{{ end }}<span class="{{ if $active }}td-sidebar-nav-active-item{{ end }}">{{ $s.LinkTitle }}</span></a></label>
{{ else -}}
<a href="{{ $manualLink }}"{{ if ne $s.LinkTitle $manualLinkTitle }} title="{{ $manualLinkTitle }}"{{ end }}{{ with $s.Params.manualLinkTarget }} target="{{ . }}"{{ if eq . "_blank" }} rel="noopener"{{ end }}{{ end }} class="align-left ps-0{{ if $active}} active{{ end }} td-sidebar-link{{ if $s.IsPage }} td-sidebar-link__page{{ else }} td-sidebar-link__section{{ end }}{{ if $treeRoot }} tree-root{{ end }}" id="{{ $mid }}">{{ with $s.Params.Icon}}<i class="{{ . }}"></i>{{ end }}<span class="{{ if $active }}td-sidebar-nav-active-item{{ end }}">{{ $s.LinkTitle }}</span></a>
{{- end }}
{{- if $withChild }}
{{- $ulNr := add $ulNr 1 }}
<ul class="ul-{{ $ulNr }}{{ if (gt $ulNr 1)}} foldable{{end}}">
{{ range $pages -}}
{{ if (not (and (eq $s $p.Site.Home) (eq .Params.toc_root true))) -}}
{{ template "section-tree-nav-section" (dict "page" $p "section" . "shouldDelayActive" $shouldDelayActive "sidebarMenuTruncate" $sidebarMenuTruncate "ulNr" $ulNr "ulShow" $ulShow) }}
{{- end }}
{{- end }}
</ul>
{{- end }}
</li>
{{- end }}

@ -0,0 +1,15 @@
{{/*
A modified version of the toc.html partial in Docsy.
*/}}
{{ $page := .Params }}
{{ if not .Params.notoc -}}
{{ with .TableOfContents -}}
<hr>
<div id="toc">
<a id="toc-title" href="#">{{ $page.Title }}</a>
{{ . }}
</div>
{{ end -}}
{{ end -}}

@ -5,6 +5,6 @@
{{ with .Page.Resources.Match "*.md" }} {{ with .Page.Resources.Match "*.md" }}
{{ range ((sort . "Params.date" "desc")) }} {{ range ((sort . "Params.date" "desc")) }}
{{ .Content }} {{ .RenderShortcodes }}
{{ end }} {{ end }}
{{ end }} {{ end }}

@ -11,6 +11,6 @@
{{ with .Site.GetPage "client-server-api/modules" }} {{ with .Site.GetPage "client-server-api/modules" }}
{{ with .Resources.GetMatch (printf "%s%s" $name ".md") }} {{ with .Resources.GetMatch (printf "%s%s" $name ".md") }}
{{ .Content }} {{ .RenderShortcodes }}
{{ end }} {{ end }}
{{ end }} {{ end }}

@ -14,174 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/*
Account for id attributes that are in the sidebar nav
*/
function populateIds() {
const navItems = document.querySelectorAll(".td-sidebar-nav li");
return Array.from(navItems).map(item => item.id).filter(id => id != "");
}
/*
Given an ID and an array of IDs, return s version of the original ID that's
not equal to any of the IDs in the array.
*/
function uniquifyHeadingId(id, uniqueIDs) {
const baseId = id;
let counter = 0;
while (uniqueIDs.includes(id)) {
counter = counter + 1;
id = baseId + "-" + counter.toString();
}
return id;
}
/*
Given an array of heading nodes, ensure they all have unique IDs.
We have to do this mostly because of client-server modules, which are
rendered separately then glued together with a template.
Because heading IDs are generated in rendering, this means they can and will
end up with duplicate IDs.
*/
function uniquifyHeadingIds(headings) {
const uniqueIDs = populateIds();
for (let heading of headings) {
const uniqueID = uniquifyHeadingId(heading.id, uniqueIDs);
uniqueIDs.push(uniqueID);
heading.id = uniqueID;
}
}
/*
The document contains "normal" headings, and these have corresponding items
in the ToC.
The document might also contain H1 headings that act as titles for blocks of
rendered data, like HTTP APIs or event schemas. Unlike "normal" headings,
these headings don't appear in the ToC. But they do have anchor IDs to enable
links to them. When someone follows a link to one of these "rendered data"
headings we want to scroll the ToC to the item corresponding to the "normal"
heading preceding the "rendered data" heading we have visited.
To support this we need to add `data` attributes to ToC items.
These attributes identify which "rendered data" headings live underneath
the heading corresponding to that ToC item.
*/
function setTocItemChildren(toc, headings) {
let tocEntryForHeading = null;
for (const heading of headings) {
// H1 headings are rendered-data headings
if (heading.tagName !== "H1") {
tocEntryForHeading = document.querySelector(`nav li a[href="#${heading.id}"]`);
} else {
// on the ToC entry for the parent heading,
// set a data-* attribute whose name is the child's fragment ID
tocEntryForHeading.setAttribute(`data-${heading.id}`, "true");
}
}
}
/*
Generate a table of contents based on the headings in the document.
*/
function makeToc() {
// make the title from the H1
const h1 = document.body.querySelector("h1");
const title = document.createElement("a");
title.id = "toc-title";
title.setAttribute("href", "#");
title.textContent = h1.textContent;
// make the content
const content = document.body.querySelector(".td-content");
let headings = [].slice.call(content.querySelectorAll("h2, h3, h4, h5, h6, .rendered-data > details > summary > h1"));
// exclude headings that don't have IDs.
headings = headings.filter(heading => heading.id);
uniquifyHeadingIds(headings);
// exclude .rendered-data > h1 headings from the ToC
const tocTargets = headings.filter(heading => heading.tagName !== "H1");
// we have to adjust heading IDs to ensure that they are unique
const nav = document.createElement("nav");
nav.id = "TableOfContents";
const section = makeTocSection(tocTargets, 0);
nav.appendChild(section.content);
// build the TOC and append to it title and content
const toc = document.createElement("div");
toc.id = "toc";
toc.appendChild(title);
toc.appendChild(nav);
// append TOC to the section navigation
const section_nav = document.body.querySelector("#td-section-nav");
let hr = document.createElement("hr");
section_nav.appendChild(hr);
section_nav.appendChild(toc);
// tell ToC items about any rendered-data headings they contain
setTocItemChildren(section.content, headings);
}
// create a single ToC entry
function makeTocEntry(heading) {
const li = document.createElement("li");
const a = document.createElement("a");
a.setAttribute("href", `#${heading.id}`);
a.textContent = heading.textContent;
li.appendChild(a);
return li;
}
/*
Each ToC section is an `<ol>` element.
ToC entries are `<li>` elements and these contain nested ToC sections,
whenever we go to the next heading level down.
*/
function makeTocSection(headings, index) {
const ol = document.createElement("ol");
let previousHeading = null;
let previousLi = null;
let i = index;
const lis = [];
for (i; i < headings.length; i++) {
const thisHeading = headings[i];
if (previousHeading && (thisHeading.tagName > previousHeading.tagName)) {
// we are going down a heading level, create a new nested section
const section = makeTocSection(headings, i);
previousLi.appendChild(section.content);
i = section.index -1;
}
else if (previousHeading && (previousHeading.tagName > thisHeading.tagName)) {
// we have come back up a level, so a section is finished
for (let li of lis) {
ol.appendChild(li);
}
return {
content: ol,
index: i
}
}
else {
// we are still processing this section, so add this heading to the current section
previousLi = makeTocEntry(thisHeading);
lis.push(previousLi);
previousHeading = thisHeading;
}
}
for (let li of lis) {
ol.appendChild(li);
}
return {
content: ol,
index: i
}
}
/* /*
Set a new ToC entry. Set a new ToC entry.
Clear any previously highlighted ToC items, set the new one, Clear any previously highlighted ToC items, set the new one,
@ -303,8 +135,6 @@ for the corresponding ToC entry.
*/ */
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
makeToc();
const toc = document.querySelector("#toc"); const toc = document.querySelector("#toc");
toc.addEventListener("click", event => { toc.addEventListener("click", event => {
if (event.target.tagName === "A") { if (event.target.tagName === "A") {

Loading…
Cancel
Save