diff --git a/Makefile b/Makefile index 49dc5848..dd9d604b 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ run: build ./mycorrhiza metarrhiza build: + go generate go build . test: diff --git a/README.md b/README.md index 64dc5454..a733e959 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,35 @@ -# mycorrhiza wiki -A wiki engine inspired by fungi. Not production-ready. +# 🍄 MycorrhizaWiki 0.8 +A wiki engine. -Current version: 0.7 (or more?) +## Installation +```sh +git clone --recurse-submodules https://github.com/bouncepaw/mycorrhiza +cd mycorrhiza +make +# That make will: +# * run the default wiki. You can edit it right away. +# * create an executable called `mycorrhiza`. Run it with path to your wiki. +``` ## Current features * Edit pages through html forms * Responsive design * Works in text browsers -* Pages (called hyphae) can be in gemtext. +* Wiki pages (called hyphae) are in gemtext * Everything is stored as simple files, no database required +* Page trees +* Changes are saved to git +* List of hyphae page +* History page +* Random page +* Light on resources: I run a home wiki on this engine 24/7 at an [Orange π Lite](http://www.orangepi.org/orangepilite/). -## Future features -* Tags -* Authorization -* History view -* Granular user rights - -## Installation -I guess you can just clone this repo and run `make` to play around with the default wiki. +## Contributing +Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where some development is coordinated. Feel free to open an issue or contact me. +## Future plans +* Tagging system +* Authorization +* Better history viewing +* Recent changes page +* More markups diff --git a/gemtext/lexer.go b/gemtext/lexer.go index 507808b1..5fd1c778 100644 --- a/gemtext/lexer.go +++ b/gemtext/lexer.go @@ -23,11 +23,6 @@ type GemLexerState struct { buf string } -// GeminiToHtml converts gemtext `content` of hypha `name` to html string. -func GeminiToHtml(name, content string) string { - return "TODO: do" -} - type Line struct { id int // interface{} may be bad. What I need is a sum of string and Transclusion @@ -78,7 +73,7 @@ func wikilink(src string, state *GemLexerState) (href, text, class string) { func lex(name, content string) (ast []Line) { var state = GemLexerState{name: name} - for _, line := range strings.Split(content, "\n") { + for _, line := range append(strings.Split(content, "\n"), "") { geminiLineToAST(line, &state, &ast) } return ast @@ -86,16 +81,21 @@ func lex(name, content string) (ast []Line) { // Lex `line` in gemtext and save it to `ast` using `state`. func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) { + addLine := func(text interface{}) { + *ast = append(*ast, Line{id: state.id, contents: text}) + } + if "" == strings.TrimSpace(line) { + if state.where == "list" { + state.where = "" + addLine(state.buf + "") + } return } startsWith := func(token string) bool { return strings.HasPrefix(line, token) } - addLine := func(text interface{}) { - *ast = append(*ast, Line{id: state.id, contents: text}) - } // Beware! Usage of goto. Some may say it is considered evil but in this case it helped to make a better-structured code. switch state.where { diff --git a/go.mod b/go.mod index d4ae4307..17f2ff6c 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,4 @@ module github.com/bouncepaw/mycorrhiza go 1.14 -require ( - golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect - golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6 // indirect -) +require github.com/valyala/quicktemplate v1.6.2 diff --git a/go.sum b/go.sum index 12c582a1..344ceb33 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,15 @@ -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= +github.com/valyala/quicktemplate v1.6.2 h1:k0vgK7zlmFzqAoIBIOrhrfmZ6JoTGJlLRPLbkPGr2/M= +github.com/valyala/quicktemplate v1.6.2/go.mod h1:mtEJpQtUiBV0SHhMX6RtiJtqxncgrfmjcUy5T68X8TM= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 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/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -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/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6 h1:qKpj8TpV+LEhel7H/fR788J+KvhWZ3o3V6N2fU/iuLU= -golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/history/history.go b/history/history.go new file mode 100644 index 00000000..787b358f --- /dev/null +++ b/history/history.go @@ -0,0 +1,77 @@ +package history + +import ( + "bytes" + "fmt" + "log" + "os/exec" + "strconv" + "time" + + "github.com/bouncepaw/mycorrhiza/util" +) + +// Start initializes git credentials. +func Start(wikiDir string) { + _, err := gitsh("config", "user.name", "wikimind") + if err != nil { + log.Fatal(err) + } + _, err = gitsh("config", "user.email", "wikimind@mycorrhiza") + if err != nil { + log.Fatal(err) + } +} + +// Revision represents a revision, duh. Hash is usually short. Username is extracted from email. +type Revision struct { + Hash string + Username string + Time time.Time + Message string +} + +// Path to git executable. Set at init() +var gitpath string + +func init() { + path, err := exec.LookPath("git") + if err != nil { + log.Fatal("Cound not find the git executable. Check your $PATH.") + } else { + log.Println("Git path is", path) + } + gitpath = path + +} + +// I pronounce it as [gɪt͡ʃ]. +func gitsh(args ...string) (out bytes.Buffer, err error) { + fmt.Printf("$ %v\n", args) + cmd := exec.Command(gitpath, args...) + + cmd.Dir = util.WikiDir + + b, err := cmd.CombinedOutput() + if err != nil { + log.Println("gitsh:", err) + } + return *bytes.NewBuffer(b), err +} + +// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted. +func unixTimestampAsTime(ts string) *time.Time { + i, err := strconv.ParseInt(ts, 10, 64) + if err != nil { + return nil + } + tm := time.Unix(i, 0) + return &tm +} + +// Rename renames from `from` to `to` using `git mv`. +func Rename(from, to string) error { + log.Println(util.ShorterPath(from), util.ShorterPath(to)) + _, err := gitsh("mv", from, to) + return err +} diff --git a/history/information.go b/history/information.go new file mode 100644 index 00000000..ff6e654f --- /dev/null +++ b/history/information.go @@ -0,0 +1,58 @@ +// information.go +// Things related to gathering existing information. +package history + +import ( + "fmt" + "regexp" + "strings" + "time" +) + +// Revisions returns slice of revisions for the given hypha name. +func Revisions(hyphaName string) ([]Revision, error) { + var ( + out, err = gitsh( + "log", "--oneline", "--no-merges", + // Hash, Commiter email, Commiter time, Commit msg separated by tab + "--pretty=format:\"%h\t%ce\t%ct\t%s\"", + "--", hyphaName+"&.*", + ) + revs []Revision + ) + if err == nil { + for _, line := range strings.Split(out.String(), "\n") { + revs = append(revs, parseRevisionLine(line)) + } + } + return revs, err +} + +// This regex is wrapped in "". For some reason, these quotes appear at some time and we have to get rid of them. +var revisionLinePattern = regexp.MustCompile("\"(.*)\t(.*)@.*\t(.*)\t(.*)\"") + +func parseRevisionLine(line string) Revision { + results := revisionLinePattern.FindStringSubmatch(line) + return Revision{ + Hash: results[1], + Username: results[2], + Time: *unixTimestampAsTime(results[3]), + Message: results[4], + } +} + +// Represent revision as a table row. +func (rev *Revision) AsHtmlTableRow(hyphaName string) string { + return fmt.Sprintf(` + + + %s + %s +`, rev.Time.Format(time.RFC822), rev.Hash, hyphaName, rev.Hash, rev.Message) +} + +// See how the file with `filepath` looked at commit with `hash`. +func FileAtRevision(filepath, hash string) (string, error) { + out, err := gitsh("show", hash+":"+filepath) + return out.String(), err +} diff --git a/history/operations.go b/history/operations.go new file mode 100644 index 00000000..3567d78d --- /dev/null +++ b/history/operations.go @@ -0,0 +1,84 @@ +// history/operations.go +// Things related to writing history. +package history + +import ( + "fmt" + + "github.com/bouncepaw/mycorrhiza/util" +) + +// OpType is the type a history operation has. Callers shall set appropriate optypes when creating history operations. +type OpType int + +const ( + TypeNone OpType = iota + TypeEditText + TypeEditBinary +) + +// HistoryOp is an object representing a history operation. +type HistoryOp struct { + // All errors are appended here. + Errs []error + opType OpType + userMsg string + name string + email string +} + +// Operation is a constructor of a history operation. +func Operation(opType OpType) *HistoryOp { + hop := &HistoryOp{ + Errs: []error{}, + opType: opType, + } + return hop +} + +// git operation maker helper +func (hop *HistoryOp) gitop(args ...string) *HistoryOp { + out, err := gitsh(args...) + if err != nil { + fmt.Println("out:", out.String()) + hop.Errs = append(hop.Errs, err) + } + return hop +} + +// WithFiles stages all passed `paths`. Paths can be rooted or not. +func (hop *HistoryOp) WithFiles(paths ...string) *HistoryOp { + for i, path := range paths { + paths[i] = util.ShorterPath(path) + } + // 1 git operation is more effective than n operations. + return hop.gitop(append([]string{"add"}, paths...)...) +} + +// Apply applies history operation by doing the commit. +func (hop *HistoryOp) Apply() *HistoryOp { + hop.gitop( + "commit", + "--author='"+hop.name+" <"+hop.email+">'", + "--message="+hop.userMsg, + ) + return hop +} + +// WithMsg sets what message will be used for the future commit. If user message exceeds one line, it is stripped down. +func (hop *HistoryOp) WithMsg(userMsg string) *HistoryOp { + for _, ch := range userMsg { + if ch == '\r' || ch == '\n' { + break + } + hop.userMsg += string(ch) + } + return hop +} + +// WithSignature sets a signature for the future commit. You need to pass a username only, the rest is upon us (including email and time). +func (hop *HistoryOp) WithSignature(username string) *HistoryOp { + hop.name = username + hop.email = username + "@mycorrhiza" // A fake email, why not + return hop +} diff --git a/http_mutators.go b/http_mutators.go index ca8cdd1d..e3cd3973 100644 --- a/http_mutators.go +++ b/http_mutators.go @@ -7,6 +7,10 @@ import ( "net/http" "os" "path/filepath" + + "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/util" ) func init() { @@ -36,24 +40,7 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) { } else { warning = `

