merge
parent
552a3f46ff
commit
63caf9ed03
|
|
@ -0,0 +1,51 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ID string
|
||||||
|
|
||||||
|
func NewID(s string) ID {
|
||||||
|
return ID(path.Clean(s)).withClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id ID) Push(child string) ID {
|
||||||
|
return ID(path.Join(id.String(), child)).withClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id ID) Pop() ID {
|
||||||
|
pid := path.Clean(ID(path.Dir(id.String())).withClean().String())
|
||||||
|
if strings.HasPrefix(pid, ".") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return NewID(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id ID) URLSafeString() string {
|
||||||
|
splits := strings.Split(string(id), "/")
|
||||||
|
for i := range splits {
|
||||||
|
splits[i] = url.PathEscape(splits[i])
|
||||||
|
}
|
||||||
|
return strings.Join(splits, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id ID) String() string {
|
||||||
|
return string(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id ID) withClean() ID {
|
||||||
|
splits := strings.Split(id.String(), string([]rune{os.PathSeparator}))
|
||||||
|
for i := range splits {
|
||||||
|
splits[i] = strings.Trim(splits[i], string([]rune{os.PathSeparator}))
|
||||||
|
splits[i] = strings.Trim(splits[i], "/")
|
||||||
|
t, err := url.PathUnescape(splits[i])
|
||||||
|
if err == nil {
|
||||||
|
splits[i] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ID(path.Join(splits...))
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"local/simpleserve/simpleserve"
|
"local/simpleserve/simpleserve"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -201,7 +200,6 @@ func (server *Server) putContentHandler(filePath string, w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) uiSearchHandler(w http.ResponseWriter, r *http.Request) error {
|
func (server *Server) uiSearchHandler(w http.ResponseWriter, r *http.Request) error {
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/ui/files")
|
|
||||||
t, err := server.uiSubTemplates()
|
t, err := server.uiSubTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -238,9 +236,8 @@ func (server *Server) uiSearchHandler(w http.ResponseWriter, r *http.Request) er
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) error {
|
func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) error {
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/ui/files")
|
id := NewID(strings.TrimPrefix(r.URL.Path, "/ui/files"))
|
||||||
id := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/")
|
if id == "" {
|
||||||
if len(id) == 0 || (len(id) == 1 && id[0] == "") {
|
|
||||||
return server.rootHandler(w, r)
|
return server.rootHandler(w, r)
|
||||||
}
|
}
|
||||||
t, err := server.uiSubTemplates()
|
t, err := server.uiSubTemplates()
|
||||||
|
|
@ -261,25 +258,25 @@ func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var parent Leaf
|
var parent Leaf
|
||||||
if len(id) > 1 {
|
if id.Pop() != "" {
|
||||||
parent, err = tree.Get(id[:len(id)-1])
|
parent, err = tree.Get(id.Pop())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to get pid %q: %v", id.Pop(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
leaf, err := tree.Get(id)
|
leaf, err := tree.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(id) > 1 && parent.Title == "" {
|
if id.Pop() != "" {
|
||||||
return err
|
return fmt.Errorf("failed to get id %q: %v", id, err)
|
||||||
}
|
}
|
||||||
leaf.Title = "My New File"
|
leaf.Title = "My New File"
|
||||||
}
|
}
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"This": map[string]string{
|
"This": map[string]interface{}{
|
||||||
"Title": leaf.Title,
|
"Title": leaf.Title,
|
||||||
"Content": leaf.Content,
|
"Content": leaf.Content,
|
||||||
"ID": ID,
|
"ID": id,
|
||||||
"PID": ID.Pop(),
|
"PID": id.Pop(),
|
||||||
"PTitle": parent.Title,
|
"PTitle": parent.Title,
|
||||||
},
|
},
|
||||||
"Tree": string(branchesJSON),
|
"Tree": string(branchesJSON),
|
||||||
|
|
@ -362,20 +359,20 @@ func (server *Server) apiV0FilesPostHandler(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
pid := server.fileId(r)
|
pid := server.fileId(r)
|
||||||
id := append(pid, strings.Split(uuid.New().String(), "-")[0])
|
id := NewID(pid).Push(strings.Split(uuid.New().String(), "-")[0])
|
||||||
if err := server.tree().Put(id, Leaf{Title: r.Header.Get("Title"), Content: string(b)}); err != nil {
|
if err := server.tree().Put(id, Leaf{Title: r.Header.Get("Title"), Content: string(b)}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return json.NewEncoder(w).Encode(map[string]map[string]string{
|
return json.NewEncoder(w).Encode(map[string]map[string]string{
|
||||||
"data": map[string]string{
|
"data": map[string]string{
|
||||||
"filePath": path.Join("/api/v0/files/", server.urlFileId(id)),
|
"filePath": path.Join("/api/v0/files/", id.URLSafeString()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) apiV0FilesIDGetHandler(w http.ResponseWriter, r *http.Request) error {
|
func (server *Server) apiV0FilesIDGetHandler(w http.ResponseWriter, r *http.Request) error {
|
||||||
id := server.fileId(r)
|
id := NewID(server.fileId(r))
|
||||||
if len(id) == 0 || id[0] == "" {
|
if id.String() == "" {
|
||||||
return fmt.Errorf("no id found: %+v", id)
|
return fmt.Errorf("no id found: %+v", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,8 +390,8 @@ func (server *Server) apiV0FilesIDGetHandler(w http.ResponseWriter, r *http.Requ
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) apiV0FilesIDDelHandler(w http.ResponseWriter, r *http.Request) error {
|
func (server *Server) apiV0FilesIDDelHandler(w http.ResponseWriter, r *http.Request) error {
|
||||||
id := server.fileId(r)
|
id := NewID(server.fileId(r))
|
||||||
if len(id) == 0 || id[0] == "" {
|
if id.String() == "" {
|
||||||
return fmt.Errorf("no id found: %+v", id)
|
return fmt.Errorf("no id found: %+v", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,32 +406,19 @@ func (server *Server) apiV0FilesIDDelHandler(w http.ResponseWriter, r *http.Requ
|
||||||
return server.tree().Put(id, leaf)
|
return server.tree().Put(id, leaf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) urlFileId(id []string) string {
|
func (server *Server) fileId(r *http.Request) string {
|
||||||
if len(id) == 0 {
|
return strings.Trim(
|
||||||
return ""
|
strings.TrimPrefix(
|
||||||
}
|
strings.Trim(r.URL.Path, "/"),
|
||||||
result := id[0]
|
"api/v0/files",
|
||||||
for i := 1; i < len(id); i++ {
|
),
|
||||||
result = strings.Join([]string{result, url.PathEscape(id[i])}, "/")
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) fileId(r *http.Request) []string {
|
|
||||||
return strings.Split(
|
|
||||||
strings.Trim(
|
|
||||||
strings.TrimPrefix(
|
|
||||||
strings.Trim(r.URL.Path, "/"),
|
|
||||||
"api/v0/files",
|
|
||||||
),
|
|
||||||
"/"),
|
|
||||||
"/",
|
"/",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) apiV0FilesIDPutHandler(w http.ResponseWriter, r *http.Request) error {
|
func (server *Server) apiV0FilesIDPutHandler(w http.ResponseWriter, r *http.Request) error {
|
||||||
id := server.fileId(r)
|
id := NewID(server.fileId(r))
|
||||||
if len(id) == 0 || id[0] == "" {
|
if id.String() == "" {
|
||||||
return fmt.Errorf("no id found: %+v", id)
|
return fmt.Errorf("no id found: %+v", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,7 +441,7 @@ func (server *Server) apiV0FilesIDPutHandler(w http.ResponseWriter, r *http.Requ
|
||||||
}
|
}
|
||||||
return json.NewEncoder(w).Encode(map[string]map[string]string{
|
return json.NewEncoder(w).Encode(map[string]map[string]string{
|
||||||
"data": map[string]string{
|
"data": map[string]string{
|
||||||
"filePath": path.Join("/api/v0/files/", server.urlFileId(id)),
|
"filePath": path.Join("/api/v0/files/", id.URLSafeString()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -496,21 +480,23 @@ func (server *Server) _apiV0SearchHandler(query string) ([][2]string, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result := [][2]string{}
|
result := [][2]string{}
|
||||||
if err := tree.ForEach(func(id []string, leaf Leaf) error {
|
if err := tree.ForEach(func(id ID, leaf Leaf) error {
|
||||||
for _, pattern := range patterns {
|
for _, pattern := range patterns {
|
||||||
if !pattern.MatchString(leaf.Content) && !pattern.MatchString(leaf.Title) {
|
if !pattern.MatchString(leaf.Content) && !pattern.MatchString(leaf.Title) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
title := leaf.Title
|
title := leaf.Title
|
||||||
for i := len(id) - 1; i >= 1; i-- {
|
pid := id.Pop()
|
||||||
parent, err := server.tree().Get(id[:i])
|
for pid != "" {
|
||||||
|
parent, err := server.tree().Get(pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
title = path.Join(parent.Title, title)
|
title = path.Join(parent.Title, title)
|
||||||
|
pid = pid.Pop()
|
||||||
}
|
}
|
||||||
result = append(result, [2]string{server.urlFileId(id), title})
|
result = append(result, [2]string{id.URLSafeString(), title})
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,11 @@ func TestServerRoutes(t *testing.T) {
|
||||||
ensureAndWrite(server.diskMediaPath("delid"), []byte("hi"))
|
ensureAndWrite(server.diskMediaPath("delid"), []byte("hi"))
|
||||||
|
|
||||||
tree := server.tree()
|
tree := server.tree()
|
||||||
if err := tree.Put([]string{"getfid"}, Leaf{Title: "", Content: "getfid body"}); err != nil {
|
if err := tree.Put(ID("getfid"), Leaf{Title: "", Content: "getfid body"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
tree.Put([]string{"putfid"}, Leaf{Title: "putfid title", Content: "initial putfid body"})
|
tree.Put(ID("putfid"), Leaf{Title: "putfid title", Content: "initial putfid body"})
|
||||||
tree.Put([]string{"delfid"}, Leaf{Title: "delfid title", Content: "delfid body"})
|
tree.Put(ID("delfid"), Leaf{Title: "delfid title", Content: "delfid body"})
|
||||||
t.Log(tree.GetRoot())
|
t.Log(tree.GetRoot())
|
||||||
|
|
||||||
ensureAndWrite(path.Join(server.root, "index.html"), []byte("mom"))
|
ensureAndWrite(path.Join(server.root, "index.html"), []byte("mom"))
|
||||||
|
|
@ -151,7 +151,7 @@ func TestServerPutTreeGetFile(t *testing.T) {
|
||||||
if err := server.Routes(); err != nil {
|
if err := server.Routes(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
server.tree().Put([]string{"my pid"}, Leaf{})
|
server.tree().Put(ID("my pid"), Leaf{})
|
||||||
var id string
|
var id string
|
||||||
t.Run("put to create an id", func(t *testing.T) {
|
t.Run("put to create an id", func(t *testing.T) {
|
||||||
r := httptest.NewRequest(http.MethodPut, "/my%20pid/my-put-id", strings.NewReader("body"))
|
r := httptest.NewRequest(http.MethodPut, "/my%20pid/my-put-id", strings.NewReader("body"))
|
||||||
|
|
@ -210,6 +210,23 @@ func TestServerPutTreeGetFile(t *testing.T) {
|
||||||
if !bytes.Contains(w.Body.Bytes(), []byte(`{"Title":"my title","Deleted":false,"Content":"`)) {
|
if !bytes.Contains(w.Body.Bytes(), []byte(`{"Title":"my title","Deleted":false,"Content":"`)) {
|
||||||
t.Fatal(w)
|
t.Fatal(w)
|
||||||
}
|
}
|
||||||
|
var branch Branch
|
||||||
|
if err := json.NewDecoder(w.Body).Decode(&branch); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("TODO: %+v", branch)
|
||||||
|
if branch.Leaf != (Leaf{}) {
|
||||||
|
t.Error(branch.Leaf)
|
||||||
|
}
|
||||||
|
if parent, ok := branch.Branches["my pid"]; !ok {
|
||||||
|
t.Error(ok, branch)
|
||||||
|
} else if parent.Leaf.Title != "Untitled" {
|
||||||
|
t.Error(parent.Leaf)
|
||||||
|
} else if child, ok := parent.Branches[NewID(id)]; !ok {
|
||||||
|
t.Error(ok, NewID("my pid").Push(id), parent)
|
||||||
|
} else if child.Leaf.Title != "my title" {
|
||||||
|
t.Error(child.Leaf)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
t.Run("get", func(t *testing.T) {
|
t.Run("get", func(t *testing.T) {
|
||||||
r := httptest.NewRequest(http.MethodGet, "/"+url.PathEscape(id), nil)
|
r := httptest.NewRequest(http.MethodGet, "/"+url.PathEscape(id), nil)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ todo:
|
||||||
- delete button does nothing
|
- delete button does nothing
|
||||||
- do not rewrite .md title vs. link cause hrefs to ./gobs.md wont work
|
- do not rewrite .md title vs. link cause hrefs to ./gobs.md wont work
|
||||||
- https://codepen.io/bisserof/pen/nrMveb
|
- https://codepen.io/bisserof/pen/nrMveb
|
||||||
- breadcrumb; https://concisecss.com/documentation/ui
|
|
||||||
- alert box; https://concisecss.com/documentation/ui
|
- alert box; https://concisecss.com/documentation/ui
|
||||||
- scrape odo
|
- scrape odo
|
||||||
- only one scroll bar
|
- only one scroll bar
|
||||||
|
|
@ -28,3 +27,4 @@ done:
|
||||||
- preview default via q param
|
- preview default via q param
|
||||||
- only 1 pid link in tree as title
|
- only 1 pid link in tree as title
|
||||||
- fix images
|
- fix images
|
||||||
|
- breadcrumb; https://concisecss.com/documentation/ui
|
||||||
|
|
|
||||||
|
|
@ -10,37 +10,23 @@ import (
|
||||||
|
|
||||||
type Branch struct {
|
type Branch struct {
|
||||||
Leaf Leaf
|
Leaf Leaf
|
||||||
Branches map[string]Branch
|
Branches map[ID]Branch
|
||||||
}
|
}
|
||||||
|
|
||||||
func (branch Branch) IsZero() bool {
|
func (branch Branch) IsZero() bool {
|
||||||
return branch.Leaf == (Leaf{}) && len(branch.Branches) == 0
|
return branch.Leaf == (Leaf{}) && len(branch.Branches) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (branch Branch) Find(baseId string) ([]string, bool) {
|
func (branch Branch) ForEach(foo func(ID, Leaf) error) error {
|
||||||
if _, ok := branch.Branches[baseId]; ok {
|
return branch.forEach(NewID(""), foo)
|
||||||
return []string{baseId}, true
|
|
||||||
}
|
|
||||||
for pid, child := range branch.Branches {
|
|
||||||
if subids, ok := child.Find(baseId); ok {
|
|
||||||
return append([]string{pid}, subids...), true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (branch Branch) ForEach(foo func([]string, Leaf) error) error {
|
func (branch Branch) forEach(preid ID, foo func(ID, Leaf) error) error {
|
||||||
return branch.forEach([]string{}, foo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (branch Branch) forEach(preid []string, foo func([]string, Leaf) error) error {
|
|
||||||
if err := foo(preid, branch.Leaf); err != nil {
|
if err := foo(preid, branch.Leaf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
postid := append(preid, "")
|
|
||||||
for id, child := range branch.Branches {
|
for id, child := range branch.Branches {
|
||||||
postid[len(postid)-1] = id
|
if err := child.forEach(preid.Push(string(id)), foo); err != nil {
|
||||||
if err := child.forEach(postid, foo); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,31 +66,23 @@ func (tree Tree) WithRoot(root string) Tree {
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) Find(baseId string) ([]string, bool) {
|
|
||||||
root, err := tree.GetRoot()
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return root.Find(baseId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree Tree) GetRootMeta() (Branch, error) {
|
func (tree Tree) GetRootMeta() (Branch, error) {
|
||||||
return tree.getRoot(false, false)
|
return tree.getRoot(NewID(""), false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) GetRoot() (Branch, error) {
|
func (tree Tree) GetRoot() (Branch, error) {
|
||||||
if !tree.cachedRoot.IsZero() {
|
if !tree.cachedRoot.IsZero() {
|
||||||
return tree.cachedRoot, nil
|
return tree.cachedRoot, nil
|
||||||
}
|
}
|
||||||
got, err := tree.getRoot(true, false)
|
got, err := tree.getRoot(NewID(""), true, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tree.cachedRoot = got
|
tree.cachedRoot = got
|
||||||
}
|
}
|
||||||
return got, err
|
return got, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) getRoot(withContent, withDeleted bool) (Branch, error) {
|
func (tree Tree) getRoot(pid ID, withContent, withDeleted bool) (Branch, error) {
|
||||||
m := Branch{Branches: map[string]Branch{}}
|
m := Branch{Branches: map[ID]Branch{}}
|
||||||
entries, err := os.ReadDir(tree.root)
|
entries, err := os.ReadDir(tree.root)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -127,25 +105,25 @@ func (tree Tree) getRoot(withContent, withDeleted bool) (Branch, error) {
|
||||||
}
|
}
|
||||||
} else if entry.IsDir() {
|
} else if entry.IsDir() {
|
||||||
subtree := tree.WithRoot(path.Join(tree.root, entry.Name()))
|
subtree := tree.WithRoot(path.Join(tree.root, entry.Name()))
|
||||||
if branch, err := subtree.getRoot(withContent, withDeleted); err != nil {
|
if branch, err := subtree.getRoot(pid.Push(entry.Name()), withContent, withDeleted); err != nil {
|
||||||
return Branch{}, err
|
return Branch{}, err
|
||||||
} else if !branch.IsZero() && (!branch.Leaf.Deleted || withDeleted) {
|
} else if !branch.IsZero() && (!branch.Leaf.Deleted || withDeleted) {
|
||||||
m.Branches[entry.Name()] = branch
|
m.Branches[pid.Push(entry.Name())] = branch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) toDir(id []string) string {
|
func (tree Tree) toDir(id ID) string {
|
||||||
return path.Dir(tree.toData(id))
|
return path.Dir(tree.toData(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) toData(id []string) string {
|
func (tree Tree) toData(id ID) string {
|
||||||
return path.Join(tree.root, path.Join(id...), "data.yaml")
|
return path.Join(tree.root, string(id), "data.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) Put(id []string, input Leaf) error {
|
func (tree Tree) Put(id ID, input Leaf) error {
|
||||||
if _, err := os.Stat(tree.toData(id)); os.IsNotExist(err) {
|
if _, err := os.Stat(tree.toData(id)); os.IsNotExist(err) {
|
||||||
b, err := yaml.Marshal(Leaf{})
|
b, err := yaml.Marshal(Leaf{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -170,7 +148,7 @@ func (tree Tree) Put(id []string, input Leaf) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) Del(id []string) error {
|
func (tree Tree) Del(id ID) error {
|
||||||
got, err := tree.Get(id)
|
got, err := tree.Get(id)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -185,13 +163,13 @@ func (tree Tree) Del(id []string) error {
|
||||||
return tree.Put(id, got)
|
return tree.Put(id, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) HardDel(id []string) error {
|
func (tree Tree) HardDel(id ID) error {
|
||||||
os.RemoveAll(tree.toDir(id))
|
os.RemoveAll(tree.toDir(id))
|
||||||
tree.cachedRoot = Branch{}
|
tree.cachedRoot = Branch{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tree Tree) Get(id []string) (Leaf, error) {
|
func (tree Tree) Get(id ID) (Leaf, error) {
|
||||||
f, err := os.Open(tree.toData(id))
|
f, err := os.Open(tree.toData(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Leaf{}, err
|
return Leaf{}, err
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,21 @@ package main
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTreeDel(t *testing.T) {
|
func TestTreeDel(t *testing.T) {
|
||||||
tree := NewTree(t.TempDir())
|
tree := NewTree(t.TempDir())
|
||||||
if err := tree.Put([]string{"id"}, Leaf{}); err != nil {
|
if err := tree.Put(ID("id"), Leaf{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := tree.Put([]string{"id", "subid"}, Leaf{}); err != nil {
|
if err := tree.Put(ID("id/subid"), Leaf{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tree.Del([]string{"id"}); err != nil {
|
if err := tree.Del(ID("id")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if got, err := tree.Get([]string{"id"}); err != nil {
|
} else if got, err := tree.Get(ID("id")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if !got.Deleted {
|
} else if !got.Deleted {
|
||||||
t.Fatal(got)
|
t.Fatal(got)
|
||||||
|
|
@ -30,7 +29,7 @@ func TestTreeDel(t *testing.T) {
|
||||||
t.Fatal(root.Branches)
|
t.Fatal(root.Branches)
|
||||||
}
|
}
|
||||||
|
|
||||||
if root, err := tree.getRoot(false, true); err != nil {
|
if root, err := tree.getRoot(NewID(""), false, true); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if len(root.Branches) != 1 {
|
} else if len(root.Branches) != 1 {
|
||||||
t.Fatal(root.Branches)
|
t.Fatal(root.Branches)
|
||||||
|
|
@ -46,7 +45,7 @@ func TestTreeCrud(t *testing.T) {
|
||||||
t.Fatal(m)
|
t.Fatal(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tree.Del([]string{"id"}); err != nil {
|
if err := tree.Del(ID("id")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,9 +54,9 @@ func TestTreeCrud(t *testing.T) {
|
||||||
Deleted: false,
|
Deleted: false,
|
||||||
Content: "leaf content",
|
Content: "leaf content",
|
||||||
}
|
}
|
||||||
if err := tree.Put([]string{"id"}, want); err != nil {
|
if err := tree.Put(ID("id"), want); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if l, err := tree.Get([]string{"id"}); err != nil {
|
} else if l, err := tree.Get(ID("id")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if l != want {
|
} else if l != want {
|
||||||
t.Fatal(want, l)
|
t.Fatal(want, l)
|
||||||
|
|
@ -73,62 +72,3 @@ func TestTreeCrud(t *testing.T) {
|
||||||
t.Fatalf("without content == with content: \n\twith=%s\n\twout=%s", with, without)
|
t.Fatalf("without content == with content: \n\twith=%s\n\twout=%s", with, without)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBranchFind(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
input string
|
|
||||||
want []string
|
|
||||||
found bool
|
|
||||||
branch Branch
|
|
||||||
}{
|
|
||||||
"empty": {
|
|
||||||
input: "id",
|
|
||||||
want: nil,
|
|
||||||
found: false,
|
|
||||||
branch: Branch{},
|
|
||||||
},
|
|
||||||
"yes top level": {
|
|
||||||
input: "id",
|
|
||||||
want: []string{"id"},
|
|
||||||
found: true,
|
|
||||||
branch: Branch{
|
|
||||||
Branches: map[string]Branch{"id": Branch{}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"yes deep level": {
|
|
||||||
input: "subsubid",
|
|
||||||
want: []string{"id", "subid", "subsubid"},
|
|
||||||
found: true,
|
|
||||||
branch: Branch{
|
|
||||||
Branches: map[string]Branch{"id": Branch{
|
|
||||||
Branches: map[string]Branch{"subid": Branch{
|
|
||||||
Branches: map[string]Branch{"subsubid": Branch{}}}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"no but has deep levels": {
|
|
||||||
input: "notsubsubid",
|
|
||||||
want: nil,
|
|
||||||
found: false,
|
|
||||||
branch: Branch{
|
|
||||||
Branches: map[string]Branch{"id": Branch{
|
|
||||||
Branches: map[string]Branch{"subid": Branch{
|
|
||||||
Branches: map[string]Branch{"subsubid": Branch{}}}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, d := range cases {
|
|
||||||
c := d
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
got, found := c.branch.Find(c.input)
|
|
||||||
if found != c.found {
|
|
||||||
t.Error(c.found, found)
|
|
||||||
}
|
|
||||||
if path.Join(got...) != path.Join(c.want...) {
|
|
||||||
t.Error(c.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
return `
|
return `
|
||||||
<div style="margin: 0; padding: 0; height: 0; width: 0;" id="${name}"></div>
|
<div style="margin: 0; padding: 0; height: 0; width: 0;" id="${name}"></div>
|
||||||
<a style="flex-grow: 1;" href="${href}#${parentname}"><button style="width: 100%; text-align: left; outline: none;">${title}</button></a>
|
<a style="flex-grow: 1;" href="${href}#${parentname}"><button style="width: 100%; text-align: left; outline: none;">${title}</button></a>
|
||||||
<a href="${href}/${crypto.randomUUID().split("-")[0]}#${parentname}"><button>+</button></a>
|
<a href="${href}/${generateUUID().split("-")[0]}#${parentname}"><button>+</button></a>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
function branchesHTML(id, branches) {
|
function branchesHTML(id, branches) {
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
var out = ``
|
var out = ``
|
||||||
for(var i in branches) {
|
for(var i in branches) {
|
||||||
out += `<details open>`
|
out += `<details open>`
|
||||||
out += branchHTML((id ? id + "/" : "") + i, branches[i])
|
out += branchHTML(i, branches[i])
|
||||||
out += `</details>`
|
out += `</details>`
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
@ -66,6 +66,6 @@
|
||||||
n += 1
|
n += 1
|
||||||
return n > 0
|
return n > 0
|
||||||
}
|
}
|
||||||
drawTree(JSON.parse(`{{ .Tree }}`))
|
drawTree(JSON.parse({{ .Tree }}))
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
||||||
|
|
@ -15,48 +15,48 @@
|
||||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||||
-->
|
-->
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
}
|
}
|
||||||
.columns {
|
.columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.rows {
|
.rows {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.thic_flex {
|
.thic_flex {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
.mia {
|
.mia {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.align_left {
|
.align_left {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.tb_buffer {
|
.tb_buffer {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
.r_buffer {
|
.r_buffer {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
.l_buffer {
|
.l_buffer {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
.monospace {
|
.monospace {
|
||||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
||||||
}
|
}
|
||||||
.lil_btn {
|
.lil_btn {
|
||||||
width: initial;
|
width: initial;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
input, label, textarea {
|
input, label, textarea {
|
||||||
margin: initial;
|
margin: initial;
|
||||||
}
|
}
|
||||||
.fullscreen {
|
.fullscreen {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -66,15 +66,15 @@
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
.lr_fullscreen {
|
.lr_fullscreen {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/*max-width: 1024px;*/
|
/*max-width: 1024px;*/
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.tb_fullscreen {
|
.tb_fullscreen {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
function http(method, remote, callback, body, headers) {
|
function http(method, remote, callback, body, headers) {
|
||||||
|
|
@ -94,5 +94,21 @@
|
||||||
}
|
}
|
||||||
xmlhttp.send(body);
|
xmlhttp.send(body);
|
||||||
}
|
}
|
||||||
|
function generateUUID() { // Public Domain/MIT // https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
|
||||||
|
var d = new Date().getTime();//Timestamp
|
||||||
|
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
var r = Math.random() * 16;//random number between 0 and 16
|
||||||
|
if(d > 0){//Use timestamp until depleted
|
||||||
|
r = (d + r)%16 | 0;
|
||||||
|
d = Math.floor(d/16);
|
||||||
|
} else {//Use microseconds since page-load if supported
|
||||||
|
r = (d2 + r)%16 | 0;
|
||||||
|
d2 = Math.floor(d2/16);
|
||||||
|
}
|
||||||
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue