Fix section linking and editing

This commit is contained in:
2026-10-04 16:01:47 +02:00
parent 06da56d0a0
commit c456e35c76
3 changed files with 57 additions and 12 deletions
+24 -11
View File
@@ -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
+32
View File
@@ -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)
}
}
+1 -1
View File
@@ -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
}