You are creating a new hypha.

` } - form := fmt.Sprintf(` -
-

Edit %[1]s

- %[3]s -
- -
- - Cancel -
-
-`, hyphaName, textAreaFill, warning) - - w.Header().Set("Content-Type", "text/html;charset=utf-8") - w.WriteHeader(http.StatusOK) - w.Write([]byte(base( - "Edit "+hyphaName, form))) + util.HTTP200Page(w, base("Edit"+hyphaName, templates.EditHTML(hyphaName, textAreaFill, warning))) } // handlerUploadText uploads a new text part for the hypha. @@ -92,6 +79,11 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) { hyphaData.textPath = fullPath } http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) + history.Operation(history.TypeEditText). + WithFiles(fullPath). + WithMsg(fmt.Sprintf("Edit ‘%s’", hyphaName)). + WithSignature("anon"). + Apply() } // handlerUploadBinary uploads a new binary part for the hypha. @@ -127,11 +119,6 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) { if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil { log.Println(err) } - if err = ioutil.WriteFile(fullPath, data, 0644); err != nil { - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", - "Could not save passed data") - return - } if !isOld { HyphaStorage[hyphaName] = &HyphaData{ binaryPath: fullPath, @@ -139,13 +126,25 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) { } } else { if hyphaData.binaryPath != fullPath { - if err := os.Remove(hyphaData.binaryPath); err != nil { + if err := history.Rename(hyphaData.binaryPath, fullPath); err != nil { log.Println(err) + } else { + log.Println("Moved", hyphaData.binaryPath, "to", fullPath) } } hyphaData.binaryPath = fullPath hyphaData.binaryType = mimeType } + if err = ioutil.WriteFile(fullPath, data, 0644); err != nil { + HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", + "Could not save passed data") + return + } log.Println("Written", len(data), "of binary data for", hyphaName, "to path", fullPath) http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) + history.Operation(history.TypeEditText). + WithFiles(fullPath, hyphaData.binaryPath). + WithMsg(fmt.Sprintf("Upload binary part for ‘%s’ with type ‘%s’", hyphaName, mimeType.Mime())). + WithSignature("anon"). + Apply() } diff --git a/http_readers.go b/http_readers.go index f00673de..f2882105 100644 --- a/http_readers.go +++ b/http_readers.go @@ -6,14 +6,67 @@ import ( "log" "net/http" "os" + "path" + "strings" "github.com/bouncepaw/mycorrhiza/gemtext" + "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/tree" + "github.com/bouncepaw/mycorrhiza/util" ) func init() { http.HandleFunc("/page/", handlerPage) http.HandleFunc("/text/", handlerText) http.HandleFunc("/binary/", handlerBinary) + http.HandleFunc("/history/", handlerHistory) + http.HandleFunc("/rev/", handlerRevision) +} + +// handlerRevision displays a specific revision of text part a page +func handlerRevision(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + shorterUrl = strings.TrimPrefix(rq.URL.Path, "/rev/") + revHash = path.Dir(shorterUrl) + hyphaName = CanonicalName(strings.TrimPrefix(shorterUrl, revHash+"/")) + contents = fmt.Sprintf(`

