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
const (
TitleEditTemplate = `Edit %s at MycorrhizaWiki`
TitleTemplate = `%s at MycorrhizaWiki`
DefaultTitle = "MycorrhizaWiki"
DefaultHeaderText = `MycorrhizaWiki 🍄`
@ -11,4 +12,5 @@ const (
DefaultStyles = `
<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.
*/
TODO: make use of family relations.
*/
package main
type Genealogy struct {
parent string
child string
}
// 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)
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)
}

1
go.mod
View File

@ -5,4 +5,5 @@ go 1.14
require (
github.com/gomarkdown/markdown v0.0.0-20200609195525-3f9352745725
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/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/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/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"
)
// There are handlers below. See main() for their usage.
// Boilerplate code present in many handlers. Good to have it.
func HandlerBase(w http.ResponseWriter, r *http.Request) (Revision, bool) {
vars := mux.Vars(r)
func HandlerBase(w http.ResponseWriter, rq *http.Request) (Revision, bool) {
vars := mux.Vars(rq)
revno := RevInMap(vars)
return GetRevision(hyphae, vars["hypha"], revno, w)
return GetRevision(vars["hypha"], revno)
}
func HandlerGetBinary(w http.ResponseWriter, r *http.Request) {
if rev, ok := HandlerBase(w, r); ok {
func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, rq); ok {
rev.ActionGetBinary(w)
}
}
func HandlerRaw(w http.ResponseWriter, r *http.Request) {
if rev, ok := HandlerBase(w, r); ok {
func HandlerRaw(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, rq); ok {
rev.ActionRaw(w)
}
}
func HandlerZen(w http.ResponseWriter, r *http.Request) {
if rev, ok := HandlerBase(w, r); ok {
func HandlerZen(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, rq); ok {
rev.ActionZen(w)
}
}
func HandlerView(w http.ResponseWriter, r *http.Request) {
if rev, ok := HandlerBase(w, r); ok {
func HandlerView(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, rq); ok {
rev.ActionView(w, HyphaPage)
}
}
func HandlerHistory(w http.ResponseWriter, r *http.Request) {
func HandlerHistory(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing")
}
func HandlerEdit(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
func HandlerEdit(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(rq)
ActionEdit(vars["hypha"], w)
}
func HandlerRewind(w http.ResponseWriter, r *http.Request) {
func HandlerRewind(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing")
}
func HandlerDelete(w http.ResponseWriter, r *http.Request) {
func HandlerDelete(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing")
}
func HandlerRename(w http.ResponseWriter, r *http.Request) {
func HandlerRename(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing")
}
// makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant.
// Expected output for string above: []string{"foo", "bar", "kek"}
func makeTagsSlice(responseTagsString string) (ret []string) {
// `responseTagsString` is string like "foo,, bar,kek". Whitespace around commas is insignificant. Expected output: []string{"foo", "bar", "kek"}
for _, tag := range strings.Split(responseTagsString, ",") {
if trimmed := strings.TrimSpace(tag); "" == trimmed {
ret = append(ret, trimmed)
@ -78,7 +81,7 @@ func makeTagsSlice(responseTagsString string) (ret []string) {
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) {
log.Println("Accessing hypha", name)
if h, ok := hyphae[name]; ok {
@ -95,26 +98,27 @@ func getHypha(name string) (*Hypha, bool) {
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.
func revisionFromHttpData(h *Hypha, r *http.Request) *Revision {
// 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.Println(idStr)
rev := &Revision{
Id: h.NewestRevisionInt() + 1,
FullName: h.FullName,
Tags: makeTagsSlice(r.PostFormValue("tags")),
Comment: r.PostFormValue("comment"),
Author: r.PostFormValue("author"),
Tags: makeTagsSlice(rq.PostFormValue("tags")),
Comment: rq.PostFormValue("comment"),
Author: rq.PostFormValue("author"),
Time: int(time.Now().Unix()),
TextMime: r.PostFormValue("text_mime"),
TextMime: rq.PostFormValue("text_mime"),
TextPath: filepath.Join(h.Path, idStr+".txt"),
// Left: BinaryMime, BinaryPath
// Fields left: BinaryMime, BinaryPath
}
return rev
}
func writeTextFileFromHttpData(rev *Revision, r *http.Request) error {
data := []byte(r.PostFormValue("text"))
// 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)
@ -122,11 +126,12 @@ func writeTextFileFromHttpData(rev *Revision, r *http.Request) error {
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
r.ParseMultipartForm(10 << 20)
rq.ParseMultipartForm(10 << 20)
// Read file
file, handler, err := r.FormFile("binary")
file, handler, err := rq.FormFile("binary")
if file != nil {
defer file.Close()
}
@ -154,22 +159,22 @@ func writeBinaryFileFromHttpData(h *Hypha, oldRev Revision, newRev *Revision, r
return nil
}
func HandlerUpdate(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
log.Println("Attempt to update hypha", mux.Vars(r)["hypha"])
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"])
oldRev := h.GetNewestRevision()
newRev := revisionFromHttpData(h, r)
newRev := revisionFromHttpData(h, rq)
if isNew {
h.CreateDir()
}
err := writeTextFileFromHttpData(newRev, r)
err := writeTextFileFromHttpData(newRev, rq)
if err != nil {
log.Println(err)
return
}
err = writeBinaryFileFromHttpData(h, oldRev, newRev, r)
err = writeBinaryFileFromHttpData(h, oldRev, newRev, rq)
if err != nil {
log.Println(err)
return

View File

@ -12,6 +12,8 @@ import (
"strings"
)
// `Hypha` represents a hypha. It is the thing MycorrhizaWiki generally serves.
// Each hypha has 1 or more revisions.
type Hypha struct {
FullName string `json:"-"`
Path string `json:"-"`
@ -22,58 +24,52 @@ type Hypha struct {
parentName string
}
func (h *Hypha) AddChild(childName string) {
h.ChildrenNames = append(h.ChildrenNames, childName)
}
// Used with action=zen|view
func (h *Hypha) AsHtml(hyphae map[string]*Hypha, rev string) (string, error) {
if "0" == rev {
rev = h.NewestRevision()
// 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()
}
r, ok := h.Revisions[rev]
if !ok {
return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, rev)
if rev, ok := h.Revisions[id]; ok {
return rev.AsHtml(w)
}
html, err := r.AsHtml(hyphae)
return html, err
}
func (h *Hypha) Name() string {
return h.FullName
return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, id)
}
// GetNewestRevision returns the most recent Revision.
func (h *Hypha) GetNewestRevision() Revision {
return *h.Revisions[h.NewestRevision()]
}
// NewestRevision returns the most recent revision's id as a string.
func (h *Hypha) NewestRevision() string {
return strconv.Itoa(h.NewestRevisionInt())
}
func (h *Hypha) NewestRevisionInt() int {
var largest int
// NewestRevision returns the most recent revision's id as an integer.
func (h *Hypha) NewestRevisionInt() (ret int) {
for k, _ := range h.Revisions {
rev, _ := strconv.Atoi(k)
if rev > largest {
largest = rev
id, _ := strconv.Atoi(k)
if id > ret {
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 {
return filepath.Join(h.Path, "meta.json")
}
// CreateDir creates directory where the hypha must reside.
// It is meant to be used with new hyphae.
func (h *Hypha) CreateDir() error {
return os.MkdirAll(h.Path, 0644)
}
func (h *Hypha) ParentName() string {
return h.parentName
}
// SaveJson dumps the hypha's metadata to `meta.json` file.
func (h *Hypha) SaveJson() {
data, err := json.Marshal(h)
if err != nil {
@ -88,24 +84,26 @@ func (h *Hypha) SaveJson() {
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
hypha, ok := hyphae[hyphaName]
if !ok {
initContents = "Describe " + hyphaName + "here."
initTextMime = "text/markdown"
} else {
newestRev := hypha.Revisions[hypha.NewestRevision()]
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("<b>Sorry, something went wrong</b>"))
w.Write([]byte(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)

58
main.go
View File

@ -11,40 +11,36 @@ import (
"github.com/gorilla/mux"
)
func GetRevision(hyphae map[string]*Hypha, hyphaName string, rev string, w http.ResponseWriter) (Revision, bool) {
log.Println("Getting hypha", hyphaName, rev)
for name, hypha := range hyphae {
if name == hyphaName {
if rev == "0" {
rev = hypha.NewestRevision()
}
for id, r := range hypha.Revisions {
if rev == id {
return *r, true
}
}
// GetRevision finds revision with id `id` of `hyphaName` in `hyphae`.
// If `id` is `"0"`, it means the last revision.
// If no such revision is found, last return value is false.
func GetRevision(hyphaName string, id string) (Revision, bool) {
log.Println("Getting hypha", hyphaName, id)
if hypha, ok := hyphae[hyphaName]; ok {
if id == "0" {
id = hypha.NewestRevision()
}
if rev, ok := hypha.Revisions[id]; ok {
return *rev, true
}
}
return Revision{}, false
}
// RevInMap finds value of `rev` (the one from URL queries like) in the passed map that is usually got from `mux.Vars(*http.Request)`.
// If there is no `rev`, return "0".
func RevInMap(m map[string]string) string {
if val, ok := m["rev"]; ok {
return val
if id, ok := m["rev"]; ok {
return id
}
return "0"
}
// `rootWikiDir` is a directory where all wiki files reside.
var rootWikiDir string
var hyphae map[string]*Hypha
func hyphaeAsMap(hyphae []*Hypha) map[string]*Hypha {
mh := make(map[string]*Hypha)
for _, h := range hyphae {
mh[h.Name()] = h
}
return mh
}
// `hyphae` is a map with all hyphae. Many functions use it.
var hyphae map[string]*Hypha
func main() {
if len(os.Args) == 1 {
@ -61,9 +57,8 @@ func main() {
log.Println("Indexing hyphae...")
hyphae = recurFindHyphae(rootWikiDir)
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.Queries("action", "getBinary", "rev", revQuery).Path(hyphaUrl).
@ -101,19 +96,20 @@ func main() {
r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl).
HandlerFunc(HandlerRename)
r.Queries(
"action", "update",
).Path(hyphaUrl).Methods("POST").
r.Queries("action", "update").Path(hyphaUrl).Methods("POST").
HandlerFunc(HandlerUpdate)
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.WriteHeader(http.StatusOK)
for _, v := range hyphae {
log.Println("Rendering latest revision of hypha", v.Name())
html, err := v.AsHtml(hyphae, "0")
for _, h := range hyphae {
log.Println("Rendering latest revision of hypha", h.FullName)
html, err := h.AsHtml("0", w)
if err != nil {
fmt.Fprintln(w, err)
}

BIN
mycorrhiza Executable file

Binary file not shown.

View File

@ -8,9 +8,10 @@ import (
"text/template"
)
// EditHyphaPage returns HTML page of hypha editor.
func EditHyphaPage(name, textMime, content, tags string) string {
keys := map[string]string{
"Title": fmt.Sprintf(TitleTemplate, "Edit "+name),
"Title": fmt.Sprintf(TitleEditTemplate, name),
"Header": renderFromString(name, "Hypha/edit/header.html"),
}
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)
}
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
bside, err := ioutil.ReadFile("Hypha/view/sidebar.html")
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)
}
/*
Collect and render page from base template
Args:
content: string or pre-rendered template
keys: map with replaced standart fields
*/
// 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": DefaultTitle,
@ -58,6 +58,7 @@ func renderBase(content string, keys map[string]string) string {
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("templates", templatePath)
tmpl, err := template.ParseFiles(filePath)
@ -71,6 +72,7 @@ func renderFromMap(data map[string]string, templatePath string) string {
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("templates", templatePath)
tmpl, err := template.ParseFiles(filePath)

View File

@ -5,13 +5,13 @@ import (
"io/ioutil"
"log"
"net/http"
"strconv"
"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 {
Id int `json:"-"`
FullName string `json:"-"`
@ -26,37 +26,42 @@ type Revision struct {
BinaryPath string `json:"-"`
}
func (r *Revision) IdAsStr() string {
return strconv.Itoa(r.Id)
// 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 (r *Revision) hasBinaryData() bool {
return r.BinaryMime != ""
func (rev *Revision) hasBinaryData() bool {
return rev.BinaryMime != ""
}
func (r *Revision) urlOfBinary() string {
return fmt.Sprintf("/%s?action=getBinary&rev=%d", r.FullName, r.Id)
}
// TODO: use templates https://github.com/bouncepaw/mycorrhiza/issues/2
func (r *Revision) AsHtml(hyphae map[string]*Hypha) (ret string, err error) {
// AsHtml returns HTML representation of the revision.
// 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) {
ret += `<article class="page">
<h1 class="page__title">` + r.FullName + `</h1>
<h1 class="page__title">` + rev.FullName + `</h1>
`
// TODO: support things other than images
if r.hasBinaryData() {
ret += fmt.Sprintf(`<img src="%s" class="page__amnt"/>`, r.urlOfBinary())
if rev.hasBinaryData() {
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 {
log.Println("Failed to render", rev.FullName)
w.WriteHeader(http.StatusInternalServerError)
return "", err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
// TODO: support more markups.
// TODO: support mycorrhiza extensions like transclusion.
switch r.TextMime {
switch rev.TextMime {
case "text/markdown":
html := markdown.ToHTML(contents, nil, nil)
ret += string(html)
@ -69,55 +74,50 @@ func (r *Revision) AsHtml(hyphae map[string]*Hypha) (ret string, err error) {
return ret, nil
}
func (r *Revision) ActionGetBinary(w http.ResponseWriter) {
fileContents, err := ioutil.ReadFile(r.BinaryPath)
// 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", r.FullName, r.Id)
log.Println("Failed to load binary data of", rev.FullName, rev.Id)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", r.BinaryMime)
w.Header().Set("Content-Type", rev.BinaryMime)
w.WriteHeader(http.StatusOK)
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) {
fileContents, err := ioutil.ReadFile(r.TextPath)
// ActionRaw is used with `?action=raw`.
// It writes text content of the revision without any parsing or rendering.
func (rev *Revision) ActionRaw(w http.ResponseWriter) {
fileContents, err := ioutil.ReadFile(rev.TextPath)
if err != nil {
log.Println("Failed to load text data of", r.FullName, r.Id)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", r.TextMime)
w.Header().Set("Content-Type", rev.TextMime)
w.WriteHeader(http.StatusOK)
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) {
html, err := r.AsHtml(hyphae)
if err != nil {
log.Println("Failed to render", r.FullName)
w.WriteHeader(http.StatusInternalServerError)
return
// 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")
}
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) {
html, err := r.AsHtml(hyphae)
if err != nil {
log.Println("Failed to render", r.FullName)
w.WriteHeader(http.StatusInternalServerError)
return
// 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)
}
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">
<h4>Edit box</h4>
<!-- It is important that there is no indent ↓ -->
<textarea class="edit-box__text" name="text" cols="80" rows="25">
{{ .Text }}
{{ .Text }}
</textarea>
<h4>Upload file</h4>

View File

@ -4,22 +4,22 @@
{{ .Head }}
</head>
<body>
<div class="shroom">
<button class="shroom__button" id="shroomBtn"><span>🍄</span> Open mycelium</button>
</div>
<div class="shroom">
<button class="shroom__button" id="shroomBtn"><span>🍄</span> Open mycelium</button>
</div>
<main class="main">{{ .Main }}</main>
<div class="left-panel" id="shroomburgerMenu">
<div class="left-panel__in">
<div class="shroom mushroom">
<button class="shroom__button" id="mushroomBtn"><span>🍄</span> Close mycelium</button>
</div>
<div class="left-panel__contents">
<header class="header">{{ .Header }}</header>
<aside class="sidebar">{{ .Sidebar }}</aside>
<footer class="footer">{{ .Footer }}</footer>
</div>
</div>
</div>
<div class="left-panel" id="shroomburgerMenu">
<div class="left-panel__in">
<div class="shroom mushroom">
<button class="shroom__button" id="mushroomBtn"><span>🍄</span> Close mycelium</button>
</div>
<div class="left-panel__contents">
<header class="header">{{ .Header }}</header>
<aside class="sidebar">{{ .Sidebar }}</aside>
<footer class="footer">{{ .Footer }}</footer>
</div>
</div>
</div>
{{ .BodyBottom }}
</body>
</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 @@
{
"revisions":{
"1":{
"name": "main.css",
"time": 1592244023,
"author": "wikimind",
"comment": "make a placeholder style",
"text_mime": "text/css"
}
}
}
{"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":""}}}

42
walk.go
View File

@ -22,40 +22,34 @@ var (
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 {
m, _ := regexp.MatchString(p, s)
return m
}
switch {
case simpleMatch(name, revTxtPattern):
revTxtM = true
case simpleMatch(name, revBinPattern):
revBinM = true
case simpleMatch(name, metaJsonPattern):
metaJsonM = true
case simpleMatch(name, hyphaPattern):
hyphaM = true
}
return
return simpleMatch(name, revTxtPattern),
simpleMatch(name, revBinPattern),
simpleMatch(name, metaJsonPattern),
simpleMatch(name, hyphaPattern)
}
// stripLeadingInt finds number in the beginning of `s` and returns it.
func stripLeadingInt(s string) string {
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) {
for k, _ := range dto {
switch k {
case "0":
delete(dto, "0")
default:
res = true
}
if _, ok := dto["0"]; ok {
delete(dto, "0")
}
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) {
revs = make(map[string]map[string]string)
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 {
hyphaM, revTxtM, revBinM, metaJsonM := matchNameToEverything(node.Name())
revTxtM, revBinM, metaJsonM, hyphaM := matchNameToEverything(node.Name())
switch {
case hyphaM && node.IsDir():
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)
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 {
// {rootWikiDir}/{the name}
return fullPath[len(rootWikiDir)+1:]
}
// recurFindHyphae recursively searches for hyphae in passed directory path.
func recurFindHyphae(fullPath string) map[string]*Hypha {
hyphae := make(map[string]*Hypha)
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
hyphae[h.FullName] = &h
return hyphae
}