diff --git a/cfg/config.go b/cfg/config.go
index e85dd13..f5e6251 100644
--- a/cfg/config.go
+++ b/cfg/config.go
@@ -8,10 +8,18 @@ import (
"path/filepath"
)
+const (
+ HyphaPattern = `[^\s\d:/?&\\][^:?&\\]*`
+ HyphaUrl = `/{hypha:` + HyphaPattern + `}`
+ RevisionPattern = `[\d]+`
+ RevQuery = `{rev:` + RevisionPattern + `}`
+)
+
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/data.go b/fs/data.go
new file mode 100644
index 0000000..0eb142d
--- /dev/null
+++ b/fs/data.go
@@ -0,0 +1,81 @@
+// This file contains methods for Hypha that calculate data about the hypha based on known information.
+package fs
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/bouncepaw/mycorrhiza/cfg"
+)
+
+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 {
+ return h.actual.TextPath
+}
+
+func (h *Hypha) parentName() string {
+ return filepath.Dir(h.FullName)
+}
+
+// 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) 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) 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
+}
+
+// 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) 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)
+}
diff --git a/fs/fs.go b/fs/fs.go
new file mode 100644
index 0000000..b10dc74
--- /dev/null
+++ b/fs/fs.go
@@ -0,0 +1,59 @@
+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
+}
+
+var Hs *Storage
+
+// InitStorage initiates filesystem-based hypha storage. It has to be called after configuration was inited.
+func InitStorage() {
+ Hs = &Storage{
+ paths: make(map[string]string),
+ root: cfg.WikiDir,
+ }
+ 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`
+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():
+ s.paths[hyphaName(path)] = path
+ }
+ }
+}
+
+func (h *Hypha) Close() {
+}
diff --git a/genealogy.go b/fs/genealogy.go
similarity index 61%
rename from genealogy.go
rename to fs/genealogy.go
index 8b2a0af..3addd3d 100644
--- a/genealogy.go
+++ b/fs/genealogy.go
@@ -1,5 +1,4 @@
-/* Genealogy is all about relationships between hyphae.*/
-package main
+package fs
import (
"fmt"
@@ -9,18 +8,7 @@ import (
"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)
+func (s *Storage) RenderHypha(h *Hypha) {
}
// If Name == "", the tree is empty.
@@ -35,11 +23,10 @@ type Tree struct {
// 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 {
+func (s *Storage) GetTree(name string, root bool) *Tree {
t := &Tree{Name: name, Root: root}
- for hyphaName, _ := range hyphae {
- t.compareNamesAndAppend(hyphaName)
+ 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], "/")
@@ -55,7 +42,7 @@ func GetTree(name string, root bool, limit ...int) *Tree {
}
// Compares names appends name2 to an array of `t`:
-func (t *Tree) compareNamesAndAppend(name2 string) {
+func (s *Storage) compareNamesAndAppend(t *Tree, name2 string) {
switch {
case t.Name == name2:
case strings.HasPrefix(t.Name, name2):
@@ -64,11 +51,11 @@ func (t *Tree) compareNamesAndAppend(name2 string) {
(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))
+ t.Descendants = append(t.Descendants, s.GetTree(name2, false))
}
}
-// AsHtml returns HTML representation of a tree.
+// 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) {
@@ -78,16 +65,13 @@ func (t *Tree) AsHtml() (html string) {
html += `
`
if t.Root {
for _, ancestor := range t.Ancestors {
- html += navitreeEntry(ancestor)
+ html += navitreeEntry(ancestor, "navitree__ancestor")
}
- }
- html += navitreeEntry(t.Name)
-
- if t.Root {
for _, siblingName := range t.Siblings {
- html += navitreeEntry(siblingName)
+ html += navitreeEntry(siblingName, "navitree__sibling")
}
}
+ html += navitreeEntry(t.Name, "navitree__name")
for _, subtree := range t.Descendants {
html += subtree.AsHtml()
@@ -99,9 +83,9 @@ func (t *Tree) AsHtml() (html string) {
// navitreeEntry is a small utility function that makes generating html easier.
// Someone please redo it in templates.
-func navitreeEntry(name string) string {
+func navitreeEntry(name, class string) string {
return fmt.Sprintf(`
- %s
+ %s
-`, name, filepath.Base(name))
+`, class, name, filepath.Base(name))
}
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
new file mode 100644
index 0000000..3ac7ed0
--- /dev/null
+++ b/fs/hypha.go
@@ -0,0 +1,319 @@
+package fs
+
+import (
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "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:"-"`
+ Invalid bool
+ Err error
+}
+
+func (h *Hypha) Invalidate(err error) *Hypha {
+ h.Invalid = true
+ h.Err = err
+ return h
+}
+
+func (s *Storage) Open(name string) *Hypha {
+ 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 {
+ return h.Invalidate(err)
+ }
+
+ err = json.Unmarshal(metaJsonText, &h)
+ if err != nil {
+ return h.Invalidate(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)
+ }
+
+ return h.OnRevision("0")
+ }
+ return h
+}
+
+// OnRevision tries to change to a revision specified by `id`.
+func (h *Hypha) OnRevision(id string) *Hypha {
+ if h.Invalid || !h.Exists {
+ return h
+ }
+ if len(h.Revisions) == 0 {
+ return h.Invalidate(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 h
+}
+
+func (h *Hypha) PlainLog(s string) {
+ if h.Exists {
+ log.Println(h.FullName, h.actual.Id, s)
+ } else {
+ log.Println("nonexistent", h.FullName, s)
+ }
+}
+
+func (h *Hypha) LogSuccMaybe(succMsg string) *Hypha {
+ if h.Invalid {
+ h.PlainLog(h.Err.Error())
+ } else {
+ h.PlainLog(succMsg)
+ }
+ return h
+}
+
+// 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) *Hypha {
+ if h.Invalid {
+ return h
+ }
+ fileContents, err := ioutil.ReadFile(h.actual.TextPath)
+ if err != nil {
+ return h.Invalidate(err)
+ }
+ w.Header().Set("Content-Type", h.mimeTypeForActionRaw())
+ w.WriteHeader(http.StatusOK)
+ w.Write(fileContents)
+ return h
+}
+
+// ActionBinary is used with `?action=binary`.
+// It writes contents of binary content file.
+func (h *Hypha) ActionBinary(w http.ResponseWriter) *Hypha {
+ if h.Invalid {
+ return h
+ }
+ fileContents, err := ioutil.ReadFile(h.actual.BinaryPath)
+ if err != nil {
+ return h.Invalidate(err)
+ }
+ w.Header().Set("Content-Type", h.actual.BinaryMime)
+ w.WriteHeader(http.StatusOK)
+ w.Write(fileContents)
+ return h
+}
+
+// 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) *Hypha {
+ if h.Invalid {
+ return h
+ }
+ html, err := h.asHtml()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return h.Invalidate(err)
+ }
+ w.Header().Set("Content-Type", "text/html;charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(html))
+ return h
+}
+
+// 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) *Hypha {
+ var html string
+ var err error
+ if h.Exists {
+ html, err = h.asHtml()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return h.Invalidate(err)
+ }
+ }
+ 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, "")))
+ }
+ return h
+}
+
+// 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
new file mode 100644
index 0000000..69ae0c6
--- /dev/null
+++ b/fs/revision.go
@@ -0,0 +1,42 @@
+package fs
+
+import (
+ "mime"
+ "strconv"
+)
+
+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"`
+}
+
+// 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/handlers.go b/handlers.go
index fab7b0f..0f52411 100644
--- a/handlers.go
+++ b/handlers.go
@@ -1,213 +1,68 @@
package main
import (
- "io/ioutil"
"log"
"net/http"
- "path/filepath"
- "strconv"
- "strings"
- "time"
- "github.com/bouncepaw/mycorrhiza/cfg"
+ "github.com/bouncepaw/mycorrhiza/fs"
+ "github.com/bouncepaw/mycorrhiza/render"
"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 {
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)
- }
+ return fs.Hs.Open(vars["hypha"]).OnRevision(RevInMap(vars))
}
func HandlerRaw(w http.ResponseWriter, rq *http.Request) {
- if rev, ok := HandlerBase(w, rq); ok {
- rev.ActionRaw(w)
- }
+ log.Println("?action=raw")
+ HandlerBase(w, rq).ActionRaw(w).LogSuccMaybe("Serving raw text")
+}
+
+func HandlerBinary(w http.ResponseWriter, rq *http.Request) {
+ log.Println("?action=binary")
+ HandlerBase(w, rq).ActionBinary(w).LogSuccMaybe("Serving binary data")
}
func HandlerZen(w http.ResponseWriter, rq *http.Request) {
- if rev, ok := HandlerBase(w, rq); ok {
- rev.ActionZen(w)
- }
+ log.Println("?action=zen")
+ HandlerBase(w, rq).ActionZen(w).LogSuccMaybe("Rendering zen")
}
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"])))
- }
-}
-
-func HandlerHistory(w http.ResponseWriter, rq *http.Request) {
- w.WriteHeader(http.StatusNotImplemented)
- log.Println("Attempt to access an unimplemented thing")
+ log.Println("?action=view")
+ HandlerBase(w, rq).
+ ActionView(w, render.HyphaPage, render.Hypha404).
+ LogSuccMaybe("Rendering hypha view")
}
func HandlerEdit(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(rq)
- ActionEdit(vars["hypha"], w)
-}
-
-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")
-}
-
-// 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
+ 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)))
}
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().
+ LogSuccMaybe("Saved changes")
- err := writeTextFileFromHttpData(newRev, rq)
- if err != nil {
- log.Println(err)
- return
+ if !h.Invalid {
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Write([]byte(render.HyphaUpdateOk(h)))
}
- 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")))
}
diff --git a/hypha.go b/hypha.go
deleted file mode 100644
index 540d31b..0000000
--- a/hypha.go
+++ /dev/null
@@ -1,113 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "strconv"
- "strings"
-
- "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) {
- 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)
-}
-
-// 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 {
- 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 acbc044..215646c 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,9 +22,6 @@ 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
-
func main() {
if len(os.Args) == 1 {
panic("Expected a root wiki pages directory")
@@ -52,51 +34,42 @@ 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.")
+ fs.InitStorage()
// Start server code. See handlers.go for handlers' implementations.
r := mux.NewRouter()
- r.Queries("action", "getBinary", "rev", revQuery).Path(hyphaUrl).
- HandlerFunc(HandlerGetBinary)
- r.Queries("action", "getBinary").Path(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", "raw", "rev", revQuery).Path(hyphaUrl).
+ 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)
- r.Queries("action", "raw").Path(hyphaUrl).
+ r.Queries("action", "raw").Path(cfg.HyphaUrl).
HandlerFunc(HandlerRaw)
- r.Queries("action", "zen", "rev", revQuery).Path(hyphaUrl).
+ r.Queries("action", "zen", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
HandlerFunc(HandlerZen)
- r.Queries("action", "zen").Path(hyphaUrl).
+ r.Queries("action", "zen").Path(cfg.HyphaUrl).
HandlerFunc(HandlerZen)
- r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl).
+ r.Queries("action", "view", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
HandlerFunc(HandlerView)
- r.Queries("action", "view").Path(hyphaUrl).
+ r.Queries("action", "view").Path(cfg.HyphaUrl).
HandlerFunc(HandlerView)
- r.Queries("action", "history").Path(hyphaUrl).
- HandlerFunc(HandlerHistory)
-
- r.Queries("action", "edit").Path(hyphaUrl).
+ r.Queries("action", "edit").Path(cfg.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", "update").Path(hyphaUrl).Methods("POST").
+ r.Queries("action", "update").Path(cfg.HyphaUrl).Methods("POST").
HandlerFunc(HandlerUpdate)
- r.HandleFunc(hyphaUrl, HandlerView)
+ r.HandleFunc(cfg.HyphaUrl, HandlerView)
// Debug page that renders all hyphae.
// TODO: make it redirect to home page.
@@ -104,14 +77,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/render.go
similarity index 70%
rename from render.go
rename to render/render.go
index 85bdd3a..7ff6a05 100644
--- a/render.go
+++ b/render/render.go
@@ -1,4 +1,4 @@
-package main
+package render
import (
"bytes"
@@ -7,6 +7,7 @@ import (
"text/template"
"github.com/bouncepaw/mycorrhiza/cfg"
+ "github.com/bouncepaw/mycorrhiza/fs"
)
// EditHyphaPage returns HTML page of hypha editor.
@@ -25,21 +26,36 @@ 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 {
+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")
+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": GetTree(name, true).AsHtml(),
+ "Tree": fs.Hs.GetTree(name, true).AsHtml(),
}, "Hypha/view/sidebar.html"); aside != "" {
sidebar = aside
}
@@ -50,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
@@ -70,8 +93,8 @@ 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)
- rev, _ := GetRevision(hyphPath, "0")
- tmpl, err := template.ParseFiles(rev.TextPath)
+ h := fs.Hs.Open(hyphPath).OnRevision("0")
+ tmpl, err := template.ParseFiles(h.TextPath())
if err != nil {
return err.Error()
}
@@ -85,8 +108,8 @@ 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)
- rev, _ := GetRevision(hyphPath, "0")
- tmpl, err := template.ParseFiles(rev.TextPath)
+ h := fs.Hs.Open(hyphPath).OnRevision("0")
+ tmpl, err := template.ParseFiles(h.TextPath())
if err != nil {
return err.Error()
}
diff --git a/revision.go b/revision.go
deleted file mode 100644
index 35b653b..0000000
--- a/revision.go
+++ /dev/null
@@ -1,148 +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)
-}
-
-// 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) {
- 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 48590d2..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/favicon.ico b/w/favicon.ico
new file mode 100644
index 0000000..857e55c
Binary files /dev/null and b/w/favicon.ico differ
diff --git a/w/m/Fruit/3.markdown b/w/m/Fruit/3.markdown
new file mode 100644
index 0000000..e99cd53
--- /dev/null
+++ b/w/m/Fruit/3.markdown
@@ -0,0 +1,4 @@
+According to real *scientists*, fruit is a type of fetus. Most of them are tasty and cool, though some of them are really sour and depressing. Be careful when choosing fruit. Best ones are:
+
+* [Apple](Apple)
+* [Pear](Pear)
\ No newline at end of file
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/w/m/Fruit/meta.json b/w/m/Fruit/meta.json
index cb14bcf..b4d4797 100644
--- a/w/m/Fruit/meta.json
+++ b/w/m/Fruit/meta.json
@@ -1,22 +1,47 @@
{
-"revisions":{
- "1":{
- "name": "Fruit",
- "time": 1591635559,
- "author": "bouncepaw",
- "comment": "create Fruit",
- "tags": ["fetus", "tasty"],
- "text_mime": "text/markdown",
- "text_name": "1.md"
- },
- "2":{
- "name": "Fruit",
- "time": 1591636222,
- "author": "fungimaster",
- "comment": "update Fruit",
- "tags": ["fetus", "tasty"],
- "text_mime": "text/markdown",
- "text_name": "2.md"
- }
-}
-}
+ "views": 0,
+ "deleted": false,
+ "revisions": {
+ "1": {
+ "tags": [
+ "fetus",
+ "tasty"
+ ],
+ "name": "Fruit",
+ "comment": "create Fruit",
+ "author": "bouncepaw",
+ "time": 1591635559,
+ "text_mime": "text/markdown",
+ "binary_mime": "",
+ "text_name": "1.md",
+ "binary_name": ""
+ },
+ "2": {
+ "tags": [
+ "fetus",
+ "tasty"
+ ],
+ "name": "Fruit",
+ "comment": "update Fruit",
+ "author": "fungimaster",
+ "time": 1591636222,
+ "text_mime": "text/markdown",
+ "binary_mime": "",
+ "text_name": "2.md",
+ "binary_name": ""
+ },
+ "3": {
+ "tags": null,
+ "name": "Fruit",
+ "comment": "Update Fruit",
+ "author": "",
+ "time": 1593279957,
+ "text_mime": "text/markdown",
+ "binary_mime": "",
+ "text_name": "3.markdown",
+ "binary_name": ""
+ }
+ },
+ "Invalid": false,
+ "Err": null
+}
\ No newline at end of file
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-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..b98e6e9 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
@@ -1,36 +1,36 @@
-
+
+
+
+
+
+
+
+
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..54053bd
--- /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": ""
+ }
+ }
+}
diff --git a/w/m/Templates/meta.json b/w/m/Templates/meta.json
new file mode 100644
index 0000000..45204e2
--- /dev/null
+++ b/w/m/Templates/meta.json
@@ -0,0 +1,21 @@
+{
+ "views": 0,
+ "deleted": false,
+ "revisions": {
+ "1": {
+ "tags": [
+ ""
+ ],
+ "name": "Templates",
+ "comment": "Update Templates",
+ "author": "",
+ "time": 1593194769,
+ "text_mime": "text/markdown",
+ "binary_mime": "",
+ "text_name": "1.markdown",
+ "binary_name": ""
+ }
+ },
+ "Invalid": false,
+ "Err": null
+}
\ No newline at end of file
diff --git a/walk.go b/walk.go
deleted file mode 100644
index d23c454..0000000
--- a/walk.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "io/ioutil"
- "log"
- "path/filepath"
- "regexp"
- "strconv"
-
- "github.com/bouncepaw/mycorrhiza/cfg"
-)
-
-const (
- hyphaPattern = `[^\s\d:/?&\\][^:?&\\]*`
- hyphaUrl = `/{hypha:` + hyphaPattern + `}`
- revisionPattern = `[\d]+`
- revQuery = `{rev:` + revisionPattern + `}`
- metaJsonPattern = `meta\.json`
-)
-
-var (
- leadingInt = regexp.MustCompile(`^[-+]?\d+`)
-)
-
-// matchNameToEverything matches `name` to all filename patterns and returns 4 boolean results.
-func matchNameToEverything(name string) (metaJsonM, hyphaM bool) {
- // simpleMatch reduces boilerplate. Errors are ignored because I trust my regex skills.
- simpleMatch := func(s string, p string) bool {
- m, _ := regexp.MatchString(p, s)
- return m
- }
- return simpleMatch(name, metaJsonPattern),
- simpleMatch(name, hyphaPattern)
-}
-
-// scanHyphaDir scans directory at `fullPath` and tells what it has found.
-func scanHyphaDir(fullPath string) (valid bool, possibleSubhyphae []string, metaJsonPath string, err error) {
- nodes, err := ioutil.ReadDir(fullPath)
- if err != nil {
- return // implicit return values
- }
-
- for _, node := range nodes {
- metaJsonM, hyphaM := matchNameToEverything(node.Name())
- switch {
- case hyphaM && node.IsDir():
- possibleSubhyphae = append(possibleSubhyphae, filepath.Join(fullPath, node.Name()))
- case metaJsonM && !node.IsDir():
- metaJsonPath = filepath.Join(fullPath, "meta.json")
- // Other nodes are ignored. It is not promised they will be ignored in future versions
- }
- }
-
- if metaJsonPath != "" {
- valid = true
- }
- return // implicit return values
-}
-
-// 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:]
-}
-
-// recurFindHyphae recursively searches for hyphae in passed directory path.
-func recurFindHyphae(fullPath string) map[string]*Hypha {
- hyphae := make(map[string]*Hypha)
- valid, possibleSubhyphae, metaJsonPath, err := scanHyphaDir(fullPath)
- if err != nil {
- return hyphae
- }
-
- // First, let's process subhyphae
- for _, possibleSubhypha := range possibleSubhyphae {
- for k, v := range recurFindHyphae(possibleSubhypha) {
- hyphae[k] = v
- }
- }
-
- // This folder is not a hypha itself, nothing to do here
- if !valid {
- return hyphae
- }
-
- // Template hypha struct. Other fields are default json values.
- h := Hypha{
- FullName: hyphaName(fullPath),
- Path: fullPath,
- parentName: filepath.Dir(hyphaName(fullPath)),
- // Children names are unknown now
- }
-
- metaJsonContents, err := ioutil.ReadFile(metaJsonPath)
- if err != nil {
- log.Printf("Error when reading `%s`; skipping", metaJsonPath)
- return hyphae
- }
- err = json.Unmarshal(metaJsonContents, &h)
- if err != nil {
- log.Printf("Error when unmarshaling `%s`; skipping", metaJsonPath)
- log.Println(err)
- return hyphae
- }
-
- // 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(fullPath, rev.BinaryName)
- }
- rev.TextPath = filepath.Join(fullPath, rev.TextName)
- }
-
- // Now the hypha should be ok, gotta send structs
- hyphae[h.FullName] = &h
- return hyphae
-}