Canvas support V1
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user