This hypha had no text at this revision.

`) + textPath = hyphaName + "&.gmi" + textContents, err = history.FileAtRevision(textPath, revHash) + ) + if err == nil { + contents = gemtext.ToHtml(hyphaName, textContents) + } + page := templates.RevisionHTML( + hyphaName, + naviTitle(hyphaName), + contents, + tree.TreeAsHtml(hyphaName, IterateHyphaNamesWith), + revHash, + ) + w.Header().Set("Content-Type", "text/html;charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte(base(hyphaName, page))) +} + +// handlerHistory lists all revisions of a hypha +func handlerHistory(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + hyphaName := HyphaNameFromRq(rq, "history") + var tbody string + if _, ok := HyphaStorage[hyphaName]; ok { + revs, err := history.Revisions(hyphaName) + if err == nil { + for _, rev := range revs { + tbody += rev.AsHtmlTableRow(hyphaName) + } + } + log.Println(revs) + } + + util.HTTP200Page(w, + base(hyphaName, templates.HistoryHTML(hyphaName, tbody))) } // handlerText serves raw source text of the hypha. @@ -43,8 +96,8 @@ func handlerPage(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) var ( hyphaName = HyphaNameFromRq(rq, "page") - contents = fmt.Sprintf(`

This hypha has no text. Why not create it?

