Improve sections
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}());
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user