Canvas support V1

This commit is contained in:
2026-10-04 15:28:06 +02:00
parent 7749d7c237
commit 2903986f44
20 changed files with 3223 additions and 482 deletions
+52 -5
View File
@@ -7,14 +7,17 @@ import (
"net/url"
"os"
"path"
"regexp"
"sort"
"strings"
"time"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
)
var md goldmark.Markdown
@@ -129,19 +132,61 @@ var (
iconAudio = readIcon("audio")
iconArchive = readIcon("archive")
iconGeneric = readIcon("generic")
iconCanvas = readIcon("canvas")
)
// renderMarkdown converts raw markdown to trusted HTML.
func renderMarkdown(raw []byte) template.HTML {
return template.HTML(rewriteTaskCheckboxes(convertMarkdown(raw, "")))
}
// convertMarkdown renders raw markdown to HTML with the wiki's table styling
// but leaves task checkboxes as goldmark emits them (disabled), so callers
// decide how they toggle.
//
// base, when set, is the URL path (slash-terminated) relative link and embed
// destinations resolve against. Pages leave it empty: the browser resolves
// them against the page URL. Markdown shown somewhere else (a canvas file
// node showing another folder's page) passes that folder.
func convertMarkdown(raw []byte, base string) []byte {
var buf bytes.Buffer
if err := md.Convert(raw, &buf); err != nil {
return ""
doc := md.Parser().Parse(text.NewReader(raw))
if base != "" {
rebaseDestinations(doc, base)
}
if err := md.Renderer().Render(&buf, raw, doc); err != nil {
return nil
}
out := rewriteTaskCheckboxes(buf.Bytes())
// Goldmark emits a bare `<table>`; tag it so it picks up the shared
// .data-table styling with the grid modifier (per-cell borders + header).
out = bytes.ReplaceAll(out, []byte("<table>"), []byte(`<table class="data-table data-table-grid">`))
return template.HTML(out)
return bytes.ReplaceAll(buf.Bytes(), []byte("<table>"), []byte(`<table class="data-table data-table-grid">`))
}
// schemeRe matches a URL that starts with a scheme ("https:", "mailto:").
var schemeRe = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9+.-]*:`)
// rebaseDestinations prefixes every relative link and image destination in
// doc with base. Absolute paths, fragments, queries and URLs with a scheme
// stay as they are.
func rebaseDestinations(doc ast.Node, base string) {
rebase := func(dest []byte) []byte {
if len(dest) == 0 || dest[0] == '/' || dest[0] == '#' || dest[0] == '?' || schemeRe.Match(dest) {
return dest
}
return append([]byte(base), dest...)
}
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
switch n := n.(type) {
case *ast.Link:
n.Destination = rebase(n.Destination)
case *ast.Image:
n.Destination = rebase(n.Destination)
}
return ast.WalkContinue, nil
})
}
// extractFirstHeading returns the text of the first ATX heading in raw markdown,
@@ -301,6 +346,8 @@ func fileIcon(name string) template.HTML {
switch ext {
case ".md", ".pdf":
return iconDoc
case canvasExt:
return iconCanvas
case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg":
return iconImage
case ".mp4", ".mkv", ".avi", ".mov":