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
+ %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
-
-
-`, 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
-
-
-
- Upload new binary part
-
-
-
-
-
-`, 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
-
-
-
- Name
- Text path
- Text type
- Binary path
- Binary type
-
-
- `
- for name, data := range HyphaStorage {
- buf += fmt.Sprintf(`
-
- %s
- %s
- %d
- %s
- %d
- `,
- 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 += `
-
-
-`
- 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 %}
+
+ {%s textAreaFill %}
+
+
+ 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(`
+
+ `)
+//line templates/http_mutators.qtpl:8
+ qw422016.E().S(textAreaFill)
+//line templates/http_mutators.qtpl:8
+ 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) %}
+
+
+
+
+
+
+
+ Time
+ Hash
+ Message
+
+
+
+ {%s= tbody %}
+
+
+
+{% 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 %}
+
+
+
+ Upload new binary part
+
+
+
+
+
+
+
+{% 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(`
+
+
+
+
+
+
+
+ Time
+ Hash
+ Message
+
+
+
+ `)
+//line templates/http_readers.qtpl:20
+ qw422016.N().S(tbody)
+//line templates/http_readers.qtpl:20
+ qw422016.N().S(`
+
+
+
+`)
+//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(`
+
+
+
+ Hypha
+ Edit
+ Raw text
+ History
+ `)
+//line templates/http_readers.qtpl:34
+ qw422016.E().S(revHash)
+//line templates/http_readers.qtpl:34
+ 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:44
+ qw422016.N().S(tree)
+//line templates/http_readers.qtpl:44
+ 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(`
+
+
+
+ Upload new binary part
+
+
+
+
+
+
+ `)
+//line templates/http_readers.qtpl:78
+ qw422016.N().S(tree)
+//line templates/http_readers.qtpl:78
+ 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.
+
+
+
+ Full name
+ Binary part type
+
+
+
+ {%s= tbody %}
+
+
+
+{% 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.
+
+
+
+ Full name
+ Binary part type
+
+
+
+ `)
+//line templates/http_stuff.qtpl:27
+ qw422016.N().S(tbody)
+//line templates/http_stuff.qtpl:27
+ qw422016.N().S(`
+
+
+
+`)
+//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))
+}