Fix section linking and editing
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user