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 != "" { if heading := extractFirstHeading(raw); heading != "" {
v.Label = heading v.Label = heading
} }
v.Markdown = true canvasMarkdownBody(v, raw, n.Subpath, dirBase(rel))
v.Body = template.HTML(convertMarkdown(canvasSubpath(raw, n.Subpath), dirBase(rel)))
return return
} }
u := fileURL(rel) u := fileURL(rel)
@@ -309,8 +308,7 @@ func (h *handler) renderCanvasFile(v *canvasNodeView, n canvasNode) {
switch { switch {
case ext == ".md": case ext == ".md":
raw, _ := os.ReadFile(fsPath) raw, _ := os.ReadFile(fsPath)
v.Markdown = true canvasMarkdownBody(v, raw, n.Subpath, dirBase(path.Dir("/"+rel)))
v.Body = template.HTML(convertMarkdown(canvasSubpath(raw, n.Subpath), dirBase(path.Dir("/"+rel))))
case isImageFile(name) || ext == ".svg" || ext == ".webp": case isImageFile(name) || ext == ".svg" || ext == ".webp":
thumb := u thumb := u
if hasThumbnail(name) { if hasThumbnail(name) {
@@ -336,21 +334,36 @@ func dirBase(rel string) string {
return "/" + rel + "/" 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 // canvasSubpath narrows markdown to a file node's "#Heading" subpath: that
// heading's section including its subsections. Unknown subpaths (or block // heading's section including its subsections, matched case-insensitively
// references, "#^id") show the whole file. // on the heading text. Without a subpath it is the whole file; ok is false
func canvasSubpath(raw []byte, subpath string) []byte { // 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, "#")) want := strings.TrimSpace(strings.TrimPrefix(subpath, "#"))
if want == "" || strings.HasPrefix(want, "^") { if want == "" {
return raw return raw, true
} }
sections := splitSections(raw) sections := splitSections(raw)
for i, s := range sections { for i, s := range sections {
if _, text := sectionHeading(s); i > 0 && strings.EqualFold(text, want) { 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 // 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") 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 { if level == 0 {
return start + 1 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 { if l, _ := sectionHeading(sections[i]); l > 0 && l <= level {
return i return i
} }