From 02fa19272d788b595b649df10c5a719894009102 Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 15 Apr 2026 12:14:40 +0200 Subject: [PATCH] Allow editing of individual sections --- CLAUDE.md | 2 ++ assets/page.html | 4 ++++ assets/sections.js | 16 ++++++++++++++++ assets/style.css | 13 ++++++++++++- main.go | 39 ++++++++++++++++++++++++++++++++++++++- render.go | 1 + sections.go | 31 +++++++++++++++++++++++++++++++ 7 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 assets/sections.js create mode 100644 sections.go diff --git a/CLAUDE.md b/CLAUDE.md index ae4f159..b5e929f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,8 @@ Prefer separate, human-readable `.html` files over inlined HTML strings in Go. E - Do not inline JS in templates or merge unrelated features into one file - `ALT+SHIFT` is the modifier for all keyboard shortcuts — do not introduce others - Editor toolbar buttons use `data-action` + `data-key`; adding `data-key` auto-registers the shortcut +- Prefer generic, descriptive CSS classes (`btn`, `btn-small`, `muted`, `danger`) over element-specific names (`save-button`, `cancel-button`, `form-name-input`). Use a modifier + base class pattern (`btn btn-small`) rather than one-off classes that duplicate shared styles. +- Where possible, re-use existing CSS classes ## Development Priorities diff --git a/assets/page.html b/assets/page.html index 92a1d94..0da172f 100644 --- a/assets/page.html +++ b/assets/page.html @@ -26,6 +26,7 @@
{{if .EditMode}}
+ {{if ge .SectionIndex 0}}{{end}}
@@ -62,6 +63,9 @@ {{if or .Content .SpecialContent}} {{end}} + {{if .Content}} + + {{end}} {{if .Entries}}
Contents
diff --git a/assets/sections.js b/assets/sections.js new file mode 100644 index 0000000..10382e2 --- /dev/null +++ b/assets/sections.js @@ -0,0 +1,16 @@ +(function () { + var content = document.querySelector('.content'); + if (!content) return; + 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. + headings.forEach(function (h, i) { + var a = document.createElement('a'); + a.href = '?edit§ion=' + (i + 1); + a.className = 'btn btn-small section-edit'; + a.textContent = 'edit'; + h.appendChild(a); + }); +}()); diff --git a/assets/style.css b/assets/style.css index 3313e25..311c045 100644 --- a/assets/style.css +++ b/assets/style.css @@ -104,6 +104,13 @@ header { color: #ffd54f; } +/* === Button modifiers === */ +.btn-small { + font-size: 0.65rem; + font-weight: normal; + vertical-align: middle; +} + /* === Main === */ main { max-width: 860px; @@ -331,7 +338,6 @@ textarea:focus { box-shadow: 0 0 5px #0a0; } - /* === Diary views === */ .diary-section { margin: 2rem 0; @@ -380,6 +386,11 @@ textarea:focus { margin-bottom: 0.75rem; } +/* === Section edit links === */ +.section-edit { + margin-left: 0.75rem; +} + /* === Empty state === */ .empty { padding: 1rem; diff --git a/main.go b/main.go index a6e96d6..50c624f 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "os" "path" "path/filepath" + "strconv" "strings" ) @@ -123,6 +124,16 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa indexPath := filepath.Join(fsPath, "index.md") rawMD, _ := os.ReadFile(indexPath) + // Determine section index (-1 = whole page). + sectionIndex := -1 + if editMode { + if s := r.URL.Query().Get("section"); s != "" { + if n, err := strconv.Atoi(s); err == nil && n >= 0 { + sectionIndex = n + } + } + } + var rendered template.HTML if len(rawMD) > 0 && !editMode { rendered = renderMarkdown(rawMD) @@ -152,13 +163,22 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa specialContent = special.Content } + rawContent := string(rawMD) + if editMode && sectionIndex >= 0 { + sections := splitSections(rawMD) + if sectionIndex < len(sections) { + rawContent = string(sections[sectionIndex]) + } + } + data := pageData{ Title: title, Crumbs: buildCrumbs(urlPath), CanEdit: true, EditMode: editMode, + SectionIndex: sectionIndex, PostURL: urlPath, - RawContent: string(rawMD), + RawContent: rawContent, Content: rendered, Entries: entries, SpecialContent: specialContent, @@ -177,6 +197,23 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs } content := r.FormValue("content") indexPath := filepath.Join(fsPath, "index.md") + + // If a section index was submitted, splice the edited section back into + // the full file rather than replacing the whole document. + if s := r.FormValue("section"); s != "" { + sectionIndex, err := strconv.Atoi(s) + if err != nil || sectionIndex < 0 { + http.Error(w, "bad section", http.StatusBadRequest) + return + } + rawMD, _ := os.ReadFile(indexPath) + sections := splitSections(rawMD) + if sectionIndex < len(sections) { + sections[sectionIndex] = []byte(content) + } + content = string(joinSections(sections)) + } + if strings.TrimSpace(content) == "" { if err := os.Remove(indexPath); err != nil && !os.IsNotExist(err) { http.Error(w, "delete failed: "+err.Error(), http.StatusInternalServerError) diff --git a/render.go b/render.go index b417060..52aeea8 100644 --- a/render.go +++ b/render.go @@ -32,6 +32,7 @@ type pageData struct { Crumbs []crumb CanEdit bool EditMode bool + SectionIndex int // -1 = whole page; >=0 = section being edited PostURL string RawContent string Content template.HTML diff --git a/sections.go b/sections.go new file mode 100644 index 0000000..bca603b --- /dev/null +++ b/sections.go @@ -0,0 +1,31 @@ +package main + +import ( + "bytes" + "regexp" +) + +var sectionHeadingRe = regexp.MustCompile(`(?m)^#{1,6} `) + +// 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. +func splitSections(raw []byte) [][]byte { + locs := sectionHeadingRe.FindAllIndex(raw, -1) + if len(locs) == 0 { + return [][]byte{raw} + } + sections := make([][]byte, 0, len(locs)+1) + prev := 0 + for _, loc := range locs { + sections = append(sections, raw[prev:loc[0]]) + prev = loc[0] + } + sections = append(sections, raw[prev:]) + return sections +} + +// joinSections reassembles sections produced by splitSections. +func joinSections(sections [][]byte) []byte { + return bytes.Join(sections, nil) +}