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:
parent
550bcbd444
commit
931bc8bae9
@ -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>`
|
||||||
)
|
)
|
||||||
|
19
genealogy.go
19
genealogy.go
@ -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
1
go.mod
@ -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
14
go.sum
@ -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=
|
||||||
|
79
handlers.go
79
handlers.go
@ -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
|
||||||
|
66
hypha.go
66
hypha.go
@ -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
58
main.go
@ -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
BIN
mycorrhiza
Executable file
Binary file not shown.
18
render.go
18
render.go
@ -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)
|
||||||
|
98
revision.go
98
revision.go
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -1 +1 @@
|
|||||||
<h1 class="header__site-title">{{ . }}</h1>
|
{{ . }}
|
||||||
|
199
w/m/sys/main.css/2.txt
Normal file
199
w/m/sys/main.css/2.txt
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
42
walk.go
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user