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

Refactor a little bit, add comments in a lot of places

This commit is contained in:
Timur Ismagilov 2020-06-19 23:11:47 +05:00
parent 550bcbd444
commit 931bc8bae9
16 changed files with 424 additions and 219 deletions

View File

@ -1,6 +1,7 @@
package main package main
const ( const (
TitleEditTemplate = `Edit %s at MycorrhizaWiki`
TitleTemplate = `%s at MycorrhizaWiki` TitleTemplate = `%s at MycorrhizaWiki`
DefaultTitle = "MycorrhizaWiki" DefaultTitle = "MycorrhizaWiki"
DefaultHeaderText = `MycorrhizaWiki 🍄` DefaultHeaderText = `MycorrhizaWiki 🍄`
@ -11,4 +12,5 @@ const (
DefaultStyles = ` DefaultStyles = `
<link rel="stylesheet" href="/sys/main.css?action=raw"> <link rel="stylesheet" href="/sys/main.css?action=raw">
` `
GenericErrorMsg = `<b>Sorry, something went wrong</b>`
) )

View File

@ -1,16 +1,19 @@
/* Genealogy is all about relationships between hyphae. For now, the only goal of this file is to help find children of hyphae as they are not marked during the hypha search phase. /* Genealogy is all about relationships between hyphae. For now, the only goal of this file is to help find children of hyphae as they are not marked during the hypha search phase.
*/
TODO: make use of family relations.
*/
package main package main
type Genealogy struct { // setRelations fills in all children names based on what hyphae call their parents.
parent string
child string
}
func setRelations(hyphae map[string]*Hypha) { func setRelations(hyphae map[string]*Hypha) {
for name, h := range hyphae { for name, h := range hyphae {
if _, ok := hyphae[h.ParentName()]; ok && h.ParentName() != "." { if _, ok := hyphae[h.parentName]; ok && h.parentName != "." {
hyphae[h.ParentName()].ChildrenNames = append(hyphae[h.ParentName()].ChildrenNames, name) 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)
}

1
go.mod
View File

@ -5,4 +5,5 @@ go 1.14
require ( require (
github.com/gomarkdown/markdown v0.0.0-20200609195525-3f9352745725 github.com/gomarkdown/markdown v0.0.0-20200609195525-3f9352745725
github.com/gorilla/mux v1.7.4 github.com/gorilla/mux v1.7.4
mvdan.cc/gogrep v0.0.0-20200420132841-24e8804e5b3c // indirect
) )

14
go.sum
View File

@ -9,7 +9,21 @@ github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191223235410-3721262b3e7c h1:PeFrxQ8YTAKg53UR8aP/nxa82lQYIdb+pd1bfg3dBDM=
golang.org/x/tools v0.0.0-20191223235410-3721262b3e7c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
mvdan.cc/gogrep v0.0.0-20200420132841-24e8804e5b3c h1:bz7/KkVXLQ6AWDoNX/hXfcAcNbLwQVAKNGt2I5vZKEE=
mvdan.cc/gogrep v0.0.0-20200420132841-24e8804e5b3c/go.mod h1:LBbI8cEsbrMdWjW4Lcs806EWonhTiZbaBCCbsalF+6c=

View File

@ -12,64 +12,67 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
// There are handlers below. See main() for their usage.
// Boilerplate code present in many handlers. Good to have it. // Boilerplate code present in many handlers. Good to have it.
func HandlerBase(w http.ResponseWriter, r *http.Request) (Revision, bool) { func HandlerBase(w http.ResponseWriter, rq *http.Request) (Revision, bool) {
vars := mux.Vars(r) vars := mux.Vars(rq)
revno := RevInMap(vars) revno := RevInMap(vars)
return GetRevision(hyphae, vars["hypha"], revno, w) return GetRevision(vars["hypha"], revno)
} }
func HandlerGetBinary(w http.ResponseWriter, r *http.Request) { func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, r); ok { if rev, ok := HandlerBase(w, rq); ok {
rev.ActionGetBinary(w) rev.ActionGetBinary(w)
} }
} }
func HandlerRaw(w http.ResponseWriter, r *http.Request) { func HandlerRaw(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, r); ok { if rev, ok := HandlerBase(w, rq); ok {
rev.ActionRaw(w) rev.ActionRaw(w)
} }
} }
func HandlerZen(w http.ResponseWriter, r *http.Request) { func HandlerZen(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, r); ok { if rev, ok := HandlerBase(w, rq); ok {
rev.ActionZen(w) rev.ActionZen(w)
} }
} }
func HandlerView(w http.ResponseWriter, r *http.Request) { func HandlerView(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, r); ok { if rev, ok := HandlerBase(w, rq); ok {
rev.ActionView(w, HyphaPage) rev.ActionView(w, HyphaPage)
} }
} }
func HandlerHistory(w http.ResponseWriter, r *http.Request) { func HandlerHistory(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented) w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing") log.Println("Attempt to access an unimplemented thing")
} }
func HandlerEdit(w http.ResponseWriter, r *http.Request) { func HandlerEdit(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(rq)
ActionEdit(vars["hypha"], w) ActionEdit(vars["hypha"], w)
} }
func HandlerRewind(w http.ResponseWriter, r *http.Request) { func HandlerRewind(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented) w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing") log.Println("Attempt to access an unimplemented thing")
} }
func HandlerDelete(w http.ResponseWriter, r *http.Request) { func HandlerDelete(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented) w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing") log.Println("Attempt to access an unimplemented thing")
} }
func HandlerRename(w http.ResponseWriter, r *http.Request) { func HandlerRename(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented) w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing") 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) { func makeTagsSlice(responseTagsString string) (ret []string) {
// `responseTagsString` is string like "foo,, bar,kek". Whitespace around commas is insignificant. Expected output: []string{"foo", "bar", "kek"}
for _, tag := range strings.Split(responseTagsString, ",") { for _, tag := range strings.Split(responseTagsString, ",") {
if trimmed := strings.TrimSpace(tag); "" == trimmed { if trimmed := strings.TrimSpace(tag); "" == trimmed {
ret = append(ret, trimmed) ret = append(ret, trimmed)
@ -78,7 +81,7 @@ func makeTagsSlice(responseTagsString string) (ret []string) {
return ret return ret
} }
// Return an existing hypha it exists in `hyphae` or create a new one. If it `isNew`, you'll have to insert it to `hyphae` yourself. // 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) { func getHypha(name string) (*Hypha, bool) {
log.Println("Accessing hypha", name) log.Println("Accessing hypha", name)
if h, ok := hyphae[name]; ok { if h, ok := hyphae[name]; ok {
@ -95,26 +98,27 @@ func getHypha(name string) (*Hypha, bool) {
return h, true return h, true
} }
// Create a new revison for hypha `h`. All data is fetched from `r`, except for BinaryMime and BinaryPath which require additional processing. You'll have te insert the revision to `h` yourself. // 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, r *http.Request) *Revision { func revisionFromHttpData(h *Hypha, rq *http.Request) *Revision {
idStr := strconv.Itoa(h.NewestRevisionInt() + 1) idStr := strconv.Itoa(h.NewestRevisionInt() + 1)
log.Println(idStr) log.Println(idStr)
rev := &Revision{ rev := &Revision{
Id: h.NewestRevisionInt() + 1, Id: h.NewestRevisionInt() + 1,
FullName: h.FullName, FullName: h.FullName,
Tags: makeTagsSlice(r.PostFormValue("tags")), Tags: makeTagsSlice(rq.PostFormValue("tags")),
Comment: r.PostFormValue("comment"), Comment: rq.PostFormValue("comment"),
Author: r.PostFormValue("author"), Author: rq.PostFormValue("author"),
Time: int(time.Now().Unix()), Time: int(time.Now().Unix()),
TextMime: r.PostFormValue("text_mime"), TextMime: rq.PostFormValue("text_mime"),
TextPath: filepath.Join(h.Path, idStr+".txt"), TextPath: filepath.Join(h.Path, idStr+".txt"),
// Left: BinaryMime, BinaryPath // Fields left: BinaryMime, BinaryPath
} }
return rev return rev
} }
func writeTextFileFromHttpData(rev *Revision, r *http.Request) error { // writeTextFileFromHttpData tries to fetch text content from `rq` for revision `rev` and write it to a corresponding text file. It used in `HandlerUpdate`.
data := []byte(r.PostFormValue("text")) func writeTextFileFromHttpData(rev *Revision, rq *http.Request) error {
data := []byte(rq.PostFormValue("text"))
err := ioutil.WriteFile(rev.TextPath, data, 0644) err := ioutil.WriteFile(rev.TextPath, data, 0644)
if err != nil { if err != nil {
log.Println("Failed to write", len(data), "bytes to", rev.TextPath) log.Println("Failed to write", len(data), "bytes to", rev.TextPath)
@ -122,11 +126,12 @@ func writeTextFileFromHttpData(rev *Revision, r *http.Request) error {
return err return err
} }
func writeBinaryFileFromHttpData(h *Hypha, oldRev Revision, newRev *Revision, r *http.Request) error { // 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 // 10 MB file size limit
r.ParseMultipartForm(10 << 20) rq.ParseMultipartForm(10 << 20)
// Read file // Read file
file, handler, err := r.FormFile("binary") file, handler, err := rq.FormFile("binary")
if file != nil { if file != nil {
defer file.Close() defer file.Close()
} }
@ -154,22 +159,22 @@ func writeBinaryFileFromHttpData(h *Hypha, oldRev Revision, newRev *Revision, r
return nil return nil
} }
func HandlerUpdate(w http.ResponseWriter, r *http.Request) { func HandlerUpdate(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(rq)
log.Println("Attempt to update hypha", mux.Vars(r)["hypha"]) log.Println("Attempt to update hypha", mux.Vars(rq)["hypha"])
h, isNew := getHypha(vars["hypha"]) h, isNew := getHypha(vars["hypha"])
oldRev := h.GetNewestRevision() oldRev := h.GetNewestRevision()
newRev := revisionFromHttpData(h, r) newRev := revisionFromHttpData(h, rq)
if isNew { if isNew {
h.CreateDir() h.CreateDir()
} }
err := writeTextFileFromHttpData(newRev, r) err := writeTextFileFromHttpData(newRev, rq)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
err = writeBinaryFileFromHttpData(h, oldRev, newRev, r) err = writeBinaryFileFromHttpData(h, oldRev, newRev, rq)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return

View File

@ -12,6 +12,8 @@ import (
"strings" "strings"
) )
// `Hypha` represents a hypha. It is the thing MycorrhizaWiki generally serves.
// Each hypha has 1 or more revisions.
type Hypha struct { type Hypha struct {
FullName string `json:"-"` FullName string `json:"-"`
Path string `json:"-"` Path string `json:"-"`
@ -22,58 +24,52 @@ type Hypha struct {
parentName string parentName string
} }
func (h *Hypha) AddChild(childName string) { // AsHtml returns HTML representation of the hypha.
h.ChildrenNames = append(h.ChildrenNames, childName) // No layout or navigation are present here. Just the hypha.
} func (h *Hypha) AsHtml(id string, w http.ResponseWriter) (string, error) {
if "0" == id {
// Used with action=zen|view id = h.NewestRevision()
func (h *Hypha) AsHtml(hyphae map[string]*Hypha, rev string) (string, error) {
if "0" == rev {
rev = h.NewestRevision()
} }
r, ok := h.Revisions[rev] if rev, ok := h.Revisions[id]; ok {
if !ok { return rev.AsHtml(w)
return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, rev)
} }
html, err := r.AsHtml(hyphae) return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, id)
return html, err
}
func (h *Hypha) Name() string {
return h.FullName
} }
// GetNewestRevision returns the most recent Revision.
func (h *Hypha) GetNewestRevision() Revision { func (h *Hypha) GetNewestRevision() Revision {
return *h.Revisions[h.NewestRevision()] return *h.Revisions[h.NewestRevision()]
} }
// NewestRevision returns the most recent revision's id as a string.
func (h *Hypha) NewestRevision() string { func (h *Hypha) NewestRevision() string {
return strconv.Itoa(h.NewestRevisionInt()) return strconv.Itoa(h.NewestRevisionInt())
} }
func (h *Hypha) NewestRevisionInt() int { // NewestRevision returns the most recent revision's id as an integer.
var largest int func (h *Hypha) NewestRevisionInt() (ret int) {
for k, _ := range h.Revisions { for k, _ := range h.Revisions {
rev, _ := strconv.Atoi(k) id, _ := strconv.Atoi(k)
if rev > largest { if id > ret {
largest = rev ret = id
} }
} }
return largest 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 { func (h *Hypha) MetaJsonPath() string {
return filepath.Join(h.Path, "meta.json") 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 { func (h *Hypha) CreateDir() error {
return os.MkdirAll(h.Path, 0644) return os.MkdirAll(h.Path, 0644)
} }
func (h *Hypha) ParentName() string { // SaveJson dumps the hypha's metadata to `meta.json` file.
return h.parentName
}
func (h *Hypha) SaveJson() { func (h *Hypha) SaveJson() {
data, err := json.Marshal(h) data, err := json.Marshal(h)
if err != nil { if err != nil {
@ -88,24 +84,26 @@ func (h *Hypha) SaveJson() {
log.Println("Saved JSON data of", h.FullName) 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) { func ActionEdit(hyphaName string, w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
var initContents, initTextMime, initTags string var initContents, initTextMime, initTags string
hypha, ok := hyphae[hyphaName] if h, ok := hyphae[hyphaName]; ok {
if !ok { newestRev := h.GetNewestRevision()
initContents = "Describe " + hyphaName + "here."
initTextMime = "text/markdown"
} else {
newestRev := hypha.Revisions[hypha.NewestRevision()]
contents, err := ioutil.ReadFile(newestRev.TextPath) contents, err := ioutil.ReadFile(newestRev.TextPath)
if err != nil { if err != nil {
log.Println("Could not read", newestRev.TextPath)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("<b>Sorry, something went wrong</b>")) w.Write([]byte(GenericErrorMsg))
return return
} }
initContents = string(contents) initContents = string(contents)
initTextMime = newestRev.TextMime initTextMime = newestRev.TextMime
initTags = strings.Join(newestRev.Tags, ",") initTags = strings.Join(newestRev.Tags, ",")
} else {
initContents = "Describe " + hyphaName + "here."
initTextMime = "text/markdown"
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)

58
main.go
View File

@ -11,40 +11,36 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
func GetRevision(hyphae map[string]*Hypha, hyphaName string, rev string, w http.ResponseWriter) (Revision, bool) { // GetRevision finds revision with id `id` of `hyphaName` in `hyphae`.
log.Println("Getting hypha", hyphaName, rev) // If `id` is `"0"`, it means the last revision.
for name, hypha := range hyphae { // If no such revision is found, last return value is false.
if name == hyphaName { func GetRevision(hyphaName string, id string) (Revision, bool) {
if rev == "0" { log.Println("Getting hypha", hyphaName, id)
rev = hypha.NewestRevision() if hypha, ok := hyphae[hyphaName]; ok {
} if id == "0" {
for id, r := range hypha.Revisions { id = hypha.NewestRevision()
if rev == id { }
return *r, true if rev, ok := hypha.Revisions[id]; ok {
} return *rev, true
}
} }
} }
return Revision{}, false 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 { func RevInMap(m map[string]string) string {
if val, ok := m["rev"]; ok { if id, ok := m["rev"]; ok {
return val return id
} }
return "0" return "0"
} }
// `rootWikiDir` is a directory where all wiki files reside.
var rootWikiDir string var rootWikiDir string
var hyphae map[string]*Hypha
func hyphaeAsMap(hyphae []*Hypha) map[string]*Hypha { // `hyphae` is a map with all hyphae. Many functions use it.
mh := make(map[string]*Hypha) var hyphae map[string]*Hypha
for _, h := range hyphae {
mh[h.Name()] = h
}
return mh
}
func main() { func main() {
if len(os.Args) == 1 { if len(os.Args) == 1 {
@ -61,9 +57,8 @@ func main() {
log.Println("Indexing hyphae...") log.Println("Indexing hyphae...")
hyphae = recurFindHyphae(rootWikiDir) hyphae = recurFindHyphae(rootWikiDir)
log.Println("Indexed", len(hyphae), "hyphae. Ready to accept requests.") log.Println("Indexed", len(hyphae), "hyphae. Ready to accept requests.")
// setRelations(hyphae)
// Start server code // Start server code. See handlers.go for handlers' implementations.
r := mux.NewRouter() r := mux.NewRouter()
r.Queries("action", "getBinary", "rev", revQuery).Path(hyphaUrl). r.Queries("action", "getBinary", "rev", revQuery).Path(hyphaUrl).
@ -101,19 +96,20 @@ func main() {
r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl).
HandlerFunc(HandlerRename) HandlerFunc(HandlerRename)
r.Queries( r.Queries("action", "update").Path(hyphaUrl).Methods("POST").
"action", "update",
).Path(hyphaUrl).Methods("POST").
HandlerFunc(HandlerUpdate) HandlerFunc(HandlerUpdate)
r.HandleFunc(hyphaUrl, HandlerView) r.HandleFunc(hyphaUrl, HandlerView)
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Debug page that renders all hyphae.
// TODO: make it redirect to home page.
// TODO: make a home page.
r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
for _, v := range hyphae { for _, h := range hyphae {
log.Println("Rendering latest revision of hypha", v.Name()) log.Println("Rendering latest revision of hypha", h.FullName)
html, err := v.AsHtml(hyphae, "0") html, err := h.AsHtml("0", w)
if err != nil { if err != nil {
fmt.Fprintln(w, err) fmt.Fprintln(w, err)
} }

BIN
mycorrhiza Executable file

Binary file not shown.

View File

@ -8,9 +8,10 @@ import (
"text/template" "text/template"
) )
// EditHyphaPage returns HTML page of hypha editor.
func EditHyphaPage(name, textMime, content, tags string) string { func EditHyphaPage(name, textMime, content, tags string) string {
keys := map[string]string{ keys := map[string]string{
"Title": fmt.Sprintf(TitleTemplate, "Edit "+name), "Title": fmt.Sprintf(TitleEditTemplate, name),
"Header": renderFromString(name, "Hypha/edit/header.html"), "Header": renderFromString(name, "Hypha/edit/header.html"),
} }
page := map[string]string{ page := map[string]string{
@ -22,7 +23,8 @@ func EditHyphaPage(name, textMime, content, tags string) string {
return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys) return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys)
} }
func HyphaPage(hyphae map[string]*Hypha, rev Revision, content string) string { // HyphaPage returns HTML page of hypha viewer.
func HyphaPage(rev Revision, content string) string {
sidebar := DefaultSidebar sidebar := DefaultSidebar
bside, err := ioutil.ReadFile("Hypha/view/sidebar.html") bside, err := ioutil.ReadFile("Hypha/view/sidebar.html")
if err == nil { if err == nil {
@ -35,12 +37,10 @@ func HyphaPage(hyphae map[string]*Hypha, rev Revision, content string) string {
return renderBase(renderFromString(content, "Hypha/view/index.html"), keys) return renderBase(renderFromString(content, "Hypha/view/index.html"), keys)
} }
/* // renderBase collects and renders page from base template
Collect and render page from base template // Args:
Args: // content: string or pre-rendered template
content: string or pre-rendered template // keys: map with replaced standart fields
keys: map with replaced standart fields
*/
func renderBase(content string, keys map[string]string) string { func renderBase(content string, keys map[string]string) string {
page := map[string]string{ page := map[string]string{
"Title": DefaultTitle, "Title": DefaultTitle,
@ -58,6 +58,7 @@ func renderBase(content string, keys map[string]string) string {
return renderFromMap(page, "base.html") 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 { func renderFromMap(data map[string]string, templatePath string) string {
filePath := path.Join("templates", templatePath) filePath := path.Join("templates", templatePath)
tmpl, err := template.ParseFiles(filePath) tmpl, err := template.ParseFiles(filePath)
@ -71,6 +72,7 @@ func renderFromMap(data map[string]string, templatePath string) string {
return buf.String() return buf.String()
} }
// renderFromMap applies `data` string to template in `templatePath` and returns the result.
func renderFromString(data string, templatePath string) string { func renderFromString(data string, templatePath string) string {
filePath := path.Join("templates", templatePath) filePath := path.Join("templates", templatePath)
tmpl, err := template.ParseFiles(filePath) tmpl, err := template.ParseFiles(filePath)

View File

@ -5,13 +5,13 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown"
) )
// In different places, revision variable is called `r`. But when there is an http.Request as well, the revision becomes `rev`. TODO: name them consistently. // Revision represents a revision, duh.
// A revision is a version of a hypha at a point in time.
type Revision struct { type Revision struct {
Id int `json:"-"` Id int `json:"-"`
FullName string `json:"-"` FullName string `json:"-"`
@ -26,37 +26,42 @@ type Revision struct {
BinaryPath string `json:"-"` BinaryPath string `json:"-"`
} }
func (r *Revision) IdAsStr() string { // IdAsStr returns revision's id as a string.
return strconv.Itoa(r.Id) 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. // During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data.
func (r *Revision) hasBinaryData() bool { func (rev *Revision) hasBinaryData() bool {
return r.BinaryMime != "" return rev.BinaryMime != ""
} }
func (r *Revision) urlOfBinary() string { // AsHtml returns HTML representation of the revision.
return fmt.Sprintf("/%s?action=getBinary&rev=%d", r.FullName, r.Id) // If there is an error, it will be told about it in `w`.
} // In any case, some http data is written to `w`.
func (rev *Revision) AsHtml(w http.ResponseWriter) (ret string, err error) {
// TODO: use templates https://github.com/bouncepaw/mycorrhiza/issues/2
func (r *Revision) AsHtml(hyphae map[string]*Hypha) (ret string, err error) {
ret += `<article class="page"> ret += `<article class="page">
<h1 class="page__title">` + r.FullName + `</h1> <h1 class="page__title">` + rev.FullName + `</h1>
` `
// TODO: support things other than images // TODO: support things other than images
if r.hasBinaryData() { if rev.hasBinaryData() {
ret += fmt.Sprintf(`<img src="%s" class="page__amnt"/>`, r.urlOfBinary()) ret += fmt.Sprintf(`<img src="/%s?action=getBinary&rev=%d" class="page__amnt"/>`, rev.FullName, rev.Id)
} }
contents, err := ioutil.ReadFile(r.TextPath) contents, err := ioutil.ReadFile(rev.TextPath)
if err != nil { if err != nil {
log.Println("Failed to render", rev.FullName)
w.WriteHeader(http.StatusInternalServerError)
return "", err return "", err
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
// TODO: support more markups. // TODO: support more markups.
// TODO: support mycorrhiza extensions like transclusion. // TODO: support mycorrhiza extensions like transclusion.
switch r.TextMime { switch rev.TextMime {
case "text/markdown": case "text/markdown":
html := markdown.ToHTML(contents, nil, nil) html := markdown.ToHTML(contents, nil, nil)
ret += string(html) ret += string(html)
@ -69,55 +74,50 @@ func (r *Revision) AsHtml(hyphae map[string]*Hypha) (ret string, err error) {
return ret, nil return ret, nil
} }
func (r *Revision) ActionGetBinary(w http.ResponseWriter) { // ActionGetBinary is used with `?action=getBinary`.
fileContents, err := ioutil.ReadFile(r.BinaryPath) // 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 { if err != nil {
log.Println("Failed to load binary data of", r.FullName, r.Id) log.Println("Failed to load binary data of", rev.FullName, rev.Id)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
w.Header().Set("Content-Type", r.BinaryMime) w.Header().Set("Content-Type", rev.BinaryMime)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(fileContents) w.Write(fileContents)
log.Println("Serving binary data of", r.FullName, r.Id) log.Println("Serving binary data of", rev.FullName, rev.Id)
} }
func (r *Revision) ActionRaw(w http.ResponseWriter) { // ActionRaw is used with `?action=raw`.
fileContents, err := ioutil.ReadFile(r.TextPath) // 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 { if err != nil {
log.Println("Failed to load text data of", r.FullName, r.Id)
w.WriteHeader(http.StatusNotFound)
return return
} }
w.Header().Set("Content-Type", r.TextMime) w.Header().Set("Content-Type", rev.TextMime)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(fileContents) w.Write(fileContents)
log.Println("Serving text data of", r.FullName, r.Id) log.Println("Serving text data of", rev.FullName, rev.Id)
} }
func (r *Revision) ActionZen(w http.ResponseWriter) { // ActionZen is used with `?action=zen`.
html, err := r.AsHtml(hyphae) // It renders the revision without any layout or navigation.
if err != nil { func (rev *Revision) ActionZen(w http.ResponseWriter) {
log.Println("Failed to render", r.FullName) html, err := rev.AsHtml(w)
w.WriteHeader(http.StatusInternalServerError) if err == nil {
return fmt.Fprint(w, html)
log.Println("Rendering", rev.FullName, "in zen mode")
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, html)
} }
func (r *Revision) ActionView(w http.ResponseWriter, layoutFun func(map[string]*Hypha, Revision, string) string) { // ActionView is used with `?action=view` or without any action.
html, err := r.AsHtml(hyphae) // It renders the revision with layout and navigation.
if err != nil { func (rev *Revision) ActionView(w http.ResponseWriter, layoutFun func(Revision, string) string) {
log.Println("Failed to render", r.FullName) html, err := rev.AsHtml(w)
w.WriteHeader(http.StatusInternalServerError) if err == nil {
return fmt.Fprint(w, layoutFun(*rev, html))
log.Println("Rendering", rev.FullName)
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, layoutFun(hyphae, *r, html))
log.Println("Rendering", r.FullName)
} }

View File

@ -9,8 +9,9 @@
<div class="edit-box__left"> <div class="edit-box__left">
<h4>Edit box</h4> <h4>Edit box</h4>
<!-- It is important that there is no indent ↓ -->
<textarea class="edit-box__text" name="text" cols="80" rows="25"> <textarea class="edit-box__text" name="text" cols="80" rows="25">
{{ .Text }} {{ .Text }}
</textarea> </textarea>
<h4>Upload file</h4> <h4>Upload file</h4>

View File

@ -4,22 +4,22 @@
{{ .Head }} {{ .Head }}
</head> </head>
<body> <body>
<div class="shroom"> <div class="shroom">
<button class="shroom__button" id="shroomBtn"><span>🍄</span> Open mycelium</button> <button class="shroom__button" id="shroomBtn"><span>🍄</span> Open mycelium</button>
</div> </div>
<main class="main">{{ .Main }}</main> <main class="main">{{ .Main }}</main>
<div class="left-panel" id="shroomburgerMenu"> <div class="left-panel" id="shroomburgerMenu">
<div class="left-panel__in"> <div class="left-panel__in">
<div class="shroom mushroom"> <div class="shroom mushroom">
<button class="shroom__button" id="mushroomBtn"><span>🍄</span> Close mycelium</button> <button class="shroom__button" id="mushroomBtn"><span>🍄</span> Close mycelium</button>
</div> </div>
<div class="left-panel__contents"> <div class="left-panel__contents">
<header class="header">{{ .Header }}</header> <header class="header">{{ .Header }}</header>
<aside class="sidebar">{{ .Sidebar }}</aside> <aside class="sidebar">{{ .Sidebar }}</aside>
<footer class="footer">{{ .Footer }}</footer> <footer class="footer">{{ .Footer }}</footer>
</div> </div>
</div> </div>
</div> </div>
{{ .BodyBottom }} {{ .BodyBottom }}
</body> </body>
</html> </html>

View File

@ -1 +1 @@
<h1 class="header__site-title">{{ . }}</h1> {{ . }}

199
w/m/sys/main.css/2.txt Normal file
View File

@ -0,0 +1,199 @@
*, *::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;
}
@media (min-width: 700px) {
body {
}
}
.shroom {
margin: 0;
}
.shroom__button {
border-radius: 8px;
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: 1em 0 0.25em;
}
.page {
font-size: 16px;
line-height: 1.666;
max-width: 40em;
hyphens: auto;
}
.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 }
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; /* 250px + 16px + 16px */
}
.shroom {
display: none;
}
.page {
font-size: 18px;
}
.left-panel {
display: block;
position: fixed;
top: 0;
bottom: 0;
width: 274px; /* 250px + 24px * 2 */
right: 0;
}
.left-panel__contents {
height: 100%;
}
}
.sidebar {
padding: 16px 0;
border-radius: 8px;
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;
}

View File

@ -1,11 +1 @@
{ {"views":0,"deleted":false,"revisions":{"1":{"tags":null,"name":"main.css","comment":"make a placeholder style","author":"wikimind","time":1592244023,"text_mime":"text/css","binary_mime":""},"2":{"tags":[""],"name":"","comment":"Update sys/main.css","author":"","time":1592589679,"text_mime":"text/css","binary_mime":""}}}
"revisions":{
"1":{
"name": "main.css",
"time": 1592244023,
"author": "wikimind",
"comment": "make a placeholder style",
"text_mime": "text/css"
}
}
}

42
walk.go
View File

@ -22,40 +22,34 @@ var (
leadingInt = regexp.MustCompile(`^[-+]?\d+`) leadingInt = regexp.MustCompile(`^[-+]?\d+`)
) )
func matchNameToEverything(name string) (hyphaM bool, revTxtM bool, revBinM bool, metaJsonM bool) { // matchNameToEverything matches `name` to all filename patterns and returns 4 boolean results.
func matchNameToEverything(name string) (revTxtM, revBinM, metaJsonM, hyphaM bool) {
// simpleMatch reduces boilerplate. Errors are ignored because I trust my regex skills.
simpleMatch := func(s string, p string) bool { simpleMatch := func(s string, p string) bool {
m, _ := regexp.MatchString(p, s) m, _ := regexp.MatchString(p, s)
return m return m
} }
switch { return simpleMatch(name, revTxtPattern),
case simpleMatch(name, revTxtPattern): simpleMatch(name, revBinPattern),
revTxtM = true simpleMatch(name, metaJsonPattern),
case simpleMatch(name, revBinPattern): simpleMatch(name, hyphaPattern)
revBinM = true
case simpleMatch(name, metaJsonPattern):
metaJsonM = true
case simpleMatch(name, hyphaPattern):
hyphaM = true
}
return
} }
// stripLeadingInt finds number in the beginning of `s` and returns it.
func stripLeadingInt(s string) string { func stripLeadingInt(s string) string {
return leadingInt.FindString(s) return leadingInt.FindString(s)
} }
// hyphaDirRevsValidate checks if `dto` is ok.
// It also deletes pair with "0" as key so there is no revision with this id.
func hyphaDirRevsValidate(dto map[string]map[string]string) (res bool) { func hyphaDirRevsValidate(dto map[string]map[string]string) (res bool) {
for k, _ := range dto { if _, ok := dto["0"]; ok {
switch k { delete(dto, "0")
case "0":
delete(dto, "0")
default:
res = true
}
} }
return res return len(dto) > 0
} }
// scanHyphaDir scans directory at `fullPath` and tells what it has found.
func scanHyphaDir(fullPath string) (valid bool, revs map[string]map[string]string, possibleSubhyphae []string, metaJsonPath string, err error) { func scanHyphaDir(fullPath string) (valid bool, revs map[string]map[string]string, possibleSubhyphae []string, metaJsonPath string, err error) {
revs = make(map[string]map[string]string) revs = make(map[string]map[string]string)
nodes, err := ioutil.ReadDir(fullPath) nodes, err := ioutil.ReadDir(fullPath)
@ -64,7 +58,7 @@ func scanHyphaDir(fullPath string) (valid bool, revs map[string]map[string]strin
} }
for _, node := range nodes { for _, node := range nodes {
hyphaM, revTxtM, revBinM, metaJsonM := matchNameToEverything(node.Name()) revTxtM, revBinM, metaJsonM, hyphaM := matchNameToEverything(node.Name())
switch { switch {
case hyphaM && node.IsDir(): case hyphaM && node.IsDir():
possibleSubhyphae = append(possibleSubhyphae, filepath.Join(fullPath, node.Name())) possibleSubhyphae = append(possibleSubhyphae, filepath.Join(fullPath, node.Name()))
@ -87,15 +81,16 @@ func scanHyphaDir(fullPath string) (valid bool, revs map[string]map[string]strin
} }
valid = hyphaDirRevsValidate(revs) valid = hyphaDirRevsValidate(revs)
return // implicit return values return // implicit return values
} }
// Hypha name is rootWikiDir/{here} // hyphaName gets name of a hypha by stripping path to the hypha in `fullPath`
func hyphaName(fullPath string) string { func hyphaName(fullPath string) string {
// {rootWikiDir}/{the name}
return fullPath[len(rootWikiDir)+1:] return fullPath[len(rootWikiDir)+1:]
} }
// recurFindHyphae recursively searches for hyphae in passed directory path.
func recurFindHyphae(fullPath string) map[string]*Hypha { func recurFindHyphae(fullPath string) map[string]*Hypha {
hyphae := make(map[string]*Hypha) hyphae := make(map[string]*Hypha)
valid, revs, possibleSubhyphae, metaJsonPath, err := scanHyphaDir(fullPath) valid, revs, possibleSubhyphae, metaJsonPath, err := scanHyphaDir(fullPath)
@ -154,5 +149,4 @@ func recurFindHyphae(fullPath string) map[string]*Hypha {
// Now the hypha should be ok, gotta send structs // Now the hypha should be ok, gotta send structs
hyphae[h.FullName] = &h hyphae[h.FullName] = &h
return hyphae return hyphae
} }