`, hyphaName) data, hyphaExists = HyphaStorage[hyphaName] + contents string ) if hyphaExists { fileContentsT, errT := ioutil.ReadFile(data.textPath) @@ -56,31 +109,8 @@ func handlerPage(w http.ResponseWriter, rq *http.Request) { contents = binaryHtmlBlock(hyphaName, data) + contents } } - form := fmt.Sprintf(` -
- -
- %[2]s - %[3]s -
-
-
- -
- - -
-
-`, hyphaName, naviTitle(hyphaName), contents) - w.Header().Set("Content-Type", "text/html;charset=utf-8") - w.WriteHeader(http.StatusOK) - w.Write([]byte(base(hyphaName, form))) + util.HTTP200Page(w, base(hyphaName, templates.PageHTML(hyphaName, + naviTitle(hyphaName), + contents, + tree.TreeAsHtml(hyphaName, IterateHyphaNamesWith)))) } diff --git a/main.go b/main.go index 7d08e0b9..cb9588f3 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,19 @@ +//go:generate go get -u github.com/valyala/quicktemplate/qtc +//go:generate qtc -dir=templates package main import ( "fmt" "log" + "math/rand" "net/http" "os" "path/filepath" "regexp" - "strings" + + "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/util" ) // WikiDir is a rooted path to the wiki storage directory. @@ -19,6 +25,13 @@ var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%]+`) // HyphaStorage is a mapping between canonical hypha names and their meta information. var HyphaStorage = make(map[string]*HyphaData) +// IterateHyphaNamesWith is a closure to be passed to subpackages to let them iterate all hypha names read-only. +func IterateHyphaNamesWith(f func(string)) { + for hyphaName, _ := range HyphaStorage { + f(hyphaName) + } +} + // HttpErr is used by many handlers to signal errors in a compact way. func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) { log.Println(errMsg, "for", name) @@ -29,70 +42,21 @@ func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) { errMsg, name))) } -// shorterPath is used by handlerList to display shorter path to the files. It simply strips WikiDir. -func shorterPath(fullPath string) string { - tmp := strings.TrimPrefix(fullPath, WikiDir) - if tmp == "" { - return "" - } - return tmp[1:] -} - // Show all hyphae func handlerList(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) - w.Header().Set("Content-Type", "text/html;charset=utf-8") - w.WriteHeader(http.StatusOK) - buf := ` -

List of pages

- - - - - - - - - - - ` - for name, data := range HyphaStorage { - buf += fmt.Sprintf(` - - - - - - - `, - name, name, - shorterPath(data.textPath), data.textType, - shorterPath(data.binaryPath), data.binaryType, - ) + var ( + tbody string + pageCount = len(HyphaStorage) + ) + for hyphaName, data := range HyphaStorage { + tbody += templates.HyphaListRowHTML(hyphaName, data.binaryType.Mime(), data.binaryPath != "") } - buf += ` - -
NameText pathText typeBinary pathBinary type
%s%s%d%s%d
-` - w.Write([]byte(base("List of pages", buf))) + util.HTTP200Page(w, base("List of pages", templates.HyphaListHTML(tbody, pageCount))) } // This part is present in all html documents. -func base(title, body string) string { - return fmt.Sprintf(` - - - - - - %s - - - %s - - -`, title, body) -} +var base = templates.BaseHTML // Reindex all hyphae by checking the wiki storage directory anew. func handlerReindex(w http.ResponseWriter, rq *http.Request) { @@ -104,24 +68,46 @@ func handlerReindex(w http.ResponseWriter, rq *http.Request) { log.Println("Indexed", len(HyphaStorage), "hyphae") } +// Redirect to a random hypha. +func handlerRandom(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var randomHyphaName string + i := rand.Intn(len(HyphaStorage)) + for hyphaName := range HyphaStorage { + if i == 0 { + randomHyphaName = hyphaName + break + } + i-- + } + http.Redirect(w, rq, "/page/"+randomHyphaName, http.StatusSeeOther) +} + func main() { log.Println("Running MycorrhizaWiki β") var err error WikiDir, err = filepath.Abs(os.Args[1]) + util.WikiDir = WikiDir if err != nil { log.Fatal(err) } + if err := os.Chdir(WikiDir); err != nil { + log.Fatal(err) + } log.Println("Wiki storage directory is", WikiDir) log.Println("Start indexing hyphae...") Index(WikiDir) log.Println("Indexed", len(HyphaStorage), "hyphae") + history.Start(WikiDir) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static")))) - // See http_readers.go for /page/, /text/, /binary/. + // See http_readers.go for /page/, /text/, /binary/, /history/. // See http_mutators.go for /upload-binary/, /upload-text/, /edit/. http.HandleFunc("/list", handlerList) http.HandleFunc("/reindex", handlerReindex) + http.HandleFunc("/random", handlerRandom) http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) { http.ServeFile(w, rq, WikiDir+"/static/favicon.ico") }) diff --git a/metarrhiza b/metarrhiza index a22fcac8..bdaaab62 160000 --- a/metarrhiza +++ b/metarrhiza @@ -1 +1 @@ -Subproject commit a22fcac89f10ad1e1db77d765788dfd8966cbb36 +Subproject commit bdaaab62574023487610d608d1e9f2f351707a7f diff --git a/templates/http_mutators.qtpl b/templates/http_mutators.qtpl new file mode 100644 index 00000000..72f40a10 --- /dev/null +++ b/templates/http_mutators.qtpl @@ -0,0 +1,14 @@ + +{% func EditHTML(hyphaName, textAreaFill, warning string) %} +
+

Edit {%s hyphaName %}

+ {%s= warning %} +
+ +
+ + Cancel +
+
+{% endfunc %} diff --git a/templates/http_mutators.qtpl.go b/templates/http_mutators.qtpl.go new file mode 100644 index 00000000..0c880ea0 --- /dev/null +++ b/templates/http_mutators.qtpl.go @@ -0,0 +1,83 @@ +// Code generated by qtc from "http_mutators.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line templates/http_mutators.qtpl:2 +package templates + +//line templates/http_mutators.qtpl:2 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line templates/http_mutators.qtpl:2 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line templates/http_mutators.qtpl:2 +func StreamEditHTML(qw422016 *qt422016.Writer, hyphaName, textAreaFill, warning string) { +//line templates/http_mutators.qtpl:2 + qw422016.N().S(` +
+

