1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-12-12 05:20:26 +00:00

Reimplement action=update

This commit is contained in:
Timur Ismagilov 2020-06-26 23:07:21 +05:00
parent a9c72d91be
commit d36f86f715
15 changed files with 256 additions and 702 deletions

View File

@ -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
}

View File

@ -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]
}
}

View File

@ -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 += `<ul class="navitree__node">`
if t.Root {
for _, ancestor := range t.Ancestors {
html += navitreeEntry(ancestor)
}
}
html += navitreeEntry(t.Name)
if t.Root {
for _, siblingName := range t.Siblings {
html += navitreeEntry(siblingName)
}
}
for _, subtree := range t.Descendants {
html += subtree.AsHtml()
}
html += `</ul>`
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(`<li class="navitree__entry">
<a class="navitree__link" href="/%s">%s</a>
</li>
`, name, filepath.Base(name))
}

View File

@ -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)))
}
*/

View File

@ -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)))
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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()

View File

@ -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 += `<article class="page">
<h1 class="page__title">` + rev.FullName + `</h1>
`
// TODO: support things other than images
if rev.hasBinaryData() {
ret += fmt.Sprintf(`<img src="/%s?action=getBinary&rev=%d" class="page__amnt"/>`, 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(`<pre>%s</pre>`, contents)
}
ret += `
</article>`
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)
}
}

View File

@ -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",

1
w/m/Templates/1.markdown Normal file
View File

@ -0,0 +1 @@
**TODO:** Reorganize templates.

View File

@ -2,7 +2,8 @@
<form class="naviwrapper__edit edit-box"
method="POST"
enctype="multipart/form-data"
action="?action=update">
action="?action=update"
id="edit-form">
<div class="edit-box__left">
<h4>Edit box</h4>

View File

@ -1,19 +1,19 @@
<div style=""><h4>Text MIME-type</h4>
<p>Good types are <code>text/markdown</code> and <code>text/plain</code></p>
<input type="text" name="text_mime" value="{{ .TextMime }}"/>
<input type="text" name="text_mime" value="{{ .TextMime }}" form="edit-form"/>
<h4>Revision comment</h4>
<p>Please make your comment helpful</p>
<input type="text" name="comment" value="Update {{ .Name }}"/>
<input type="text" name="comment" value="Update {{ .Name }}" form="edit-form"/>
<h4>Edit tags</h4>
<p>Tags are separated by commas, whitespace is ignored</p>
<input type="text" name="tags" value="{{ .Tags }}"/>
<input type="text" name="tags" value="{{ .Tags }}" form="edit-form"/>
<h4>Upload file</h4>
<p>If this hypha has a file like that, the text above is meant to be a description of it</p>
<input type="file" name="binary"/>
<input type="file" name="binary" form="edit-form"/>
<p><input type="submit" value="update"/></p>
<p><input type="submit" value="update" form="edit-form"/></p>
</div>

21
w/m/Templates/meta.json Normal file
View File

@ -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
}

120
walk.gog
View File

@ -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
}