Update section behavior
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
</script>
|
||||
<form id="edit-form" class="edit-form" method="POST" action="{{.PostURL}}">
|
||||
{{if ge .SectionIndex 0}}<input type="hidden" name="section" value="{{.SectionIndex}}">{{end}}
|
||||
{{if .Span}}<input type="hidden" name="span" value="1">{{end}}
|
||||
{{if ge .InsertBefore 0}}<input type="hidden" name="insert_before" value="{{.InsertBefore}}">{{end}}
|
||||
<div class="editor-toolbar">
|
||||
<button type="button" class="btn btn-tool" data-action="undo" title="Undo">↶</button>
|
||||
|
||||
@@ -4,13 +4,18 @@
|
||||
var headings = content.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
if (!headings.length) return;
|
||||
|
||||
// Section 0 is pre-heading content, editable via full-page edit.
|
||||
// Sections 1..N each start at a heading; that is the index sent to the server.
|
||||
// Skip headings that already carry a server-rendered edit link.
|
||||
headings.forEach(function (h, i) {
|
||||
if (h.querySelector('a.btn')) return;
|
||||
var a = document.createElement('a');
|
||||
a.href = '?edit§ion=' + (i + 1);
|
||||
|
||||
if (h.tagName === 'H1') {
|
||||
a.href = '?edit';
|
||||
} else if (h.tagName === 'H2') {
|
||||
a.href = '?edit§ion=' + (i + 1) + '&span';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
a.className = 'btn btn-small btn-float';
|
||||
a.textContent = 'edit';
|
||||
h.appendChild(document.createTextNode(' '))
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -208,22 +207,6 @@ func findDiaryContext(root, fsPath, urlPath string) (depth int, diaryRootFS, dia
|
||||
return 0, "", "", false
|
||||
}
|
||||
|
||||
// headingTextRe matches an ATX heading at the start of a section. The
|
||||
// heading text is everything after the `#`s and the required space, on the
|
||||
// first line.
|
||||
var headingTextRe = regexp.MustCompile(`^(#{1,6})\s+([^\n]*)`)
|
||||
|
||||
// sectionHeading returns the heading level (1..6) and trimmed text of a
|
||||
// section produced by splitSections. Returns level=0 for the pre-heading
|
||||
// section (index 0).
|
||||
func sectionHeading(section []byte) (level int, text string) {
|
||||
m := headingTextRe.FindSubmatch(section)
|
||||
if m == nil {
|
||||
return 0, ""
|
||||
}
|
||||
return len(m[1]), strings.TrimSpace(string(m[2]))
|
||||
}
|
||||
|
||||
// findSectionIndex returns the absolute section index whose heading text
|
||||
// matches target (e.g. "2026-05" or "2026-05-28"). Returns the first match.
|
||||
func findSectionIndex(sections [][]byte, target string) (int, bool) {
|
||||
|
||||
@@ -261,6 +261,7 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
|
||||
// Determine section index (-1 = whole page).
|
||||
sectionIndex := -1
|
||||
insertBefore := -1
|
||||
sectionSpan := false
|
||||
if editMode {
|
||||
if s := r.URL.Query().Get("section"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n >= 0 {
|
||||
@@ -272,6 +273,7 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
|
||||
insertBefore = n
|
||||
}
|
||||
}
|
||||
sectionSpan = r.URL.Query().Has("span")
|
||||
}
|
||||
|
||||
var special *specialPage
|
||||
@@ -323,7 +325,11 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
|
||||
} else if editMode && sectionIndex >= 0 {
|
||||
sections := splitSections(rawMD)
|
||||
if sectionIndex < len(sections) {
|
||||
rawContent = string(sections[sectionIndex])
|
||||
end := sectionIndex + 1
|
||||
if sectionSpan {
|
||||
end = secionSpanEnd(sections, sectionIndex)
|
||||
}
|
||||
rawContent = string(joinSections(sections[sectionIndex:end]))
|
||||
}
|
||||
} else if editMode && rawContent == "" && urlPath != "/" {
|
||||
rawContent = "# " + pageTitle(urlPath) + "\n\n"
|
||||
@@ -336,6 +342,7 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
|
||||
IsRoot: urlPath == "/",
|
||||
SectionIndex: sectionIndex,
|
||||
InsertBefore: insertBefore,
|
||||
Span: sectionSpan,
|
||||
PostURL: urlPath,
|
||||
RawContent: rawContent,
|
||||
Content: rendered,
|
||||
@@ -390,11 +397,7 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs
|
||||
indexPath := filepath.Join(fsPath, "index.md")
|
||||
redirectTarget := urlPath
|
||||
|
||||
// insert_before splices a new section into the file *at* index N rather
|
||||
// than replacing index N (used by the diary "create new day" flow).
|
||||
// section replaces the section at index N (used by per-section edits).
|
||||
// Exactly one of insert_before / section should be set; insert_before
|
||||
// wins if both are present.
|
||||
// handle section saving
|
||||
if s := r.FormValue("insert_before"); s != "" {
|
||||
insertIndex, err := strconv.Atoi(s)
|
||||
if err != nil || insertIndex < 0 {
|
||||
@@ -431,8 +434,18 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs
|
||||
http.Error(w, "section no longer exists — the page changed since you opened the editor", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
sections[sectionIndex] = []byte(content)
|
||||
content = string(joinSections(sections))
|
||||
|
||||
// handling of section editing that spans until the next heading of the same level
|
||||
end := sectionIndex + 1
|
||||
if r.FormValue("span") != "" {
|
||||
end = secionSpanEnd(sections, sectionIndex)
|
||||
}
|
||||
merged := make([][]byte, 0, len(sections)-(end-sectionIndex)+1)
|
||||
merged = append(merged, sections[:sectionIndex]...)
|
||||
merged = append(merged, []byte(content))
|
||||
merged = append(merged, sections[end:]...)
|
||||
content = string(joinSections(merged))
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -52,6 +52,7 @@ type pageData struct {
|
||||
IsRoot bool
|
||||
SectionIndex int // -1 = whole page; >=0 = section being edited
|
||||
InsertBefore int // -1 = no insert; >=0 = splice new section at this index
|
||||
Span bool // causes section editing to also cover every sub-section to the next section of the same level
|
||||
PostURL string
|
||||
RawContent string
|
||||
Content template.HTML
|
||||
|
||||
+34
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
@@ -10,6 +11,11 @@ import (
|
||||
|
||||
var sectionHeadingRe = regexp.MustCompile(`(?m)^#{1,6} `)
|
||||
|
||||
// headingTextRe matches an ATX heading at the start of a section. The
|
||||
// heading text is everything after the `#`s and the required space, on the
|
||||
// first line.
|
||||
var headingTextRe = regexp.MustCompile(`^(#{1,6})\s+([^\n]*)`)
|
||||
|
||||
// splitSections splits raw markdown into sections.
|
||||
// Section 0 is any content before the first heading.
|
||||
// Each subsequent section begins at a heading line and runs to the next.
|
||||
@@ -64,3 +70,31 @@ func joinSections(sections [][]byte) []byte {
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// sectionHeading returns the heading level (1..6) and trimmed text of a
|
||||
// section produced by splitSections. Returns level=0 for the pre-heading
|
||||
// section (index 0).
|
||||
func sectionHeading(section []byte) (level int, text string) {
|
||||
m := headingTextRe.FindSubmatch(section)
|
||||
if m == nil {
|
||||
return 0, ""
|
||||
}
|
||||
return len(m[1]), strings.TrimSpace(string(m[2]))
|
||||
}
|
||||
|
||||
// sectionSpanEnd returns the exclusive end index of the section span starting
|
||||
// at start: the first following section whose heading level is <= start's.
|
||||
// Sections nested deeper than start (its subsections) belong to the span, so
|
||||
// editing a `##` covers every `###`+ under it up to the next `##` or `#`.
|
||||
func secionSpanEnd(sections [][]byte, start int) int {
|
||||
level, _ := sectionHeading(sections[start])
|
||||
if level == 0 {
|
||||
return start + 1
|
||||
}
|
||||
for i := start + 1; i <= len(sections); i++ {
|
||||
if l, _ := sectionHeading(sections[i]); l > 0 && l <= level {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return len(sections)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user