From c456e35c76e9c3faa047da5ce32b8b2dfd2690b2 Mon Sep 17 00:00:00 2001 From: luxick Date: Sun, 4 Oct 2026 16:01:47 +0200 Subject: [PATCH] Fix section linking and editing --- canvas.go | 35 ++++++++++++++++++++++++----------- canvas_test.go | 32 ++++++++++++++++++++++++++++++++ sections.go | 2 +- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/canvas.go b/canvas.go index 9311480..155124f 100644 --- a/canvas.go +++ b/canvas.go @@ -298,8 +298,7 @@ func (h *handler) renderCanvasFile(v *canvasNodeView, n canvasNode) { if heading := extractFirstHeading(raw); heading != "" { v.Label = heading } - v.Markdown = true - v.Body = template.HTML(convertMarkdown(canvasSubpath(raw, n.Subpath), dirBase(rel))) + canvasMarkdownBody(v, raw, n.Subpath, dirBase(rel)) return } u := fileURL(rel) @@ -309,8 +308,7 @@ func (h *handler) renderCanvasFile(v *canvasNodeView, n canvasNode) { switch { case ext == ".md": raw, _ := os.ReadFile(fsPath) - v.Markdown = true - v.Body = template.HTML(convertMarkdown(canvasSubpath(raw, n.Subpath), dirBase(path.Dir("/"+rel)))) + canvasMarkdownBody(v, raw, n.Subpath, dirBase(path.Dir("/"+rel))) case isImageFile(name) || ext == ".svg" || ext == ".webp": thumb := u if hasThumbnail(name) { @@ -336,21 +334,36 @@ func dirBase(rel string) string { return "/" + rel + "/" } +// canvasMarkdownBody renders a markdown file node: the whole file, or the +// section its subpath names. A subpath that names no heading says so on the +// card rather than showing the whole file (as jsoncanvas-desktop does). +func canvasMarkdownBody(v *canvasNodeView, raw []byte, subpath, base string) { + part, ok := canvasSubpath(raw, subpath) + if !ok { + v.Body = execNodeTemplate("canvas-missing", subpath) + return + } + v.Markdown = true + v.Body = template.HTML(convertMarkdown(part, base)) +} + // canvasSubpath narrows markdown to a file node's "#Heading" subpath: that -// heading's section including its subsections. Unknown subpaths (or block -// references, "#^id") show the whole file. -func canvasSubpath(raw []byte, subpath string) []byte { +// heading's section including its subsections, matched case-insensitively +// on the heading text. Without a subpath it is the whole file; ok is false +// for a subpath that names no heading, which includes block references +// ("#^id"). +func canvasSubpath(raw []byte, subpath string) (part []byte, ok bool) { want := strings.TrimSpace(strings.TrimPrefix(subpath, "#")) - if want == "" || strings.HasPrefix(want, "^") { - return raw + if want == "" { + return raw, true } sections := splitSections(raw) for i, s := range sections { if _, text := sectionHeading(s); i > 0 && strings.EqualFold(text, want) { - return joinSections(sections[i:secionSpanEnd(sections, i)]) + return joinSections(sections[i:secionSpanEnd(sections, i)]), true } } - return raw + return nil, false } // canvasTaskCheckboxes makes a text node's task checkboxes clickable. The diff --git a/canvas_test.go b/canvas_test.go index 48c0c65..2cc7cb1 100644 --- a/canvas_test.go +++ b/canvas_test.go @@ -252,3 +252,35 @@ func TestCanvasListedWithIcon(t *testing.T) { t.Error("canvas files do not get the canvas icon") } } + +func TestCanvasSubpath(t *testing.T) { + doc := []byte("# LAN Party 2026\nIntro.\n## Teilnehmer\n| Nr. | Name |\n| --- | --- |\n| 1. | Marci |\n") + cases := []struct { + subpath, want string + ok bool + }{ + {"", string(doc), true}, + // The last section of the file runs to its end. + {"#teilnehmer", "## Teilnehmer\n| Nr. | Name |\n| --- | --- |\n| 1. | Marci |\n", true}, + // A heading takes the sections nested under it along. + {"#LAN Party 2026", string(doc), true}, + {"#Gäste", "", false}, + {"#^block", "", false}, + } + for _, c := range cases { + got, ok := canvasSubpath(doc, c.subpath) + if ok != c.ok || string(got) != c.want { + t.Errorf("canvasSubpath(%q) = %q, %v; want %q, %v", c.subpath, got, ok, c.want, c.ok) + } + } +} + +func TestSectionSpanEndAtLastSection(t *testing.T) { + sections := splitSections([]byte("# A\n## B\ntext\n")) + if end := secionSpanEnd(sections, 2); end != 3 { + t.Errorf("span of the last section ends at %d, want 3", end) + } + if end := secionSpanEnd(sections, 1); end != 3 { + t.Errorf("span of the top section ends at %d, want 3", end) + } +} diff --git a/sections.go b/sections.go index 884f80e..977e1d0 100644 --- a/sections.go +++ b/sections.go @@ -91,7 +91,7 @@ func secionSpanEnd(sections [][]byte, start int) int { if level == 0 { return start + 1 } - for i := start + 1; i <= len(sections); i++ { + for i := start + 1; i < len(sections); i++ { if l, _ := sectionHeading(sections[i]); l > 0 && l <= level { return i }