Improve sections

This commit is contained in:
2026-04-28 17:43:20 +02:00
parent 1f7cfd637a
commit 73a8b4f78f
6 changed files with 59 additions and 2 deletions
+11
View File
@@ -0,0 +1,11 @@
(function () {
document.querySelectorAll('.content h1, .content h2, .content h3, .content h4, .content h5, .content h6').forEach(function (h) {
if (!h.id) return;
var a = document.createElement('a');
a.href = '#' + h.id;
a.className = 'heading-anchor';
a.setAttribute('aria-label', 'Link to this section');
a.textContent = '§';
h.insertBefore(a, h.firstChild);
});
}());
+1
View File
@@ -81,6 +81,7 @@
{{end}}
{{if or .Content .SpecialContent}}
<script src="/_/content.js"></script>
<script src="/_/anchors.js"></script>
<script src="/_/toc.js"></script>
{{end}}
{{if .Content}}
+9
View File
@@ -234,6 +234,15 @@ main {
max-width: 100%;
}
.heading-anchor {
color: var(--text-muted);
margin-right: 0.4em;
font-weight: normal;
}
.heading-anchor:hover {
color: var(--primary-hover);
}
/* === File listing === */
.listing {
border: 1px solid var(--secondary);
+1 -1
View File
@@ -21,7 +21,7 @@
var a = document.createElement("a");
a.href = "#" + h.id;
var clone = h.cloneNode(true);
clone.querySelectorAll(".btn, .muted").forEach(function (el) { el.remove(); });
clone.querySelectorAll(".btn, .muted, .heading-anchor").forEach(function (el) { el.remove(); });
a.textContent = clone.textContent.trim();
li.appendChild(a);
list.appendChild(li);
+11 -1
View File
@@ -243,6 +243,7 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs
}
content := r.FormValue("content")
indexPath := filepath.Join(fsPath, "index.md")
redirectTarget := urlPath
// If a section index was submitted, splice the edited section back into
// the full file rather than replacing the whole document.
@@ -258,6 +259,15 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs
sections[sectionIndex] = []byte(content)
}
content = string(joinSections(sections))
// Section index ≥ 1 is a heading-anchored section. Redirect to its
// anchor so the user lands on the section they just saved, even if
// the heading text changed.
if sectionIndex >= 1 {
ids := headingIDs([]byte(content))
if sectionIndex-1 < len(ids) {
redirectTarget = urlPath + "#" + ids[sectionIndex-1]
}
}
}
if strings.TrimSpace(content) == "" {
@@ -275,7 +285,7 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs
return
}
}
http.Redirect(w, r, urlPath, http.StatusSeeOther)
http.Redirect(w, r, redirectTarget, http.StatusSeeOther)
}
// readPageSettings parses a .page-settings file in dir.
+26
View File
@@ -3,6 +3,9 @@ package main
import (
"bytes"
"regexp"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
var sectionHeadingRe = regexp.MustCompile(`(?m)^#{1,6} `)
@@ -25,6 +28,29 @@ func splitSections(raw []byte) [][]byte {
return sections
}
// headingIDs returns the auto-generated id of every heading in raw markdown,
// in document order. The kth heading (1-indexed) corresponds to section k from
// splitSections. Uses the package-level goldmark parser so duplicate-id
// numbering matches what the renderer emits.
func headingIDs(raw []byte) []string {
doc := md.Parser().Parse(text.NewReader(raw))
var ids []string
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if _, ok := n.(*ast.Heading); ok {
if v, ok := n.AttributeString("id"); ok {
if b, ok := v.([]byte); ok {
ids = append(ids, string(b))
}
}
}
return ast.WalkContinue, nil
})
return ids
}
// joinSections reassembles sections produced by splitSections.
// Inserts a newline between sections when a non-empty section lacks a
// trailing newline, so an edited section cannot inline the next heading.