Edit `) +//line templates/http_mutators.qtpl:4 + qw422016.E().S(hyphaName) +//line templates/http_mutators.qtpl:4 + qw422016.N().S(`

+ `) +//line templates/http_mutators.qtpl:5 + qw422016.N().S(warning) +//line templates/http_mutators.qtpl:5 + qw422016.N().S(` +
+ +
+ + Cancel +
+
+`) +//line templates/http_mutators.qtpl:14 +} + +//line templates/http_mutators.qtpl:14 +func WriteEditHTML(qq422016 qtio422016.Writer, hyphaName, textAreaFill, warning string) { +//line templates/http_mutators.qtpl:14 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_mutators.qtpl:14 + StreamEditHTML(qw422016, hyphaName, textAreaFill, warning) +//line templates/http_mutators.qtpl:14 + qt422016.ReleaseWriter(qw422016) +//line templates/http_mutators.qtpl:14 +} + +//line templates/http_mutators.qtpl:14 +func EditHTML(hyphaName, textAreaFill, warning string) string { +//line templates/http_mutators.qtpl:14 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_mutators.qtpl:14 + WriteEditHTML(qb422016, hyphaName, textAreaFill, warning) +//line templates/http_mutators.qtpl:14 + qs422016 := string(qb422016.B) +//line templates/http_mutators.qtpl:14 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_mutators.qtpl:14 + return qs422016 +//line templates/http_mutators.qtpl:14 +} diff --git a/templates/http_readers.qtpl b/templates/http_readers.qtpl new file mode 100644 index 00000000..0c3b4ea5 --- /dev/null +++ b/templates/http_readers.qtpl @@ -0,0 +1,81 @@ +{% func HistoryHTML(hyphaName, tbody string) %} +
+ + + + + + + + + + + {%s= tbody %} + +
TimeHashMessage
+
+{% endfunc %} + +{% func RevisionHTML(hyphaName, naviTitle, contents, tree, revHash string) %} +
+ +
+

Please note that viewing binary parts of hyphae is not supported in history for now.

+ {%s= naviTitle %} + {%s= contents %} +
+
+ +
+{% endfunc %} + +If `contents` == "", a helpful message is shown instead. +{% func PageHTML(hyphaName, naviTitle, contents, tree string) %} +
+ +
+ {%s= naviTitle %} + {% if contents == "" %} +

This hypha has no text. Why not create it?

+ {% else %} + {%s= contents %} + {% endif %} +
+
+
+ +
+ + +
+
+ +
+{% endfunc %} diff --git a/templates/http_readers.qtpl.go b/templates/http_readers.qtpl.go new file mode 100644 index 00000000..97d4a0be --- /dev/null +++ b/templates/http_readers.qtpl.go @@ -0,0 +1,286 @@ +// Code generated by qtc from "http_readers.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line templates/http_readers.qtpl:1 +package templates + +//line templates/http_readers.qtpl:1 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line templates/http_readers.qtpl:1 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line templates/http_readers.qtpl:1 +func StreamHistoryHTML(qw422016 *qt422016.Writer, hyphaName, tbody string) { +//line templates/http_readers.qtpl:1 + qw422016.N().S(` +
+ + + + + + + + + + + `) +//line templates/http_readers.qtpl:20 + qw422016.N().S(tbody) +//line templates/http_readers.qtpl:20 + qw422016.N().S(` + +
TimeHashMessage
+
+`) +//line templates/http_readers.qtpl:24 +} + +//line templates/http_readers.qtpl:24 +func WriteHistoryHTML(qq422016 qtio422016.Writer, hyphaName, tbody string) { +//line templates/http_readers.qtpl:24 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_readers.qtpl:24 + StreamHistoryHTML(qw422016, hyphaName, tbody) +//line templates/http_readers.qtpl:24 + qt422016.ReleaseWriter(qw422016) +//line templates/http_readers.qtpl:24 +} + +//line templates/http_readers.qtpl:24 +func HistoryHTML(hyphaName, tbody string) string { +//line templates/http_readers.qtpl:24 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_readers.qtpl:24 + WriteHistoryHTML(qb422016, hyphaName, tbody) +//line templates/http_readers.qtpl:24 + qs422016 := string(qb422016.B) +//line templates/http_readers.qtpl:24 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_readers.qtpl:24 + return qs422016 +//line templates/http_readers.qtpl:24 +} + +//line templates/http_readers.qtpl:26 +func StreamRevisionHTML(qw422016 *qt422016.Writer, hyphaName, naviTitle, contents, tree, revHash string) { +//line templates/http_readers.qtpl:26 + qw422016.N().S(` +
+ +
+

