JSON canvas initial

This commit is contained in:
2026-09-17 17:07:04 +02:00
parent b14941b3e0
commit 7797d4a456
22 changed files with 3277 additions and 19 deletions
+40
View File
@@ -47,6 +47,7 @@ var (
pageTmpl = template.Must(template.New("page").Funcs(tmplFuncs).ParseFS(assets, "assets/layout.html", "assets/page/main.html"))
editTmpl = template.Must(template.New("edit").Funcs(tmplFuncs).ParseFS(assets, "assets/layout.html", "assets/editor/main.html"))
searchTmpl = template.Must(template.New("search").Funcs(tmplFuncs).ParseFS(assets, "assets/layout.html", "assets/search/main.html"))
canvasTmpl = template.Must(template.New("canvas").Funcs(tmplFuncs).ParseFS(assets, "assets/layout.html", "assets/canvas/main.html"))
)
// specialPage is the result returned by a pageTypeHandler.
@@ -124,6 +125,7 @@ func main() {
http.HandleFunc("/_logout", h.handleLogout)
http.HandleFunc("/_reindex", h.handleReindex)
http.HandleFunc("/_search", h.handleSearchSuggest)
http.HandleFunc("/_canvas/render", h.handleCanvasRender)
http.HandleFunc("/quickadd", h.handleQuickAdd)
http.Handle("/", h)
@@ -212,6 +214,16 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// A canvas is addressed as a view of its folder (?canvas=Name), so a direct
// hit on the file collapses onto that canonical form — the same shape as
// diary.go's virtual-URL redirects. ?raw keeps the bytes reachable.
if r.Method == http.MethodGet && strings.HasSuffix(urlPath, canvasExt) && !r.URL.Query().Has("raw") {
if target, ok := canvasViewURL(urlPath); ok {
http.Redirect(w, r, target, http.StatusFound)
return
}
}
info, err := os.Stat(fsPath)
if err != nil {
if os.IsNotExist(err) {
@@ -248,6 +260,19 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
return
}
// An explicit ?canvas= selects this page's canvas view. Checked ahead of
// the pageTypeHandler redirect loop so an explicit view request always wins
// over a folder-type redirect (e.g. a canvas kept in a diary year folder).
//
// This is deliberately not a pageTypeHandler: that interface injects content
// into the standard page body, while a canvas replaces the whole body with a
// full-bleed surface and its own controls — a view mode like ?edit, which is
// likewise a template branch here rather than a handler.
if name := r.URL.Query().Get("canvas"); name != "" {
h.serveCanvas(w, r, urlPath, fsPath, name)
return
}
for _, ph := range pageTypeHandlers {
if target, ok := ph.redirect(h.root, fsPath, urlPath, r); ok {
http.Redirect(w, r, target, http.StatusFound)
@@ -290,6 +315,14 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
rendered = renderMarkdown(rawMD)
}
// Canvases in this folder surface as a view switcher above the content, so
// a page and its diagrams read as one thing. Suppressed in the editor, which
// is about the markdown only.
var canvases []canvasRef
if !editMode {
canvases = listCanvases(fsPath, urlPath, "")
}
view, sortKey, order := readPageSettings(fsPath).viewSettings()
var entries []entry
if !editMode && (special == nil || !special.SuppressListing) {
@@ -353,6 +386,7 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
SpecialContent: specialContent,
SidebarWidget: sidebarWidget,
SuppressTOC: suppressTOC,
Canvases: canvases,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -368,6 +402,12 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fsPath string) {
query := r.URL.Query()
// Checked first so ?canvas=X&delete removes that canvas rather than falling
// through to the page-level delete below.
if name := query.Get("canvas"); name != "" {
h.handleCanvasPost(w, r, urlPath, fsPath, name)
return
}
if query.Has("delete") {
h.handleDelete(w, r, urlPath, fsPath)
return