From 8f2515b37a985b24bc70278409dcb80da1e8e052 Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Wed, 24 Jun 2020 19:01:51 +0500 Subject: [PATCH 1/6] Reimplement action=raw and action=getBinary --- cfg/config.go | 7 ++ fs/fs.go | 60 +++++++++++++++ fs/hypha.go | 137 ++++++++++++++++++++++++++++++++++ fs/revision.go | 17 +++++ genealogy.go => genealogy.gog | 0 handlers.go | 71 +++++++++++------- hypha.go => hypha.gog | 39 ---------- main.go | 88 +++++++++------------- render.go => render.gog | 0 revision.go => revision.gog | 13 ---- w/m/Fruit/Pineapple/1.html | 1 + w/m/Fruit/Pineapple/meta.json | 19 +++++ walk.go => walk.gog | 0 13 files changed, 317 insertions(+), 135 deletions(-) create mode 100644 fs/fs.go create mode 100644 fs/hypha.go create mode 100644 fs/revision.go rename genealogy.go => genealogy.gog (100%) rename hypha.go => hypha.gog (62%) rename render.go => render.gog (100%) rename revision.go => revision.gog (90%) create mode 100644 w/m/Fruit/Pineapple/1.html create mode 100644 w/m/Fruit/Pineapple/meta.json rename walk.go => walk.gog (100%) diff --git a/cfg/config.go b/cfg/config.go index 89cdd6c..4597353 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -8,6 +8,13 @@ import ( "path/filepath" ) +const ( + HyphaPattern = `[^\s\d:/?&\\][^:?&\\]*` + HyphaUrl = `/{hypha:` + HyphaPattern + `}` + RevisionPattern = `[\d]+` + RevQuery = `{rev:` + RevisionPattern + `}` +) + var ( WikiDir string TemplatesDir string diff --git a/fs/fs.go b/fs/fs.go new file mode 100644 index 0000000..4323340 --- /dev/null +++ b/fs/fs.go @@ -0,0 +1,60 @@ +package fs + +import ( + "github.com/bouncepaw/mycorrhiza/cfg" + "io/ioutil" + "log" + "path/filepath" + "regexp" +) + +type Storage struct { + // hypha name => path + paths map[string]string + root string +} + +// InitStorage initiates filesystem-based hypha storage. It has to be called after configuration was inited. +func InitStorage() *Storage { + s := &Storage{ + paths: make(map[string]string), + root: cfg.WikiDir, + } + s.indexHyphae(s.root) + log.Println(s.paths) + log.Printf("Indexed %v hyphae\n", len(s.paths)) + return s +} + +// hyphaName gets name of a hypha by stripping path to the hypha in `fullPath` +func hyphaName(fullPath string) string { + // {cfg.WikiDir}/{the name} + return fullPath[len(cfg.WikiDir)+1:] +} + +// indexHyphae searches for all hyphae that seem valid in `path` and saves their absolute paths to `s.paths`. This function is recursive. +func (s *Storage) indexHyphae(path string) { + nodes, err := ioutil.ReadDir(path) + if err != nil { + log.Fatal("Error when checking", path, ":", err, "; skipping") + return + } + + for _, node := range nodes { + matchesHypha, err := regexp.MatchString(cfg.HyphaPattern, node.Name()) + if err != nil { + log.Fatal("Error when matching", node.Name(), err, "\n") + return + } + switch name := filepath.Join(path, node.Name()); { + case matchesHypha && node.IsDir(): + s.indexHyphae(name) + case node.Name() == "meta.json" && !node.IsDir(): + log.Printf("%v seems to be a hypha, adding it to the list\n", path) + s.paths[hyphaName(path)] = path + } + } +} + +func (h *Hypha) Close() { +} diff --git a/fs/hypha.go b/fs/hypha.go new file mode 100644 index 0000000..559457a --- /dev/null +++ b/fs/hypha.go @@ -0,0 +1,137 @@ +package fs + +import ( + "encoding/json" + "errors" + "io/ioutil" + "log" + "net/http" + "path/filepath" + "strconv" + + "github.com/bouncepaw/mycorrhiza/cfg" +) + +type Hypha struct { + Exists bool `json:"-"` + FullName string `json:"-"` + ViewCount int `json:"views"` + Deleted bool `json:"deleted"` + Revisions map[string]*Revision `json:"revisions"` + actual *Revision `json:"-"` +} + +func (s *Storage) Open(name string) (*Hypha, error) { + h := &Hypha{ + Exists: true, + FullName: name, + } + path, ok := s.paths[name] + // This hypha does not exist yet + if !ok { + log.Println("Hypha", name, "does not exist") + h.Exists = false + h.Revisions = make(map[string]*Revision) + } else { + metaJsonText, err := ioutil.ReadFile(filepath.Join(path, "meta.json")) + if err != nil { + log.Fatal(err) + return nil, err + } + + err = json.Unmarshal(metaJsonText, &h) + if err != nil { + log.Fatal(err) + return nil, err + } + // fill in rooted paths to content files and full names + for idStr, rev := range h.Revisions { + rev.FullName = filepath.Join(h.parentName(), rev.ShortName) + rev.Id, _ = strconv.Atoi(idStr) + if rev.BinaryName != "" { + rev.BinaryPath = filepath.Join(path, rev.BinaryName) + } + rev.TextPath = filepath.Join(path, rev.TextName) + } + + err = h.OnRevision("0") + return h, err + } + log.Println(h) + return h, nil +} + +func (h *Hypha) parentName() string { + return filepath.Dir(h.FullName) +} + +func (h *Hypha) metaJsonPath() string { + return filepath.Join(cfg.WikiDir, h.FullName, "meta.json") +} + +// OnRevision tries to change to a revision specified by `id`. +func (h *Hypha) OnRevision(id string) error { + if len(h.Revisions) == 0 { + return errors.New("This hypha has no revisions") + } + if id == "0" { + id = h.NewestId() + } + // Revision must be there, so no error checking + if rev, _ := h.Revisions[id]; true { + h.actual = rev + } + return nil +} + +// NewestId finds the largest id among all revisions. +func (h *Hypha) NewestId() string { + var largest int + for k, _ := range h.Revisions { + id, _ := strconv.Atoi(k) + if id > largest { + largest = id + } + } + return strconv.Itoa(largest) +} + +func (h *Hypha) PlainLog(s string) { + log.Println(h.FullName, h.actual.Id, s) +} + +func (h *Hypha) mimeTypeForActionRaw() string { + // If text mime type is text/html, it is not good as it will be rendered. + if h.actual.TextMime == "text/html" { + return "text/plain" + } + return h.actual.TextMime +} + +// ActionRaw is used with `?action=raw`. +// It writes text content of the revision without any parsing or rendering. +func (h *Hypha) ActionRaw(w http.ResponseWriter) { + fileContents, err := ioutil.ReadFile(h.actual.TextPath) + if err != nil { + log.Fatal(err) + return + } + w.Header().Set("Content-Type", h.mimeTypeForActionRaw()) + w.WriteHeader(http.StatusOK) + w.Write(fileContents) + h.PlainLog("Serving raw text") +} + +// ActionGetBinary is used with `?action=getBinary`. +// It writes contents of binary content file. +func (h *Hypha) ActionGetBinary(w http.ResponseWriter) { + fileContents, err := ioutil.ReadFile(h.actual.BinaryPath) + if err != nil { + log.Fatal(err) + return + } + w.Header().Set("Content-Type", h.actual.BinaryMime) + w.WriteHeader(http.StatusOK) + w.Write(fileContents) + h.PlainLog("Serving raw text") +} diff --git a/fs/revision.go b/fs/revision.go new file mode 100644 index 0000000..67f0346 --- /dev/null +++ b/fs/revision.go @@ -0,0 +1,17 @@ +package fs + +type Revision struct { + Id int `json:"-"` + FullName string `json:"-"` + Tags []string `json:"tags"` + ShortName string `json:"name"` + Comment string `json:"comment"` + Author string `json:"author"` + Time int `json:"time"` + TextMime string `json:"text_mime"` + BinaryMime string `json:"binary_mime"` + TextPath string `json:"-"` + BinaryPath string `json:"-"` + TextName string `json:"text_name"` + BinaryName string `json:"binary_name"` +} diff --git a/genealogy.go b/genealogy.gog similarity index 100% rename from genealogy.go rename to genealogy.gog diff --git a/handlers.go b/handlers.go index fab7b0f..4479ea1 100644 --- a/handlers.go +++ b/handlers.go @@ -1,65 +1,75 @@ package main import ( - "io/ioutil" "log" + // "io/ioutil" + // "log" "net/http" - "path/filepath" - "strconv" - "strings" - "time" + // "path/filepath" + // "strconv" + // "strings" + // "time" - "github.com/bouncepaw/mycorrhiza/cfg" + "github.com/bouncepaw/mycorrhiza/fs" "github.com/gorilla/mux" ) // There are handlers below. See main() for their usage. // Boilerplate code present in many handlers. Good to have it. -func HandlerBase(w http.ResponseWriter, rq *http.Request) (Revision, bool) { +func HandlerBase(w http.ResponseWriter, rq *http.Request) (*fs.Hypha, bool) { vars := mux.Vars(rq) - revno := RevInMap(vars) - return GetRevision(vars["hypha"], revno) -} - -func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) { - if rev, ok := HandlerBase(w, rq); ok { - rev.ActionGetBinary(w) + h, err := hs.Open(vars["hypha"]) + if err != nil { + log.Println(err) + return nil, false } + err = h.OnRevision(RevInMap(vars)) + if err != nil { + log.Println(err) + } + + log.Println(*h) + return h, true } func HandlerRaw(w http.ResponseWriter, rq *http.Request) { - if rev, ok := HandlerBase(w, rq); ok { - rev.ActionRaw(w) + log.Println("?action=raw") + if h, ok := HandlerBase(w, rq); ok { + h.ActionRaw(w) } } +func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) { + log.Println("?action=getBinary") + if h, ok := HandlerBase(w, rq); ok { + h.ActionGetBinary(w) + } +} + +/* func HandlerZen(w http.ResponseWriter, rq *http.Request) { - if rev, ok := HandlerBase(w, rq); ok { - rev.ActionZen(w) + if h, ok := HandlerBase(w, rq); ok { + h.ActionZen(w) } } func HandlerView(w http.ResponseWriter, rq *http.Request) { - if rev, ok := HandlerBase(w, rq); ok { - rev.ActionView(w, HyphaPage) - } else { // Hypha does not exist - log.Println("Hypha does not exist, showing 404") - w.WriteHeader(http.StatusNotFound) - w.Write([]byte(Hypha404(mux.Vars(rq)["hypha"]))) + if h, ok := HandlerBase(w, rq); ok { + h.ActionView(w, HyphaPage) } } -func HandlerHistory(w http.ResponseWriter, rq *http.Request) { - w.WriteHeader(http.StatusNotImplemented) - log.Println("Attempt to access an unimplemented thing") -} - func HandlerEdit(w http.ResponseWriter, rq *http.Request) { vars := mux.Vars(rq) ActionEdit(vars["hypha"], w) } +func HandlerHistory(w http.ResponseWriter, rq *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + log.Println("Attempt to access an unimplemented thing") +} + func HandlerRewind(w http.ResponseWriter, rq *http.Request) { w.WriteHeader(http.StatusNotImplemented) log.Println("Attempt to access an unimplemented thing") @@ -74,7 +84,9 @@ func HandlerRename(w http.ResponseWriter, rq *http.Request) { w.WriteHeader(http.StatusNotImplemented) log.Println("Attempt to access an unimplemented thing") } +*/ +/* // makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant. // Expected output for string above: []string{"foo", "bar", "kek"} func makeTagsSlice(responseTagsString string) (ret []string) { @@ -211,3 +223,4 @@ func HandlerUpdate(w http.ResponseWriter, rq *http.Request) { d := map[string]string{"Name": h.FullName} w.Write([]byte(renderFromMap(d, "updateOk.html"))) } +*/ diff --git a/hypha.go b/hypha.gog similarity index 62% rename from hypha.go rename to hypha.gog index 540d31b..bbdc6ee 100644 --- a/hypha.go +++ b/hypha.gog @@ -14,18 +14,6 @@ import ( "github.com/bouncepaw/mycorrhiza/cfg" ) -// `Hypha` represents a hypha. It is the thing MycorrhizaWiki generally serves. -// Each hypha has 1 or more revisions. -type Hypha struct { - FullName string `json:"-"` - Path string `json:"-"` - ViewCount int `json:"views"` - Deleted bool `json:"deleted"` - Revisions map[string]*Revision `json:"revisions"` - ChildrenNames []string `json:"-"` - parentName string -} - // AsHtml returns HTML representation of the hypha. // No layout or navigation are present here. Just the hypha. func (h *Hypha) AsHtml(id string, w http.ResponseWriter) (string, error) { @@ -38,33 +26,6 @@ func (h *Hypha) AsHtml(id string, w http.ResponseWriter) (string, error) { return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, id) } -// GetNewestRevision returns the most recent Revision. -func (h *Hypha) GetNewestRevision() Revision { - return *h.Revisions[h.NewestRevision()] -} - -// NewestRevision returns the most recent revision's id as a string. -func (h *Hypha) NewestRevision() string { - return strconv.Itoa(h.NewestRevisionInt()) -} - -// NewestRevision returns the most recent revision's id as an integer. -func (h *Hypha) NewestRevisionInt() (ret int) { - for k, _ := range h.Revisions { - id, _ := strconv.Atoi(k) - if id > ret { - ret = id - } - } - return ret -} - -// MetaJsonPath returns rooted path to the hypha's `meta.json` file. -// It is not promised that the file exists. -func (h *Hypha) MetaJsonPath() string { - return filepath.Join(h.Path, "meta.json") -} - // CreateDir creates directory where the hypha must reside. // It is meant to be used with new hyphae. func (h *Hypha) CreateDir() error { diff --git a/main.go b/main.go index acbc044..b4301de 100644 --- a/main.go +++ b/main.go @@ -9,25 +9,10 @@ import ( "time" "github.com/bouncepaw/mycorrhiza/cfg" + "github.com/bouncepaw/mycorrhiza/fs" "github.com/gorilla/mux" ) -// GetRevision finds revision with id `id` of `hyphaName` in `hyphae`. -// If `id` is `"0"`, it means the last revision. -// If no such revision is found, last return value is false. -func GetRevision(hyphaName string, id string) (Revision, bool) { - log.Println("Getting hypha", hyphaName, id) - if hypha, ok := hyphae[hyphaName]; ok { - if id == "0" { - id = hypha.NewestRevision() - } - if rev, ok := hypha.Revisions[id]; ok { - return *rev, true - } - } - return Revision{}, false -} - // RevInMap finds value of `rev` (the one from URL queries like) in the passed map that is usually got from `mux.Vars(*http.Request)`. // If there is no `rev`, return "0". func RevInMap(m map[string]string) string { @@ -37,8 +22,7 @@ func RevInMap(m map[string]string) string { return "0" } -// `hyphae` is a map with all hyphae. Many functions use it. -var hyphae map[string]*Hypha +var hs *fs.Storage func main() { if len(os.Args) == 1 { @@ -52,51 +36,51 @@ func main() { log.Println("Welcome to MycorrhizaWiki α") cfg.InitConfig(wikiDir) log.Println("Indexing hyphae...") - hyphae = recurFindHyphae(wikiDir) - log.Println("Indexed", len(hyphae), "hyphae. Ready to accept requests.") + hs = fs.InitStorage() // Start server code. See handlers.go for handlers' implementations. r := mux.NewRouter() - r.Queries("action", "getBinary", "rev", revQuery).Path(hyphaUrl). + r.Queries("action", "getBinary", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). HandlerFunc(HandlerGetBinary) - r.Queries("action", "getBinary").Path(hyphaUrl). + r.Queries("action", "getBinary").Path(cfg.HyphaUrl). HandlerFunc(HandlerGetBinary) - r.Queries("action", "raw", "rev", revQuery).Path(hyphaUrl). + r.Queries("action", "raw", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). HandlerFunc(HandlerRaw) - r.Queries("action", "raw").Path(hyphaUrl). + r.Queries("action", "raw").Path(cfg.HyphaUrl). HandlerFunc(HandlerRaw) + /* + r.Queries("action", "zen", "rev", revQuery).Path(hyphaUrl). + HandlerFunc(HandlerZen) + r.Queries("action", "zen").Path(hyphaUrl). + HandlerFunc(HandlerZen) - r.Queries("action", "zen", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerZen) - r.Queries("action", "zen").Path(hyphaUrl). - HandlerFunc(HandlerZen) + r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl). + HandlerFunc(HandlerView) + r.Queries("action", "view").Path(hyphaUrl). + HandlerFunc(HandlerView) - r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerView) - r.Queries("action", "view").Path(hyphaUrl). - HandlerFunc(HandlerView) + r.Queries("action", "history").Path(hyphaUrl). + HandlerFunc(HandlerHistory) - r.Queries("action", "history").Path(hyphaUrl). - HandlerFunc(HandlerHistory) + r.Queries("action", "edit").Path(hyphaUrl). + HandlerFunc(HandlerEdit) - r.Queries("action", "edit").Path(hyphaUrl). - HandlerFunc(HandlerEdit) + r.Queries("action", "rewind", "rev", revQuery).Path(hyphaUrl). + HandlerFunc(HandlerRewind) - r.Queries("action", "rewind", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerRewind) + r.Queries("action", "delete").Path(hyphaUrl). + HandlerFunc(HandlerDelete) - r.Queries("action", "delete").Path(hyphaUrl). - HandlerFunc(HandlerDelete) + r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). + HandlerFunc(HandlerRename) - r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). - HandlerFunc(HandlerRename) + r.Queries("action", "update").Path(hyphaUrl).Methods("POST"). + HandlerFunc(HandlerUpdate) + */ - r.Queries("action", "update").Path(hyphaUrl).Methods("POST"). - HandlerFunc(HandlerUpdate) - - r.HandleFunc(hyphaUrl, HandlerView) + // r.HandleFunc(hyphaUrl, HandlerView) // Debug page that renders all hyphae. // TODO: make it redirect to home page. @@ -104,14 +88,10 @@ func main() { r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) - for _, h := range hyphae { - log.Println("Rendering latest revision of hypha", h.FullName) - html, err := h.AsHtml("0", w) - if err != nil { - fmt.Fprintln(w, err) - } - fmt.Fprintln(w, html) - } + fmt.Fprintln(w, ` +

Check out Fruit maybe.

+

TODO: make this page usable

+ `) }) http.Handle("/", r) diff --git a/render.go b/render.gog similarity index 100% rename from render.go rename to render.gog diff --git a/revision.go b/revision.gog similarity index 90% rename from revision.go rename to revision.gog index 35b653b..3117b2a 100644 --- a/revision.go +++ b/revision.gog @@ -114,19 +114,6 @@ func (rev *Revision) ActionGetBinary(w http.ResponseWriter) { log.Println("Serving binary data of", rev.FullName, rev.Id) } -// ActionRaw is used with `?action=raw`. -// It writes text content of the revision without any parsing or rendering. -func (rev *Revision) ActionRaw(w http.ResponseWriter) { - fileContents, err := ioutil.ReadFile(rev.TextPath) - if err != nil { - return - } - w.Header().Set("Content-Type", rev.TextMime) - w.WriteHeader(http.StatusOK) - w.Write(fileContents) - log.Println("Serving text data of", rev.FullName, rev.Id) -} - // ActionZen is used with `?action=zen`. // It renders the revision without any layout or navigation. func (rev *Revision) ActionZen(w http.ResponseWriter) { diff --git a/w/m/Fruit/Pineapple/1.html b/w/m/Fruit/Pineapple/1.html new file mode 100644 index 0000000..61b3b19 --- /dev/null +++ b/w/m/Fruit/Pineapple/1.html @@ -0,0 +1 @@ +Pineapple is apple from a pine \ No newline at end of file diff --git a/w/m/Fruit/Pineapple/meta.json b/w/m/Fruit/Pineapple/meta.json new file mode 100644 index 0000000..877bb4a --- /dev/null +++ b/w/m/Fruit/Pineapple/meta.json @@ -0,0 +1,19 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": [ + "" + ], + "name": "Pineapple", + "comment": "Update Fruit/Pineapple", + "author": "", + "time": 1592997100, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/walk.go b/walk.gog similarity index 100% rename from walk.go rename to walk.gog From 7202cf5ab780ca056d0525c3e2db66009329ac8d Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Wed, 24 Jun 2020 21:19:44 +0500 Subject: [PATCH 2/6] Reimplement action=zen --- fs/fs.go | 2 -- fs/hypha.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++- handlers.go | 21 +------------------- main.go | 27 ++++++++----------------- 4 files changed, 65 insertions(+), 42 deletions(-) diff --git a/fs/fs.go b/fs/fs.go index 4323340..e140b58 100644 --- a/fs/fs.go +++ b/fs/fs.go @@ -21,7 +21,6 @@ func InitStorage() *Storage { root: cfg.WikiDir, } s.indexHyphae(s.root) - log.Println(s.paths) log.Printf("Indexed %v hyphae\n", len(s.paths)) return s } @@ -50,7 +49,6 @@ func (s *Storage) indexHyphae(path string) { case matchesHypha && node.IsDir(): s.indexHyphae(name) case node.Name() == "meta.json" && !node.IsDir(): - log.Printf("%v seems to be a hypha, adding it to the list\n", path) s.paths[hyphaName(path)] = path } } diff --git a/fs/hypha.go b/fs/hypha.go index 559457a..225acd0 100644 --- a/fs/hypha.go +++ b/fs/hypha.go @@ -3,6 +3,7 @@ package fs import ( "encoding/json" "errors" + "fmt" "io/ioutil" "log" "net/http" @@ -10,6 +11,7 @@ import ( "strconv" "github.com/bouncepaw/mycorrhiza/cfg" + "github.com/gomarkdown/markdown" ) type Hypha struct { @@ -57,7 +59,6 @@ func (s *Storage) Open(name string) (*Hypha, error) { err = h.OnRevision("0") return h, err } - log.Println(h) return h, nil } @@ -108,6 +109,45 @@ func (h *Hypha) mimeTypeForActionRaw() string { return h.actual.TextMime } +// hasBinaryData returns true if the revision has any binary data associated. +// During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data. (is it?) +func (h *Hypha) hasBinaryData() bool { + return h.actual.BinaryMime != "" +} + +func (h *Hypha) asHtml() (string, error) { + rev := h.actual + ret := `
+

` + rev.FullName + `

+` + // What about using
? + if h.hasBinaryData() { + ret += fmt.Sprintf(``, rev.FullName, rev.Id) + } + + contents, err := ioutil.ReadFile(rev.TextPath) + if err != nil { + log.Println("Failed to render", rev.FullName, ":", err) + return "", err + } + + // TODO: support more markups. + // TODO: support mycorrhiza extensions like transclusion. + switch rev.TextMime { + case "text/markdown": + html := markdown.ToHTML(contents, nil, nil) + ret += string(html) + default: + ret += fmt.Sprintf(`
%s
`, contents) + } + + ret += ` +
` + + return ret, nil +} + // ActionRaw is used with `?action=raw`. // It writes text content of the revision without any parsing or rendering. func (h *Hypha) ActionRaw(w http.ResponseWriter) { @@ -135,3 +175,18 @@ func (h *Hypha) ActionGetBinary(w http.ResponseWriter) { w.Write(fileContents) h.PlainLog("Serving raw text") } + +// ActionZen is used with `?action=zen`. +// It renders the hypha but without any layout or styles. Pure. Zen. +func (h *Hypha) ActionZen(w http.ResponseWriter) { + html, err := h.asHtml() + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html;charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte(html)) + h.PlainLog("Rendering zen") +} diff --git a/handlers.go b/handlers.go index 4479ea1..1b8a9a2 100644 --- a/handlers.go +++ b/handlers.go @@ -47,13 +47,13 @@ func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) { } } -/* func HandlerZen(w http.ResponseWriter, rq *http.Request) { if h, ok := HandlerBase(w, rq); ok { h.ActionZen(w) } } +/* func HandlerView(w http.ResponseWriter, rq *http.Request) { if h, ok := HandlerBase(w, rq); ok { h.ActionView(w, HyphaPage) @@ -65,25 +65,6 @@ func HandlerEdit(w http.ResponseWriter, rq *http.Request) { ActionEdit(vars["hypha"], w) } -func HandlerHistory(w http.ResponseWriter, rq *http.Request) { - w.WriteHeader(http.StatusNotImplemented) - log.Println("Attempt to access an unimplemented thing") -} - -func HandlerRewind(w http.ResponseWriter, rq *http.Request) { - w.WriteHeader(http.StatusNotImplemented) - log.Println("Attempt to access an unimplemented thing") -} - -func HandlerDelete(w http.ResponseWriter, rq *http.Request) { - w.WriteHeader(http.StatusNotImplemented) - log.Println("Attempt to access an unimplemented thing") -} - -func HandlerRename(w http.ResponseWriter, rq *http.Request) { - w.WriteHeader(http.StatusNotImplemented) - log.Println("Attempt to access an unimplemented thing") -} */ /* diff --git a/main.go b/main.go index b4301de..8d3cdb1 100644 --- a/main.go +++ b/main.go @@ -50,31 +50,20 @@ func main() { HandlerFunc(HandlerRaw) r.Queries("action", "raw").Path(cfg.HyphaUrl). HandlerFunc(HandlerRaw) - /* - r.Queries("action", "zen", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerZen) - r.Queries("action", "zen").Path(hyphaUrl). - HandlerFunc(HandlerZen) + r.Queries("action", "zen", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). + HandlerFunc(HandlerZen) + r.Queries("action", "zen").Path(cfg.HyphaUrl). + HandlerFunc(HandlerZen) + + /* r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl). HandlerFunc(HandlerView) r.Queries("action", "view").Path(hyphaUrl). HandlerFunc(HandlerView) - r.Queries("action", "history").Path(hyphaUrl). - HandlerFunc(HandlerHistory) - - r.Queries("action", "edit").Path(hyphaUrl). - HandlerFunc(HandlerEdit) - - r.Queries("action", "rewind", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerRewind) - - r.Queries("action", "delete").Path(hyphaUrl). - HandlerFunc(HandlerDelete) - - r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). - HandlerFunc(HandlerRename) + r.Queries("action", "edit").Path(hyphaUrl). + HandlerFunc(HandlerEdit) r.Queries("action", "update").Path(hyphaUrl).Methods("POST"). HandlerFunc(HandlerUpdate) From 472aea92f8d2a25e832bfd4505ce57cd9278b2cd Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Thu, 25 Jun 2020 22:18:50 +0500 Subject: [PATCH 3/6] Reimplement view, create render module, add favicon --- cfg/config.go | 5 +- fs/fs.go | 11 +- fs/genealogy.go | 91 ++++++++ fs/hypha.go | 40 +++- handlers.go | 13 +- main.go | 28 +-- render/render.go | 101 ++++++++ w/config.json | 1 + w/favicon.ico | Bin 0 -> 173902 bytes .../Hypha/edit/header.html/1.html} | 2 +- .../Hypha/edit/header.html/meta.json | 17 ++ .../default-dark/Hypha/edit/index.html/1.html | 16 ++ .../Hypha/edit/index.html/meta.json | 17 ++ .../Hypha/edit/sidebar.html/1.html | 19 ++ .../Hypha/edit/sidebar.html/meta.json | 19 ++ .../default-dark/Hypha/view/404.html/1.html} | 8 +- .../Hypha/view/404.html/meta.json | 17 ++ .../Hypha/view/index.html/1.html} | 2 +- .../Hypha/view/index.html/meta.json | 17 ++ .../Hypha/view/sidebar.html/1.html} | 18 +- .../Hypha/view/sidebar.html/meta.json | 17 ++ .../Templates/default-dark/base.html/1.html} | 60 ++--- .../default-dark/base.html/meta.json | 17 ++ w/m/Templates/default-dark/main.css/1.css | 221 ++++++++++++++++++ w/m/Templates/default-dark/main.css/meta.json | 19 ++ w/m/Templates/default-dark/main.js/1.mjs | 7 + w/m/Templates/default-dark/main.js/meta.json | 17 ++ .../default-dark/updateOk.html/1.html} | 16 +- .../default-dark/updateOk.html/meta.json | 17 ++ .../Hypha/edit/header.html/1.html | 1 + .../Hypha/edit/header.html/meta.json | 17 ++ .../Hypha/edit/index.html/1.html} | 72 +++--- .../Hypha/edit/index.html/meta.json | 17 ++ .../default-light/Hypha/view/404.html/1.html | 4 + .../Hypha/view/404.html/meta.json | 17 ++ .../Hypha/view/index.html/1.html | 1 + .../Hypha/view/index.html/meta.json | 17 ++ .../Hypha/view/sidebar.html/1.html | 9 + .../Hypha/view/sidebar.html/meta.json | 17 ++ w/m/Templates/default-light/base.html/1.html | 30 +++ .../default-light/base.html/meta.json | 17 ++ w/m/Templates/default-light/main.css/1.css | 221 ++++++++++++++++++ .../default-light/main.css/meta.json | 19 ++ w/m/Templates/default-light/main.js/1.mjs | 7 + w/m/Templates/default-light/main.js/meta.json | 17 ++ .../default-light/updateOk.html/1.html | 8 + .../default-light/updateOk.html/meta.json | 17 ++ 47 files changed, 1222 insertions(+), 119 deletions(-) create mode 100644 fs/genealogy.go create mode 100644 render/render.go create mode 100644 w/favicon.ico rename w/{templates/Hypha/edit/header.html => m/Templates/default-dark/Hypha/edit/header.html/1.html} (98%) create mode 100644 w/m/Templates/default-dark/Hypha/edit/header.html/meta.json create mode 100644 w/m/Templates/default-dark/Hypha/edit/index.html/1.html create mode 100644 w/m/Templates/default-dark/Hypha/edit/index.html/meta.json create mode 100644 w/m/Templates/default-dark/Hypha/edit/sidebar.html/1.html create mode 100644 w/m/Templates/default-dark/Hypha/edit/sidebar.html/meta.json rename w/{templates/Hypha/view/404.html => m/Templates/default-dark/Hypha/view/404.html/1.html} (97%) create mode 100644 w/m/Templates/default-dark/Hypha/view/404.html/meta.json rename w/{templates/Hypha/view/index.html => m/Templates/default-dark/Hypha/view/index.html/1.html} (88%) create mode 100644 w/m/Templates/default-dark/Hypha/view/index.html/meta.json rename w/{templates/Hypha/view/sidebar.html => m/Templates/default-dark/Hypha/view/sidebar.html/1.html} (96%) create mode 100644 w/m/Templates/default-dark/Hypha/view/sidebar.html/meta.json rename w/{templates/base.html => m/Templates/default-dark/base.html/1.html} (84%) create mode 100644 w/m/Templates/default-dark/base.html/meta.json create mode 100644 w/m/Templates/default-dark/main.css/1.css create mode 100644 w/m/Templates/default-dark/main.css/meta.json create mode 100644 w/m/Templates/default-dark/main.js/1.mjs create mode 100644 w/m/Templates/default-dark/main.js/meta.json rename w/{templates/updateOk.html => m/Templates/default-dark/updateOk.html/1.html} (94%) create mode 100644 w/m/Templates/default-dark/updateOk.html/meta.json create mode 100644 w/m/Templates/default-light/Hypha/edit/header.html/1.html create mode 100644 w/m/Templates/default-light/Hypha/edit/header.html/meta.json rename w/{templates/Hypha/edit/index.html => m/Templates/default-light/Hypha/edit/index.html/1.html} (96%) create mode 100644 w/m/Templates/default-light/Hypha/edit/index.html/meta.json create mode 100644 w/m/Templates/default-light/Hypha/view/404.html/1.html create mode 100644 w/m/Templates/default-light/Hypha/view/404.html/meta.json create mode 100644 w/m/Templates/default-light/Hypha/view/index.html/1.html create mode 100644 w/m/Templates/default-light/Hypha/view/index.html/meta.json create mode 100644 w/m/Templates/default-light/Hypha/view/sidebar.html/1.html create mode 100644 w/m/Templates/default-light/Hypha/view/sidebar.html/meta.json create mode 100644 w/m/Templates/default-light/base.html/1.html create mode 100644 w/m/Templates/default-light/base.html/meta.json create mode 100644 w/m/Templates/default-light/main.css/1.css create mode 100644 w/m/Templates/default-light/main.css/meta.json create mode 100644 w/m/Templates/default-light/main.js/1.mjs create mode 100644 w/m/Templates/default-light/main.js/meta.json create mode 100644 w/m/Templates/default-light/updateOk.html/1.html create mode 100644 w/m/Templates/default-light/updateOk.html/meta.json diff --git a/cfg/config.go b/cfg/config.go index 4597353..7c0db8f 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -26,12 +26,13 @@ var ( TitleTemplate = `%s` GenericErrorMsg = `Sorry, something went wrong` SiteTitle = `MycorrhizaWiki` + Theme = `default-light` ) func InitConfig(wd string) bool { log.Println("WikiDir is", wd) WikiDir = wd - TemplatesDir = filepath.Join(filepath.Dir(WikiDir), "templates") + TemplatesDir = "Templates" configJsonPath = filepath.Join(filepath.Dir(WikiDir), "config.json") if _, err := os.Stat(configJsonPath); os.IsNotExist(err) { @@ -53,6 +54,7 @@ func readConfig() bool { cfg := struct { Address string `json:"address"` + Theme string `json:"theme"` SiteTitle string `json:"site-title"` TitleTemplates struct { EditHypha string `json:"edit-hypha"` @@ -67,6 +69,7 @@ func readConfig() bool { } Address = cfg.Address + Theme = cfg.Theme SiteTitle = cfg.SiteTitle TitleEditTemplate = cfg.TitleTemplates.EditHypha TitleTemplate = cfg.TitleTemplates.ViewHypha diff --git a/fs/fs.go b/fs/fs.go index e140b58..b10dc74 100644 --- a/fs/fs.go +++ b/fs/fs.go @@ -14,15 +14,16 @@ type Storage struct { root string } +var Hs *Storage + // InitStorage initiates filesystem-based hypha storage. It has to be called after configuration was inited. -func InitStorage() *Storage { - s := &Storage{ +func InitStorage() { + Hs = &Storage{ paths: make(map[string]string), root: cfg.WikiDir, } - s.indexHyphae(s.root) - log.Printf("Indexed %v hyphae\n", len(s.paths)) - return s + Hs.indexHyphae(Hs.root) + log.Printf("Indexed %v hyphae\n", len(Hs.paths)) } // hyphaName gets name of a hypha by stripping path to the hypha in `fullPath` diff --git a/fs/genealogy.go b/fs/genealogy.go new file mode 100644 index 0000000..3addd3d --- /dev/null +++ b/fs/genealogy.go @@ -0,0 +1,91 @@ +package fs + +import ( + "fmt" + "log" + "path/filepath" + "sort" + "strings" +) + +func (s *Storage) RenderHypha(h *Hypha) { +} + +// If Name == "", the tree is empty. +type Tree struct { + Name string + Ancestors []string + Siblings []string + Descendants []*Tree + Root bool +} + +// GetTree generates a Tree for the given hypha name. +// It can also generate trees for non-existent hyphae, that's why we use `name string` instead of making it a method on `Hypha`. +// In `root` is `false`, siblings will not be fetched. +func (s *Storage) GetTree(name string, root bool) *Tree { + t := &Tree{Name: name, Root: root} + for hyphaName, _ := range s.paths { + s.compareNamesAndAppend(t, hyphaName) + } + sort.Slice(t.Ancestors, func(i, j int) bool { + return strings.Count(t.Ancestors[i], "/") < strings.Count(t.Ancestors[j], "/") + }) + sort.Strings(t.Siblings) + sort.Slice(t.Descendants, func(i, j int) bool { + a := t.Descendants[i].Name + b := t.Descendants[j].Name + return len(a) < len(b) + }) + log.Printf("Generate tree for %v: %v %v\n", t.Name, t.Ancestors, t.Siblings) + return t +} + +// Compares names appends name2 to an array of `t`: +func (s *Storage) compareNamesAndAppend(t *Tree, name2 string) { + switch { + case t.Name == name2: + case strings.HasPrefix(t.Name, name2): + t.Ancestors = append(t.Ancestors, name2) + case t.Root && (strings.Count(t.Name, "/") == strings.Count(name2, "/") && + (filepath.Dir(t.Name) == filepath.Dir(name2))): + t.Siblings = append(t.Siblings, name2) + case strings.HasPrefix(name2, t.Name): + t.Descendants = append(t.Descendants, s.GetTree(name2, false)) + } +} + +// asHtml returns HTML representation of a tree. +// It recursively itself on the tree's children. +// TODO: redo with templates. I'm not in mood for it now. +func (t *Tree) AsHtml() (html string) { + if t.Name == "" { + return "" + } + html += `` + return html +} + +// navitreeEntry is a small utility function that makes generating html easier. +// Someone please redo it in templates. +func navitreeEntry(name, class string) string { + return fmt.Sprintf(`
  • + %s +
  • +`, class, name, filepath.Base(name)) +} diff --git a/fs/hypha.go b/fs/hypha.go index 225acd0..737a070 100644 --- a/fs/hypha.go +++ b/fs/hypha.go @@ -23,6 +23,10 @@ type Hypha struct { actual *Revision `json:"-"` } +func (h *Hypha) TextPath() string { + return h.actual.TextPath +} + func (s *Storage) Open(name string) (*Hypha, error) { h := &Hypha{ Exists: true, @@ -98,7 +102,11 @@ func (h *Hypha) NewestId() string { } func (h *Hypha) PlainLog(s string) { - log.Println(h.FullName, h.actual.Id, s) + if h.Exists { + log.Println(h.FullName, h.actual.Id, s) + } else { + log.Println("nonexistent", h.FullName, s) + } } func (h *Hypha) mimeTypeForActionRaw() string { @@ -122,8 +130,7 @@ func (h *Hypha) asHtml() (string, error) { ` // What about using
    ? if h.hasBinaryData() { - ret += fmt.Sprintf(``, rev.FullName, rev.Id) + ret += fmt.Sprintf(``, rev.FullName, rev.Id) } contents, err := ioutil.ReadFile(rev.TextPath) @@ -162,9 +169,9 @@ func (h *Hypha) ActionRaw(w http.ResponseWriter) { h.PlainLog("Serving raw text") } -// ActionGetBinary is used with `?action=getBinary`. +// ActionBinary is used with `?action=binary`. // It writes contents of binary content file. -func (h *Hypha) ActionGetBinary(w http.ResponseWriter) { +func (h *Hypha) ActionBinary(w http.ResponseWriter) { fileContents, err := ioutil.ReadFile(h.actual.BinaryPath) if err != nil { log.Fatal(err) @@ -190,3 +197,26 @@ func (h *Hypha) ActionZen(w http.ResponseWriter) { w.Write([]byte(html)) h.PlainLog("Rendering zen") } + +// ActionView is used with `?action=view` or no action at all. +// It renders the page, the layout and everything else. +func (h *Hypha) ActionView(w http.ResponseWriter, renderExists, renderNotExists func(string, string) string) { + var html string + var err error + if h.Exists { + html, err = h.asHtml() + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + w.Header().Set("Content-Type", "text/html;charset=utf-8") + w.WriteHeader(http.StatusOK) + if h.Exists { + w.Write([]byte(renderExists(h.FullName, html))) + } else { + w.Write([]byte(renderNotExists(h.FullName, ""))) + } + h.PlainLog("Rendering hypha view") +} diff --git a/handlers.go b/handlers.go index 1b8a9a2..d8a1ada 100644 --- a/handlers.go +++ b/handlers.go @@ -11,6 +11,7 @@ import ( // "time" "github.com/bouncepaw/mycorrhiza/fs" + "github.com/bouncepaw/mycorrhiza/render" "github.com/gorilla/mux" ) @@ -19,7 +20,7 @@ import ( // Boilerplate code present in many handlers. Good to have it. func HandlerBase(w http.ResponseWriter, rq *http.Request) (*fs.Hypha, bool) { vars := mux.Vars(rq) - h, err := hs.Open(vars["hypha"]) + h, err := fs.Hs.Open(vars["hypha"]) if err != nil { log.Println(err) return nil, false @@ -40,10 +41,10 @@ func HandlerRaw(w http.ResponseWriter, rq *http.Request) { } } -func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) { - log.Println("?action=getBinary") +func HandlerBinary(w http.ResponseWriter, rq *http.Request) { + log.Println("?action=binary") if h, ok := HandlerBase(w, rq); ok { - h.ActionGetBinary(w) + h.ActionBinary(w) } } @@ -53,13 +54,13 @@ func HandlerZen(w http.ResponseWriter, rq *http.Request) { } } -/* func HandlerView(w http.ResponseWriter, rq *http.Request) { if h, ok := HandlerBase(w, rq); ok { - h.ActionView(w, HyphaPage) + h.ActionView(w, render.HyphaPage, render.Hypha404) } } +/* func HandlerEdit(w http.ResponseWriter, rq *http.Request) { vars := mux.Vars(rq) ActionEdit(vars["hypha"], w) diff --git a/main.go b/main.go index 8d3cdb1..4cbcb8d 100644 --- a/main.go +++ b/main.go @@ -22,8 +22,6 @@ func RevInMap(m map[string]string) string { return "0" } -var hs *fs.Storage - func main() { if len(os.Args) == 1 { panic("Expected a root wiki pages directory") @@ -36,15 +34,19 @@ func main() { log.Println("Welcome to MycorrhizaWiki α") cfg.InitConfig(wikiDir) log.Println("Indexing hyphae...") - hs = fs.InitStorage() + fs.InitStorage() // Start server code. See handlers.go for handlers' implementations. r := mux.NewRouter() - r.Queries("action", "getBinary", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). - HandlerFunc(HandlerGetBinary) - r.Queries("action", "getBinary").Path(cfg.HyphaUrl). - HandlerFunc(HandlerGetBinary) + r.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) { + http.ServeFile(w, rq, filepath.Join(filepath.Dir(cfg.WikiDir), "favicon.ico")) + }) + + r.Queries("action", "binary", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). + HandlerFunc(HandlerBinary) + r.Queries("action", "binary").Path(cfg.HyphaUrl). + HandlerFunc(HandlerBinary) r.Queries("action", "raw", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). HandlerFunc(HandlerRaw) @@ -56,12 +58,12 @@ func main() { r.Queries("action", "zen").Path(cfg.HyphaUrl). HandlerFunc(HandlerZen) - /* - r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerView) - r.Queries("action", "view").Path(hyphaUrl). - HandlerFunc(HandlerView) + r.Queries("action", "view", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). + HandlerFunc(HandlerView) + r.Queries("action", "view").Path(cfg.HyphaUrl). + HandlerFunc(HandlerView) + /* r.Queries("action", "edit").Path(hyphaUrl). HandlerFunc(HandlerEdit) @@ -69,7 +71,7 @@ func main() { HandlerFunc(HandlerUpdate) */ - // r.HandleFunc(hyphaUrl, HandlerView) + r.HandleFunc(cfg.HyphaUrl, HandlerView) // Debug page that renders all hyphae. // TODO: make it redirect to home page. diff --git a/render/render.go b/render/render.go new file mode 100644 index 0000000..75c3e86 --- /dev/null +++ b/render/render.go @@ -0,0 +1,101 @@ +package render + +import ( + "bytes" + "fmt" + "path" + "text/template" + + "github.com/bouncepaw/mycorrhiza/cfg" + "github.com/bouncepaw/mycorrhiza/fs" +) + +// EditHyphaPage returns HTML page of hypha editor. +func EditHyphaPage(name, textMime, content, tags string) string { + page := map[string]string{ + "Text": content, + "TextMime": textMime, + "Name": name, + "Tags": tags, + } + keys := map[string]string{ + "Title": fmt.Sprintf(cfg.TitleEditTemplate, name), + "Header": renderFromString(name, "Hypha/edit/header.html"), + "Sidebar": renderFromMap(page, "Hypha/edit/sidebar.html"), + } + return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys) +} + +// Hypha404 returns 404 page for nonexistent page. +func Hypha404(name, _ string) string { + return HyphaGeneric(name, name, "Hypha/view/404.html") +} + +// HyphaPage returns HTML page of hypha viewer. +func HyphaPage(name, content string) string { + return HyphaGeneric(name, content, "Hypha/view/index.html") +} + +// HyphaGeneric is used when building renderers for all types of hypha pages +func HyphaGeneric(name string, content, templatePath string) string { + var sidebar string + if aside := renderFromMap(map[string]string{ + "Tree": fs.Hs.GetTree(name, true).AsHtml(), + }, "Hypha/view/sidebar.html"); aside != "" { + sidebar = aside + } + keys := map[string]string{ + "Title": fmt.Sprintf(cfg.TitleTemplate, name), + "Sidebar": sidebar, + } + return renderBase(renderFromString(content, templatePath), keys) +} + +// renderBase collects and renders page from base template +// Args: +// content: string or pre-rendered template +// keys: map with replaced standart fields +func renderBase(content string, keys map[string]string) string { + page := map[string]string{ + "Title": cfg.SiteTitle, + "Main": "", + "SiteTitle": cfg.SiteTitle, + } + for key, val := range keys { + page[key] = val + } + page["Main"] = content + return renderFromMap(page, "base.html") +} + +// renderFromMap applies `data` map to template in `templatePath` and returns the result. +func renderFromMap(data map[string]string, templatePath string) string { + hyphPath := path.Join(cfg.TemplatesDir, cfg.Theme, templatePath) + h, _ := fs.Hs.Open(hyphPath) + h.OnRevision("0") + tmpl, err := template.ParseFiles(h.TextPath()) + if err != nil { + return err.Error() + } + buf := new(bytes.Buffer) + if err := tmpl.Execute(buf, data); err != nil { + return err.Error() + } + return buf.String() +} + +// renderFromMap applies `data` string to template in `templatePath` and returns the result. +func renderFromString(data string, templatePath string) string { + hyphPath := path.Join(cfg.TemplatesDir, cfg.Theme, templatePath) + h, _ := fs.Hs.Open(hyphPath) + h.OnRevision("0") + tmpl, err := template.ParseFiles(h.TextPath()) + if err != nil { + return err.Error() + } + buf := new(bytes.Buffer) + if err := tmpl.Execute(buf, data); err != nil { + return err.Error() + } + return buf.String() +} diff --git a/w/config.json b/w/config.json index 09fcba9..409bcd2 100644 --- a/w/config.json +++ b/w/config.json @@ -1,5 +1,6 @@ { "address": "127.0.0.1:1737", + "theme": "default-light", "site-title": "🍄 MycorrhizaWiki", "title-templates": { "edit-hypha": "Edit %s at MycorrhizaWiki", diff --git a/w/favicon.ico b/w/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..857e55c892c5a9c4406b4ef484e959e49c38094a GIT binary patch literal 173902 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 F{|D>JzbF6z literal 0 HcmV?d00001 diff --git a/w/templates/Hypha/edit/header.html b/w/m/Templates/default-dark/Hypha/edit/header.html/1.html similarity index 98% rename from w/templates/Hypha/edit/header.html rename to w/m/Templates/default-dark/Hypha/edit/header.html/1.html index 31dfc0f..37a4b91 100644 --- a/w/templates/Hypha/edit/header.html +++ b/w/m/Templates/default-dark/Hypha/edit/header.html/1.html @@ -1 +1 @@ -

    Edit {{ . }}

    +

    Edit {{ . }}

    diff --git a/w/m/Templates/default-dark/Hypha/edit/header.html/meta.json b/w/m/Templates/default-dark/Hypha/edit/header.html/meta.json new file mode 100644 index 0000000..cf72dcd --- /dev/null +++ b/w/m/Templates/default-dark/Hypha/edit/header.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "header.html", + "comment": "Create Templates/default-dark/Hypha/edit/header.html", + "author": "", + "time": 1592996801, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} diff --git a/w/m/Templates/default-dark/Hypha/edit/index.html/1.html b/w/m/Templates/default-dark/Hypha/edit/index.html/1.html new file mode 100644 index 0000000..842b43b --- /dev/null +++ b/w/m/Templates/default-dark/Hypha/edit/index.html/1.html @@ -0,0 +1,16 @@ + diff --git a/w/m/Templates/default-dark/Hypha/edit/index.html/meta.json b/w/m/Templates/default-dark/Hypha/edit/index.html/meta.json new file mode 100644 index 0000000..7534c5d --- /dev/null +++ b/w/m/Templates/default-dark/Hypha/edit/index.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "index.html", + "comment": "Create Templates/default-dark/Hypha/edit/index.html", + "author": "", + "time": 1592996876, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} diff --git a/w/m/Templates/default-dark/Hypha/edit/sidebar.html/1.html b/w/m/Templates/default-dark/Hypha/edit/sidebar.html/1.html new file mode 100644 index 0000000..3f257cf --- /dev/null +++ b/w/m/Templates/default-dark/Hypha/edit/sidebar.html/1.html @@ -0,0 +1,19 @@ +

    Text MIME-type

    +

    Good types are text/markdown and text/plain

    + + +

    Revision comment

    +

    Please make your comment helpful

    + + +

    Edit tags

    +

    Tags are separated by commas, whitespace is ignored

    + + +

    Upload file

    +

    If this hypha has a file like that, the text above is meant to be a description of it

    + + + +

    +
    diff --git a/w/m/Templates/default-dark/Hypha/edit/sidebar.html/meta.json b/w/m/Templates/default-dark/Hypha/edit/sidebar.html/meta.json new file mode 100644 index 0000000..1fe3342 --- /dev/null +++ b/w/m/Templates/default-dark/Hypha/edit/sidebar.html/meta.json @@ -0,0 +1,19 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": [ + "" + ], + "name": "sidebar.html", + "comment": "Create Templates/default-dark/Hypha/edit/sidebar.html", + "author": "", + "time": 1593003792, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} diff --git a/w/templates/Hypha/view/404.html b/w/m/Templates/default-dark/Hypha/view/404.html/1.html similarity index 97% rename from w/templates/Hypha/view/404.html rename to w/m/Templates/default-dark/Hypha/view/404.html/1.html index e7a14c5..7f558fb 100644 --- a/w/templates/Hypha/view/404.html +++ b/w/m/Templates/default-dark/Hypha/view/404.html/1.html @@ -1,4 +1,4 @@ -

    {{ . }}

    -

    -The hypha you are trying to access does not exist yet. Why not create it? -

    +

    {{ . }}

    +

    +The hypha you are trying to access does not exist yet. Why not create it? +

    diff --git a/w/m/Templates/default-dark/Hypha/view/404.html/meta.json b/w/m/Templates/default-dark/Hypha/view/404.html/meta.json new file mode 100644 index 0000000..1c22d20 --- /dev/null +++ b/w/m/Templates/default-dark/Hypha/view/404.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "404.html", + "comment": "Create Templates/default-light/Hypha/view/404.html", + "author": "", + "time": 1592996917, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/templates/Hypha/view/index.html b/w/m/Templates/default-dark/Hypha/view/index.html/1.html similarity index 88% rename from w/templates/Hypha/view/index.html rename to w/m/Templates/default-dark/Hypha/view/index.html/1.html index 55a25b3..f75b53a 100644 --- a/w/templates/Hypha/view/index.html +++ b/w/m/Templates/default-dark/Hypha/view/index.html/1.html @@ -1 +1 @@ -{{ . }} +{{ . }} diff --git a/w/m/Templates/default-dark/Hypha/view/index.html/meta.json b/w/m/Templates/default-dark/Hypha/view/index.html/meta.json new file mode 100644 index 0000000..4b75feb --- /dev/null +++ b/w/m/Templates/default-dark/Hypha/view/index.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "index.html", + "comment": "Create Templates/default-light/Hypha/view/index.html", + "author": "", + "time": 1592996954, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/templates/Hypha/view/sidebar.html b/w/m/Templates/default-dark/Hypha/view/sidebar.html/1.html similarity index 96% rename from w/templates/Hypha/view/sidebar.html rename to w/m/Templates/default-dark/Hypha/view/sidebar.html/1.html index 7e361d4..3f751f3 100644 --- a/w/templates/Hypha/view/sidebar.html +++ b/w/m/Templates/default-dark/Hypha/view/sidebar.html/1.html @@ -1,9 +1,9 @@ - -{{ .Tree }} + +{{ .Tree }} diff --git a/w/m/Templates/default-dark/Hypha/view/sidebar.html/meta.json b/w/m/Templates/default-dark/Hypha/view/sidebar.html/meta.json new file mode 100644 index 0000000..0c89342 --- /dev/null +++ b/w/m/Templates/default-dark/Hypha/view/sidebar.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "sidebar.html", + "comment": "Create Templates/default-light/Hypha/view/sidebar.html", + "author": "", + "time": 1592996977, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/templates/base.html b/w/m/Templates/default-dark/base.html/1.html similarity index 84% rename from w/templates/base.html rename to w/m/Templates/default-dark/base.html/1.html index 3ce1fdc..b5dbe7a 100644 --- a/w/templates/base.html +++ b/w/m/Templates/default-dark/base.html/1.html @@ -1,30 +1,30 @@ - - - {{ .Title }} - - - -
    - -
    -
    {{ .Main }}
    -
    -
    -
    - -
    -
    - -
    -

    {{ .SiteTitle }}

    -
    - - -
    -
    -
    - - - + + + {{ .Title }} + + + +
    + +
    +
    {{ .Main }}
    +
    +
    +
    + +
    +
    + +
    +

    {{ .SiteTitle }}

    +
    + + +
    +
    +
    + + + diff --git a/w/m/Templates/default-dark/base.html/meta.json b/w/m/Templates/default-dark/base.html/meta.json new file mode 100644 index 0000000..228d738 --- /dev/null +++ b/w/m/Templates/default-dark/base.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "base.html", + "comment": "Create Templates/default-dark/base.html", + "author": "", + "time": 1592996503, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} diff --git a/w/m/Templates/default-dark/main.css/1.css b/w/m/Templates/default-dark/main.css/1.css new file mode 100644 index 0000000..fcc208e --- /dev/null +++ b/w/m/Templates/default-dark/main.css/1.css @@ -0,0 +1,221 @@ +*, *::before, *::after { + box-sizing: border-box; +} + +html { + height: 100%; +} + +body { + font: 15px/1.5 system-ui, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Helvetica', 'PT Sans', 'Roboto', 'Arial', sans-serif; + max-width: 500px; + min-height: 100%; + margin: 0 auto; + padding: 12px 24px; + background-color: #272b30; + color: #c8c8c8; +} + +.msg { + background-color: #f4f4f4; + padding: 1rem; + border-radius: 1rem; +} + +.shroom { + margin: 0; +} + +.shroom__button { + border-radius: 1rem; + padding: 8px 16px 8px 0; + border: none; + background: #f0f2f4; + color: #444; + font: inherit; + font-size: 15px; + font-weight: 500; + text-align: left; +} + +.shroom span { + margin-left: 16px; + margin-right: 8px; + font-size: 20px; + vertical-align: -0.04em; +} + +.mushroom .shroom__button { + background: #44484a; + color: #dddfe4; +} + + +.header { + padding: 8px 0; +} + +.header h1 { + margin: 0; + font-size: 18px; + font-weight: 600; + letter-spacing: 0.02em; + color: #20ce92; +} + + +a { + color: #019fe3; +} + +/*a:visited { + color: #44a; +}*/ + +h1, h2, h3, h4, h5, h6 { + margin: 0.5em 0 0.25em; +} + +.page { + font-size: 16px; + line-height: 1.666; + max-width: 40em; + hyphens: auto; +} + +.page pre { + white-space: break-spaces; +} + +.page__amnt { + max-width: 100%; +} + +.page__title { + font-family: 'PT Serif', 'Georgia', serif; + font-size: 36px; + font-weight: normal; + color: #20ce92; +} + +.edit-box { + display: grid; + grid-template-columns: 7fr 5fr; +} +.edit-box .naviwrapper__buttons { + grid-column: 1; + grid-row: 2; +} +.edit-box__left { grid-column: 1; grid-row: 2 } +.edit-box__right { grid-column: 2; grid-row: 1 / span 2; padding-right: 16px } +.edit-box__text { + border-radius: 1rem; + color: #c8c8c8; + padding: 16px; + background-color: rgba(255,255,255,.05); +} + +footer { + padding: 1em 0; + font-size: 12px; + color: #7a8288; +} + +footer a, footer a:visited { + color: #7a8288; +} + +.left-panel { + display: none; +} + +.left-panel.active { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fafafa; +} + +.left-panel.active .sidebar { + background: #fff; +} + +.left-panel__in { + width: 100%; + height: 100%; + max-width: 500px; + margin: 0 auto; + padding: 12px 24px; + +} + +.left-panel__contents { + width: 100%; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.left-panel .shroom { + margin-bottom: 16px; +} + +@media (min-width: 700px) { + body { + max-width: 1200px; + padding: 8px 16px; + padding-right: 274px; + } + + .shroom { + display: none; + } + + .page { + font-size: 18px; + } + + .left-panel { + display: block; + position: fixed; + top: 0; + bottom: 0; + min-width: 274px; + right: 0; + } + + .left-panel__contents { + height: 100%; + } +} + +.sidebar { + padding: 16px; + border-radius: 1rem; + background-color: rgba(255,255,255,.05); +} + +.hypha-actions ul { + margin: 0; + padding: 0; +} + +.hypha-actions li { + list-style: none; +} + +.hypha-actions a { + display: block; + padding: 6px 16px; + font: inherit; + text-decoration: none; + color: #7a8288; + transition: 0.1s background; +} + +aside .hypha-actions a:hover { + background-color: #272b30; + color: #73ca73; +} diff --git a/w/m/Templates/default-dark/main.css/meta.json b/w/m/Templates/default-dark/main.css/meta.json new file mode 100644 index 0000000..c34401f --- /dev/null +++ b/w/m/Templates/default-dark/main.css/meta.json @@ -0,0 +1,19 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": [ + "" + ], + "name": "main.css", + "comment": "Update sys/main.css", + "author": "", + "time": 1592666188, + "text_mime": "text/css", + "binary_mime": "", + "text_name": "1.css", + "binary_name": "" + } + } +} diff --git a/w/m/Templates/default-dark/main.js/1.mjs b/w/m/Templates/default-dark/main.js/1.mjs new file mode 100644 index 0000000..7a221e6 --- /dev/null +++ b/w/m/Templates/default-dark/main.js/1.mjs @@ -0,0 +1,7 @@ +var menu = document.getElementById('shroomburgerMenu'); +document.getElementById('shroomBtn').addEventListener('click', function() { + menu.classList.add('active'); +}); +document.getElementById('mushroomBtn').addEventListener('click', function() { + menu.classList.remove('active'); +}); diff --git a/w/m/Templates/default-dark/main.js/meta.json b/w/m/Templates/default-dark/main.js/meta.json new file mode 100644 index 0000000..98d4ad5 --- /dev/null +++ b/w/m/Templates/default-dark/main.js/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "main.js", + "comment": "Update sys/main.js", + "author": "", + "time": 1592937088, + "text_mime": "text/javascript", + "binary_mime": "", + "text_name": "1.mjs", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/templates/updateOk.html b/w/m/Templates/default-dark/updateOk.html/1.html similarity index 94% rename from w/templates/updateOk.html rename to w/m/Templates/default-dark/updateOk.html/1.html index f64067d..f4ddcde 100644 --- a/w/templates/updateOk.html +++ b/w/m/Templates/default-dark/updateOk.html/1.html @@ -1,8 +1,8 @@ - - - Saved {{ .Name }} - - -

    Saved successfully. Go back

    - - + + + Saved {{ .Name }} + + +

    Saved successfully. Go back

    + + diff --git a/w/m/Templates/default-dark/updateOk.html/meta.json b/w/m/Templates/default-dark/updateOk.html/meta.json new file mode 100644 index 0000000..4c7c3c3 --- /dev/null +++ b/w/m/Templates/default-dark/updateOk.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "updateOk.html", + "comment": "Create Templates/default-dark/updateOk.html", + "author": "", + "time": 1592996644, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} diff --git a/w/m/Templates/default-light/Hypha/edit/header.html/1.html b/w/m/Templates/default-light/Hypha/edit/header.html/1.html new file mode 100644 index 0000000..37a4b91 --- /dev/null +++ b/w/m/Templates/default-light/Hypha/edit/header.html/1.html @@ -0,0 +1 @@ +

    Edit {{ . }}

    diff --git a/w/m/Templates/default-light/Hypha/edit/header.html/meta.json b/w/m/Templates/default-light/Hypha/edit/header.html/meta.json new file mode 100644 index 0000000..5ba815a --- /dev/null +++ b/w/m/Templates/default-light/Hypha/edit/header.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "header.html", + "comment": "Create Templates/default-light/Hypha/edit/header.html", + "author": "", + "time": 1592996801, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/templates/Hypha/edit/index.html b/w/m/Templates/default-light/Hypha/edit/index.html/1.html similarity index 96% rename from w/templates/Hypha/edit/index.html rename to w/m/Templates/default-light/Hypha/edit/index.html/1.html index b98e6e9..49ab078 100644 --- a/w/templates/Hypha/edit/index.html +++ b/w/m/Templates/default-light/Hypha/edit/index.html/1.html @@ -1,36 +1,36 @@ - + diff --git a/w/m/Templates/default-light/Hypha/edit/index.html/meta.json b/w/m/Templates/default-light/Hypha/edit/index.html/meta.json new file mode 100644 index 0000000..36d0d70 --- /dev/null +++ b/w/m/Templates/default-light/Hypha/edit/index.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "index.html", + "comment": "Create Templates/default-light/Hypha/edit/index.html", + "author": "", + "time": 1592996876, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/m/Templates/default-light/Hypha/view/404.html/1.html b/w/m/Templates/default-light/Hypha/view/404.html/1.html new file mode 100644 index 0000000..7f558fb --- /dev/null +++ b/w/m/Templates/default-light/Hypha/view/404.html/1.html @@ -0,0 +1,4 @@ +

    {{ . }}

    +

    +The hypha you are trying to access does not exist yet. Why not create it? +

    diff --git a/w/m/Templates/default-light/Hypha/view/404.html/meta.json b/w/m/Templates/default-light/Hypha/view/404.html/meta.json new file mode 100644 index 0000000..1c22d20 --- /dev/null +++ b/w/m/Templates/default-light/Hypha/view/404.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "404.html", + "comment": "Create Templates/default-light/Hypha/view/404.html", + "author": "", + "time": 1592996917, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/m/Templates/default-light/Hypha/view/index.html/1.html b/w/m/Templates/default-light/Hypha/view/index.html/1.html new file mode 100644 index 0000000..f75b53a --- /dev/null +++ b/w/m/Templates/default-light/Hypha/view/index.html/1.html @@ -0,0 +1 @@ +{{ . }} diff --git a/w/m/Templates/default-light/Hypha/view/index.html/meta.json b/w/m/Templates/default-light/Hypha/view/index.html/meta.json new file mode 100644 index 0000000..4b75feb --- /dev/null +++ b/w/m/Templates/default-light/Hypha/view/index.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "index.html", + "comment": "Create Templates/default-light/Hypha/view/index.html", + "author": "", + "time": 1592996954, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/m/Templates/default-light/Hypha/view/sidebar.html/1.html b/w/m/Templates/default-light/Hypha/view/sidebar.html/1.html new file mode 100644 index 0000000..3f751f3 --- /dev/null +++ b/w/m/Templates/default-light/Hypha/view/sidebar.html/1.html @@ -0,0 +1,9 @@ + +{{ .Tree }} diff --git a/w/m/Templates/default-light/Hypha/view/sidebar.html/meta.json b/w/m/Templates/default-light/Hypha/view/sidebar.html/meta.json new file mode 100644 index 0000000..0c89342 --- /dev/null +++ b/w/m/Templates/default-light/Hypha/view/sidebar.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "sidebar.html", + "comment": "Create Templates/default-light/Hypha/view/sidebar.html", + "author": "", + "time": 1592996977, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/m/Templates/default-light/base.html/1.html b/w/m/Templates/default-light/base.html/1.html new file mode 100644 index 0000000..04fdc5c --- /dev/null +++ b/w/m/Templates/default-light/base.html/1.html @@ -0,0 +1,30 @@ + + + {{ .Title }} + + + +
    + +
    +
    {{ .Main }}
    +
    +
    +
    + +
    +
    + +
    +

    {{ .SiteTitle }}

    +
    + + +
    +
    +
    + + + diff --git a/w/m/Templates/default-light/base.html/meta.json b/w/m/Templates/default-light/base.html/meta.json new file mode 100644 index 0000000..4cf6420 --- /dev/null +++ b/w/m/Templates/default-light/base.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "base.html", + "comment": "Create Templates/default-light/base.html", + "author": "", + "time": 1592996503, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/m/Templates/default-light/main.css/1.css b/w/m/Templates/default-light/main.css/1.css new file mode 100644 index 0000000..08686ba --- /dev/null +++ b/w/m/Templates/default-light/main.css/1.css @@ -0,0 +1,221 @@ +*, *::before, *::after { + box-sizing: border-box; +} + +html { + height: 100%; +} + +body { + font: 15px/1.5 system-ui, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Helvetica', 'PT Sans', 'Roboto', 'Arial', sans-serif; + max-width: 500px; + min-height: 100%; + margin: 0 auto; + padding: 12px 24px; +} + +.msg { + background-color: #f4f4f4; + padding: 1rem; + border-radius: 1rem; +} + +.shroom { + margin: 0; +} + +.shroom__button { + border-radius: 1rem; + padding: 8px 16px 8px 0; + border: none; + background: #f0f2f4; + color: #444; + font: inherit; + font-size: 15px; + font-weight: 500; + text-align: left; +} + +.shroom span { + margin-left: 16px; + margin-right: 8px; + font-size: 20px; + vertical-align: -0.04em; +} + +.mushroom .shroom__button { + background: #44484a; + color: #dddfe4; +} + + +.header { + padding: 8px 0; +} + +.header h1 { + margin: 0; + font-size: 18px; + font-weight: 600; + letter-spacing: 0.02em; + color: #222428; +} + + +a { + color: #44e; +} + +a:visited { + color: #44a; +} + +h1, h2, h3, h4, h5, h6 { + margin: 0.5em 0 0.25em; +} + +.page { + font-size: 16px; + line-height: 1.666; + max-width: 40em; + hyphens: auto; +} + +.page pre { + white-space: break-spaces; +} + +.page__amnt { + max-width: 100%; +} + +.page__title { + font-family: 'PT Serif', 'Georgia', serif; + font-size: 36px; + font-weight: normal; +} + +.edit-box { + display: grid; + grid-template-columns: 7fr 5fr; +} +.edit-box .naviwrapper__buttons { + grid-column: 1; + grid-row: 2; +} +.edit-box__left { grid-column: 1; grid-row: 2 } +.edit-box__right { grid-column: 2; grid-row: 1 / span 2; padding-right: 16px } + +footer { + padding: 1em 0; + font-size: 12px; + color: #888; +} + +footer a, footer a:visited { + color: #666; +} + +.left-panel { + display: none; +} + +.left-panel.active { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fafafa; +} + +.left-panel.active .sidebar { + background: #fff; +} + +.left-panel__in { + width: 100%; + height: 100%; + max-width: 500px; + margin: 0 auto; + padding: 12px 24px; + +} + +.left-panel__contents { + width: 100%; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.left-panel .shroom { + margin-bottom: 16px; +} + +@media (min-width: 700px) { + body { + max-width: 1200px; + padding: 8px 16px; + padding-right: 274px; + } + + .shroom { + display: none; + } + + .page { + font-size: 18px; + } + + .left-panel { + display: block; + position: fixed; + top: 0; + bottom: 0; + width: 274px; + right: 0; + } + + .left-panel__contents { + height: 100%; + } +} + +.sidebar { + padding: 16px 0; + border-radius: 1rem; + background: #f4f4f4; +} + +.hypha-actions ul { + margin: 0; + padding: 0; +} + +.hypha-actions li { + list-style: none; +} + +.hypha-actions a { + display: block; + padding: 6px 16px; + font: inherit; + text-decoration: none; + color: #666; + transition: 0.1s background; +} + +aside .hypha-actions a:hover { + background: #eaeaea; +} + + + + + + + + + + \ No newline at end of file diff --git a/w/m/Templates/default-light/main.css/meta.json b/w/m/Templates/default-light/main.css/meta.json new file mode 100644 index 0000000..c34401f --- /dev/null +++ b/w/m/Templates/default-light/main.css/meta.json @@ -0,0 +1,19 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": [ + "" + ], + "name": "main.css", + "comment": "Update sys/main.css", + "author": "", + "time": 1592666188, + "text_mime": "text/css", + "binary_mime": "", + "text_name": "1.css", + "binary_name": "" + } + } +} diff --git a/w/m/Templates/default-light/main.js/1.mjs b/w/m/Templates/default-light/main.js/1.mjs new file mode 100644 index 0000000..7a221e6 --- /dev/null +++ b/w/m/Templates/default-light/main.js/1.mjs @@ -0,0 +1,7 @@ +var menu = document.getElementById('shroomburgerMenu'); +document.getElementById('shroomBtn').addEventListener('click', function() { + menu.classList.add('active'); +}); +document.getElementById('mushroomBtn').addEventListener('click', function() { + menu.classList.remove('active'); +}); diff --git a/w/m/Templates/default-light/main.js/meta.json b/w/m/Templates/default-light/main.js/meta.json new file mode 100644 index 0000000..98d4ad5 --- /dev/null +++ b/w/m/Templates/default-light/main.js/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "main.js", + "comment": "Update sys/main.js", + "author": "", + "time": 1592937088, + "text_mime": "text/javascript", + "binary_mime": "", + "text_name": "1.mjs", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/w/m/Templates/default-light/updateOk.html/1.html b/w/m/Templates/default-light/updateOk.html/1.html new file mode 100644 index 0000000..f4ddcde --- /dev/null +++ b/w/m/Templates/default-light/updateOk.html/1.html @@ -0,0 +1,8 @@ + + + Saved {{ .Name }} + + +

    Saved successfully. Go back

    + + diff --git a/w/m/Templates/default-light/updateOk.html/meta.json b/w/m/Templates/default-light/updateOk.html/meta.json new file mode 100644 index 0000000..5662fd2 --- /dev/null +++ b/w/m/Templates/default-light/updateOk.html/meta.json @@ -0,0 +1,17 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": null, + "name": "updateOk.html", + "comment": "Create Templates/default-light/updateOk.html", + "author": "", + "time": 1592996644, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file From a9c72d91be4b38b87ea6a910a6324c423a93eba3 Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Fri, 26 Jun 2020 01:31:58 +0500 Subject: [PATCH 4/6] Reimplement editor --- cfg/config.go | 7 ++- fs/html.go | 54 +++++++++++++++++ fs/hypha.go | 60 +++++++++---------- handlers.go | 16 +++-- main.go | 6 +- render/render.go | 15 +++++ w/config.json | 2 +- .../default-dark/Hypha/edit/index.html/1.html | 3 +- .../Hypha/edit/index.html/1.html | 20 +------ .../Hypha/edit/sidebar.html/1.html | 19 ++++++ .../Hypha/edit/sidebar.html/meta.json | 19 ++++++ 11 files changed, 154 insertions(+), 67 deletions(-) create mode 100644 fs/html.go create mode 100644 w/m/Templates/default-light/Hypha/edit/sidebar.html/1.html create mode 100644 w/m/Templates/default-light/Hypha/edit/sidebar.html/meta.json diff --git a/cfg/config.go b/cfg/config.go index 7c0db8f..f5e6251 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -16,9 +16,10 @@ const ( ) var ( - WikiDir string - TemplatesDir string - configJsonPath string + DescribeHyphaHerePattern = "Describe %s here" + WikiDir string + TemplatesDir string + configJsonPath string // Default values that can be overriden in config.json Address = "127.0.0.1:80" diff --git a/fs/html.go b/fs/html.go new file mode 100644 index 0000000..f64059d --- /dev/null +++ b/fs/html.go @@ -0,0 +1,54 @@ +package fs + +import ( + "fmt" + "io/ioutil" + "log" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) + +func markdownToHtml(md string) string { + extensions := parser.CommonExtensions | parser.AutoHeadingIDs + p := parser.NewWithExtensions(extensions) + + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + + return string(markdown.ToHTML([]byte(md), p, renderer)) +} + +func (h *Hypha) asHtml() (string, error) { + rev := h.actual + ret := `
    +

    ` + rev.FullName + `

    +` + // What about using
    ? + if h.hasBinaryData() { + ret += fmt.Sprintf(``, rev.FullName, rev.Id) + } + + contents, err := ioutil.ReadFile(rev.TextPath) + if err != nil { + log.Println("Failed to render", rev.FullName, ":", err) + return "", err + } + + // TODO: support more markups. + // TODO: support mycorrhiza extensions like transclusion. + switch rev.TextMime { + case "text/markdown": + html := markdown.ToHTML(contents, nil, nil) + ret += string(html) + default: + ret += fmt.Sprintf(`
    %s
    `, contents) + } + + ret += ` +
    ` + + return ret, nil +} diff --git a/fs/hypha.go b/fs/hypha.go index 737a070..3f198f2 100644 --- a/fs/hypha.go +++ b/fs/hypha.go @@ -9,9 +9,9 @@ import ( "net/http" "path/filepath" "strconv" + "strings" "github.com/bouncepaw/mycorrhiza/cfg" - "github.com/gomarkdown/markdown" ) type Hypha struct { @@ -27,6 +27,32 @@ func (h *Hypha) TextPath() string { return h.actual.TextPath } +func (h *Hypha) TagsJoined() string { + if h.Exists { + return strings.Join(h.actual.Tags, ", ") + } + return "" +} + +func (h *Hypha) TextMime() string { + if h.Exists { + return h.actual.TextMime + } + return "text/markdown" +} + +func (h *Hypha) TextContent() string { + if h.Exists { + contents, err := ioutil.ReadFile(h.TextPath()) + if err != nil { + log.Println("Could not read", h.FullName) + return "Error: could not hypha text content file. It is recommended to cancel editing. Please contact the wiki admin. If you are the admin, see the logs." + } + return string(contents) + } + return fmt.Sprintf(cfg.DescribeHyphaHerePattern, h.FullName) +} + func (s *Storage) Open(name string) (*Hypha, error) { h := &Hypha{ Exists: true, @@ -123,38 +149,6 @@ func (h *Hypha) hasBinaryData() bool { return h.actual.BinaryMime != "" } -func (h *Hypha) asHtml() (string, error) { - rev := h.actual - ret := `
    -

    ` + rev.FullName + `

    -` - // What about using
    ? - if h.hasBinaryData() { - ret += fmt.Sprintf(``, rev.FullName, rev.Id) - } - - contents, err := ioutil.ReadFile(rev.TextPath) - if err != nil { - log.Println("Failed to render", rev.FullName, ":", err) - return "", err - } - - // TODO: support more markups. - // TODO: support mycorrhiza extensions like transclusion. - switch rev.TextMime { - case "text/markdown": - html := markdown.ToHTML(contents, nil, nil) - ret += string(html) - default: - ret += fmt.Sprintf(`
    %s
    `, contents) - } - - ret += ` -
    ` - - return ret, nil -} - // ActionRaw is used with `?action=raw`. // It writes text content of the revision without any parsing or rendering. func (h *Hypha) ActionRaw(w http.ResponseWriter) { diff --git a/handlers.go b/handlers.go index d8a1ada..6f1fe71 100644 --- a/handlers.go +++ b/handlers.go @@ -29,8 +29,6 @@ func HandlerBase(w http.ResponseWriter, rq *http.Request) (*fs.Hypha, bool) { if err != nil { log.Println(err) } - - log.Println(*h) return h, true } @@ -60,14 +58,20 @@ func HandlerView(w http.ResponseWriter, rq *http.Request) { } } -/* func HandlerEdit(w http.ResponseWriter, rq *http.Request) { vars := mux.Vars(rq) - ActionEdit(vars["hypha"], w) + h, err := fs.Hs.Open(vars["hypha"]) + // How could this happen? + if err != nil { + log.Println(err) + return + } + h.OnRevision("0") + w.Header().Set("Content-Type", "text/html;charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte(render.HyphaEdit(h))) } -*/ - /* // makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant. // Expected output for string above: []string{"foo", "bar", "kek"} diff --git a/main.go b/main.go index 4cbcb8d..09a3682 100644 --- a/main.go +++ b/main.go @@ -63,10 +63,10 @@ func main() { r.Queries("action", "view").Path(cfg.HyphaUrl). HandlerFunc(HandlerView) - /* - r.Queries("action", "edit").Path(hyphaUrl). - HandlerFunc(HandlerEdit) + r.Queries("action", "edit").Path(cfg.HyphaUrl). + HandlerFunc(HandlerEdit) + /* r.Queries("action", "update").Path(hyphaUrl).Methods("POST"). HandlerFunc(HandlerUpdate) */ diff --git a/render/render.go b/render/render.go index 75c3e86..9af9434 100644 --- a/render/render.go +++ b/render/render.go @@ -26,6 +26,21 @@ func EditHyphaPage(name, textMime, content, tags string) string { return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys) } +func HyphaEdit(h *fs.Hypha) string { + page := map[string]string{ + "Name": h.FullName, + "Tags": h.TagsJoined(), + "TextMime": h.TextMime(), + "Text": h.TextContent(), + } + keys := map[string]string{ + "Title": fmt.Sprintf(cfg.TitleEditTemplate, h.FullName), + "Header": renderFromString(h.FullName, "Hypha/edit/header.html"), + "Sidebar": renderFromMap(page, "Hypha/edit/sidebar.html"), + } + return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys) +} + // Hypha404 returns 404 page for nonexistent page. func Hypha404(name, _ string) string { return HyphaGeneric(name, name, "Hypha/view/404.html") diff --git a/w/config.json b/w/config.json index 409bcd2..3497a5a 100644 --- a/w/config.json +++ b/w/config.json @@ -1,6 +1,6 @@ { "address": "127.0.0.1:1737", - "theme": "default-light", + "theme": "default-dark", "site-title": "🍄 MycorrhizaWiki", "title-templates": { "edit-hypha": "Edit %s at MycorrhizaWiki", diff --git a/w/m/Templates/default-dark/Hypha/edit/index.html/1.html b/w/m/Templates/default-dark/Hypha/edit/index.html/1.html index 842b43b..a102c1e 100644 --- a/w/m/Templates/default-dark/Hypha/edit/index.html/1.html +++ b/w/m/Templates/default-dark/Hypha/edit/index.html/1.html @@ -9,8 +9,7 @@

    Edit box

    +{{ .Text }} diff --git a/w/m/Templates/default-light/Hypha/edit/index.html/1.html b/w/m/Templates/default-light/Hypha/edit/index.html/1.html index 49ab078..943cf5f 100644 --- a/w/m/Templates/default-light/Hypha/edit/index.html/1.html +++ b/w/m/Templates/default-light/Hypha/edit/index.html/1.html @@ -3,34 +3,16 @@ method="POST" enctype="multipart/form-data" action="?action=update"> -

    Edit box

    +{{ .Text }}

    Upload file

    If this hypha has a file like that, the text above is meant to be a description of it

    - -
    -

    Text MIME-type

    -

    Good types are text/markdown and text/plain

    - - -

    Revision comment

    -

    Please make your comment helpful

    - - -

    Edit tags

    -

    Tags are separated by commas, whitespace is ignored

    - -
    diff --git a/w/m/Templates/default-light/Hypha/edit/sidebar.html/1.html b/w/m/Templates/default-light/Hypha/edit/sidebar.html/1.html new file mode 100644 index 0000000..3f257cf --- /dev/null +++ b/w/m/Templates/default-light/Hypha/edit/sidebar.html/1.html @@ -0,0 +1,19 @@ +

    Text MIME-type

    +

    Good types are text/markdown and text/plain

    + + +

    Revision comment

    +

    Please make your comment helpful

    + + +

    Edit tags

    +

    Tags are separated by commas, whitespace is ignored

    + + +

    Upload file

    +

    If this hypha has a file like that, the text above is meant to be a description of it

    + + + +

    +
    diff --git a/w/m/Templates/default-light/Hypha/edit/sidebar.html/meta.json b/w/m/Templates/default-light/Hypha/edit/sidebar.html/meta.json new file mode 100644 index 0000000..1fe3342 --- /dev/null +++ b/w/m/Templates/default-light/Hypha/edit/sidebar.html/meta.json @@ -0,0 +1,19 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": [ + "" + ], + "name": "sidebar.html", + "comment": "Create Templates/default-dark/Hypha/edit/sidebar.html", + "author": "", + "time": 1593003792, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} From d36f86f715cff0828a886b77732d6075c1db3b00 Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Fri, 26 Jun 2020 23:07:21 +0500 Subject: [PATCH 5/6] Reimplement action=update --- fs/hypha.go | 184 ++++++++++++++++-- fs/revision.go | 25 +++ genealogy.gog | 107 ---------- handlers.go | 161 ++------------- hypha.gog | 74 ------- main.go | 6 +- render.gog | 96 --------- render/render.go | 13 +- revision.gog | 135 ------------- w/config.json | 2 +- w/m/Templates/1.markdown | 1 + .../Hypha/edit/index.html/1.html | 3 +- .../Hypha/edit/sidebar.html/1.html | 10 +- w/m/Templates/meta.json | 21 ++ walk.gog | 120 ------------ 15 files changed, 256 insertions(+), 702 deletions(-) delete mode 100644 genealogy.gog delete mode 100644 hypha.gog delete mode 100644 render.gog delete mode 100644 revision.gog create mode 100644 w/m/Templates/1.markdown create mode 100644 w/m/Templates/meta.json delete mode 100644 walk.gog diff --git a/fs/hypha.go b/fs/hypha.go index 3f198f2..7b0abb2 100644 --- a/fs/hypha.go +++ b/fs/hypha.go @@ -7,9 +7,11 @@ import ( "io/ioutil" "log" "net/http" + "os" "path/filepath" "strconv" "strings" + "time" "github.com/bouncepaw/mycorrhiza/cfg" ) @@ -21,6 +23,22 @@ type Hypha struct { Deleted bool `json:"deleted"` Revisions map[string]*Revision `json:"revisions"` actual *Revision `json:"-"` + Invalid bool + Err error +} + +func (h *Hypha) Invalidate(err error) *Hypha { + h.Invalid = true + h.Err = err + return h +} + +func (h *Hypha) MetaJsonPath() string { + return filepath.Join(h.Path(), "meta.json") +} + +func (h *Hypha) Path() string { + return filepath.Join(cfg.WikiDir, h.FullName) } func (h *Hypha) TextPath() string { @@ -53,7 +71,7 @@ func (h *Hypha) TextContent() string { return fmt.Sprintf(cfg.DescribeHyphaHerePattern, h.FullName) } -func (s *Storage) Open(name string) (*Hypha, error) { +func (s *Storage) Open(name string) *Hypha { h := &Hypha{ Exists: true, FullName: name, @@ -67,14 +85,12 @@ func (s *Storage) Open(name string) (*Hypha, error) { } else { metaJsonText, err := ioutil.ReadFile(filepath.Join(path, "meta.json")) if err != nil { - log.Fatal(err) - return nil, err + return h.Invalidate(err) } err = json.Unmarshal(metaJsonText, &h) if err != nil { - log.Fatal(err) - return nil, err + return h.Invalidate(err) } // fill in rooted paths to content files and full names for idStr, rev := range h.Revisions { @@ -86,10 +102,9 @@ func (s *Storage) Open(name string) (*Hypha, error) { rev.TextPath = filepath.Join(path, rev.TextName) } - err = h.OnRevision("0") - return h, err + return h.OnRevision("0") } - return h, nil + return h } func (h *Hypha) parentName() string { @@ -101,9 +116,12 @@ func (h *Hypha) metaJsonPath() string { } // OnRevision tries to change to a revision specified by `id`. -func (h *Hypha) OnRevision(id string) error { +func (h *Hypha) OnRevision(id string) *Hypha { + if h.Invalid || !h.Exists { + return h + } if len(h.Revisions) == 0 { - return errors.New("This hypha has no revisions") + return h.Invalidate(errors.New("This hypha has no revisions")) } if id == "0" { id = h.NewestId() @@ -112,7 +130,7 @@ func (h *Hypha) OnRevision(id string) error { if rev, _ := h.Revisions[id]; true { h.actual = rev } - return nil + return h } // NewestId finds the largest id among all revisions. @@ -214,3 +232,147 @@ func (h *Hypha) ActionView(w http.ResponseWriter, renderExists, renderNotExists } h.PlainLog("Rendering hypha view") } + +// CreateDirIfNeeded creates directory where the hypha must reside if needed. +// It is not needed if the dir already exists. +func (h *Hypha) CreateDirIfNeeded() *Hypha { + if h.Invalid { + return h + } + // os.MkdirAll created dir if it is not there. Basically, checks it for us. + err := os.MkdirAll(filepath.Join(cfg.WikiDir, h.FullName), os.ModePerm) + if err != nil { + h.Invalidate(err) + } + return h +} + +// makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant. +// Expected output for string above: []string{"foo", "bar", "kek"} +func makeTagsSlice(responseTagsString string) (ret []string) { + for _, tag := range strings.Split(responseTagsString, ",") { + if trimmed := strings.TrimSpace(tag); "" == trimmed { + ret = append(ret, trimmed) + } + } + return ret +} + +// revisionFromHttpData creates a new revison for hypha `h`. All data is fetched from `rq`, except for BinaryMime and BinaryPath which require additional processing. The revision is inserted for you. You'll have to pop it out if there is an error. +func (h *Hypha) AddRevisionFromHttpData(rq *http.Request) *Hypha { + if h.Invalid { + return h + } + id := 1 + if h.Exists { + id = h.actual.Id + 1 + } + log.Printf("Creating revision %d from http data", id) + rev := &Revision{ + Id: id, + FullName: h.FullName, + ShortName: filepath.Base(h.FullName), + Tags: makeTagsSlice(rq.PostFormValue("tags")), + Comment: rq.PostFormValue("comment"), + Author: rq.PostFormValue("author"), + Time: int(time.Now().Unix()), + TextMime: rq.PostFormValue("text_mime"), + // Fields left: BinaryMime, BinaryPath, BinaryName, TextName, TextPath + } + rev.generateTextFilename() // TextName is set now + rev.TextPath = filepath.Join(h.Path(), rev.TextName) + return h.AddRevision(rev) +} + +func (h *Hypha) AddRevision(rev *Revision) *Hypha { + if h.Invalid { + return h + } + h.Revisions[strconv.Itoa(rev.Id)] = rev + h.actual = rev + return h +} + +// WriteTextFileFromHttpData tries to fetch text content from `rq` for revision `rev` and write it to a corresponding text file. It used in `HandlerUpdate`. +func (h *Hypha) WriteTextFileFromHttpData(rq *http.Request) *Hypha { + if h.Invalid { + return h + } + data := []byte(rq.PostFormValue("text")) + err := ioutil.WriteFile(h.TextPath(), data, 0644) + if err != nil { + log.Println("Failed to write", len(data), "bytes to", h.TextPath()) + h.Invalidate(err) + } + return h +} + +// WriteBinaryFileFromHttpData tries to fetch binary content from `rq` for revision `newRev` and write it to a corresponding binary file. If there is no content, it is taken from a previous revision, if there is any. +func (h *Hypha) WriteBinaryFileFromHttpData(rq *http.Request) *Hypha { + if h.Invalid { + return h + } + // 10 MB file size limit + rq.ParseMultipartForm(10 << 20) + // Read file + file, handler, err := rq.FormFile("binary") + if file != nil { + defer file.Close() + } + // If file is not passed: + if err != nil { + // Let's hope there are no other errors 🙏 + // TODO: actually check if there any other errors + log.Println("No binary data passed for", h.FullName) + // It is expected there is at least one revision + if len(h.Revisions) > 1 { + prevRev := h.Revisions[strconv.Itoa(h.actual.Id-1)] + h.actual.BinaryMime = prevRev.BinaryMime + h.actual.BinaryPath = prevRev.BinaryPath + h.actual.BinaryName = prevRev.BinaryName + log.Println("Set previous revision's binary data") + } + return h + } + // If file is passed: + h.actual.BinaryMime = handler.Header.Get("Content-Type") + h.actual.generateBinaryFilename() + h.actual.BinaryPath = filepath.Join(h.Path(), h.actual.BinaryName) + + data, err := ioutil.ReadAll(file) + if err != nil { + return h.Invalidate(err) + } + log.Println("Got", len(data), "of binary data for", h.FullName) + err = ioutil.WriteFile(h.actual.BinaryPath, data, 0644) + if err != nil { + return h.Invalidate(err) + } + log.Println("Written", len(data), "of binary data for", h.FullName) + return h +} + +// SaveJson dumps the hypha's metadata to `meta.json` file. +func (h *Hypha) SaveJson() *Hypha { + if h.Invalid { + return h + } + data, err := json.MarshalIndent(h, "", "\t") + if err != nil { + return h.Invalidate(err) + } + err = ioutil.WriteFile(h.MetaJsonPath(), data, 0644) + if err != nil { + return h.Invalidate(err) + } + log.Println("Saved JSON data of", h.FullName) + return h +} + +// Store adds `h` to the `Hs` if it is not already there +func (h *Hypha) Store() *Hypha { + if !h.Invalid { + Hs.paths[h.FullName] = h.Path() + } + return h +} diff --git a/fs/revision.go b/fs/revision.go index 67f0346..69ae0c6 100644 --- a/fs/revision.go +++ b/fs/revision.go @@ -1,5 +1,10 @@ package fs +import ( + "mime" + "strconv" +) + type Revision struct { Id int `json:"-"` FullName string `json:"-"` @@ -15,3 +20,23 @@ type Revision struct { TextName string `json:"text_name"` BinaryName string `json:"binary_name"` } + +// TODO: https://github.com/bouncepaw/mycorrhiza/issues/4 +// Some filenames are wrong? +func (rev *Revision) generateTextFilename() { + ts, err := mime.ExtensionsByType(rev.TextMime) + if err != nil || ts == nil { + rev.TextName = strconv.Itoa(rev.Id) + ".txt" + } else { + rev.TextName = strconv.Itoa(rev.Id) + ts[0] + } +} + +func (rev *Revision) generateBinaryFilename() { + ts, err := mime.ExtensionsByType(rev.BinaryMime) + if err != nil || ts == nil { + rev.BinaryName = strconv.Itoa(rev.Id) + ".bin" + } else { + rev.BinaryName = strconv.Itoa(rev.Id) + ts[0] + } +} diff --git a/genealogy.gog b/genealogy.gog deleted file mode 100644 index 8b2a0af..0000000 --- a/genealogy.gog +++ /dev/null @@ -1,107 +0,0 @@ -/* Genealogy is all about relationships between hyphae.*/ -package main - -import ( - "fmt" - "log" - "path/filepath" - "sort" - "strings" -) - -// setRelations fills in all children names based on what hyphae call their parents. -func setRelations(hyphae map[string]*Hypha) { - for name, h := range hyphae { - if _, ok := hyphae[h.parentName]; ok && h.parentName != "." { - hyphae[h.parentName].ChildrenNames = append(hyphae[h.parentName].ChildrenNames, name) - } - } -} - -// AddChild adds a name to the list of children names of the hypha. -func (h *Hypha) AddChild(childName string) { - h.ChildrenNames = append(h.ChildrenNames, childName) -} - -// If Name == "", the tree is empty. -type Tree struct { - Name string - Ancestors []string - Siblings []string - Descendants []*Tree - Root bool -} - -// GetTree generates a Tree for the given hypha name. -// It can also generate trees for non-existent hyphae, that's why we use `name string` instead of making it a method on `Hypha`. -// In `root` is `false`, siblings will not be fetched. -// Parameter `limit` is unused now but it is meant to limit how many subhyphae can be shown. -func GetTree(name string, root bool, limit ...int) *Tree { - t := &Tree{Name: name, Root: root} - for hyphaName, _ := range hyphae { - t.compareNamesAndAppend(hyphaName) - } - sort.Slice(t.Ancestors, func(i, j int) bool { - return strings.Count(t.Ancestors[i], "/") < strings.Count(t.Ancestors[j], "/") - }) - sort.Strings(t.Siblings) - sort.Slice(t.Descendants, func(i, j int) bool { - a := t.Descendants[i].Name - b := t.Descendants[j].Name - return len(a) < len(b) - }) - log.Printf("Generate tree for %v: %v %v\n", t.Name, t.Ancestors, t.Siblings) - return t -} - -// Compares names appends name2 to an array of `t`: -func (t *Tree) compareNamesAndAppend(name2 string) { - switch { - case t.Name == name2: - case strings.HasPrefix(t.Name, name2): - t.Ancestors = append(t.Ancestors, name2) - case t.Root && (strings.Count(t.Name, "/") == strings.Count(name2, "/") && - (filepath.Dir(t.Name) == filepath.Dir(name2))): - t.Siblings = append(t.Siblings, name2) - case strings.HasPrefix(name2, t.Name): - t.Descendants = append(t.Descendants, GetTree(name2, false)) - } -} - -// AsHtml returns HTML representation of a tree. -// It recursively itself on the tree's children. -// TODO: redo with templates. I'm not in mood for it now. -func (t *Tree) AsHtml() (html string) { - if t.Name == "" { - return "" - } - html += `` - return html -} - -// navitreeEntry is a small utility function that makes generating html easier. -// Someone please redo it in templates. -func navitreeEntry(name string) string { - return fmt.Sprintf(`
  • - %s -
  • -`, name, filepath.Base(name)) -} diff --git a/handlers.go b/handlers.go index 6f1fe71..1dbe3f0 100644 --- a/handlers.go +++ b/handlers.go @@ -20,14 +20,10 @@ import ( // Boilerplate code present in many handlers. Good to have it. func HandlerBase(w http.ResponseWriter, rq *http.Request) (*fs.Hypha, bool) { vars := mux.Vars(rq) - h, err := fs.Hs.Open(vars["hypha"]) - if err != nil { - log.Println(err) - return nil, false - } - err = h.OnRevision(RevInMap(vars)) - if err != nil { - log.Println(err) + h := fs.Hs.Open(vars["hypha"]).OnRevision(RevInMap(vars)) + if h.Invalid { + log.Println(h.Err) + return h, false } return h, true } @@ -60,153 +56,30 @@ func HandlerView(w http.ResponseWriter, rq *http.Request) { func HandlerEdit(w http.ResponseWriter, rq *http.Request) { vars := mux.Vars(rq) - h, err := fs.Hs.Open(vars["hypha"]) - // How could this happen? - if err != nil { - log.Println(err) - return - } - h.OnRevision("0") + h := fs.Hs.Open(vars["hypha"]).OnRevision("0") w.Header().Set("Content-Type", "text/html;charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte(render.HyphaEdit(h))) } -/* -// makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant. -// Expected output for string above: []string{"foo", "bar", "kek"} -func makeTagsSlice(responseTagsString string) (ret []string) { - for _, tag := range strings.Split(responseTagsString, ",") { - if trimmed := strings.TrimSpace(tag); "" == trimmed { - ret = append(ret, trimmed) - } - } - return ret -} - -// getHypha returns an existing hypha if it exists in `hyphae` or creates a new one. If it `isNew`, you'll have to insert it to `hyphae` yourself. -func getHypha(name string) (*Hypha, bool) { - log.Println("Accessing hypha", name) - if h, ok := hyphae[name]; ok { - log.Println("Got hypha", name) - return h, false - } - log.Println("Create hypha", name) - h := &Hypha{ - FullName: name, - Path: filepath.Join(cfg.WikiDir, name), - Revisions: make(map[string]*Revision), - parentName: filepath.Dir(name), - } - return h, true -} - -// revisionFromHttpData creates a new revison for hypha `h`. All data is fetched from `rq`, except for BinaryMime and BinaryPath which require additional processing. You'll have te insert the revision to `h` yourself. -func revisionFromHttpData(h *Hypha, rq *http.Request) *Revision { - idStr := strconv.Itoa(h.NewestRevisionInt() + 1) - log.Printf("Creating revision %s from http data", idStr) - rev := &Revision{ - Id: h.NewestRevisionInt() + 1, - FullName: h.FullName, - ShortName: filepath.Base(h.FullName), - Tags: makeTagsSlice(rq.PostFormValue("tags")), - Comment: rq.PostFormValue("comment"), - Author: rq.PostFormValue("author"), - Time: int(time.Now().Unix()), - TextMime: rq.PostFormValue("text_mime"), - // Fields left: BinaryMime, BinaryPath, BinaryName, TextName, TextPath - } - rev.desiredTextFilename() // TextName is set now - rev.TextPath = filepath.Join(h.Path, rev.TextName) - return rev -} - -// writeTextFileFromHttpData tries to fetch text content from `rq` for revision `rev` and write it to a corresponding text file. It used in `HandlerUpdate`. -func writeTextFileFromHttpData(rev *Revision, rq *http.Request) error { - data := []byte(rq.PostFormValue("text")) - err := ioutil.WriteFile(rev.TextPath, data, 0644) - if err != nil { - log.Println("Failed to write", len(data), "bytes to", rev.TextPath) - } - return err -} - -// writeBinaryFileFromHttpData tries to fetch binary content from `rq` for revision `newRev` and write it to a corresponding binary file. If there is no content, it is taken from `oldRev`. -func writeBinaryFileFromHttpData(h *Hypha, oldRev Revision, newRev *Revision, rq *http.Request) error { - // 10 MB file size limit - rq.ParseMultipartForm(10 << 20) - // Read file - file, handler, err := rq.FormFile("binary") - if file != nil { - defer file.Close() - } - if err != nil { - log.Println("No binary data passed for", newRev.FullName) - newRev.BinaryMime = oldRev.BinaryMime - newRev.BinaryPath = oldRev.BinaryPath - newRev.BinaryName = oldRev.BinaryName - log.Println("Set previous revision's binary data") - return nil - } - newRev.BinaryMime = handler.Header.Get("Content-Type") - newRev.BinaryPath = filepath.Join(h.Path, newRev.IdAsStr()+".bin") - newRev.BinaryName = newRev.desiredBinaryFilename() - data, err := ioutil.ReadAll(file) - if err != nil { - log.Println(err) - return err - } - log.Println("Got", len(data), "of binary data for", newRev.FullName) - err = ioutil.WriteFile(newRev.BinaryPath, data, 0644) - if err != nil { - log.Println("Failed to write", len(data), "bytes to", newRev.TextPath) - return err - } - log.Println("Written", len(data), "of binary data for", newRev.FullName) - return nil -} - func HandlerUpdate(w http.ResponseWriter, rq *http.Request) { vars := mux.Vars(rq) - log.Println("Attempt to update hypha", mux.Vars(rq)["hypha"]) - h, isNew := getHypha(vars["hypha"]) - var oldRev Revision - if !isNew { - oldRev = h.GetNewestRevision() - } else { - h = &Hypha{ - FullName: vars["hypha"], - Path: filepath.Join(cfg.WikiDir, vars["hypha"]), - Revisions: make(map[string]*Revision), - parentName: filepath.Dir(vars["hypha"]), - } - h.CreateDir() - oldRev = Revision{} - } - newRev := revisionFromHttpData(h, rq) + log.Println("Attempt to update hypha", vars["hypha"]) + h := fs.Hs. + Open(vars["hypha"]). + CreateDirIfNeeded(). + AddRevisionFromHttpData(rq). + WriteTextFileFromHttpData(rq). + WriteBinaryFileFromHttpData(rq). + SaveJson(). + Store() - err := writeTextFileFromHttpData(newRev, rq) - if err != nil { - log.Println(err) + if h.Invalid { + log.Println(h.Err) return } - err = writeBinaryFileFromHttpData(h, oldRev, newRev, rq) - if err != nil { - log.Println(err) - return - } - - h.Revisions[newRev.IdAsStr()] = newRev - h.SaveJson() - if isNew { - hyphae[h.FullName] = h - } - - log.Println("Current hyphae storage is", hyphae) w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "text/html; charset=utf-8") - d := map[string]string{"Name": h.FullName} - w.Write([]byte(renderFromMap(d, "updateOk.html"))) + w.Write([]byte(render.HyphaUpdateOk(h))) } -*/ diff --git a/hypha.gog b/hypha.gog deleted file mode 100644 index bbdc6ee..0000000 --- a/hypha.gog +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/bouncepaw/mycorrhiza/cfg" -) - -// AsHtml returns HTML representation of the hypha. -// No layout or navigation are present here. Just the hypha. -func (h *Hypha) AsHtml(id string, w http.ResponseWriter) (string, error) { - if "0" == id { - id = h.NewestRevision() - } - if rev, ok := h.Revisions[id]; ok { - return rev.AsHtml(w) - } - return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, id) -} - -// CreateDir creates directory where the hypha must reside. -// It is meant to be used with new hyphae. -func (h *Hypha) CreateDir() error { - return os.MkdirAll(h.Path, os.ModePerm) -} - -// SaveJson dumps the hypha's metadata to `meta.json` file. -func (h *Hypha) SaveJson() { - data, err := json.MarshalIndent(h, "", "\t") - if err != nil { - log.Println("Failed to create JSON of hypha.", err) - return - } - err = ioutil.WriteFile(h.MetaJsonPath(), data, 0644) - if err != nil { - log.Println("Failed to save JSON of hypha.", err) - return - } - log.Println("Saved JSON data of", h.FullName) -} - -// ActionEdit is called with `?acton=edit`. -// It represents the hypha editor. -func ActionEdit(hyphaName string, w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - var initContents, initTextMime, initTags string - if h, ok := hyphae[hyphaName]; ok { - newestRev := h.GetNewestRevision() - contents, err := ioutil.ReadFile(newestRev.TextPath) - if err != nil { - log.Println("Could not read", newestRev.TextPath) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(cfg.GenericErrorMsg)) - return - } - initContents = string(contents) - initTextMime = newestRev.TextMime - initTags = strings.Join(newestRev.Tags, ",") - } else { - initContents = "Describe " + hyphaName + "here." - initTextMime = "text/markdown" - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte(EditHyphaPage(hyphaName, initTextMime, initContents, initTags))) -} diff --git a/main.go b/main.go index 09a3682..215646c 100644 --- a/main.go +++ b/main.go @@ -66,10 +66,8 @@ func main() { r.Queries("action", "edit").Path(cfg.HyphaUrl). HandlerFunc(HandlerEdit) - /* - r.Queries("action", "update").Path(hyphaUrl).Methods("POST"). - HandlerFunc(HandlerUpdate) - */ + r.Queries("action", "update").Path(cfg.HyphaUrl).Methods("POST"). + HandlerFunc(HandlerUpdate) r.HandleFunc(cfg.HyphaUrl, HandlerView) diff --git a/render.gog b/render.gog deleted file mode 100644 index dc9d13a..0000000 --- a/render.gog +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "path" - "text/template" - - "github.com/bouncepaw/mycorrhiza/cfg" -) - -// EditHyphaPage returns HTML page of hypha editor. -func EditHyphaPage(name, textMime, content, tags string) string { - keys := map[string]string{ - "Title": fmt.Sprintf(cfg.TitleEditTemplate, name), - "Header": renderFromString(name, "Hypha/edit/header.html"), - "Sidebar": renderFromString("", "Hypha/edit/sidebar.html"), - } - page := map[string]string{ - "Text": content, - "TextMime": textMime, - "Name": name, - "Tags": tags, - } - return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys) -} - -// Hypha404 returns 404 page for nonexistent page. -func Hypha404(name string) string { - return HyphaGeneric(name, name, "Hypha/view/404.html") -} - -// HyphaPage returns HTML page of hypha viewer. -func HyphaPage(rev Revision, content string) string { - return HyphaGeneric(rev.FullName, content, "Hypha/view/index.html") -} - -// HyphaGeneric is used when building renderers for all types of hypha pages -func HyphaGeneric(name string, content, templatePath string) string { - var sidebar string - if aside := renderFromMap(map[string]string{ - "Tree": GetTree(name, true).AsHtml(), - }, "Hypha/view/sidebar.html"); aside != "" { - sidebar = aside - } - keys := map[string]string{ - "Title": fmt.Sprintf(cfg.TitleTemplate, name), - "Sidebar": sidebar, - } - return renderBase(renderFromString(content, templatePath), keys) -} - -// renderBase collects and renders page from base template -// Args: -// content: string or pre-rendered template -// keys: map with replaced standart fields -func renderBase(content string, keys map[string]string) string { - page := map[string]string{ - "Title": cfg.SiteTitle, - "Main": "", - "SiteTitle": cfg.SiteTitle, - } - for key, val := range keys { - page[key] = val - } - page["Main"] = content - return renderFromMap(page, "base.html") -} - -// renderFromMap applies `data` map to template in `templatePath` and returns the result. -func renderFromMap(data map[string]string, templatePath string) string { - filePath := path.Join(cfg.TemplatesDir, templatePath) - tmpl, err := template.ParseFiles(filePath) - if err != nil { - return err.Error() - } - buf := new(bytes.Buffer) - if err := tmpl.Execute(buf, data); err != nil { - return err.Error() - } - return buf.String() -} - -// renderFromMap applies `data` string to template in `templatePath` and returns the result. -func renderFromString(data string, templatePath string) string { - filePath := path.Join(cfg.TemplatesDir, templatePath) - tmpl, err := template.ParseFiles(filePath) - if err != nil { - return err.Error() - } - buf := new(bytes.Buffer) - if err := tmpl.Execute(buf, data); err != nil { - return err.Error() - } - return buf.String() -} diff --git a/render/render.go b/render/render.go index 9af9434..7ff6a05 100644 --- a/render/render.go +++ b/render/render.go @@ -66,6 +66,13 @@ func HyphaGeneric(name string, content, templatePath string) string { return renderBase(renderFromString(content, templatePath), keys) } +func HyphaUpdateOk(h *fs.Hypha) string { + data := map[string]string{ + "Name": h.FullName, + } + return renderFromMap(data, "updateOk.html") +} + // renderBase collects and renders page from base template // Args: // content: string or pre-rendered template @@ -86,8 +93,7 @@ func renderBase(content string, keys map[string]string) string { // renderFromMap applies `data` map to template in `templatePath` and returns the result. func renderFromMap(data map[string]string, templatePath string) string { hyphPath := path.Join(cfg.TemplatesDir, cfg.Theme, templatePath) - h, _ := fs.Hs.Open(hyphPath) - h.OnRevision("0") + h := fs.Hs.Open(hyphPath).OnRevision("0") tmpl, err := template.ParseFiles(h.TextPath()) if err != nil { return err.Error() @@ -102,8 +108,7 @@ func renderFromMap(data map[string]string, templatePath string) string { // renderFromMap applies `data` string to template in `templatePath` and returns the result. func renderFromString(data string, templatePath string) string { hyphPath := path.Join(cfg.TemplatesDir, cfg.Theme, templatePath) - h, _ := fs.Hs.Open(hyphPath) - h.OnRevision("0") + h := fs.Hs.Open(hyphPath).OnRevision("0") tmpl, err := template.ParseFiles(h.TextPath()) if err != nil { return err.Error() diff --git a/revision.gog b/revision.gog deleted file mode 100644 index 3117b2a..0000000 --- a/revision.gog +++ /dev/null @@ -1,135 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "log" - "mime" - "net/http" - "strconv" - - "github.com/gomarkdown/markdown" -) - -// Revision represents a revision, duh. -// A revision is a version of a hypha at a point in time. -type Revision struct { - Id int `json:"-"` - FullName string `json:"-"` - Tags []string `json:"tags"` - ShortName string `json:"name"` - Comment string `json:"comment"` - Author string `json:"author"` - Time int `json:"time"` - TextMime string `json:"text_mime"` - BinaryMime string `json:"binary_mime"` - TextPath string `json:"-"` - BinaryPath string `json:"-"` - TextName string `json:"text_name"` - BinaryName string `json:"binary_name"` -} - -// IdAsStr returns revision's id as a string. -func (rev *Revision) IdAsStr() string { - return strconv.Itoa(rev.Id) -} - -// hasBinaryData returns true if the revision has any binary data associated. -// During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data. -func (rev *Revision) hasBinaryData() bool { - return rev.BinaryMime != "" -} - -// desiredBinaryFilename returns string that represents filename to use when saving a binary content file of a new revison. -// It also sets the corresponding field in `rev`. -func (rev *Revision) desiredBinaryFilename() string { - ts, err := mime.ExtensionsByType(rev.BinaryMime) - if err != nil || ts == nil { - rev.BinaryName = rev.IdAsStr() + ".bin" - } else { - rev.BinaryName = rev.IdAsStr() + ts[0] - } - return rev.BinaryName -} - -// desiredTextFilename returns string that represents filename to use when saving a text content file of a new revison. -// It also sets the corresponding field in `rev`. -func (rev *Revision) desiredTextFilename() string { - ts, err := mime.ExtensionsByType(rev.TextMime) - if err != nil || ts == nil { - log.Println("No idea how I should name this one:", rev.TextMime) - rev.TextName = rev.IdAsStr() + ".txt" - } else { - log.Println("A good extension would be one of these:", ts) - rev.TextName = rev.IdAsStr() + ts[0] - } - return rev.TextName -} - -// AsHtml returns HTML representation of the revision. -// If there is an error, it will be told about it in `w`. -func (rev *Revision) AsHtml(w http.ResponseWriter) (ret string, err error) { - ret += `
    -

    ` + rev.FullName + `

    -` - // TODO: support things other than images - if rev.hasBinaryData() { - ret += fmt.Sprintf(``, rev.FullName, rev.Id) - } - - contents, err := ioutil.ReadFile(rev.TextPath) - if err != nil { - log.Println("Failed to render", rev.FullName) - w.WriteHeader(http.StatusInternalServerError) - return "", err - } - - // TODO: support more markups. - // TODO: support mycorrhiza extensions like transclusion. - switch rev.TextMime { - case "text/markdown": - html := markdown.ToHTML(contents, nil, nil) - ret += string(html) - default: - ret += fmt.Sprintf(`
    %s
    `, contents) - } - - ret += ` -
    ` - return ret, nil -} - -// ActionGetBinary is used with `?action=getBinary`. -// It writes binary data of the revision. It also sets the MIME-type. -func (rev *Revision) ActionGetBinary(w http.ResponseWriter) { - fileContents, err := ioutil.ReadFile(rev.BinaryPath) - if err != nil { - log.Println("Failed to load binary data of", rev.FullName, rev.Id) - w.WriteHeader(http.StatusNotFound) - return - } - w.Header().Set("Content-Type", rev.BinaryMime) - w.WriteHeader(http.StatusOK) - w.Write(fileContents) - log.Println("Serving binary data of", rev.FullName, rev.Id) -} - -// ActionZen is used with `?action=zen`. -// It renders the revision without any layout or navigation. -func (rev *Revision) ActionZen(w http.ResponseWriter) { - html, err := rev.AsHtml(w) - if err == nil { - fmt.Fprint(w, html) - log.Println("Rendering", rev.FullName, "in zen mode") - } -} - -// ActionView is used with `?action=view` or without any action. -// It renders the revision with layout and navigation. -func (rev *Revision) ActionView(w http.ResponseWriter, layoutFun func(Revision, string) string) { - html, err := rev.AsHtml(w) - if err == nil { - fmt.Fprint(w, layoutFun(*rev, html)) - log.Println("Rendering", rev.FullName) - } -} diff --git a/w/config.json b/w/config.json index 3497a5a..409bcd2 100644 --- a/w/config.json +++ b/w/config.json @@ -1,6 +1,6 @@ { "address": "127.0.0.1:1737", - "theme": "default-dark", + "theme": "default-light", "site-title": "🍄 MycorrhizaWiki", "title-templates": { "edit-hypha": "Edit %s at MycorrhizaWiki", diff --git a/w/m/Templates/1.markdown b/w/m/Templates/1.markdown new file mode 100644 index 0000000..bf3de04 --- /dev/null +++ b/w/m/Templates/1.markdown @@ -0,0 +1 @@ +**TODO:** Reorganize templates. \ No newline at end of file diff --git a/w/m/Templates/default-light/Hypha/edit/index.html/1.html b/w/m/Templates/default-light/Hypha/edit/index.html/1.html index 943cf5f..d12ced6 100644 --- a/w/m/Templates/default-light/Hypha/edit/index.html/1.html +++ b/w/m/Templates/default-light/Hypha/edit/index.html/1.html @@ -2,7 +2,8 @@