Please note that viewing binary parts of hyphae is not supported in history for now.

+ `) +//line templates/http_readers.qtpl:39 + qw422016.N().S(naviTitle) +//line templates/http_readers.qtpl:39 + qw422016.N().S(` + `) +//line templates/http_readers.qtpl:40 + qw422016.N().S(contents) +//line templates/http_readers.qtpl:40 + qw422016.N().S(` +
+
+ +
+`) +//line templates/http_readers.qtpl:47 +} + +//line templates/http_readers.qtpl:47 +func WriteRevisionHTML(qq422016 qtio422016.Writer, hyphaName, naviTitle, contents, tree, revHash string) { +//line templates/http_readers.qtpl:47 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_readers.qtpl:47 + StreamRevisionHTML(qw422016, hyphaName, naviTitle, contents, tree, revHash) +//line templates/http_readers.qtpl:47 + qt422016.ReleaseWriter(qw422016) +//line templates/http_readers.qtpl:47 +} + +//line templates/http_readers.qtpl:47 +func RevisionHTML(hyphaName, naviTitle, contents, tree, revHash string) string { +//line templates/http_readers.qtpl:47 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_readers.qtpl:47 + WriteRevisionHTML(qb422016, hyphaName, naviTitle, contents, tree, revHash) +//line templates/http_readers.qtpl:47 + qs422016 := string(qb422016.B) +//line templates/http_readers.qtpl:47 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_readers.qtpl:47 + return qs422016 +//line templates/http_readers.qtpl:47 +} + +// If `contents` == "", a helpful message is shown instead. + +//line templates/http_readers.qtpl:50 +func StreamPageHTML(qw422016 *qt422016.Writer, hyphaName, naviTitle, contents, tree string) { +//line templates/http_readers.qtpl:50 + qw422016.N().S(` +
+ +
+ `) +//line templates/http_readers.qtpl:61 + qw422016.N().S(naviTitle) +//line templates/http_readers.qtpl:61 + qw422016.N().S(` + `) +//line templates/http_readers.qtpl:62 + if contents == "" { +//line templates/http_readers.qtpl:62 + qw422016.N().S(` +

This hypha has no text. Why not create it?

