package main
import (
"bytes"
"fmt"
"html/template"
"net/url"
"os"
"path"
"sort"
"strings"
"time"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
var md goldmark.Markdown
// initMarkdown builds the package-level goldmark instance. Neither extension
// resolves anything against the filesystem — wiki links are search queries and
// embed destinations are relative to the page — so this needs no wiki root.
func initMarkdown() {
md = goldmark.New(
goldmark.WithExtensions(extension.GFM, extension.Table, newWikiLinkExt(), newEmbedExt()),
goldmark.WithParserOptions(parser.WithAutoHeadingID()),
goldmark.WithRendererOptions(html.WithUnsafe(), html.WithHardWraps()),
)
}
type entry struct {
Icon template.HTML
Name, URL, Meta string
// ThumbURL is set for thumbnailable files; the thumbnail view renders an
// when it is non-empty and falls back to Icon otherwise.
ThumbURL string
// IsVideo marks a thumbnail tile as a video so the grid can overlay a play
// badge. Only meaningful when ThumbURL is set.
IsVideo bool
// modTime/size carry the raw sort keys; the template only reads the
// formatted Meta string.
modTime time.Time
size int64
}
type pageData struct {
Title string
CanEdit bool
EditMode bool
IsRoot bool
SectionIndex int // -1 = whole page; >=0 = section being edited
InsertBefore int // -1 = no insert; >=0 = splice new section at this index
Span bool // causes section editing to also cover every sub-section to the next section of the same level
PostURL string
RawContent string
Content template.HTML
Entries []entry
View string // listing view style: "list" or "thumbnail"
Sort string // listing sort key: "name" / "modified" / "size"
Order string // listing sort order: "asc" / "desc"
SpecialContent template.HTML
SidebarWidget template.HTML
SuppressTOC bool
RenderMS int64
// Canvases lists every .canvas file in this folder as a view-switcher tab.
// ActiveCanvas names the one currently being viewed ("" on a normal page),
// and is what makes this page render as a canvas rather than as markdown.
Canvases []canvasRef
ActiveCanvas string
// CanvasJSON is the whole canvas document plus its server-rendered node
// HTML, inlined into the page so opening a canvas costs one request.
CanvasJSON template.JS
// CanvasError carries a parse/validation failure to the template, which
// shows it in place of the canvas instead of failing the request.
CanvasError string
}
// layoutData is the contract every view built on assets/layout.html satisfies.
// The layout is shared by pageData and searchPageData, and a method only one of
// them has fails at render time with a half-written response rather than at
// build time — so each is asserted against the interface here.
type layoutData interface {
ShowPageChrome() bool
}
var (
_ layoutData = pageData{}
_ layoutData = searchPageData{}
)
// ShowPageChrome reports whether the standard page furniture — search box,
// folder-tree rail, right sidebar, and the collapsed ACTIONS dropdown — should
// render. The editor and the canvas view both take the full width and supply
// their own controls, so both opt out.
func (d pageData) ShowPageChrome() bool {
return !d.EditMode && d.ActiveCanvas == ""
}
// Allowed values for the listing view settings. Unknown values in the file or
// a POST body fall back to the first (default) value of each set.
const (
viewList = "list"
viewThumbnail = "thumbnail"
sortName = "name"
sortModified = "modified"
sortSize = "size"
orderAsc = "asc"
orderDesc = "desc"
)
// pageSettings holds the parsed contents of a .page-settings file. View, Sort,
// and Order are always valid once parsed (defaults applied on read).
type pageSettings struct {
Type string
View string
Sort string
Order string
}
// viewSettings returns the listing view/sort/order, applying defaults when the
// receiver is nil (no .page-settings file).
func (s *pageSettings) viewSettings() (view, sortKey, order string) {
if s == nil {
return viewList, sortName, orderAsc
}
return s.View, s.Sort, s.Order
}
func validateView(v string) string {
if v == viewThumbnail {
return viewThumbnail
}
return viewList
}
func validateSort(v string) string {
switch v {
case sortModified, sortSize:
return v
default:
return sortName
}
}
func validateOrder(v string) string {
if v == orderDesc {
return orderDesc
}
return orderAsc
}
var (
iconUp = readIcon("up")
iconFolder = readIcon("folder")
iconDoc = readIcon("doc")
iconImage = readIcon("image")
iconVideo = readIcon("video")
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 {
var buf bytes.Buffer
if err := md.Convert(raw, &buf); err != nil {
return ""
}
out := rewriteTaskCheckboxes(buf.Bytes())
// Goldmark emits a bare `