JSON canvas initial
This commit is contained in:
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -82,16 +83,20 @@ func (h *handler) handleMove(w http.ResponseWriter, r *http.Request, srcURL, src
|
||||
// link.
|
||||
rewritten := map[string][]byte{}
|
||||
if updateLinks {
|
||||
needle := []byte("[[" + oldPath)
|
||||
walkErr := walkIndexFiles(h.root, func(fsPath string) error {
|
||||
// rewriteInto reads a file, hands it to rewrite, and writes the result
|
||||
// back atomically — recording the original bytes so every file touched
|
||||
// in this phase can be rolled back together.
|
||||
rewriteInto := func(fsPath string, needle []byte, rewrite func([]byte) ([]byte, bool)) error {
|
||||
orig, err := os.ReadFile(fsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Cheap reject before parsing: a file that never mentions the old
|
||||
// path cannot contain a reference to it.
|
||||
if !bytes.Contains(orig, needle) {
|
||||
return nil
|
||||
}
|
||||
updated, changed := rewriteWikiLinks(orig, oldPath, newPath)
|
||||
updated, changed := rewrite(orig)
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
@@ -100,7 +105,26 @@ func (h *handler) handleMove(w http.ResponseWriter, r *http.Request, srcURL, src
|
||||
}
|
||||
rewritten[fsPath] = orig
|
||||
return nil
|
||||
}
|
||||
|
||||
linkNeedle := []byte("[[" + oldPath)
|
||||
walkErr := walkIndexFiles(h.root, func(fsPath string) error {
|
||||
return rewriteInto(fsPath, linkNeedle, func(b []byte) ([]byte, bool) {
|
||||
return rewriteWikiLinks(b, oldPath, newPath)
|
||||
})
|
||||
})
|
||||
if walkErr == nil {
|
||||
// Canvases reference pages too: as [[wikilinks]] inside text nodes
|
||||
// and as the path of a file node. Skipping them would leave a move
|
||||
// silently breaking every canvas that pointed at the moved page —
|
||||
// the failure mode an auto-rewriting link system exists to prevent.
|
||||
// The needle is the bare path because a canvas holds it unbracketed.
|
||||
walkErr = walkCanvasFiles(h.root, func(fsPath string) error {
|
||||
return rewriteInto(fsPath, []byte(oldPath), func(b []byte) ([]byte, bool) {
|
||||
return rewriteCanvasLinks(b, oldPath, newPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
if walkErr != nil {
|
||||
rollbackRewrites(rewritten)
|
||||
http.Error(w, "rewrite failed: "+walkErr.Error(), http.StatusInternalServerError)
|
||||
@@ -258,15 +282,8 @@ func rewriteWikiLinks(content []byte, oldPath, newPath string) ([]byte, bool) {
|
||||
if parts == nil {
|
||||
return match
|
||||
}
|
||||
target := strings.TrimSpace(string(parts[1]))
|
||||
normTarget := normalizeMovePath(target)
|
||||
var newTarget string
|
||||
switch {
|
||||
case normTarget == oldPath:
|
||||
newTarget = newPath
|
||||
case strings.HasPrefix(normTarget, oldPath+"/"):
|
||||
newTarget = newPath + strings.TrimPrefix(normTarget, oldPath)
|
||||
default:
|
||||
newTarget, ok := rewriteMovedPath(strings.TrimSpace(string(parts[1])), oldPath, newPath)
|
||||
if !ok {
|
||||
return match
|
||||
}
|
||||
changed = true
|
||||
@@ -279,6 +296,67 @@ func rewriteWikiLinks(content []byte, oldPath, newPath string) ([]byte, bool) {
|
||||
return out, changed
|
||||
}
|
||||
|
||||
// rewriteMovedPath maps a wiki path onto its position after a move, matching
|
||||
// either the moved path itself or anything beneath it. ok=false means the path
|
||||
// is unaffected. Shared by the wiki-link rewriter and the canvas rewriter so
|
||||
// both agree on what "under the moved page" means.
|
||||
func rewriteMovedPath(target, oldPath, newPath string) (string, bool) {
|
||||
norm := normalizeMovePath(target)
|
||||
switch {
|
||||
case norm == oldPath:
|
||||
return newPath, true
|
||||
case strings.HasPrefix(norm, oldPath+"/"):
|
||||
return newPath + strings.TrimPrefix(norm, oldPath), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// rewriteCanvasLinks updates every reference to a moved page inside a canvas:
|
||||
// the [[wikilinks]] in text nodes, the target of a file node, and a group's
|
||||
// background image. An unparseable canvas is left exactly as it is rather than
|
||||
// half-rewritten — a move is not the moment to start editing a broken file.
|
||||
//
|
||||
// The result is re-marshalled from the parsed document, so it is normalised the
|
||||
// same way a save through the editor would normalise it.
|
||||
func rewriteCanvasLinks(content []byte, oldPath, newPath string) ([]byte, bool) {
|
||||
doc, err := parseCanvas(content)
|
||||
if err != nil {
|
||||
return content, false
|
||||
}
|
||||
changed := false
|
||||
for i := range doc.Nodes {
|
||||
n := &doc.Nodes[i]
|
||||
switch n.Type {
|
||||
case canvasNodeText:
|
||||
if updated, ok := rewriteWikiLinks([]byte(n.Text), oldPath, newPath); ok {
|
||||
n.Text = string(updated)
|
||||
changed = true
|
||||
}
|
||||
case canvasNodeFile:
|
||||
if target, ok := rewriteMovedPath(n.File, oldPath, newPath); ok {
|
||||
n.File = target
|
||||
changed = true
|
||||
}
|
||||
case canvasNodeGroup:
|
||||
if n.Background == "" {
|
||||
continue
|
||||
}
|
||||
if target, ok := rewriteMovedPath(n.Background, oldPath, newPath); ok {
|
||||
n.Background = target
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return content, false
|
||||
}
|
||||
out, err := json.MarshalIndent(doc, "", "\t")
|
||||
if err != nil {
|
||||
return content, false
|
||||
}
|
||||
return append(out, '\n'), true
|
||||
}
|
||||
|
||||
// rollbackRewrites restores the given files to their pre-rewrite contents.
|
||||
// Errors are logged; best-effort since we're already in a failure path.
|
||||
func rollbackRewrites(rewritten map[string][]byte) {
|
||||
@@ -319,6 +397,35 @@ func walkIndexFiles(root string, visit func(fsPath string) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// walkCanvasFiles visits every `*.canvas` under root, skipping hidden entries
|
||||
// (which excludes the `.<name>.canvas.bak` backups). Same shape and same
|
||||
// reasoning as walkIndexFiles: no stat per regular file.
|
||||
func walkCanvasFiles(root string, visit func(fsPath string) error) error {
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(root, name)
|
||||
if e.IsDir() {
|
||||
if err := walkCanvasFiles(full, visit); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isCanvasFile(name) {
|
||||
if err := visit(full); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFileAtomic writes data to a temp file in the same directory as path
|
||||
// and renames it into place so readers never observe a partial file.
|
||||
func writeFileAtomic(path string, data []byte, perm os.FileMode) error {
|
||||
|
||||
Reference in New Issue
Block a user