+ `) +//line templates/http_readers.qtpl:64 + } else { +//line templates/http_readers.qtpl:64 + qw422016.N().S(` + `) +//line templates/http_readers.qtpl:65 + qw422016.N().S(contents) +//line templates/http_readers.qtpl:65 + qw422016.N().S(` + `) +//line templates/http_readers.qtpl:66 + } +//line templates/http_readers.qtpl:66 + qw422016.N().S(` +
+
+
+ +
+ + +
+
+ +
+`) +//line templates/http_readers.qtpl:81 +} + +//line templates/http_readers.qtpl:81 +func WritePageHTML(qq422016 qtio422016.Writer, hyphaName, naviTitle, contents, tree string) { +//line templates/http_readers.qtpl:81 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_readers.qtpl:81 + StreamPageHTML(qw422016, hyphaName, naviTitle, contents, tree) +//line templates/http_readers.qtpl:81 + qt422016.ReleaseWriter(qw422016) +//line templates/http_readers.qtpl:81 +} + +//line templates/http_readers.qtpl:81 +func PageHTML(hyphaName, naviTitle, contents, tree string) string { +//line templates/http_readers.qtpl:81 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_readers.qtpl:81 + WritePageHTML(qb422016, hyphaName, naviTitle, contents, tree) +//line templates/http_readers.qtpl:81 + qs422016 := string(qb422016.B) +//line templates/http_readers.qtpl:81 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_readers.qtpl:81 + return qs422016 +//line templates/http_readers.qtpl:81 +} diff --git a/templates/http_stuff.qtpl b/templates/http_stuff.qtpl new file mode 100644 index 00000000..9bacf51e --- /dev/null +++ b/templates/http_stuff.qtpl @@ -0,0 +1,42 @@ +{% func BaseHTML(title, body string) %} + + + + + + {%s title %} + + + {%s= body %} + + +{% endfunc %} + +{% func HyphaListHTML(tbody string, pageCount int) %} +
+

List of hyphae

+

This wiki has {%d pageCount %} hyphae.

+ + + + + + + + + {%s= tbody %} + +
Full nameBinary part type
+
+{% endfunc %} + +{% func HyphaListRowHTML(hyphaName, binaryMime string, binaryPresent bool) %} + + {%s hyphaName %} + {% if binaryPresent %} + {%s binaryMime %} + {% else %} + + {% endif %} + +{% endfunc %} diff --git a/templates/http_stuff.qtpl.go b/templates/http_stuff.qtpl.go new file mode 100644 index 00000000..8fa00963 --- /dev/null +++ b/templates/http_stuff.qtpl.go @@ -0,0 +1,194 @@ +// Code generated by qtc from "http_stuff.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line templates/http_stuff.qtpl:1 +package templates + +//line templates/http_stuff.qtpl:1 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line templates/http_stuff.qtpl:1 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line templates/http_stuff.qtpl:1 +func StreamBaseHTML(qw422016 *qt422016.Writer, title, body string) { +//line templates/http_stuff.qtpl:1 + qw422016.N().S(` + + + + + + `) +//line templates/http_stuff.qtpl:7 + qw422016.E().S(title) +//line templates/http_stuff.qtpl:7 + qw422016.N().S(` + + + `) +//line templates/http_stuff.qtpl:10 + qw422016.N().S(body) +//line templates/http_stuff.qtpl:10 + qw422016.N().S(` + + +`) +//line templates/http_stuff.qtpl:13 +} + +//line templates/http_stuff.qtpl:13 +func WriteBaseHTML(qq422016 qtio422016.Writer, title, body string) { +//line templates/http_stuff.qtpl:13 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_stuff.qtpl:13 + StreamBaseHTML(qw422016, title, body) +//line templates/http_stuff.qtpl:13 + qt422016.ReleaseWriter(qw422016) +//line templates/http_stuff.qtpl:13 +} + +//line templates/http_stuff.qtpl:13 +func BaseHTML(title, body string) string { +//line templates/http_stuff.qtpl:13 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_stuff.qtpl:13 + WriteBaseHTML(qb422016, title, body) +//line templates/http_stuff.qtpl:13 + qs422016 := string(qb422016.B) +//line templates/http_stuff.qtpl:13 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_stuff.qtpl:13 + return qs422016 +//line templates/http_stuff.qtpl:13 +} + +//line templates/http_stuff.qtpl:15 +func StreamHyphaListHTML(qw422016 *qt422016.Writer, tbody string, pageCount int) { +//line templates/http_stuff.qtpl:15 + qw422016.N().S(` +
+

List of hyphae

+

This wiki has `) +//line templates/http_stuff.qtpl:18 + qw422016.N().D(pageCount) +//line templates/http_stuff.qtpl:18 + qw422016.N().S(` hyphae.

+ + + + + + + + + `) +//line templates/http_stuff.qtpl:27 + qw422016.N().S(tbody) +//line templates/http_stuff.qtpl:27 + qw422016.N().S(` + +
Full nameBinary part type
+
+`) +//line templates/http_stuff.qtpl:31 +} + +//line templates/http_stuff.qtpl:31 +func WriteHyphaListHTML(qq422016 qtio422016.Writer, tbody string, pageCount int) { +//line templates/http_stuff.qtpl:31 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_stuff.qtpl:31 + StreamHyphaListHTML(qw422016, tbody, pageCount) +//line templates/http_stuff.qtpl:31 + qt422016.ReleaseWriter(qw422016) +//line templates/http_stuff.qtpl:31 +} + +//line templates/http_stuff.qtpl:31 +func HyphaListHTML(tbody string, pageCount int) string { +//line templates/http_stuff.qtpl:31 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_stuff.qtpl:31 + WriteHyphaListHTML(qb422016, tbody, pageCount) +//line templates/http_stuff.qtpl:31 + qs422016 := string(qb422016.B) +//line templates/http_stuff.qtpl:31 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_stuff.qtpl:31 + return qs422016 +//line templates/http_stuff.qtpl:31 +} + +//line templates/http_stuff.qtpl:33 +func StreamHyphaListRowHTML(qw422016 *qt422016.Writer, hyphaName, binaryMime string, binaryPresent bool) { +//line templates/http_stuff.qtpl:33 + qw422016.N().S(` + + `) +//line templates/http_stuff.qtpl:35 + qw422016.E().S(hyphaName) +//line templates/http_stuff.qtpl:35 + qw422016.N().S(` + `) +//line templates/http_stuff.qtpl:36 + if binaryPresent { +//line templates/http_stuff.qtpl:36 + qw422016.N().S(` + `) +//line templates/http_stuff.qtpl:37 + qw422016.E().S(binaryMime) +//line templates/http_stuff.qtpl:37 + qw422016.N().S(` + `) +//line templates/http_stuff.qtpl:38 + } else { +//line templates/http_stuff.qtpl:38 + qw422016.N().S(` + + `) +//line templates/http_stuff.qtpl:40 + } +//line templates/http_stuff.qtpl:40 + qw422016.N().S(` + +`) +//line templates/http_stuff.qtpl:42 +} + +//line templates/http_stuff.qtpl:42 +func WriteHyphaListRowHTML(qq422016 qtio422016.Writer, hyphaName, binaryMime string, binaryPresent bool) { +//line templates/http_stuff.qtpl:42 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_stuff.qtpl:42 + StreamHyphaListRowHTML(qw422016, hyphaName, binaryMime, binaryPresent) +//line templates/http_stuff.qtpl:42 + qt422016.ReleaseWriter(qw422016) +//line templates/http_stuff.qtpl:42 +} + +//line templates/http_stuff.qtpl:42 +func HyphaListRowHTML(hyphaName, binaryMime string, binaryPresent bool) string { +//line templates/http_stuff.qtpl:42 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_stuff.qtpl:42 + WriteHyphaListRowHTML(qb422016, hyphaName, binaryMime, binaryPresent) +//line templates/http_stuff.qtpl:42 + qs422016 := string(qb422016.B) +//line templates/http_stuff.qtpl:42 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_stuff.qtpl:42 + return qs422016 +//line templates/http_stuff.qtpl:42 +} diff --git a/tree/tree.go b/tree/tree.go new file mode 100644 index 00000000..1655664f --- /dev/null +++ b/tree/tree.go @@ -0,0 +1,92 @@ +package tree + +import ( + "fmt" + "path" + "sort" + "strings" +) + +// If Name == "", the tree is empty. +type tree struct { + name string + siblings []string + descendants []*tree + root bool + hyphaIterator func(func(string)) +} + +// TreeAsHtml generates a tree for `hyphaName`. `hyphaStorage` has this type because package `tree` has no access to `HyphaData` data type. One day it shall have it, I guess. +func TreeAsHtml(hyphaName string, hyphaIterator func(func(string))) string { + t := &tree{name: hyphaName, root: true, hyphaIterator: hyphaIterator} + t.fill() + return t.asHtml() +} + +// subtree adds a descendant tree to `t` and returns that tree. +func (t *tree) fork(descendantName string) *tree { + subt := &tree{ + name: descendantName, + root: false, + hyphaIterator: t.hyphaIterator, + } + t.descendants = append(t.descendants, subt) + return subt +} + +// Compares names and does something with them, may generate a subtree. +func (t *tree) compareNamesAndAppend(name2 string) { + switch { + case t.name == name2: + case t.root && path.Dir(t.name) == path.Dir(name2): + t.siblings = append(t.siblings, name2) + case t.name == path.Dir(name2): + t.fork(name2).fill() + } +} + +// Fills t.siblings and t.descendants, sorts them and does the same to the descendants. +func (t *tree) fill() { + t.hyphaIterator(func(hyphaName string) { + t.compareNamesAndAppend(hyphaName) + }) + sort.Strings(t.siblings) + sort.Slice(t.descendants, func(i, j int) bool { + return t.descendants[i].name < t.descendants[j].name + }) +} + +// asHtml returns HTML representation of a tree. +// It applies itself recursively on the tree's children. +func (t *tree) asHtml() (html string) { + if t.root { + html += navitreeEntry(t.name, "navitree__pagename") + } else { + html += navitreeEntry(t.name, "navitree__name") + } + + for _, subtree := range t.descendants { + html += subtree.asHtml() + } + + if t.root { + for _, siblingName := range t.siblings { + html += navitreeEntry(siblingName, "navitree__sibling") + } + } + + return `` +} + +// Strip hypha name from all ancestor names, replace _ with spaces, title case +func beautifulName(uglyName string) string { + return strings.Title(strings.ReplaceAll(path.Base(uglyName), "_", " ")) +} + +// navitreeEntry is a small utility function that makes generating html easier. +func navitreeEntry(name, class string) string { + return fmt.Sprintf(`
  • + %s +
  • +`, class, name, beautifulName(name)) +} diff --git a/util/util.go b/util/util.go new file mode 100644 index 00000000..ee77e079 --- /dev/null +++ b/util/util.go @@ -0,0 +1,27 @@ +package util + +import ( + "net/http" + "strings" +) + +var WikiDir string + +// ShorterPath is used by handlerList to display shorter path to the files. It simply strips WikiDir. +func ShorterPath(path string) string { + if strings.HasPrefix(path, WikiDir) { + tmp := strings.TrimPrefix(path, WikiDir) + if tmp == "" { + return "" + } + return tmp[1:] + } + return path +} + +// HTTP200Page wraps some frequently used things for successful 200 responses. +func HTTP200Page(w http.ResponseWriter, page string) { + w.Header().Set("Content-Type", "text/html;charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte(page)) +}