diff --git a/.gitignore b/.gitignore
index db5b71e..c450f48 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-hypha
mycorrhiza
diff --git a/README.md b/README.md
index 82876c6..db4db81 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,10 @@
-# 🍄 MycorrhizaWiki 0.11
+# 🍄 MycorrhizaWiki 0.12
A wiki engine.
+[Main wiki](https://mycorrhiza.lesarbr.es)
+
## Building
+Also see [detailed instructions](https://mycorrhiza.lesarbr.es/page/deploy) on wiki.
```sh
git clone --recurse-submodules https://github.com/bouncepaw/mycorrhiza
cd mycorrhiza
@@ -22,36 +25,40 @@ Options:
What auth method to use. Variants: "none", "fixed" (default "none")
-fixed-credentials-path string
Used when -auth-method=fixed. Path to file with user credentials. (default "mycocredentials.json")
+ -header-links-hypha string
+ Optional hypha that overrides the header links
-home string
The home page (default "home")
+ -icon string
+ What to show in the navititle in the beginning, before the colon (default "🍄")
+ -name string
+ What is the name of your wiki (default "wiki")
-port string
Port to serve the wiki at (default "1737")
- -title string
- How to call your wiki in the navititle (default "🍄")
- -user-tree string
+ -url string
+ URL at which your wiki can be found. Used to generate feeds (default "http://0.0.0.0:$port")
+ -user-hypha string
Hypha which is a superhypha of all user pages (default "u")
```
## Features
-* Edit pages through html forms
-* Responsive design
+* Edit pages through html forms, graphical preview
+* Responsive design, dark theme (synced with system theme)
* Works in text browsers
* Wiki pages (called hyphae) are written in mycomarkup
-* Everything is stored as simple files, no database required. You can run a wiki on almost any directory and get something to work with.
-* Page trees
+* Everything is stored as simple files, no database required. You can run a wiki on almost any directory and get something to work with
+* Page trees; links to previous and next pages
* Changes are saved to git
* List of hyphae page
* History page
* Random page
-* Recent changes page
+* Recent changes page; RSS, Atom and JSON feeds available
* Hyphae can be deleted (while still preserving history)
* Hyphae can be renamed (recursive renaming of subhyphae is also supported)
-* Light on resources: I run a home wiki on this engine 24/7 at an [Orange π Lite](http://www.orangepi.org/orangepilite/).
+* Light on resources
* Authorization with pre-set credentials
## 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.
+Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where some development is coordinated. You can also sponsor on [boosty](https://boosty.to/bouncepaw). Feel free to open an issue or contact directly.
-## Future plans
-* Tagging system
-* Better history viewing
+You can view list of all planned features on [our kanban board](https://github.com/bouncepaw/mycorrhiza/projects/1).
diff --git a/flag.go b/flag.go
index 8a9857a..47a2134 100644
--- a/flag.go
+++ b/flag.go
@@ -10,12 +10,15 @@ import (
)
func init() {
- flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at")
- flag.StringVar(&util.HomePage, "home", "home", "The home page")
- flag.StringVar(&util.SiteTitle, "title", "🍄", "How to call your wiki in the navititle")
- flag.StringVar(&util.UserTree, "user-tree", "u", "Hypha which is a superhypha of all user pages")
+ flag.StringVar(&util.URL, "url", "http://0.0.0.0:$port", "URL at which your wiki can be found. Used to generate feeds and social media previews")
+ flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at using HTTP")
+ flag.StringVar(&util.HomePage, "home", "home", "The home page name")
+ flag.StringVar(&util.SiteNavIcon, "icon", "🍄", "What to show in the navititle in the beginning, before the colon")
+ flag.StringVar(&util.SiteName, "name", "wiki", "What is the name of your wiki")
+ flag.StringVar(&util.UserHypha, "user-hypha", "u", "Hypha which is a superhypha of all user pages")
flag.StringVar(&util.AuthMethod, "auth-method", "none", "What auth method to use. Variants: \"none\", \"fixed\"")
flag.StringVar(&util.FixedCredentialsPath, "fixed-credentials-path", "mycocredentials.json", "Used when -auth-method=fixed. Path to file with user credentials.")
+ flag.StringVar(&util.HeaderLinksHypha, "header-links-hypha", "", "Optional hypha that overrides the header links")
}
// Do the things related to cli args and die maybe
@@ -34,19 +37,19 @@ func parseCliArgs() {
log.Fatal(err)
}
- if !isCanonicalName(util.HomePage) {
- log.Fatal("Error: you must use a proper name for the homepage")
+ if util.URL == "http://0.0.0.0:$port" {
+ util.URL = "http://0.0.0.0:" + util.ServerPort
}
- if !isCanonicalName(util.UserTree) {
- log.Fatal("Error: you must use a proper name for user tree")
- }
+ util.HomePage = CanonicalName(util.HomePage)
+ util.UserHypha = CanonicalName(util.UserHypha)
+ util.HeaderLinksHypha = CanonicalName(util.HeaderLinksHypha)
switch util.AuthMethod {
case "none":
case "fixed":
user.AuthUsed = true
- user.PopulateFixedUserStorage()
+ user.ReadUsersFromFilesystem()
default:
log.Fatal("Error: unknown auth method:", util.AuthMethod)
}
diff --git a/go.mod b/go.mod
index 995478b..137cbae 100644
--- a/go.mod
+++ b/go.mod
@@ -4,5 +4,7 @@ go 1.14
require (
github.com/adrg/xdg v0.2.2
+ github.com/gorilla/feeds v1.1.1
+ github.com/kr/pretty v0.2.1 // indirect
github.com/valyala/quicktemplate v1.6.3
)
diff --git a/go.sum b/go.sum
index dbbc51b..f950cb8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,21 @@
github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo=
github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
+github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
@@ -19,5 +29,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
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=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/history/history.go b/history/history.go
index 247b4c5..f3d117d 100644
--- a/history/history.go
+++ b/history/history.go
@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os/exec"
+ "regexp"
"strconv"
"strings"
"time"
@@ -13,6 +14,8 @@ import (
"github.com/bouncepaw/mycorrhiza/util"
)
+var renameMsgPattern = regexp.MustCompile(`^Rename ‘(.*)’ to ‘.*’`)
+
// Start initializes git credentials.
func Start(wikiDir string) {
_, err := gitsh("config", "user.name", "wikimind")
@@ -27,10 +30,46 @@ func Start(wikiDir string) {
// 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
+ Hash string
+ Username string
+ Time time.Time
+ Message string
+ hyphaeAffectedBuf []string
+}
+
+// determine what hyphae were affected by this revision
+func (rev *Revision) hyphaeAffected() (hyphae []string) {
+ if nil != rev.hyphaeAffectedBuf {
+ return rev.hyphaeAffectedBuf
+ }
+ hyphae = make([]string, 0)
+ var (
+ // List of files affected by this revision, one per line.
+ out, err = gitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
+ // set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
+ set = make(map[string]bool)
+ isNewName = func(hyphaName string) bool {
+ if _, present := set[hyphaName]; present {
+ return false
+ }
+ set[hyphaName] = true
+ return true
+ }
+ )
+ if err != nil {
+ return hyphae
+ }
+ for _, filename := range strings.Split(out.String(), "\n") {
+ if strings.IndexRune(filename, '.') >= 0 {
+ dotPos := strings.LastIndexByte(filename, '.')
+ hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
+ if isNewName(hyphaName) {
+ hyphae = append(hyphae, hyphaName)
+ }
+ }
+ }
+ rev.hyphaeAffectedBuf = hyphae
+ return hyphae
}
// TimeString returns a human readable time representation.
@@ -40,42 +79,38 @@ func (rev Revision) TimeString() string {
// HyphaeLinks returns a comma-separated list of hyphae that were affected by this revision as HTML string.
func (rev Revision) HyphaeLinks() (html string) {
- // diff-tree --no-commit-id --name-only -r
- var (
- // List of files affected by this revision, one per line.
- out, err = gitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
- // set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most).
- set = make(map[string]bool)
- isNewName = func(hyphaName string) bool {
- if _, present := set[hyphaName]; present {
- return false
- } else {
- set[hyphaName] = true
- return true
- }
- }
- )
- if err != nil {
- return ""
- }
- for _, filename := range strings.Split(out.String(), "\n") {
- // If filename has an ampersand:
- if strings.IndexRune(filename, '.') >= 0 {
- // Remove ampersanded suffix from filename:
- ampersandPos := strings.LastIndexByte(filename, '.')
- hyphaName := string([]byte(filename)[0:ampersandPos]) // is it safe?
- if isNewName(hyphaName) {
- // Entries are separated by commas
- if len(set) > 1 {
- html += `, `
- }
- html += fmt.Sprintf(`%[1]s `, hyphaName)
- }
+ hyphae := rev.hyphaeAffected()
+ for i, hyphaName := range hyphae {
+ if i > 0 {
+ html += `, `
}
+ html += fmt.Sprintf(`%[1]s `, hyphaName)
}
return html
}
+func (rev *Revision) descriptionForFeed() (html string) {
+ return fmt.Sprintf(
+ `
+
+ %[3]s
+
+ `,
+ currentYear, currentMonth,
+ strconv.Itoa(currentYear)+" "+rev.Time.Month().String())
+ }
+ html += rev.asHistoryEntry(hyphaName)
+ }
+ return html
+}
+
+func (rev *Revision) asHistoryEntry(hyphaName string) (html string) {
+ author := ""
+ if rev.Username != "anon" {
+ author = fmt.Sprintf(`
+ by %[2]s `, util.UserHypha, rev.Username)
+ }
+ return fmt.Sprintf(`
+
+
+ %[2]s
+ %[3]s
+ %[4]s
+ %[5]s
+
+`, hyphaName, rev.timeToDisplay(), rev.Hash, rev.Message, author)
+}
+
+// Return time like mm-dd 13:42
+func (rev *Revision) timeToDisplay() string {
+ D := rev.Time.Day()
+ h, m, _ := rev.Time.Clock()
+ return fmt.Sprintf("%02d — %02d:%02d", D, h, m)
+}
+
// 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(.*)\"")
@@ -66,16 +175,6 @@ func parseRevisionLine(line string) Revision {
}
}
-// Represent revision as a table row.
-func (rev *Revision) AsHtmlTableRow(hyphaName string) string {
- return fmt.Sprintf(`
-
- %s
- %s
- %s
- `, rev.TimeString(), 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)
diff --git a/history/operations.go b/history/operations.go
index e16c367..a8177ba 100644
--- a/history/operations.go
+++ b/history/operations.go
@@ -24,6 +24,7 @@ const (
TypeEditBinary
TypeDeleteHypha
TypeRenameHypha
+ TypeUnattachHypha
)
// HistoryOp is an object representing a history operation.
@@ -108,6 +109,12 @@ func (hop *HistoryOp) Apply() *HistoryOp {
return hop
}
+// Abort aborts the history operation.
+func (hop *HistoryOp) Abort() *HistoryOp {
+ gitMutex.Unlock()
+ 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 {
@@ -121,7 +128,7 @@ func (hop *HistoryOp) WithMsg(userMsg string) *HistoryOp {
// WithUser sets a user for the commit.
func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp {
- if u.Group != user.UserAnon {
+ if u.Group != "anon" {
hop.name = u.Name
hop.email = u.Name + "@mycorrhiza"
}
diff --git a/http_auth.go b/http_auth.go
index 2e1039b..cfe0799 100644
--- a/http_auth.go
+++ b/http_auth.go
@@ -28,7 +28,7 @@ func handlerLogout(w http.ResponseWriter, rq *http.Request) {
log.Println("Unknown user tries to log out")
w.WriteHeader(http.StatusForbidden)
}
- w.Write([]byte(base("Logout?", templates.LogoutHTML(can))))
+ w.Write([]byte(base("Logout?", templates.LogoutHTML(can), u)))
}
func handlerLogoutConfirm(w http.ResponseWriter, rq *http.Request) {
@@ -44,7 +44,7 @@ func handlerLoginData(w http.ResponseWriter, rq *http.Request) {
err = user.LoginDataHTTP(w, rq, username, password)
)
if err != "" {
- w.Write([]byte(base(err, templates.LoginErrorHTML(err))))
+ w.Write([]byte(base(err, templates.LoginErrorHTML(err), user.EmptyUser())))
} else {
http.Redirect(w, rq, "/", http.StatusSeeOther)
}
@@ -58,5 +58,5 @@ func handlerLogin(w http.ResponseWriter, rq *http.Request) {
} else {
w.WriteHeader(http.StatusForbidden)
}
- w.Write([]byte(base("Login", templates.LoginHTML())))
+ w.Write([]byte(base("Login", templates.LoginHTML(), user.EmptyUser())))
}
diff --git a/http_history.go b/http_history.go
new file mode 100644
index 0000000..3adda8a
--- /dev/null
+++ b/http_history.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/templates"
+ "github.com/bouncepaw/mycorrhiza/user"
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+func init() {
+ http.HandleFunc("/history/", handlerHistory)
+ http.HandleFunc("/recent-changes/", handlerRecentChanges)
+ http.HandleFunc("/recent-changes-rss", handlerRecentChangesRSS)
+ http.HandleFunc("/recent-changes-atom", handlerRecentChangesAtom)
+ http.HandleFunc("/recent-changes-json", handlerRecentChangesJSON)
+}
+
+// handlerHistory lists all revisions of a hypha
+func handlerHistory(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ hyphaName := HyphaNameFromRq(rq, "history")
+ var list string
+
+ // History can be found for files that do not exist anymore.
+ revs, err := history.Revisions(hyphaName)
+ if err == nil {
+ list = history.HistoryWithRevisions(hyphaName, revs)
+ }
+ log.Println("Found", len(revs), "revisions for", hyphaName)
+
+ util.HTTP200Page(w,
+ base(hyphaName, templates.HistoryHTML(rq, hyphaName, list), user.FromRequest(rq)))
+}
+
+// Recent changes
+func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ var (
+ noPrefix = strings.TrimPrefix(rq.URL.String(), "/recent-changes/")
+ n, err = strconv.Atoi(noPrefix)
+ )
+ if err == nil && n < 101 {
+ util.HTTP200Page(w, base(strconv.Itoa(n)+" recent changes", history.RecentChanges(n), user.FromRequest(rq)))
+ } else {
+ http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther)
+ }
+}
+
+func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func() (string, error), name string) {
+ log.Println(rq.URL)
+ if content, err := f(); err != nil {
+ w.Header().Set("Content-Type", "text/plain;charset=utf-8")
+ w.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprint(w, "An error while generating "+name+": "+err.Error())
+ } else {
+ w.Header().Set("Content-Type", "application/rss+xml")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, content)
+ }
+}
+
+func handlerRecentChangesRSS(w http.ResponseWriter, rq *http.Request) {
+ genericHandlerOfFeeds(w, rq, history.RecentChangesRSS, "RSS")
+}
+
+func handlerRecentChangesAtom(w http.ResponseWriter, rq *http.Request) {
+ genericHandlerOfFeeds(w, rq, history.RecentChangesAtom, "Atom")
+}
+
+func handlerRecentChangesJSON(w http.ResponseWriter, rq *http.Request) {
+ genericHandlerOfFeeds(w, rq, history.RecentChangesJSON, "JSON feed")
+}
diff --git a/http_mutators.go b/http_mutators.go
index 232fed3..ca48d07 100644
--- a/http_mutators.go
+++ b/http_mutators.go
@@ -5,6 +5,7 @@ import (
"log"
"net/http"
+ "github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
@@ -15,11 +16,65 @@ func init() {
http.HandleFunc("/edit/", handlerEdit)
http.HandleFunc("/delete-ask/", handlerDeleteAsk)
http.HandleFunc("/rename-ask/", handlerRenameAsk)
+ http.HandleFunc("/unattach-ask/", handlerUnattachAsk)
// And those that do mutate something:
http.HandleFunc("/upload-binary/", handlerUploadBinary)
http.HandleFunc("/upload-text/", handlerUploadText)
http.HandleFunc("/delete-confirm/", handlerDeleteConfirm)
http.HandleFunc("/rename-confirm/", handlerRenameConfirm)
+ http.HandleFunc("/unattach-confirm/", handlerUnattachConfirm)
+}
+
+func handlerUnattachAsk(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ var (
+ hyphaName = HyphaNameFromRq(rq, "unattach-ask")
+ hd, isOld = HyphaStorage[hyphaName]
+ hasAmnt = hd != nil && hd.binaryPath != ""
+ )
+ if !hasAmnt {
+ HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach")
+ log.Println("Rejected (no amnt):", rq.URL)
+ return
+ } else if ok := user.CanProceed(rq, "unattach-confirm"); !ok {
+ HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments")
+ log.Println("Rejected (no rights):", rq.URL)
+ return
+ }
+ util.HTTP200Page(w, base("Unattach "+hyphaName+"?", templates.UnattachAskHTML(rq, hyphaName, isOld), user.FromRequest(rq)))
+}
+
+func handlerUnattachConfirm(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ var (
+ hyphaName = HyphaNameFromRq(rq, "unattach-confirm")
+ hyphaData, isOld = HyphaStorage[hyphaName]
+ hasAmnt = hyphaData != nil && hyphaData.binaryPath != ""
+ u = user.FromRequest(rq)
+ )
+ if !u.CanProceed("unattach-confirm") {
+ HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments")
+ log.Println("Rejected (no rights):", rq.URL)
+ return
+ }
+ if !hasAmnt {
+ HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach")
+ log.Println("Rejected (no amnt):", rq.URL)
+ return
+ } else if !isOld {
+ // The precondition is to have the hypha in the first place.
+ HttpErr(w, http.StatusPreconditionFailed, hyphaName,
+ "Error: no such hypha",
+ "Could not unattach this hypha because it does not exist")
+ return
+ }
+ if hop := hyphaData.UnattachHypha(hyphaName, u); len(hop.Errs) != 0 {
+ HttpErr(w, http.StatusInternalServerError, hyphaName,
+ "Error: could not unattach hypha",
+ fmt.Sprintf("Could not unattach this hypha due to internal errors. Server errors: %v
", hop.Errs))
+ return
+ }
+ http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
}
func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) {
@@ -27,13 +82,14 @@ func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) {
var (
hyphaName = HyphaNameFromRq(rq, "rename-ask")
_, isOld = HyphaStorage[hyphaName]
+ u = user.FromRequest(rq)
)
- if ok := user.CanProceed(rq, "rename-confirm"); !ok {
+ if !u.CanProceed("rename-confirm") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.")
log.Println("Rejected", rq.URL)
return
}
- util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(rq, hyphaName, isOld)))
+ util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(rq, hyphaName, isOld), u))
}
func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
@@ -44,7 +100,7 @@ func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
newName = CanonicalName(rq.PostFormValue("new-name"))
_, newNameIsUsed = HyphaStorage[newName]
recursive = rq.PostFormValue("recursive") == "true"
- u = user.FromRequest(rq).OrAnon()
+ u = user.FromRequest(rq)
)
switch {
case !u.CanProceed("rename-confirm"):
@@ -79,13 +135,14 @@ func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) {
var (
hyphaName = HyphaNameFromRq(rq, "delete-ask")
_, isOld = HyphaStorage[hyphaName]
+ u = user.FromRequest(rq)
)
- if ok := user.CanProceed(rq, "delete-ask"); !ok {
+ if !u.CanProceed("delete-ask") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
log.Println("Rejected", rq.URL)
return
}
- util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(rq, hyphaName, isOld)))
+ util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(rq, hyphaName, isOld), u))
}
// handlerDeleteConfirm deletes a hypha for sure
@@ -126,8 +183,9 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
warning string
textAreaFill string
err error
+ u = user.FromRequest(rq)
)
- if ok := user.CanProceed(rq, "edit"); !ok {
+ if !u.CanProceed("edit") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
log.Println("Rejected", rq.URL)
return
@@ -142,7 +200,7 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
} else {
warning = `You are creating a new hypha.
`
}
- util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(rq, hyphaName, textAreaFill, warning)))
+ util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(rq, hyphaName, textAreaFill, warning), u))
}
// handlerUploadText uploads a new text part for the hypha.
@@ -151,9 +209,10 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
var (
hyphaName = HyphaNameFromRq(rq, "upload-text")
textData = rq.PostFormValue("text")
- u = user.FromRequest(rq).OrAnon()
+ action = rq.PostFormValue("action")
+ u = user.FromRequest(rq)
)
- if ok := user.CanProceed(rq, "upload-text"); !ok {
+ if !u.CanProceed("upload-text") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
log.Println("Rejected", rq.URL)
return
@@ -162,7 +221,9 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed")
return
}
- if hop := UploadText(hyphaName, textData, u); len(hop.Errs) != 0 {
+ if action == "Preview" {
+ util.HTTP200Page(w, base("Preview "+hyphaName, templates.PreviewHTML(rq, hyphaName, textData, "", markup.Doc(hyphaName, textData).AsHTML()), u))
+ } else if hop := UploadText(hyphaName, textData, u); len(hop.Errs) != 0 {
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error())
} else {
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
diff --git a/http_readers.go b/http_readers.go
index b8887e9..970200d 100644
--- a/http_readers.go
+++ b/http_readers.go
@@ -13,6 +13,7 @@ import (
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/tree"
+ "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
@@ -20,7 +21,6 @@ func init() {
http.HandleFunc("/page/", handlerPage)
http.HandleFunc("/text/", handlerText)
http.HandleFunc("/binary/", handlerBinary)
- http.HandleFunc("/history/", handlerHistory)
http.HandleFunc("/rev/", handlerRevision)
}
@@ -35,40 +35,23 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) {
contents = fmt.Sprintf(`This hypha had no text at this revision.
`)
textPath = hyphaName + ".myco"
textContents, err = history.FileAtRevision(textPath, revHash)
+ u = user.FromRequest(rq)
)
if err == nil {
- contents = markup.ToHtml(hyphaName, textContents)
+ contents = markup.Doc(hyphaName, textContents).AsHTML()
}
+ treeHTML, _, _ := tree.Tree(hyphaName, IterateHyphaNamesWith)
page := templates.RevisionHTML(
rq,
hyphaName,
naviTitle(hyphaName),
contents,
- tree.TreeAsHtml(hyphaName, IterateHyphaNamesWith),
+ treeHTML,
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
-
- // History can be found for files that do not exist anymore.
- revs, err := history.Revisions(hyphaName)
- if err == nil {
- for _, rev := range revs {
- tbody += rev.AsHtmlTableRow(hyphaName)
- }
- }
- log.Println("Found", len(revs), "revisions for", hyphaName)
-
- util.HTTP200Page(w,
- base(hyphaName, templates.HistoryHTML(rq, hyphaName, tbody)))
+ w.Write([]byte(base(hyphaName, page, u)))
}
// handlerText serves raw source text of the hypha.
@@ -99,20 +82,32 @@ func handlerPage(w http.ResponseWriter, rq *http.Request) {
var (
hyphaName = HyphaNameFromRq(rq, "page")
data, hyphaExists = HyphaStorage[hyphaName]
+ hasAmnt = hyphaExists && data.binaryPath != ""
contents string
+ openGraph string
+ u = user.FromRequest(rq)
)
if hyphaExists {
fileContentsT, errT := ioutil.ReadFile(data.textPath)
_, errB := os.Stat(data.binaryPath)
if errT == nil {
- contents = markup.ToHtml(hyphaName, string(fileContentsT))
+ md := markup.Doc(hyphaName, string(fileContentsT))
+ contents = md.AsHTML()
+ openGraph = md.OpenGraphHTML()
}
if !os.IsNotExist(errB) {
contents = binaryHtmlBlock(hyphaName, data) + contents
}
}
- util.HTTP200Page(w, base(hyphaName, templates.PageHTML(rq, hyphaName,
- naviTitle(hyphaName),
- contents,
- tree.TreeAsHtml(hyphaName, IterateHyphaNamesWith))))
+ treeHTML, prevHypha, nextHypha := tree.Tree(hyphaName, IterateHyphaNamesWith)
+ util.HTTP200Page(w,
+ templates.BaseHTML(
+ hyphaName,
+ templates.PageHTML(rq, hyphaName,
+ naviTitle(hyphaName),
+ contents,
+ treeHTML, prevHypha, nextHypha,
+ hasAmnt),
+ u,
+ openGraph))
}
diff --git a/hypha.go b/hypha.go
index e412c14..be7fef4 100644
--- a/hypha.go
+++ b/hypha.go
@@ -8,9 +8,10 @@ import (
"mime/multipart"
"os"
"path/filepath"
- "strings"
+ "regexp"
"github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
@@ -33,6 +34,12 @@ func init() {
return
}
markup.HyphaIterate = IterateHyphaNamesWith
+ markup.HyphaImageForOG = func(hyphaName string) string {
+ if hd, isOld := GetHyphaData(hyphaName); isOld && hd.binaryPath != "" {
+ return util.URL + "/binary/" + hyphaName
+ }
+ return util.URL + "/favicon.ico"
+ }
}
// GetHyphaData finds a hypha addressed by `hyphaName` and returns its `hyphaData`. `hyphaData` is set to a zero value if this hypha does not exist. `isOld` is false if this hypha does not exist.
@@ -78,8 +85,12 @@ func uploadHelp(hop *history.HistoryOp, hyphaName, ext string, data []byte, u *u
// New hyphae must be added to the hypha storage
if !isOld {
HyphaStorage[hyphaName] = hyphaData
+ hyphae.IncrementCount()
}
*originalFullPath = fullPath
+ if isOld && hop.Type == history.TypeEditText && !history.FileChanged(fullPath) {
+ return hop.Abort()
+ }
return hop.WithFiles(fullPath).
WithUser(u).
Apply()
@@ -115,6 +126,30 @@ func (hd *HyphaData) DeleteHypha(hyphaName string, u *user.User) *history.Histor
Apply()
if len(hop.Errs) == 0 {
delete(HyphaStorage, hyphaName)
+ hyphae.DecrementCount()
+ }
+ return hop
+}
+
+// UnattachHypha unattaches hypha and makes a history record about that.
+func (hd *HyphaData) UnattachHypha(hyphaName string, u *user.User) *history.HistoryOp {
+ hop := history.Operation(history.TypeUnattachHypha).
+ WithFilesRemoved(hd.binaryPath).
+ WithMsg(fmt.Sprintf("Unattach ‘%s’", hyphaName)).
+ WithUser(u).
+ Apply()
+ if len(hop.Errs) == 0 {
+ hd, ok := HyphaStorage[hyphaName]
+ if ok {
+ if hd.binaryPath != "" {
+ hd.binaryPath = ""
+ }
+ // If nothing is left of the hypha
+ if hd.textPath == "" {
+ delete(HyphaStorage, hyphaName)
+ hyphae.DecrementCount()
+ }
+ }
}
return hop
}
@@ -160,8 +195,9 @@ func relocateHyphaData(hyphaNames []string, replaceName func(string) string) {
// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way.
func RenameHypha(hyphaName, newName string, recursive bool, u *user.User) *history.HistoryOp {
var (
+ re = regexp.MustCompile(`(?i)` + hyphaName)
replaceName = func(str string) string {
- return strings.Replace(str, hyphaName, newName, 1)
+ return re.ReplaceAllString(CanonicalName(str), newName)
}
hyphaNames = findHyphaeToRename(hyphaName, recursive)
renameMap, err = renamingPairs(hyphaNames, replaceName)
@@ -212,7 +248,7 @@ func binaryHtmlBlock(hyphaName string, hd *HyphaData) string {
default:
return fmt.Sprintf(`
-
This hypha's media cannot be rendered. Access it directly
+
This hypha's media cannot be rendered. Download it
`, hyphaName)
}
@@ -244,6 +280,7 @@ func Index(path string) {
} else {
hyphaData = &HyphaData{}
HyphaStorage[hyphaName] = hyphaData
+ hyphae.IncrementCount()
}
if isText {
hyphaData.textPath = hyphaPartPath
@@ -276,3 +313,17 @@ func FetchTextPart(d *HyphaData) (string, error) {
}
return string(text), nil
}
+
+func setHeaderLinks() {
+ if userLinksHypha, ok := GetHyphaData(util.HeaderLinksHypha); !ok {
+ util.SetDefaultHeaderLinks()
+ } else {
+ contents, err := ioutil.ReadFile(userLinksHypha.textPath)
+ if err != nil || len(contents) == 0 {
+ util.SetDefaultHeaderLinks()
+ } else {
+ text := string(contents)
+ util.ParseHeaderLinks(text, markup.Rocketlink)
+ }
+ }
+}
diff --git a/hyphae/count.go b/hyphae/count.go
new file mode 100644
index 0000000..d090606
--- /dev/null
+++ b/hyphae/count.go
@@ -0,0 +1,38 @@
+package hyphae
+
+import (
+ "sync"
+)
+
+// Its value is number of all existing hyphae. Hypha mutators are expected to manipulate the value. It is concurrent-safe.
+var count = struct {
+ value int
+ sync.Mutex
+}{}
+
+// Set the value of hyphae count to zero.
+func ResetCount() {
+ count.Lock()
+ count.value = 0
+ count.Unlock()
+}
+
+// Increment the value of hyphae count.
+func IncrementCount() {
+ count.Lock()
+ count.value++
+ count.Unlock()
+}
+
+// Decrement the value of hyphae count.
+func DecrementCount() {
+ count.Lock()
+ count.value--
+ count.Unlock()
+}
+
+// Count how many hyphae there are.
+func Count() int {
+ // it is concurrent-safe to not lock here, right?
+ return count.value
+}
diff --git a/hyphae/hypha.go b/hyphae/hypha.go
new file mode 100644
index 0000000..d70cd1e
--- /dev/null
+++ b/hyphae/hypha.go
@@ -0,0 +1,21 @@
+package hyphae
+
+// TODO: do
+import ()
+
+type Hypha struct {
+ Name string
+ Exists bool
+ TextPath string
+ BinaryPath string
+ OutLinks []string
+ BackLinks []string
+}
+
+// AddHypha adds a hypha named `name` with such `textPath` and `binaryPath`. Both paths can be empty. Does //not// check for hypha's existence beforehand. Count is handled.
+func AddHypha(name, textPath, binaryPath string) {
+}
+
+// DeleteHypha clears both paths and all out-links from the named hypha and marks it as non-existent. It does not actually delete it from the memdb. Count is handled.
+func DeleteHypha(name string) {
+}
diff --git a/main.go b/main.go
index 51d7610..c87c96e 100644
--- a/main.go
+++ b/main.go
@@ -4,16 +4,17 @@ package main
import (
"fmt"
+ "io/ioutil"
"log"
"math/rand"
"net/http"
"os"
"path/filepath"
"regexp"
- "strconv"
"strings"
"github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
@@ -40,9 +41,18 @@ func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) {
log.Println(errMsg, "for", name)
w.Header().Set("Content-Type", "text/html;charset=utf-8")
w.WriteHeader(status)
- fmt.Fprint(w, base(title, fmt.Sprintf(
- `%s. Go back to the hypha.
`,
- errMsg, name)))
+ fmt.Fprint(
+ w,
+ base(
+ title,
+ fmt.Sprintf(
+ `%s. Go back to the hypha.
`,
+ errMsg,
+ name,
+ ),
+ user.EmptyUser(),
+ ),
+ )
}
// Show all hyphae
@@ -50,12 +60,13 @@ func handlerList(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
tbody string
- pageCount = len(HyphaStorage)
+ pageCount = hyphae.Count()
+ u = user.FromRequest(rq)
)
for hyphaName, data := range HyphaStorage {
tbody += templates.HyphaListRowHTML(hyphaName, ExtensionToMime(filepath.Ext(data.binaryPath)), data.binaryPath != "")
}
- util.HTTP200Page(w, base("List of pages", templates.HyphaListHTML(tbody, pageCount)))
+ util.HTTP200Page(w, base("List of pages", templates.HyphaListHTML(tbody, pageCount), u))
}
// This part is present in all html documents.
@@ -69,18 +80,32 @@ func handlerReindex(w http.ResponseWriter, rq *http.Request) {
log.Println("Rejected", rq.URL)
return
}
+ hyphae.ResetCount()
HyphaStorage = make(map[string]*HyphaData)
log.Println("Wiki storage directory is", WikiDir)
log.Println("Start indexing hyphae...")
Index(WikiDir)
- log.Println("Indexed", len(HyphaStorage), "hyphae")
+ log.Println("Indexed", hyphae.Count(), "hyphae")
+ http.Redirect(w, rq, "/", http.StatusSeeOther)
+}
+
+// Update header links by reading the configured hypha, if there is any, or resorting to default values.
+func handlerUpdateHeaderLinks(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ if ok := user.CanProceed(rq, "update-header-links"); !ok {
+ HttpErr(w, http.StatusForbidden, util.HomePage, "Not enough rights", "You must be a moderator to update header links.")
+ log.Println("Rejected", rq.URL)
+ return
+ }
+ setHeaderLinks()
+ http.Redirect(w, rq, "/", http.StatusSeeOther)
}
// 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))
+ i := rand.Intn(hyphae.Count())
for hyphaName := range HyphaStorage {
if i == 0 {
randomHyphaName = hyphaName
@@ -91,20 +116,6 @@ func handlerRandom(w http.ResponseWriter, rq *http.Request) {
http.Redirect(w, rq, "/page/"+randomHyphaName, http.StatusSeeOther)
}
-// Recent changes
-func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- noPrefix = strings.TrimPrefix(rq.URL.String(), "/recent-changes/")
- n, err = strconv.Atoi(noPrefix)
- )
- if err == nil && n < 101 {
- util.HTTP200Page(w, base(strconv.Itoa(n)+" recent changes", history.RecentChanges(n)))
- } else {
- http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther)
- }
-}
-
func handlerStyle(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
if _, err := os.Stat(WikiDir + "/static/common.css"); err == nil {
@@ -115,6 +126,50 @@ func handlerStyle(w http.ResponseWriter, rq *http.Request) {
}
}
+func handlerIcon(w http.ResponseWriter, rq *http.Request) {
+ iconName := strings.TrimPrefix(rq.URL.Path, "/static/icon/")
+ if iconName == "https" {
+ iconName = "http"
+ }
+ files, err := ioutil.ReadDir(WikiDir + "/static/icon")
+ if err == nil {
+ for _, f := range files {
+ if strings.HasPrefix(f.Name(), iconName+"-protocol-icon") {
+ http.ServeFile(w, rq, WikiDir+"/static/icon/"+f.Name())
+ return
+ }
+ }
+ }
+ w.Header().Set("Content-Type", "image/svg+xml")
+ switch iconName {
+ case "gemini":
+ w.Write([]byte(templates.IconGemini()))
+ case "mailto":
+ w.Write([]byte(templates.IconMailto()))
+ case "gopher":
+ w.Write([]byte(templates.IconGopher()))
+ default:
+ w.Write([]byte(templates.IconHTTP()))
+ }
+}
+
+func handlerAbout(w http.ResponseWriter, rq *http.Request) {
+ w.Header().Set("Content-Type", "text/html;charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(base("About "+util.SiteName, templates.AboutHTML(), user.FromRequest(rq))))
+}
+
+func handlerRobotsTxt(w http.ResponseWriter, rq *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(
+ `User-agent: *
+Allow: /page/
+Allow: /recent-changes
+Disallow: /
+Crawl-delay: 5`))
+}
+
func main() {
log.Println("Running MycorrhizaWiki β")
parseCliArgs()
@@ -122,26 +177,30 @@ func main() {
log.Fatal(err)
}
log.Println("Wiki storage directory is", WikiDir)
- log.Println("Start indexing hyphae...")
Index(WikiDir)
- log.Println("Indexed", len(HyphaStorage), "hyphae")
+ log.Println("Indexed", hyphae.Count(), "hyphae")
history.Start(WikiDir)
+ setHeaderLinks()
- http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static"))))
- // See http_readers.go for /page/, /text/, /binary/, /history/.
- // See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/, /rename-ask/, /rename-confirm/.
+ // See http_readers.go for /page/, /text/, /binary/
+ // See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/, /rename-ask/, /rename-confirm/, /unattach-ask/, /unattach-confirm/
// See http_auth.go for /login, /login-data, /logout, /logout-confirm
+ // See http_history.go for /history/, /recent-changes
http.HandleFunc("/list", handlerList)
http.HandleFunc("/reindex", handlerReindex)
+ http.HandleFunc("/update-header-links", handlerUpdateHeaderLinks)
http.HandleFunc("/random", handlerRandom)
- http.HandleFunc("/recent-changes/", handlerRecentChanges)
+ http.HandleFunc("/about", handlerAbout)
+ http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static"))))
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
http.ServeFile(w, rq, WikiDir+"/static/favicon.ico")
})
http.HandleFunc("/static/common.css", handlerStyle)
+ http.HandleFunc("/static/icon/", handlerIcon)
http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
http.Redirect(w, rq, "/page/"+util.HomePage, http.StatusSeeOther)
})
+ http.HandleFunc("/robots.txt", handlerRobotsTxt)
log.Fatal(http.ListenAndServe("0.0.0.0:"+util.ServerPort, nil))
}
diff --git a/markup/img.go b/markup/img.go
index 6445ed1..99c919b 100644
--- a/markup/img.go
+++ b/markup/img.go
@@ -4,6 +4,8 @@ import (
"fmt"
"regexp"
"strings"
+
+ "github.com/bouncepaw/mycorrhiza/util"
)
var imgRe = regexp.MustCompile(`^img\s+{`)
@@ -184,6 +186,14 @@ func (img *Img) binaryPathFor(path string) string {
}
}
+func (img *Img) ogBinaryPathFor(path string) string {
+ path = img.binaryPathFor(path)
+ if strings.HasPrefix(path, "/binary/") {
+ return util.URL + path
+ }
+ return path
+}
+
func (img *Img) pagePathFor(path string) string {
path = strings.TrimSpace(path)
if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 {
@@ -218,7 +228,7 @@ func (img *Img) checkLinks() map[string]bool {
}
HyphaIterate(func(hyphaName string) {
for _, entry := range img.entries {
- if hyphaName == entry.trimmedPath {
+ if hyphaName == xclCanonicalName(img.hyphaName, entry.trimmedPath) {
m[entry.trimmedPath] = true
}
}
diff --git a/markup/lexer.go b/markup/lexer.go
index 08476d6..4516eb0 100644
--- a/markup/lexer.go
+++ b/markup/lexer.go
@@ -9,6 +9,9 @@ import (
// HyphaExists holds function that checks that a hypha is present.
var HyphaExists func(string) bool
+//
+var HyphaImageForOG func(string) string
+
// HyphaAccess holds function that accesses a hypha by its name.
var HyphaAccess func(string) (rawText, binaryHtml string, err error)
@@ -24,29 +27,37 @@ type GemLexerState struct {
id int
buf string
// Temporaries
- img *Img
+ img *Img
+ table *Table
}
type Line struct {
id int
- // interface{} may be bad. What I need is a sum of string and Transclusion
+ // interface{} may be bad. TODO: a proper type
contents interface{}
}
-func lex(name, content string) (ast []Line) {
- var state = GemLexerState{name: name}
+func (md *MycoDoc) lex() (ast []Line) {
+ var state = GemLexerState{name: md.hyphaName}
- for _, line := range append(strings.Split(content, "\n"), "") {
- geminiLineToAST(line, &state, &ast)
+ for _, line := range append(strings.Split(md.contents, "\n"), "") {
+ lineToAST(line, &state, &ast)
}
return ast
}
// Lex `line` in markup and save it to `ast` using `state`.
-func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
+func lineToAST(line string, state *GemLexerState, ast *[]Line) {
addLine := func(text interface{}) {
*ast = append(*ast, Line{id: state.id, contents: text})
}
+ addParagraphIfNeeded := func() {
+ if state.where == "p" {
+ state.where = ""
+ addLine(fmt.Sprintf("%s
", state.id, strings.ReplaceAll(ParagraphToHtml(state.name, state.buf), "\n", " ")))
+ state.buf = ""
+ }
+ }
// Process empty lines depending on the current state
if "" == strings.TrimSpace(line) {
@@ -59,6 +70,11 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
addLine(state.buf + "")
case "pre":
state.buf += "\n"
+ case "launchpad":
+ state.where = ""
+ addLine(state.buf + " ")
+ case "p":
+ addParagraphIfNeeded()
}
return
}
@@ -74,13 +90,17 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
switch state.where {
case "img":
goto imgState
+ case "table":
+ goto tableState
case "pre":
goto preformattedState
case "list":
goto listState
case "number":
goto numberState
- default:
+ case "launchpad":
+ goto launchpadState
+ default: // "p" or ""
goto normalState
}
@@ -91,6 +111,13 @@ imgState:
}
return
+tableState:
+ if shouldGoBackToNormal := state.table.Process(line); shouldGoBackToNormal {
+ state.where = ""
+ addLine(*state.table)
+ }
+ return
+
preformattedState:
switch {
case startsWith("```"):
@@ -135,48 +162,84 @@ numberState:
}
return
+launchpadState:
+ switch {
+ case startsWith("=>"):
+ href, text, class := Rocketlink(line, state.name)
+ state.buf += fmt.Sprintf(` %s `, class, href, text)
+ case startsWith("```"):
+ state.where = "pre"
+ addLine(state.buf + "")
+ state.id++
+ state.buf = fmt.Sprintf("", state.id, strings.TrimPrefix(line, "```"))
+ default:
+ state.where = ""
+ addLine(state.buf + "")
+ goto normalState
+ }
+ return
+
normalState:
state.id++
switch {
case startsWith("```"):
+ addParagraphIfNeeded()
state.where = "pre"
state.buf = fmt.Sprintf("", state.id, strings.TrimPrefix(line, "```"))
case startsWith("* "):
+ addParagraphIfNeeded()
state.where = "list"
state.buf = fmt.Sprintf("\n", state.id)
goto listState
case startsWith("*. "):
+ addParagraphIfNeeded()
state.where = "number"
state.buf = fmt.Sprintf("\n", state.id)
goto numberState
case startsWith("###### "):
+ addParagraphIfNeeded()
addHeading(6)
case startsWith("##### "):
+ addParagraphIfNeeded()
addHeading(5)
case startsWith("#### "):
+ addParagraphIfNeeded()
addHeading(4)
case startsWith("### "):
+ addParagraphIfNeeded()
addHeading(3)
case startsWith("## "):
+ addParagraphIfNeeded()
addHeading(2)
case startsWith("# "):
+ addParagraphIfNeeded()
addHeading(1)
case startsWith(">"):
- addLine(fmt.Sprintf(
- "%s ", state.id, remover(">")(line)))
+ addParagraphIfNeeded()
+ addLine(
+ fmt.Sprintf(
+ "%s ",
+ state.id,
+ ParagraphToHtml(state.name, remover(">")(line)),
+ ),
+ )
case startsWith("=>"):
- href, text, class := Rocketlink(line, state.name)
- addLine(fmt.Sprintf(
- `%s
`, state.id, class, href, text))
+ addParagraphIfNeeded()
+ state.where = "launchpad"
+ state.buf = fmt.Sprintf("\n", state.id)
+ goto launchpadState
case startsWith("<="):
+ addParagraphIfNeeded()
addLine(parseTransclusion(line, state.name))
case line == "----":
+ addParagraphIfNeeded()
*ast = append(*ast, Line{id: -1, contents: " "})
case MatchesImg(line):
+ addParagraphIfNeeded()
img, shouldGoBackToNormal := ImgFromFirstLine(line, state.name)
if shouldGoBackToNormal {
addLine(*img)
@@ -184,7 +247,15 @@ normalState:
state.where = "img"
state.img = img
}
+ case MatchesTable(line):
+ addParagraphIfNeeded()
+ state.where = "table"
+ state.table = TableFromFirstLine(line, state.name)
+
+ case state.where == "p":
+ state.buf += "\n" + line
default:
- addLine(fmt.Sprintf("%s
", state.id, ParagraphToHtml(state.name, line)))
+ state.where = "p"
+ state.buf = line
}
}
diff --git a/markup/link.go b/markup/link.go
index eb52c63..af3e51c 100644
--- a/markup/link.go
+++ b/markup/link.go
@@ -1,6 +1,7 @@
package markup
import (
+ "fmt"
"path"
"strings"
)
@@ -15,11 +16,19 @@ func LinkParts(addr, display, hyphaName string) (href, text, class string) {
} else {
text = strings.TrimSpace(display)
}
- class = "wikilink_internal"
+ class = "wikilink wikilink_internal"
switch {
case strings.ContainsRune(addr, ':'):
- return addr, text, "wikilink_external"
+ pos := strings.IndexRune(addr, ':')
+ destination := addr[:pos]
+ if display == "" {
+ text = addr[pos+1:]
+ if strings.HasPrefix(text, "//") && len(text) > 2 {
+ text = text[2:]
+ }
+ }
+ return addr, text, fmt.Sprintf("wikilink wikilink_external wikilink_%s", destination)
case strings.HasPrefix(addr, "/"):
return addr, text, class
case strings.HasPrefix(addr, "./"):
diff --git a/markup/mycomarkup.go b/markup/mycomarkup.go
index fa47e2b..656ca28 100644
--- a/markup/mycomarkup.go
+++ b/markup/mycomarkup.go
@@ -2,8 +2,12 @@
package markup
import (
+ "fmt"
"html"
+ "regexp"
"strings"
+
+ "github.com/bouncepaw/mycorrhiza/util"
)
// A Mycomarkup-formatted document
@@ -11,26 +15,79 @@ type MycoDoc struct {
// data
hyphaName string
contents string
-
- // state
- recursionDepth int
-
+ // indicators
+ parsedAlready bool
// results
+ ast []Line
+ html string
+ firstImageURL string
+ description string
}
// Constructor
func Doc(hyphaName, contents string) *MycoDoc {
- return &MycoDoc{
+ md := &MycoDoc{
hyphaName: hyphaName,
contents: contents,
}
+ return md
+}
+
+func (md *MycoDoc) Lex(recursionLevel int) *MycoDoc {
+ if !md.parsedAlready {
+ md.ast = md.lex()
+ }
+ md.parsedAlready = true
+ return md
}
// AsHtml returns an html representation of the document
-func (md *MycoDoc) AsHtml() string {
- return ""
+func (md *MycoDoc) AsHTML() string {
+ md.html = Parse(md.Lex(0).ast, 0, 0, 0)
+ return md.html
}
+// Used to clear opengraph description from html tags. This method is usually bad because of dangers of malformed HTML, but I'm going to use it only for Mycorrhiza-generated HTML, so it's okay. The question mark is required; without it the whole string is eaten away.
+var htmlTagRe = regexp.MustCompile(`<.*?>`)
+
+// OpenGraphHTML returns an html representation of og: meta tags.
+func (md *MycoDoc) OpenGraphHTML() string {
+ md.ogFillVars()
+ return strings.Join([]string{
+ ogTag("title", md.hyphaName),
+ ogTag("type", "article"),
+ ogTag("image", md.firstImageURL),
+ ogTag("url", util.URL+"/page/"+md.hyphaName),
+ ogTag("determiner", ""),
+ ogTag("description", htmlTagRe.ReplaceAllString(md.description, "")),
+ }, "\n")
+}
+
+func (md *MycoDoc) ogFillVars() *MycoDoc {
+ foundDesc := false
+ md.firstImageURL = HyphaImageForOG(md.hyphaName)
+ for _, line := range md.ast {
+ switch v := line.contents.(type) {
+ case string:
+ if !foundDesc {
+ md.description = v
+ foundDesc = true
+ }
+ case Img:
+ if len(v.entries) > 0 {
+ md.firstImageURL = v.entries[0].path.String()
+ }
+ }
+ }
+ return md
+}
+
+func ogTag(property, content string) string {
+ return fmt.Sprintf(` `, property, content)
+}
+
+/* The rest of this file is currently unused. TODO: use it I guess */
+
type BlockType int
const (
diff --git a/markup/paragraph.go b/markup/paragraph.go
index 8f90085..f506bb6 100644
--- a/markup/paragraph.go
+++ b/markup/paragraph.go
@@ -5,6 +5,7 @@ import (
"fmt"
"html"
"strings"
+ "unicode"
)
type spanTokenType int
@@ -34,8 +35,10 @@ func tagFromState(stt spanTokenType, tagState map[spanTokenType]bool, tagName, o
}
}
-func getLinkNode(input *bytes.Buffer, hyphaName string) string {
- input.Next(2)
+func getLinkNode(input *bytes.Buffer, hyphaName string, isBracketedLink bool) string {
+ if isBracketedLink {
+ input.Next(2) // drop those [[
+ }
var (
escaping = false
addrBuf = bytes.Buffer{}
@@ -47,11 +50,13 @@ func getLinkNode(input *bytes.Buffer, hyphaName string) string {
if escaping {
currBuf.WriteByte(b)
escaping = false
- } else if b == '|' && currBuf == &addrBuf {
+ } else if isBracketedLink && b == '|' && currBuf == &addrBuf {
currBuf = &displayBuf
- } else if b == ']' && bytes.HasPrefix(input.Bytes(), []byte{']'}) {
+ } else if isBracketedLink && b == ']' && bytes.HasPrefix(input.Bytes(), []byte{']'}) {
input.Next(1)
break
+ } else if !isBracketedLink && unicode.IsSpace(rune(b)) {
+ break
} else {
currBuf.WriteByte(b)
}
@@ -65,6 +70,12 @@ func getTextNode(input *bytes.Buffer) string {
var (
textNodeBuffer = bytes.Buffer{}
escaping = false
+ startsWith = func(t string) bool {
+ return bytes.HasPrefix(input.Bytes(), []byte(t))
+ }
+ couldBeLinkStart = func() bool {
+ return startsWith("https://") || startsWith("http://") || startsWith("gemini://") || startsWith("gopher://") || startsWith("ftp://")
+ }
)
// Always read the first byte in advance to avoid endless loops that kill computers (sad experience)
if input.Len() != 0 {
@@ -82,6 +93,9 @@ func getTextNode(input *bytes.Buffer) string {
} else if strings.IndexByte("/*`^,![~", b) >= 0 {
input.UnreadByte()
break
+ } else if couldBeLinkStart() {
+ textNodeBuffer.WriteByte(b)
+ break
} else {
textNodeBuffer.WriteByte(b)
}
@@ -106,6 +120,9 @@ func ParagraphToHtml(hyphaName, input string) string {
startsWith = func(t string) bool {
return bytes.HasPrefix(p.Bytes(), []byte(t))
}
+ noTagsActive = func() bool {
+ return !(tagState[spanItalic] || tagState[spanBold] || tagState[spanMono] || tagState[spanSuper] || tagState[spanSub] || tagState[spanMark] || tagState[spanLink])
+ }
)
for p.Len() != 0 {
@@ -132,7 +149,9 @@ func ParagraphToHtml(hyphaName, input string) string {
ret.WriteString(tagFromState(spanMark, tagState, "s", "~~"))
p.Next(2)
case startsWith("[["):
- ret.WriteString(getLinkNode(p, hyphaName))
+ ret.WriteString(getLinkNode(p, hyphaName, true))
+ case (startsWith("https://") || startsWith("http://") || startsWith("gemini://") || startsWith("gopher://") || startsWith("ftp://")) && noTagsActive():
+ ret.WriteString(getLinkNode(p, hyphaName, false))
default:
ret.WriteString(html.EscapeString(getTextNode(p)))
}
diff --git a/markup/parser.go b/markup/parser.go
index 23887d1..665f8ba 100644
--- a/markup/parser.go
+++ b/markup/parser.go
@@ -1,35 +1,26 @@
package markup
-import ()
-
const maxRecursionLevel = 3
-type GemParserState struct {
- recursionLevel int
-}
-
-func Parse(ast []Line, from, to int, state GemParserState) (html string) {
- if state.recursionLevel > maxRecursionLevel {
+func Parse(ast []Line, from, to int, recursionLevel int) (html string) {
+ if recursionLevel > maxRecursionLevel {
return "Transclusion depth limit"
}
for _, line := range ast {
if line.id >= from && (line.id <= to || to == 0) || line.id == -1 {
switch v := line.contents.(type) {
case Transclusion:
- html += Transclude(v, state)
+ html += Transclude(v, recursionLevel)
case Img:
html += v.ToHtml()
+ case Table:
+ html += v.asHtml()
case string:
html += v
default:
- html += "Unknown"
+ html += "Unknown element. "
}
}
}
return html
}
-
-func ToHtml(name, text string) string {
- state := GemParserState{}
- return Parse(lex(name, text), 0, 0, state)
-}
diff --git a/markup/table.go b/markup/table.go
new file mode 100644
index 0000000..a4dca62
--- /dev/null
+++ b/markup/table.go
@@ -0,0 +1,231 @@
+package markup
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+ "unicode"
+ // "github.com/bouncepaw/mycorrhiza/util"
+)
+
+var tableRe = regexp.MustCompile(`^table\s+{`)
+
+func MatchesTable(line string) bool {
+ return tableRe.MatchString(line)
+}
+
+func TableFromFirstLine(line, hyphaName string) *Table {
+ return &Table{
+ hyphaName: hyphaName,
+ caption: line[strings.IndexRune(line, '{')+1:],
+ rows: make([]*tableRow, 0),
+ }
+}
+
+func (t *Table) Process(line string) (shouldGoBackToNormal bool) {
+ if strings.TrimSpace(line) == "}" && !t.inMultiline {
+ return true
+ }
+ if !t.inMultiline {
+ t.pushRow()
+ }
+ var (
+ inLink bool
+ skipNext bool
+ escaping bool
+ lookingForNonSpace = !t.inMultiline
+ countingColspan bool
+ )
+ for i, r := range line {
+ switch {
+ case skipNext:
+ skipNext = false
+ continue
+
+ case lookingForNonSpace && unicode.IsSpace(r):
+ case lookingForNonSpace && (r == '!' || r == '|'):
+ t.currCellMarker = r
+ t.currColspan = 1
+ lookingForNonSpace = false
+ countingColspan = true
+ case lookingForNonSpace:
+ t.currCellMarker = '^' // ^ represents implicit |, not part of syntax
+ t.currColspan = 1
+ lookingForNonSpace = false
+ t.currCellBuilder.WriteRune(r)
+
+ case escaping:
+ t.currCellBuilder.WriteRune(r)
+ case inLink && r == ']' && len(line)-1 > i && line[i+1] == ']':
+ t.currCellBuilder.WriteString("]]")
+ inLink = false
+ skipNext = true
+ case inLink:
+ t.currCellBuilder.WriteRune(r)
+
+ case t.inMultiline && r == '}':
+ t.inMultiline = false
+ case t.inMultiline && i == len(line)-1:
+ t.currCellBuilder.WriteRune('\n')
+ case t.inMultiline:
+ t.currCellBuilder.WriteRune(r)
+
+ // Not in multiline:
+ case (r == '|' || r == '!') && !countingColspan:
+ t.pushCell()
+ t.currCellMarker = r
+ t.currColspan = 1
+ countingColspan = true
+ case r == t.currCellMarker && (r == '|' || r == '!') && countingColspan:
+ t.currColspan++
+ case r == '{':
+ t.inMultiline = true
+ countingColspan = false
+ case r == '[' && len(line)-1 > i && line[i+1] == '[':
+ t.currCellBuilder.WriteString("[[")
+ inLink = true
+ skipNext = true
+ case i == len(line)-1:
+ t.pushCell()
+ default:
+ t.currCellBuilder.WriteRune(r)
+ countingColspan = false
+ }
+ }
+ return false
+}
+
+type Table struct {
+ // data
+ hyphaName string
+ caption string
+ rows []*tableRow
+ // state
+ inMultiline bool
+ // tmp
+ currCellMarker rune
+ currColspan uint
+ currCellBuilder strings.Builder
+}
+
+func (t *Table) pushRow() {
+ t.rows = append(t.rows, &tableRow{
+ cells: make([]*tableCell, 0),
+ })
+}
+
+func (t *Table) pushCell() {
+ tc := &tableCell{
+ content: t.currCellBuilder.String(),
+ colspan: t.currColspan,
+ }
+ switch t.currCellMarker {
+ case '|', '^':
+ tc.kind = tableCellDatum
+ case '!':
+ tc.kind = tableCellHeader
+ }
+ // We expect the table to have at least one row ready, so no nil-checking
+ tr := t.rows[len(t.rows)-1]
+ tr.cells = append(tr.cells, tc)
+ t.currCellBuilder = strings.Builder{}
+}
+
+func (t *Table) asHtml() (html string) {
+ if t.caption != "" {
+ html += fmt.Sprintf("%s ", t.caption)
+ }
+ if len(t.rows) > 0 && t.rows[0].looksLikeThead() {
+ html += fmt.Sprintf("%s ", t.rows[0].asHtml(t.hyphaName))
+ t.rows = t.rows[1:]
+ }
+ html += "\n\n"
+ for _, tr := range t.rows {
+ html += tr.asHtml(t.hyphaName)
+ }
+ return fmt.Sprintf(``, html)
+}
+
+type tableRow struct {
+ cells []*tableCell
+}
+
+func (tr *tableRow) asHtml(hyphaName string) (html string) {
+ for _, tc := range tr.cells {
+ html += tc.asHtml(hyphaName)
+ }
+ return fmt.Sprintf("%s \n", html)
+}
+
+// Most likely, rows with more than two header cells are theads. I allow one extra datum cell for tables like this:
+// | ! a ! b
+// ! c | d | e
+// ! f | g | h
+func (tr *tableRow) looksLikeThead() bool {
+ var (
+ headerAmount = 0
+ datumAmount = 0
+ )
+ for _, tc := range tr.cells {
+ switch tc.kind {
+ case tableCellHeader:
+ headerAmount++
+ case tableCellDatum:
+ datumAmount++
+ }
+ }
+ return headerAmount >= 2 && datumAmount <= 1
+}
+
+type tableCell struct {
+ kind tableCellKind
+ colspan uint
+ content string
+}
+
+func (tc *tableCell) asHtml(hyphaName string) string {
+ return fmt.Sprintf(
+ "<%[1]s %[2]s>%[3]s%[1]s>\n",
+ tc.kind.tagName(),
+ tc.colspanAttribute(),
+ tc.contentAsHtml(hyphaName),
+ )
+}
+
+func (tc *tableCell) colspanAttribute() string {
+ if tc.colspan <= 1 {
+ return ""
+ }
+ return fmt.Sprintf(`colspan="%d"`, tc.colspan)
+}
+
+func (tc *tableCell) contentAsHtml(hyphaName string) (html string) {
+ for _, line := range strings.Split(tc.content, "\n") {
+ if line = strings.TrimSpace(line); line != "" {
+ if html != "" {
+ html += ` `
+ }
+ html += ParagraphToHtml(hyphaName, line)
+ }
+ }
+ return html
+}
+
+type tableCellKind int
+
+const (
+ tableCellUnknown tableCellKind = iota
+ tableCellHeader
+ tableCellDatum
+)
+
+func (tck tableCellKind) tagName() string {
+ switch tck {
+ case tableCellHeader:
+ return "th"
+ case tableCellDatum:
+ return "td"
+ default:
+ return "p"
+ }
+}
diff --git a/markup/xclusion.go b/markup/xclusion.go
index b4a4370..a5df6c8 100644
--- a/markup/xclusion.go
+++ b/markup/xclusion.go
@@ -17,14 +17,14 @@ type Transclusion struct {
}
// Transclude transcludes `xcl` and returns html representation.
-func Transclude(xcl Transclusion, state GemParserState) (html string) {
- state.recursionLevel++
+func Transclude(xcl Transclusion, recursionLevel int) (html string) {
+ recursionLevel++
tmptOk := ``
tmptFailed := `
- Failed to transclude %s
+ Hypha %s does not exist
`
if xcl.from == xclError || xcl.to == xclError || xcl.from > xcl.to {
return fmt.Sprintf(tmptFailed, xcl.name, xcl.name)
@@ -34,7 +34,8 @@ func Transclude(xcl Transclusion, state GemParserState) (html string) {
if err != nil {
return fmt.Sprintf(tmptFailed, xcl.name, xcl.name)
}
- xclText := Parse(lex(xcl.name, rawText), xcl.from, xcl.to, state)
+ md := Doc(xcl.name, rawText)
+ xclText := Parse(md.lex(), xcl.from, xcl.to, recursionLevel)
return fmt.Sprintf(tmptOk, xcl.name, xcl.name, binaryHtml+xclText)
}
diff --git a/metarrhiza b/metarrhiza
index 7828352..be5b922 160000
--- a/metarrhiza
+++ b/metarrhiza
@@ -1 +1 @@
-Subproject commit 7828352598c19afe5f2e13df0219656ac7b44c9c
+Subproject commit be5b922e9b564551601d21ed45bf7d9ced65c6bb
diff --git a/name.go b/name.go
index 1ceb0ba..2a859d8 100644
--- a/name.go
+++ b/name.go
@@ -23,16 +23,24 @@ func CanonicalName(name string) string {
func naviTitle(canonicalName string) string {
var (
html = fmt.Sprintf(`
- %s `, util.HomePage, util.SiteTitle)
+ %s : `, util.HomePage, util.SiteNavIcon)
prevAcc = `/page/`
parts = strings.Split(canonicalName, "/")
+ rel = "up"
)
- for _, part := range parts {
- html += fmt.Sprintf(`
- /
- %s `,
+ for i, part := range parts {
+ if i > 0 {
+ html += `/ `
+ }
+ if i == len(parts)-1 {
+ rel = "bookmark"
+ }
+ html += fmt.Sprintf(
+ `%s `,
prevAcc+part,
- strings.Title(part))
+ rel,
+ util.BeautifulName(part),
+ )
prevAcc += part + "/"
}
return html + " "
diff --git a/templates/asset.qtpl b/templates/asset.qtpl
new file mode 100644
index 0000000..2e6b4f2
--- /dev/null
+++ b/templates/asset.qtpl
@@ -0,0 +1,21 @@
+{% func DefaultCSS() %}
+{% cat "default.css" %}
+{% endfunc %}
+
+Next three are from https://remixicon.com/
+{% func IconHTTP() %}
+{% cat "icon/http-protocol-icon.svg" %}
+{% endfunc %}
+
+{% func IconGemini() %}
+{% cat "icon/gemini-protocol-icon.svg" %}
+{% endfunc %}
+
+{% func IconMailto() %}
+{% cat "icon/mailto-protocol-icon.svg" %}
+{% endfunc %}
+
+This is a modified version of https://www.svgrepo.com/svg/232085/rat
+{% func IconGopher() %}
+{% cat "icon/gopher-protocol-icon.svg" %}
+{% endfunc %}
diff --git a/templates/asset.qtpl.go b/templates/asset.qtpl.go
new file mode 100644
index 0000000..95a984b
--- /dev/null
+++ b/templates/asset.qtpl.go
@@ -0,0 +1,414 @@
+// Code generated by qtc from "asset.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+//line templates/asset.qtpl:1
+package templates
+
+//line templates/asset.qtpl:1
+import (
+ qtio422016 "io"
+
+ qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line templates/asset.qtpl:1
+var (
+ _ = qtio422016.Copy
+ _ = qt422016.AcquireByteBuffer
+)
+
+//line templates/asset.qtpl:1
+func StreamDefaultCSS(qw422016 *qt422016.Writer) {
+//line templates/asset.qtpl:1
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:2
+ qw422016.N().S(`/* Layout stuff */
+@media screen and (min-width: 800px) {
+ main { padding:1rem 2rem; margin: 0 auto; width: 800px; }
+ .hypha-tabs { padding: 1rem 2rem; margin: 0 auto; width: 800px; }
+ header { margin: 0 auto; width: 800px; }
+ .header-links__entry { margin-right: 1.5rem; }
+ .header-links__entry_user { margin: 0 2rem 0 auto; }
+ .header-links__entry:nth-of-type(1),
+ .hypha-tabs__tab:nth-of-type(1) { margin-left: 2rem; }
+ .hypha-tabs__tab { margin-right: 1.5rem; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); border-bottom: 2px #ddd solid; padding: 0 .5rem; }
+}
+@media screen and (max-width: 800px) {
+ main { padding: 1rem; margin: 0; width: 100%; }
+ .hypha-tabs{ padding: 1rem; margin: 0; width: 100%; }
+ .hypha-tabs__tab { box-shadow: none; margin-right: .5rem; padding: .25rem .5rem; }
+ header { width: 100%; }
+ .header-links__entry { margin-right: .5rem; }
+}
+*, *::before, *::after {box-sizing: border-box;}
+html { height:100%; padding:0; }
+body {height:100%; margin:0; font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;}
+main {border-radius: 0 0 .25rem .25rem; }
+main > form {margin-bottom:1rem;}
+textarea {font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;}
+.edit_no-preview {height:100%;}
+.edit_with-preview .edit-form textarea { min-height: 500px; }
+.edit__preview { border: 2px dashed #ddd; }
+.edit-form {height:90%;}
+.edit-form textarea {width:100%;height:90%;}
+.edit-form__save { font-weight: bold; }
+.icon {margin-right: .25rem; vertical-align: bottom; }
+
+main h1:not(.navi-title) {font-size:1.7rem;}
+blockquote { margin-left: 0; padding-left: 1rem; }
+.wikilink_external::before { display: inline-block; width: 18px; height: 16px; vertical-align: sub; }
+/* .wikilink_external { padding-left: 16px; } */
+.wikilink_gopher::before { content: url("/static/icon/gopher"); }
+.wikilink_http::before { content: url("/static/icon/http"); }
+.wikilink_https::before { content: url("/static/icon/http"); }
+/* .wikilink_https { background: transparent url("/static/icon/http") center left no-repeat; } */
+.wikilink_gemini::before { content: url("/static/icon/gemini"); }
+.wikilink_mailto::before { content: url("/static/icon/mailto"); }
+
+article { overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; line-height: 150%; }
+article h1, article h2, article h3, article h4, article h5, article h6 { margin: 1.5rem 0 0 0; }
+article p { margin: .5rem 0; }
+article ul, ol { padding-left: 1.5rem; margin: .5rem 0; }
+article code { padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
+article pre.codeblock { padding:.5rem; white-space: pre-wrap; border-radius: .25rem;}
+.codeblock code {padding:0; font-size:15px;}
+.transclusion { border-radius: .25rem; }
+.transclusion__content > *:not(.binary-container) {margin: 0.5rem; }
+.transclusion__link {display: block; text-align: right; font-style: italic; margin-top: .5rem; margin-right: .25rem; text-decoration: none;}
+.transclusion__link::before {content: "⇐ ";}
+
+/* Derived from https://commons.wikimedia.org/wiki/File:U%2B21D2.svg */
+.launchpad__entry { list-style-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' width='25' height='12'%3E%3Cg transform='scale(0.7,0.8) translate(-613.21429,-421)'%3E%3Cpath fill='%23999' d='M 638.06773,429.49751 L 631.01022,436.87675 L 630.1898,436.02774 L 632.416,433.30375 L 613.46876,433.30375 L 613.46876,431.66382 L 633.82089,431.66382 L 635.57789,429.5261 L 633.79229,427.35979 L 613.46876,427.35979 L 613.46876,425.71985 L 632.416,425.71985 L 630.1898,422.99587 L 631.01022,422.08788 L 638.06773,429.49751 z '/%3E%3C/g%3E%3C/svg%3E"); }
+
+.binary-container_with-img img,
+.binary-container_with-video video,
+.binary-container_with-audio audio {width: 100%}
+
+.navi-title { padding-bottom: .5rem; margin: .25rem 0; }
+.navi-title a {text-decoration:none; }
+.navi-title__separator { margin: 0 .25rem; }
+.navi-title__colon { margin-right: .5rem; }
+.upload-amnt { clear: both; padding: .5rem; border-radius: .25rem; }
+.upload-amnt__unattach { display: block; }
+aside { clear: both; }
+
+.img-gallery { text-align: center; margin-top: .25rem; margin-bottom: .25rem; }
+.img-gallery_many-images { border-radius: .25rem; padding: .5rem; }
+.img-gallery img { max-width: 100%; max-height: 50vh; }
+figure { margin: 0; }
+figcaption { padding-bottom: .5rem; }
+
+#new-name {width:100%;}
+
+header { margin-bottom: .5rem; }
+.header-links__entry_user { font-style:italic; }
+.header-links__link { text-decoration: none; display: block; width: 100%; height: 100%; padding: .25rem; }
+.hypha-tabs { padding: 0; }
+.header-links__list, .hypha-tabs__flex { margin: 0; padding: 0; display: flex; flex-wrap: wrap; }
+.header-links__entry, .hypha-tabs__tab { list-style-type: none; }
+.hypha-tabs__tab a { text-decoration: none; }
+.hypha-tabs__tab_active { font-weight: bold; }
+
+.rc-entry { display: grid; list-style-type: none; padding: .25rem; grid-template-columns: 1fr 1fr; }
+.rc-entry__time { font-style: italic; }
+.rc-entry__hash { font-style: italic; text-align: right; }
+.rc-entry__links { grid-column: 1 / span 2; }
+.rc-entry__author { font-style: italic; }
+
+.prevnext__el { display: block-inline; min-width: 40%; padding: .5rem; margin-bottom: .25rem; text-decoration: none; border-radius: .25rem; }
+.prevnext__prev { float: left; }
+.prevnext__next { float: right; text-align: right; }
+
+.page-separator { clear: both; }
+.history__entries { background-color: #eee; margin: 0; padding: 0; border-radius: .25rem; }
+.history__month-anchor { text-decoration: none; color: inherit; }
+.history__entry { list-style-type: none; padding: .25rem; }
+.history-entry { padding: .25rem; }
+.history-entry__time { font-weight: bold; }
+.history-entry__author { font-style: italic; }
+
+table { border: #ddd 1px solid; border-radius: .25rem; min-width: 4rem; }
+td { padding: .25rem; }
+caption { caption-side: top; font-size: small; }
+
+/* Color stuff */
+/* Lighter stuff #eee */
+article code,
+article .codeblock,
+.transclusion,
+.img-gallery_many-images,
+.rc-entry,
+.prevnext__el,
+table { background-color: #eee; }
+
+@media screen and (max-width: 800px) {
+ .hypha-tabs { background-color: white; }
+ .hypha-tabs__tab { box-shadow: none; }
+}
+
+/* Other stuff */
+html { background-color: #ddd;
+background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23bbbbbb' fill-opacity='0.4'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
+} /* heropatterns.com */
+header { background-color: #bbb; }
+.header-links__link { color: black; }
+.header-links__link:hover { background-color: #eee; }
+
+main, .hypha-tabs__tab { background-color: white; }
+.hypha-tabs__tab { clip-path: inset(-20px -20px 0 -20px); }
+.hypha-tabs__tab a { color: black; }
+.hypha-tabs__tab_active { border-bottom: 2px white solid; }
+
+blockquote { border-left: 4px black solid; }
+.wikilink_new {color:#a55858;}
+.transclusion code, .transclusion .codeblock {background-color:#ddd;}
+.transclusion__link { color: black; }
+.wikilink_new:visited {color:#a55858;}
+.navi-title { border-bottom: #eee 1px solid; }
+.upload-amnt { border: #eee 1px solid; }
+td { border: #ddd 1px solid; }
+
+/* Dark theme! */
+@media (prefers-color-scheme: dark) {
+html { background: #222; color: #ddd; }
+main, article, .hypha-tabs__tab, header { background-color: #343434; color: #ddd; }
+
+a, .wikilink_external { color: #f1fa8c; }
+a:visited, .wikilink_external:visited { color: #ffb86c; }
+.wikilink_new, .wikilink_new:visited { color: #dd4444; }
+
+.header-links__link, .header-links__link:visited,
+.prevnext__el, .prevnext__el:visited { color: #ddd; }
+.header-links__link:hover { background-color: #444; }
+
+.hypha-tabs__tab a, .hypha-tabs__tab { color: #ddd; background-color: #232323; border: 0; }
+.hypha-tabs__tab_active { background-color: #343434; }
+
+blockquote { border-left: 4px #ddd solid; }
+
+.transclusion .transclusion__link { color: #ddd; }
+article code,
+article .codeblock,
+.transclusion,
+.img-gallery_many-images,
+.rc-entry,
+.history__entry,
+.prevnext__el,
+.upload-amnt,
+textarea,
+table { border: 0; background-color: #444444; color: #ddd; }
+.transclusion code,
+.transclusion .codeblock { background-color: #454545; }
+mark { background: rgba(130, 80, 30, 5); color: inherit; }
+@media screen and (max-width: 800px) {
+ .hypha-tabs { background-color: #232323; }
+}
+}
+
+`)
+//line templates/asset.qtpl:2
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:3
+}
+
+//line templates/asset.qtpl:3
+func WriteDefaultCSS(qq422016 qtio422016.Writer) {
+//line templates/asset.qtpl:3
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/asset.qtpl:3
+ StreamDefaultCSS(qw422016)
+//line templates/asset.qtpl:3
+ qt422016.ReleaseWriter(qw422016)
+//line templates/asset.qtpl:3
+}
+
+//line templates/asset.qtpl:3
+func DefaultCSS() string {
+//line templates/asset.qtpl:3
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/asset.qtpl:3
+ WriteDefaultCSS(qb422016)
+//line templates/asset.qtpl:3
+ qs422016 := string(qb422016.B)
+//line templates/asset.qtpl:3
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/asset.qtpl:3
+ return qs422016
+//line templates/asset.qtpl:3
+}
+
+// Next three are from https://remixicon.com/
+
+//line templates/asset.qtpl:6
+func StreamIconHTTP(qw422016 *qt422016.Writer) {
+//line templates/asset.qtpl:6
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:7
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:7
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:8
+}
+
+//line templates/asset.qtpl:8
+func WriteIconHTTP(qq422016 qtio422016.Writer) {
+//line templates/asset.qtpl:8
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/asset.qtpl:8
+ StreamIconHTTP(qw422016)
+//line templates/asset.qtpl:8
+ qt422016.ReleaseWriter(qw422016)
+//line templates/asset.qtpl:8
+}
+
+//line templates/asset.qtpl:8
+func IconHTTP() string {
+//line templates/asset.qtpl:8
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/asset.qtpl:8
+ WriteIconHTTP(qb422016)
+//line templates/asset.qtpl:8
+ qs422016 := string(qb422016.B)
+//line templates/asset.qtpl:8
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/asset.qtpl:8
+ return qs422016
+//line templates/asset.qtpl:8
+}
+
+//line templates/asset.qtpl:10
+func StreamIconGemini(qw422016 *qt422016.Writer) {
+//line templates/asset.qtpl:10
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:11
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:11
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:12
+}
+
+//line templates/asset.qtpl:12
+func WriteIconGemini(qq422016 qtio422016.Writer) {
+//line templates/asset.qtpl:12
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/asset.qtpl:12
+ StreamIconGemini(qw422016)
+//line templates/asset.qtpl:12
+ qt422016.ReleaseWriter(qw422016)
+//line templates/asset.qtpl:12
+}
+
+//line templates/asset.qtpl:12
+func IconGemini() string {
+//line templates/asset.qtpl:12
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/asset.qtpl:12
+ WriteIconGemini(qb422016)
+//line templates/asset.qtpl:12
+ qs422016 := string(qb422016.B)
+//line templates/asset.qtpl:12
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/asset.qtpl:12
+ return qs422016
+//line templates/asset.qtpl:12
+}
+
+//line templates/asset.qtpl:14
+func StreamIconMailto(qw422016 *qt422016.Writer) {
+//line templates/asset.qtpl:14
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:15
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:15
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:16
+}
+
+//line templates/asset.qtpl:16
+func WriteIconMailto(qq422016 qtio422016.Writer) {
+//line templates/asset.qtpl:16
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/asset.qtpl:16
+ StreamIconMailto(qw422016)
+//line templates/asset.qtpl:16
+ qt422016.ReleaseWriter(qw422016)
+//line templates/asset.qtpl:16
+}
+
+//line templates/asset.qtpl:16
+func IconMailto() string {
+//line templates/asset.qtpl:16
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/asset.qtpl:16
+ WriteIconMailto(qb422016)
+//line templates/asset.qtpl:16
+ qs422016 := string(qb422016.B)
+//line templates/asset.qtpl:16
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/asset.qtpl:16
+ return qs422016
+//line templates/asset.qtpl:16
+}
+
+// This is a modified version of https://www.svgrepo.com/svg/232085/rat
+
+//line templates/asset.qtpl:19
+func StreamIconGopher(qw422016 *qt422016.Writer) {
+//line templates/asset.qtpl:19
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:20
+ qw422016.N().S(`
+
+
+
+`)
+//line templates/asset.qtpl:20
+ qw422016.N().S(`
+`)
+//line templates/asset.qtpl:21
+}
+
+//line templates/asset.qtpl:21
+func WriteIconGopher(qq422016 qtio422016.Writer) {
+//line templates/asset.qtpl:21
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/asset.qtpl:21
+ StreamIconGopher(qw422016)
+//line templates/asset.qtpl:21
+ qt422016.ReleaseWriter(qw422016)
+//line templates/asset.qtpl:21
+}
+
+//line templates/asset.qtpl:21
+func IconGopher() string {
+//line templates/asset.qtpl:21
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/asset.qtpl:21
+ WriteIconGopher(qb422016)
+//line templates/asset.qtpl:21
+ qs422016 := string(qb422016.B)
+//line templates/asset.qtpl:21
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/asset.qtpl:21
+ return qs422016
+//line templates/asset.qtpl:21
+}
diff --git a/templates/auth.qtpl b/templates/auth.qtpl
index bbb7d25..32ab3d8 100644
--- a/templates/auth.qtpl
+++ b/templates/auth.qtpl
@@ -6,7 +6,7 @@
{% if user.AuthUsed %}
Login
diff --git a/templates/auth.qtpl.go b/templates/auth.qtpl.go
index 27d13fe..ed88d8d 100644
--- a/templates/auth.qtpl.go
+++ b/templates/auth.qtpl.go
@@ -33,7 +33,7 @@ func StreamLoginHTML(qw422016 *qt422016.Writer) {
qw422016.N().S(`
Login
diff --git a/templates/common.qtpl b/templates/common.qtpl
index 321da84..b5e06a1 100644
--- a/templates/common.qtpl
+++ b/templates/common.qtpl
@@ -20,32 +20,40 @@ var navEntries = []navEntry{
%}
{% func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) %}
-{% code
- u := user.FromRequest(rq).OrAnon()
+{% code
+ u := user.FromRequest(rq)
%}
-
-
+
+
+
{%- for _, entry := range navEntries -%}
{%- if navType == "revision" && entry.path == "revision" -%}
- {%s revisionHash[0] %}
+
+ {%s revisionHash[0] %}
+
{%- elseif navType == entry.path -%}
- {%s entry.title %}
- {%- elseif entry.path != "revision" && u.Group.CanAccessRoute(entry.path) -%}
- {%s entry.title %}
+
+ {%s entry.title %}
+
+ {%- elseif entry.path != "revision" && u.CanProceed(entry.path) -%}
+
+ {%s entry.title %}
+
{%- endif -%}
{%- endfor -%}
- {%s= userMenuHTML(u) %}
{% endfunc %}
{% func userMenuHTML(u *user.User) %}
-
- {% if u.Group == user.UserAnon %}
- Login
- {% else %}
- {%s u.Name %}
- {% endif %}
-
+{% if user.AuthUsed %}
+
+{% endif %}
{% endfunc %}
diff --git a/templates/common.qtpl.go b/templates/common.qtpl.go
index de53098..590cc02 100644
--- a/templates/common.qtpl.go
+++ b/templates/common.qtpl.go
@@ -50,153 +50,165 @@ func streamnavHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, navTy
qw422016.N().S(`
`)
//line templates/common.qtpl:24
- u := user.FromRequest(rq).OrAnon()
+ u := user.FromRequest(rq)
//line templates/common.qtpl:25
qw422016.N().S(`
-
-
+
+
+
`)
-//line templates/common.qtpl:28
+//line templates/common.qtpl:29
for _, entry := range navEntries {
-//line templates/common.qtpl:29
+//line templates/common.qtpl:30
if navType == "revision" && entry.path == "revision" {
-//line templates/common.qtpl:29
- qw422016.N().S(` `)
//line templates/common.qtpl:30
+ qw422016.N().S(`
+ `)
+//line templates/common.qtpl:32
qw422016.E().S(revisionHash[0])
-//line templates/common.qtpl:30
- qw422016.N().S(`
-`)
-//line templates/common.qtpl:31
- } else if navType == entry.path {
-//line templates/common.qtpl:31
- qw422016.N().S(` `)
//line templates/common.qtpl:32
- qw422016.E().S(entry.title)
-//line templates/common.qtpl:32
- qw422016.N().S(`
-`)
-//line templates/common.qtpl:33
- } else if entry.path != "revision" && u.Group.CanAccessRoute(entry.path) {
-//line templates/common.qtpl:33
- qw422016.N().S(` `)
-//line templates/common.qtpl:34
- qw422016.E().S(entry.title)
-//line templates/common.qtpl:34
- qw422016.N().S(`
-`)
-//line templates/common.qtpl:35
- }
-//line templates/common.qtpl:36
- }
-//line templates/common.qtpl:36
- qw422016.N().S(` `)
-//line templates/common.qtpl:37
- qw422016.N().S(userMenuHTML(u))
-//line templates/common.qtpl:37
- qw422016.N().S(`
-
-
-`)
-//line templates/common.qtpl:40
-}
-
-//line templates/common.qtpl:40
-func writenavHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, navType string, revisionHash ...string) {
-//line templates/common.qtpl:40
- qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/common.qtpl:40
- streamnavHTML(qw422016, rq, hyphaName, navType, revisionHash...)
-//line templates/common.qtpl:40
- qt422016.ReleaseWriter(qw422016)
-//line templates/common.qtpl:40
-}
-
-//line templates/common.qtpl:40
-func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) string {
-//line templates/common.qtpl:40
- qb422016 := qt422016.AcquireByteBuffer()
-//line templates/common.qtpl:40
- writenavHTML(qb422016, rq, hyphaName, navType, revisionHash...)
-//line templates/common.qtpl:40
- qs422016 := string(qb422016.B)
-//line templates/common.qtpl:40
- qt422016.ReleaseByteBuffer(qb422016)
-//line templates/common.qtpl:40
- return qs422016
-//line templates/common.qtpl:40
-}
-
-//line templates/common.qtpl:42
-func streamuserMenuHTML(qw422016 *qt422016.Writer, u *user.User) {
-//line templates/common.qtpl:42
- qw422016.N().S(`
-
- `)
-//line templates/common.qtpl:44
- if u.Group == user.UserAnon {
-//line templates/common.qtpl:44
- qw422016.N().S(`
- Login
- `)
-//line templates/common.qtpl:46
- } else {
-//line templates/common.qtpl:46
- qw422016.N().S(`
- `)
-//line templates/common.qtpl:47
- qw422016.E().S(u.Name)
-//line templates/common.qtpl:47
- qw422016.N().S(`
- `)
-//line templates/common.qtpl:48
- }
-//line templates/common.qtpl:48
- qw422016.N().S(`
+ qw422016.N().S(`
`)
-//line templates/common.qtpl:50
+//line templates/common.qtpl:34
+ } else if navType == entry.path {
+//line templates/common.qtpl:34
+ qw422016.N().S(`
+ `)
+//line templates/common.qtpl:36
+ qw422016.E().S(entry.title)
+//line templates/common.qtpl:36
+ qw422016.N().S(`
+
+`)
+//line templates/common.qtpl:38
+ } else if entry.path != "revision" && u.CanProceed(entry.path) {
+//line templates/common.qtpl:38
+ qw422016.N().S(`
+ `)
+//line templates/common.qtpl:40
+ qw422016.E().S(entry.title)
+//line templates/common.qtpl:40
+ qw422016.N().S(`
+
+`)
+//line templates/common.qtpl:42
+ }
+//line templates/common.qtpl:43
+ }
+//line templates/common.qtpl:43
+ qw422016.N().S(`
+
+`)
+//line templates/common.qtpl:46
}
-//line templates/common.qtpl:50
-func writeuserMenuHTML(qq422016 qtio422016.Writer, u *user.User) {
-//line templates/common.qtpl:50
+//line templates/common.qtpl:46
+func writenavHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, navType string, revisionHash ...string) {
+//line templates/common.qtpl:46
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/common.qtpl:50
- streamuserMenuHTML(qw422016, u)
-//line templates/common.qtpl:50
+//line templates/common.qtpl:46
+ streamnavHTML(qw422016, rq, hyphaName, navType, revisionHash...)
+//line templates/common.qtpl:46
qt422016.ReleaseWriter(qw422016)
-//line templates/common.qtpl:50
+//line templates/common.qtpl:46
}
-//line templates/common.qtpl:50
-func userMenuHTML(u *user.User) string {
-//line templates/common.qtpl:50
+//line templates/common.qtpl:46
+func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) string {
+//line templates/common.qtpl:46
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/common.qtpl:50
- writeuserMenuHTML(qb422016, u)
-//line templates/common.qtpl:50
+//line templates/common.qtpl:46
+ writenavHTML(qb422016, rq, hyphaName, navType, revisionHash...)
+//line templates/common.qtpl:46
qs422016 := string(qb422016.B)
-//line templates/common.qtpl:50
+//line templates/common.qtpl:46
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/common.qtpl:50
+//line templates/common.qtpl:46
return qs422016
-//line templates/common.qtpl:50
+//line templates/common.qtpl:46
+}
+
+//line templates/common.qtpl:48
+func streamuserMenuHTML(qw422016 *qt422016.Writer, u *user.User) {
+//line templates/common.qtpl:48
+ qw422016.N().S(`
+`)
+//line templates/common.qtpl:49
+ if user.AuthUsed {
+//line templates/common.qtpl:49
+ qw422016.N().S(`
+
+`)
+//line templates/common.qtpl:57
+ }
+//line templates/common.qtpl:57
+ qw422016.N().S(`
+`)
+//line templates/common.qtpl:58
+}
+
+//line templates/common.qtpl:58
+func writeuserMenuHTML(qq422016 qtio422016.Writer, u *user.User) {
+//line templates/common.qtpl:58
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/common.qtpl:58
+ streamuserMenuHTML(qw422016, u)
+//line templates/common.qtpl:58
+ qt422016.ReleaseWriter(qw422016)
+//line templates/common.qtpl:58
+}
+
+//line templates/common.qtpl:58
+func userMenuHTML(u *user.User) string {
+//line templates/common.qtpl:58
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/common.qtpl:58
+ writeuserMenuHTML(qb422016, u)
+//line templates/common.qtpl:58
+ qs422016 := string(qb422016.B)
+//line templates/common.qtpl:58
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/common.qtpl:58
+ return qs422016
+//line templates/common.qtpl:58
}
diff --git a/templates/css.qtpl b/templates/css.qtpl
deleted file mode 100644
index 7a6f6e3..0000000
--- a/templates/css.qtpl
+++ /dev/null
@@ -1,54 +0,0 @@
-{% func DefaultCSS() %}
-@media screen and (min-width: 700px) {
- main {margin: 0 auto; width: 700px;}
-}
-@media screen and (max-width: 700px) {
- main {margin: 0; width: 100%;}
-}
-*, *::before, *::after {box-sizing: border-box;}
-html {height:100%; padding:0; background-color:#ddd;
-background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23bbbbbb' fill-opacity='0.4'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");} /* heropatterns.com */
-body {height:100%; margin:0; font-size:16px; font-family:sans-serif;}
-main {padding:1rem; background-color: white; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); }
-main > form {margin-bottom:1rem;}
-textarea {font-size:15px;}
-.edit {height:100%;}
-.edit-form {height:90%;}
-.edit-form textarea {width:100%;height:90%;}
-
-main h1:not(.navi-title) {font-size:1.7rem;}
-blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;}
-.wikilink_new {color:#a55858;}
-.wikilink_new:visited {color:#a55858;}
-.wikilink_external::after {content:"🌐"; margin-left: .5rem; font-size: small; text-decoration: none; align: bottom;}
-article code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
-article pre.codeblock {background-color:#eee; padding:.5rem; white-space: pre-wrap; border-radius: .25rem;}
-.codeblock code {padding:0; font-size:15px;}
-.transclusion code, .transclusion .codeblock {background-color:#ddd;}
-.transclusion {background-color:#eee; border-radius: .25rem; }
-.transclusion__content > *:not(.binary-container) {margin: 0.5rem; }
-.transclusion__link {display: block; text-align: right; font-style: italic; margin-top: 0.5rem; color: black; text-decoration: none;}
-.transclusion__link::before {content: "⇐ ";}
-
-.binary-container_with-img img,
-.binary-container_with-video video,
-.binary-container_with-audio audio {width: 100%}
-.navi-title a {text-decoration:none;}
-.img-gallery { text-align: center; margin-top: .25rem; }
-.img-gallery_many-images { background-color: #eee; border-radius: .25rem; padding: .5rem; }
-.img-gallery img { max-width: 100%; max-height: 50vh; }
-figure { margin: 0; }
-figcaption { padding-bottom: .5rem; }
-
-nav ul {display:flex; padding-left:0; flex-wrap:wrap; margin-top:0;}
-nav ul li {list-style-type:none;margin-right:1rem;}
-
-#new-name {width:100%;}
-.navlinks__user {font-style:italic;}
-
-.rc-entry { display: grid; list-style-type: none; padding: .25rem; background-color: #eee; grid-template-columns: 1fr 1fr; }
-.rc-entry__time { font-style: italic; }
-.rc-entry__hash { font-style: italic; text-align: right; }
-.rc-entry__links { grid-column: 1 / span 2; }
-.rc-entry__author { font-style: italic; }
-{% endfunc %}
diff --git a/templates/css.qtpl.go b/templates/css.qtpl.go
deleted file mode 100644
index 72a25fc..0000000
--- a/templates/css.qtpl.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// Code generated by qtc from "css.qtpl". DO NOT EDIT.
-// See https://github.com/valyala/quicktemplate for details.
-
-//line templates/css.qtpl:1
-package templates
-
-//line templates/css.qtpl:1
-import (
- qtio422016 "io"
-
- qt422016 "github.com/valyala/quicktemplate"
-)
-
-//line templates/css.qtpl:1
-var (
- _ = qtio422016.Copy
- _ = qt422016.AcquireByteBuffer
-)
-
-//line templates/css.qtpl:1
-func StreamDefaultCSS(qw422016 *qt422016.Writer) {
-//line templates/css.qtpl:1
- qw422016.N().S(`
-@media screen and (min-width: 700px) {
- main {margin: 0 auto; width: 700px;}
-}
-@media screen and (max-width: 700px) {
- main {margin: 0; width: 100%;}
-}
-*, *::before, *::after {box-sizing: border-box;}
-html {height:100%; padding:0; background-color:#ddd;
-background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23bbbbbb' fill-opacity='0.4'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");} /* heropatterns.com */
-body {height:100%; margin:0; font-size:16px; font-family:sans-serif;}
-main {padding:1rem; background-color: white; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); }
-main > form {margin-bottom:1rem;}
-textarea {font-size:15px;}
-.edit {height:100%;}
-.edit-form {height:90%;}
-.edit-form textarea {width:100%;height:90%;}
-
-main h1:not(.navi-title) {font-size:1.7rem;}
-blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;}
-.wikilink_new {color:#a55858;}
-.wikilink_new:visited {color:#a55858;}
-.wikilink_external::after {content:"🌐"; margin-left: .5rem; font-size: small; text-decoration: none; align: bottom;}
-article code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
-article pre.codeblock {background-color:#eee; padding:.5rem; white-space: pre-wrap; border-radius: .25rem;}
-.codeblock code {padding:0; font-size:15px;}
-.transclusion code, .transclusion .codeblock {background-color:#ddd;}
-.transclusion {background-color:#eee; border-radius: .25rem; }
-.transclusion__content > *:not(.binary-container) {margin: 0.5rem; }
-.transclusion__link {display: block; text-align: right; font-style: italic; margin-top: 0.5rem; color: black; text-decoration: none;}
-.transclusion__link::before {content: "⇐ ";}
-
-.binary-container_with-img img,
-.binary-container_with-video video,
-.binary-container_with-audio audio {width: 100%}
-.navi-title a {text-decoration:none;}
-.img-gallery { text-align: center; margin-top: .25rem; }
-.img-gallery_many-images { background-color: #eee; border-radius: .25rem; padding: .5rem; }
-.img-gallery img { max-width: 100%; max-height: 50vh; }
-figure { margin: 0; }
-figcaption { padding-bottom: .5rem; }
-
-nav ul {display:flex; padding-left:0; flex-wrap:wrap; margin-top:0;}
-nav ul li {list-style-type:none;margin-right:1rem;}
-
-#new-name {width:100%;}
-.navlinks__user {font-style:italic;}
-
-.rc-entry { display: grid; list-style-type: none; padding: .25rem; background-color: #eee; grid-template-columns: 1fr 1fr; }
-.rc-entry__time { font-style: italic; }
-.rc-entry__hash { font-style: italic; text-align: right; }
-.rc-entry__links { grid-column: 1 / span 2; }
-.rc-entry__author { font-style: italic; }
-`)
-//line templates/css.qtpl:54
-}
-
-//line templates/css.qtpl:54
-func WriteDefaultCSS(qq422016 qtio422016.Writer) {
-//line templates/css.qtpl:54
- qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/css.qtpl:54
- StreamDefaultCSS(qw422016)
-//line templates/css.qtpl:54
- qt422016.ReleaseWriter(qw422016)
-//line templates/css.qtpl:54
-}
-
-//line templates/css.qtpl:54
-func DefaultCSS() string {
-//line templates/css.qtpl:54
- qb422016 := qt422016.AcquireByteBuffer()
-//line templates/css.qtpl:54
- WriteDefaultCSS(qb422016)
-//line templates/css.qtpl:54
- qs422016 := string(qb422016.B)
-//line templates/css.qtpl:54
- qt422016.ReleaseByteBuffer(qb422016)
-//line templates/css.qtpl:54
- return qs422016
-//line templates/css.qtpl:54
-}
diff --git a/templates/default.css b/templates/default.css
new file mode 100644
index 0000000..0d1ccda
--- /dev/null
+++ b/templates/default.css
@@ -0,0 +1,183 @@
+/* Layout stuff */
+@media screen and (min-width: 800px) {
+ main { padding:1rem 2rem; margin: 0 auto; width: 800px; }
+ .hypha-tabs { padding: 1rem 2rem; margin: 0 auto; width: 800px; }
+ header { margin: 0 auto; width: 800px; }
+ .header-links__entry { margin-right: 1.5rem; }
+ .header-links__entry_user { margin: 0 2rem 0 auto; }
+ .header-links__entry:nth-of-type(1),
+ .hypha-tabs__tab:nth-of-type(1) { margin-left: 2rem; }
+ .hypha-tabs__tab { margin-right: 1.5rem; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); border-bottom: 2px #ddd solid; padding: 0 .5rem; }
+}
+@media screen and (max-width: 800px) {
+ main { padding: 1rem; margin: 0; width: 100%; }
+ .hypha-tabs{ padding: 1rem; margin: 0; width: 100%; }
+ .hypha-tabs__tab { box-shadow: none; margin-right: .5rem; padding: .25rem .5rem; }
+ header { width: 100%; }
+ .header-links__entry { margin-right: .5rem; }
+}
+*, *::before, *::after {box-sizing: border-box;}
+html { height:100%; padding:0; }
+body {height:100%; margin:0; font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;}
+main {border-radius: 0 0 .25rem .25rem; }
+main > form {margin-bottom:1rem;}
+textarea {font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;}
+.edit_no-preview {height:100%;}
+.edit_with-preview .edit-form textarea { min-height: 500px; }
+.edit__preview { border: 2px dashed #ddd; }
+.edit-form {height:90%;}
+.edit-form textarea {width:100%;height:90%;}
+.edit-form__save { font-weight: bold; }
+.icon {margin-right: .25rem; vertical-align: bottom; }
+
+main h1:not(.navi-title) {font-size:1.7rem;}
+blockquote { margin-left: 0; padding-left: 1rem; }
+.wikilink_external::before { display: inline-block; width: 18px; height: 16px; vertical-align: sub; }
+/* .wikilink_external { padding-left: 16px; } */
+.wikilink_gopher::before { content: url("/static/icon/gopher"); }
+.wikilink_http::before { content: url("/static/icon/http"); }
+.wikilink_https::before { content: url("/static/icon/http"); }
+/* .wikilink_https { background: transparent url("/static/icon/http") center left no-repeat; } */
+.wikilink_gemini::before { content: url("/static/icon/gemini"); }
+.wikilink_mailto::before { content: url("/static/icon/mailto"); }
+
+article { overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; line-height: 150%; }
+article h1, article h2, article h3, article h4, article h5, article h6 { margin: 1.5rem 0 0 0; }
+article p { margin: .5rem 0; }
+article ul, ol { padding-left: 1.5rem; margin: .5rem 0; }
+article code { padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
+article pre.codeblock { padding:.5rem; white-space: pre-wrap; border-radius: .25rem;}
+.codeblock code {padding:0; font-size:15px;}
+.transclusion { border-radius: .25rem; }
+.transclusion__content > *:not(.binary-container) {margin: 0.5rem; }
+.transclusion__link {display: block; text-align: right; font-style: italic; margin-top: .5rem; margin-right: .25rem; text-decoration: none;}
+.transclusion__link::before {content: "⇐ ";}
+
+/* Derived from https://commons.wikimedia.org/wiki/File:U%2B21D2.svg */
+.launchpad__entry { list-style-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' width='25' height='12'%3E%3Cg transform='scale(0.7,0.8) translate(-613.21429,-421)'%3E%3Cpath fill='%23999' d='M 638.06773,429.49751 L 631.01022,436.87675 L 630.1898,436.02774 L 632.416,433.30375 L 613.46876,433.30375 L 613.46876,431.66382 L 633.82089,431.66382 L 635.57789,429.5261 L 633.79229,427.35979 L 613.46876,427.35979 L 613.46876,425.71985 L 632.416,425.71985 L 630.1898,422.99587 L 631.01022,422.08788 L 638.06773,429.49751 z '/%3E%3C/g%3E%3C/svg%3E"); }
+
+.binary-container_with-img img,
+.binary-container_with-video video,
+.binary-container_with-audio audio {width: 100%}
+
+.navi-title { padding-bottom: .5rem; margin: .25rem 0; }
+.navi-title a {text-decoration:none; }
+.navi-title__separator { margin: 0 .25rem; }
+.navi-title__colon { margin-right: .5rem; }
+.upload-amnt { clear: both; padding: .5rem; border-radius: .25rem; }
+.upload-amnt__unattach { display: block; }
+aside { clear: both; }
+
+.img-gallery { text-align: center; margin-top: .25rem; margin-bottom: .25rem; }
+.img-gallery_many-images { border-radius: .25rem; padding: .5rem; }
+.img-gallery img { max-width: 100%; max-height: 50vh; }
+figure { margin: 0; }
+figcaption { padding-bottom: .5rem; }
+
+#new-name {width:100%;}
+
+header { margin-bottom: .5rem; }
+.header-links__entry_user { font-style:italic; }
+.header-links__link { text-decoration: none; display: block; width: 100%; height: 100%; padding: .25rem; }
+.hypha-tabs { padding: 0; }
+.header-links__list, .hypha-tabs__flex { margin: 0; padding: 0; display: flex; flex-wrap: wrap; }
+.header-links__entry, .hypha-tabs__tab { list-style-type: none; }
+.hypha-tabs__tab a { text-decoration: none; }
+.hypha-tabs__tab_active { font-weight: bold; }
+
+.rc-entry { display: grid; list-style-type: none; padding: .25rem; grid-template-columns: 1fr 1fr; }
+.rc-entry__time { font-style: italic; }
+.rc-entry__hash { font-style: italic; text-align: right; }
+.rc-entry__links { grid-column: 1 / span 2; }
+.rc-entry__author { font-style: italic; }
+
+.prevnext__el { display: block-inline; min-width: 40%; padding: .5rem; margin-bottom: .25rem; text-decoration: none; border-radius: .25rem; }
+.prevnext__prev { float: left; }
+.prevnext__next { float: right; text-align: right; }
+
+.page-separator { clear: both; }
+.history__entries { background-color: #eee; margin: 0; padding: 0; border-radius: .25rem; }
+.history__month-anchor { text-decoration: none; color: inherit; }
+.history__entry { list-style-type: none; padding: .25rem; }
+.history-entry { padding: .25rem; }
+.history-entry__time { font-weight: bold; }
+.history-entry__author { font-style: italic; }
+
+table { border: #ddd 1px solid; border-radius: .25rem; min-width: 4rem; }
+td { padding: .25rem; }
+caption { caption-side: top; font-size: small; }
+
+/* Color stuff */
+/* Lighter stuff #eee */
+article code,
+article .codeblock,
+.transclusion,
+.img-gallery_many-images,
+.rc-entry,
+.prevnext__el,
+table { background-color: #eee; }
+
+@media screen and (max-width: 800px) {
+ .hypha-tabs { background-color: white; }
+ .hypha-tabs__tab { box-shadow: none; }
+}
+
+/* Other stuff */
+html { background-color: #ddd;
+background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23bbbbbb' fill-opacity='0.4'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
+} /* heropatterns.com */
+header { background-color: #bbb; }
+.header-links__link { color: black; }
+.header-links__link:hover { background-color: #eee; }
+
+main, .hypha-tabs__tab { background-color: white; }
+.hypha-tabs__tab { clip-path: inset(-20px -20px 0 -20px); }
+.hypha-tabs__tab a { color: black; }
+.hypha-tabs__tab_active { border-bottom: 2px white solid; }
+
+blockquote { border-left: 4px black solid; }
+.wikilink_new {color:#a55858;}
+.transclusion code, .transclusion .codeblock {background-color:#ddd;}
+.transclusion__link { color: black; }
+.wikilink_new:visited {color:#a55858;}
+.navi-title { border-bottom: #eee 1px solid; }
+.upload-amnt { border: #eee 1px solid; }
+td { border: #ddd 1px solid; }
+
+/* Dark theme! */
+@media (prefers-color-scheme: dark) {
+html { background: #222; color: #ddd; }
+main, article, .hypha-tabs__tab, header { background-color: #343434; color: #ddd; }
+
+a, .wikilink_external { color: #f1fa8c; }
+a:visited, .wikilink_external:visited { color: #ffb86c; }
+.wikilink_new, .wikilink_new:visited { color: #dd4444; }
+
+.header-links__link, .header-links__link:visited,
+.prevnext__el, .prevnext__el:visited { color: #ddd; }
+.header-links__link:hover { background-color: #444; }
+
+.hypha-tabs__tab a, .hypha-tabs__tab { color: #ddd; background-color: #232323; border: 0; }
+.hypha-tabs__tab_active { background-color: #343434; }
+
+blockquote { border-left: 4px #ddd solid; }
+
+.transclusion .transclusion__link { color: #ddd; }
+article code,
+article .codeblock,
+.transclusion,
+.img-gallery_many-images,
+.rc-entry,
+.history__entry,
+.prevnext__el,
+.upload-amnt,
+textarea,
+table { border: 0; background-color: #444444; color: #ddd; }
+.transclusion code,
+.transclusion .codeblock { background-color: #454545; }
+mark { background: rgba(130, 80, 30, 5); color: inherit; }
+@media screen and (max-width: 800px) {
+ .hypha-tabs { background-color: #232323; }
+}
+}
+
diff --git a/templates/delete.qtpl b/templates/delete.qtpl
index d91c851..1d45e0a 100644
--- a/templates/delete.qtpl
+++ b/templates/delete.qtpl
@@ -2,8 +2,8 @@
This dialog is to be shown to a user when they try to delete a hypha.
{% func DeleteAskHTML(rq *http.Request, hyphaName string, isOld bool) %}
-
{%= navHTML(rq, hyphaName, "delete-ask") %}
+
{% if isOld %}
Delete {%s hyphaName %}?
diff --git a/templates/delete.qtpl.go b/templates/delete.qtpl.go
index 4f58b04..1e67350 100644
--- a/templates/delete.qtpl.go
+++ b/templates/delete.qtpl.go
@@ -26,12 +26,12 @@ var (
func StreamDeleteAskHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
//line templates/delete.qtpl:4
qw422016.N().S(`
-
`)
-//line templates/delete.qtpl:6
+//line templates/delete.qtpl:5
streamnavHTML(qw422016, rq, hyphaName, "delete-ask")
-//line templates/delete.qtpl:6
+//line templates/delete.qtpl:5
qw422016.N().S(`
+
`)
//line templates/delete.qtpl:7
if isOld {
diff --git a/templates/http_mutators.qtpl b/templates/http_mutators.qtpl
index 049f8ae..39a37ba 100644
--- a/templates/http_mutators.qtpl
+++ b/templates/http_mutators.qtpl
@@ -1,16 +1,35 @@
{% import "net/http" %}
{% func EditHTML(rq *http.Request, hyphaName, textAreaFill, warning string) %}
-
{%s= navHTML(rq, hyphaName, "edit") %}
+
Edit {%s hyphaName %}
{%s= warning %}
{% endfunc %}
+
+{% func PreviewHTML(rq *http.Request, hyphaName, textAreaFill, warning string, renderedPage string) %}
+{%s= navHTML(rq, hyphaName, "edit") %}
+
+ Edit {%s hyphaName %} (preview)
+ {%s= warning %}
+
+ {%s textAreaFill %}
+
+
+
+ Cancel
+
+ Note that the hypha is not saved yet. You can preview the changes ↓
+
+
+{% endfunc %}
diff --git a/templates/http_mutators.qtpl.go b/templates/http_mutators.qtpl.go
index 62a24a1..8f25c90 100644
--- a/templates/http_mutators.qtpl.go
+++ b/templates/http_mutators.qtpl.go
@@ -24,12 +24,12 @@ var (
func StreamEditHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, textAreaFill, warning string) {
//line templates/http_mutators.qtpl:3
qw422016.N().S(`
-
`)
-//line templates/http_mutators.qtpl:5
+//line templates/http_mutators.qtpl:4
qw422016.N().S(navHTML(rq, hyphaName, "edit"))
-//line templates/http_mutators.qtpl:5
+//line templates/http_mutators.qtpl:4
qw422016.N().S(`
+
Edit `)
//line templates/http_mutators.qtpl:6
qw422016.E().S(hyphaName)
@@ -52,40 +52,118 @@ func StreamEditHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, text
//line templates/http_mutators.qtpl:10
qw422016.N().S(`
-
+
+
Cancel
+//line templates/http_mutators.qtpl:14
+ qw422016.N().S(`" class="edit-form__cancel">Cancel
`)
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
}
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
func WriteEditHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, textAreaFill, warning string) {
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
StreamEditHTML(qw422016, rq, hyphaName, textAreaFill, warning)
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
qt422016.ReleaseWriter(qw422016)
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
}
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
func EditHTML(rq *http.Request, hyphaName, textAreaFill, warning string) string {
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
WriteEditHTML(qb422016, rq, hyphaName, textAreaFill, warning)
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
qs422016 := string(qb422016.B)
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
return qs422016
-//line templates/http_mutators.qtpl:16
+//line templates/http_mutators.qtpl:17
+}
+
+//line templates/http_mutators.qtpl:19
+func StreamPreviewHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, textAreaFill, warning string, renderedPage string) {
+//line templates/http_mutators.qtpl:19
+ qw422016.N().S(`
+`)
+//line templates/http_mutators.qtpl:20
+ qw422016.N().S(navHTML(rq, hyphaName, "edit"))
+//line templates/http_mutators.qtpl:20
+ qw422016.N().S(`
+
+ Edit `)
+//line templates/http_mutators.qtpl:22
+ qw422016.E().S(hyphaName)
+//line templates/http_mutators.qtpl:22
+ qw422016.N().S(` (preview)
+ `)
+//line templates/http_mutators.qtpl:23
+ qw422016.N().S(warning)
+//line templates/http_mutators.qtpl:23
+ qw422016.N().S(`
+
+ `)
+//line templates/http_mutators.qtpl:26
+ qw422016.E().S(textAreaFill)
+//line templates/http_mutators.qtpl:26
+ qw422016.N().S(`
+
+
+
+ Cancel
+
+ Note that the hypha is not saved yet. You can preview the changes ↓
+ `)
+//line templates/http_mutators.qtpl:33
+ qw422016.N().S(renderedPage)
+//line templates/http_mutators.qtpl:33
+ qw422016.N().S(`
+
+`)
+//line templates/http_mutators.qtpl:35
+}
+
+//line templates/http_mutators.qtpl:35
+func WritePreviewHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, textAreaFill, warning string, renderedPage string) {
+//line templates/http_mutators.qtpl:35
+ qw422016 := qt422016.AcquireWriter(qq422016)
+//line templates/http_mutators.qtpl:35
+ StreamPreviewHTML(qw422016, rq, hyphaName, textAreaFill, warning, renderedPage)
+//line templates/http_mutators.qtpl:35
+ qt422016.ReleaseWriter(qw422016)
+//line templates/http_mutators.qtpl:35
+}
+
+//line templates/http_mutators.qtpl:35
+func PreviewHTML(rq *http.Request, hyphaName, textAreaFill, warning string, renderedPage string) string {
+//line templates/http_mutators.qtpl:35
+ qb422016 := qt422016.AcquireByteBuffer()
+//line templates/http_mutators.qtpl:35
+ WritePreviewHTML(qb422016, rq, hyphaName, textAreaFill, warning, renderedPage)
+//line templates/http_mutators.qtpl:35
+ qs422016 := string(qb422016.B)
+//line templates/http_mutators.qtpl:35
+ qt422016.ReleaseByteBuffer(qb422016)
+//line templates/http_mutators.qtpl:35
+ return qs422016
+//line templates/http_mutators.qtpl:35
}
diff --git a/templates/http_readers.qtpl b/templates/http_readers.qtpl
index a1cb2f3..a9d7304 100644
--- a/templates/http_readers.qtpl
+++ b/templates/http_readers.qtpl
@@ -1,27 +1,20 @@
{% import "net/http" %}
+{% import "path" %}
{% import "github.com/bouncepaw/mycorrhiza/user" %}
-{% func HistoryHTML(rq *http.Request, hyphaName, tbody string) %}
-
+{% func HistoryHTML(rq *http.Request, hyphaName, list string) %}
{%= navHTML(rq, hyphaName, "history") %}
-
-
-
- Time
- Hash
- Message
-
-
-
- {%s= tbody %}
-
-
+
+
+ History of {%s hyphaName %}
+ {%s= list %}
+
{% endfunc %}
{% func RevisionHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) %}
-
{%= navHTML(rq, hyphaName, "revision", revHash) %}
+
Please note that viewing binary parts of hyphae is not supported in history for now.
{%s= naviTitle %}
@@ -35,9 +28,9 @@
{% endfunc %}
If `contents` == "", a helpful message is shown instead.
-{% func PageHTML(rq *http.Request, hyphaName, naviTitle, contents, tree string) %}
-
+{% func PageHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName string, hasAmnt bool) %}
{%= navHTML(rq, hyphaName, "page") %}
+
{%s= naviTitle %}
{% if contents == "" %}
@@ -46,16 +39,26 @@ If `contents` == "", a helpful message is shown instead.
{%s= contents %}
{% endif %}
-
-{% if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon %}
+
+{% if u := user.FromRequest(rq); !user.AuthUsed || u.Group != "anon" %}
+ method="post" enctype="multipart/form-data"
+ class="upload-amnt">
+ {% if hasAmnt %}
+ Unattach current attachment?
+ {% endif %}
Upload a new attachment
-
{% endif %}
{%s= tree %}
diff --git a/templates/http_readers.qtpl.go b/templates/http_readers.qtpl.go
index 3ffac47..d9000da 100644
--- a/templates/http_readers.qtpl.go
+++ b/templates/http_readers.qtpl.go
@@ -8,188 +8,226 @@ package templates
import "net/http"
//line templates/http_readers.qtpl:2
+import "path"
+
+//line templates/http_readers.qtpl:3
import "github.com/bouncepaw/mycorrhiza/user"
-//line templates/http_readers.qtpl:4
+//line templates/http_readers.qtpl:5
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
-//line templates/http_readers.qtpl:4
+//line templates/http_readers.qtpl:5
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
-//line templates/http_readers.qtpl:4
-func StreamHistoryHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, tbody string) {
-//line templates/http_readers.qtpl:4
+//line templates/http_readers.qtpl:5
+func StreamHistoryHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, list string) {
+//line templates/http_readers.qtpl:5
qw422016.N().S(`
-
`)
//line templates/http_readers.qtpl:6
streamnavHTML(qw422016, rq, hyphaName, "history")
//line templates/http_readers.qtpl:6
qw422016.N().S(`
-
-
-
- Time
- Hash
- Message
-
-
-
+
+
+ History of `)
+//line templates/http_readers.qtpl:9
+ qw422016.E().S(hyphaName)
+//line templates/http_readers.qtpl:9
+ qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:16
- qw422016.N().S(tbody)
-//line templates/http_readers.qtpl:16
+//line templates/http_readers.qtpl:10
+ qw422016.N().S(list)
+//line templates/http_readers.qtpl:10
qw422016.N().S(`
-
-
+
`)
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
}
-//line templates/http_readers.qtpl:20
-func WriteHistoryHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, tbody string) {
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
+func WriteHistoryHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, list string) {
+//line templates/http_readers.qtpl:13
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/http_readers.qtpl:20
- StreamHistoryHTML(qw422016, rq, hyphaName, tbody)
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
+ StreamHistoryHTML(qw422016, rq, hyphaName, list)
+//line templates/http_readers.qtpl:13
qt422016.ReleaseWriter(qw422016)
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
}
-//line templates/http_readers.qtpl:20
-func HistoryHTML(rq *http.Request, hyphaName, tbody string) string {
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
+func HistoryHTML(rq *http.Request, hyphaName, list string) string {
+//line templates/http_readers.qtpl:13
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/http_readers.qtpl:20
- WriteHistoryHTML(qb422016, rq, hyphaName, tbody)
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
+ WriteHistoryHTML(qb422016, rq, hyphaName, list)
+//line templates/http_readers.qtpl:13
qs422016 := string(qb422016.B)
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
return qs422016
-//line templates/http_readers.qtpl:20
+//line templates/http_readers.qtpl:13
}
-//line templates/http_readers.qtpl:22
+//line templates/http_readers.qtpl:15
func StreamRevisionHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) {
-//line templates/http_readers.qtpl:22
+//line templates/http_readers.qtpl:15
+ qw422016.N().S(`
+`)
+//line templates/http_readers.qtpl:16
+ streamnavHTML(qw422016, rq, hyphaName, "revision", revHash)
+//line templates/http_readers.qtpl:16
qw422016.N().S(`
-`)
-//line templates/http_readers.qtpl:24
- streamnavHTML(qw422016, rq, hyphaName, "revision", revHash)
-//line templates/http_readers.qtpl:24
- qw422016.N().S(`
Please note that viewing binary parts of hyphae is not supported in history for now.
`)
-//line templates/http_readers.qtpl:27
+//line templates/http_readers.qtpl:20
qw422016.N().S(naviTitle)
-//line templates/http_readers.qtpl:27
+//line templates/http_readers.qtpl:20
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:28
+//line templates/http_readers.qtpl:21
qw422016.N().S(contents)
-//line templates/http_readers.qtpl:28
+//line templates/http_readers.qtpl:21
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:25
qw422016.N().S(tree)
-//line templates/http_readers.qtpl:32
+//line templates/http_readers.qtpl:25
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
}
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
func WriteRevisionHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) {
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
StreamRevisionHTML(qw422016, rq, hyphaName, naviTitle, contents, tree, revHash)
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
qt422016.ReleaseWriter(qw422016)
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
}
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
func RevisionHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) string {
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
WriteRevisionHTML(qb422016, rq, hyphaName, naviTitle, contents, tree, revHash)
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
qs422016 := string(qb422016.B)
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
return qs422016
-//line templates/http_readers.qtpl:35
+//line templates/http_readers.qtpl:28
}
// If `contents` == "", a helpful message is shown instead.
-//line templates/http_readers.qtpl:38
-func StreamPageHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree string) {
-//line templates/http_readers.qtpl:38
+//line templates/http_readers.qtpl:31
+func StreamPageHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName string, hasAmnt bool) {
+//line templates/http_readers.qtpl:31
+ qw422016.N().S(`
+`)
+//line templates/http_readers.qtpl:32
+ streamnavHTML(qw422016, rq, hyphaName, "page")
+//line templates/http_readers.qtpl:32
qw422016.N().S(`
-`)
-//line templates/http_readers.qtpl:40
- streamnavHTML(qw422016, rq, hyphaName, "page")
-//line templates/http_readers.qtpl:40
- qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:42
+//line templates/http_readers.qtpl:35
qw422016.N().S(naviTitle)
-//line templates/http_readers.qtpl:42
+//line templates/http_readers.qtpl:35
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:43
+//line templates/http_readers.qtpl:36
if contents == "" {
-//line templates/http_readers.qtpl:43
+//line templates/http_readers.qtpl:36
qw422016.N().S(`
This hypha has no text. Why not create it ?
`)
-//line templates/http_readers.qtpl:45
+//line templates/http_readers.qtpl:38
} else {
-//line templates/http_readers.qtpl:45
+//line templates/http_readers.qtpl:38
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:46
+//line templates/http_readers.qtpl:39
qw422016.N().S(contents)
-//line templates/http_readers.qtpl:46
+//line templates/http_readers.qtpl:39
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:47
+//line templates/http_readers.qtpl:40
}
-//line templates/http_readers.qtpl:47
+//line templates/http_readers.qtpl:40
qw422016.N().S(`
-
+
`)
//line templates/http_readers.qtpl:50
- if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon {
+ if u := user.FromRequest(rq); !user.AuthUsed || u.Group != "anon" {
//line templates/http_readers.qtpl:50
qw422016.N().S(`
+ method="post" enctype="multipart/form-data"
+ class="upload-amnt">
+ `)
+//line templates/http_readers.qtpl:54
+ if hasAmnt {
+//line templates/http_readers.qtpl:54
+ qw422016.N().S(`
+ Unattach current attachment?
+ `)
+//line templates/http_readers.qtpl:56
+ }
+//line templates/http_readers.qtpl:56
+ qw422016.N().S(`
Upload a new attachment
-
`)
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:62
}
-//line templates/http_readers.qtpl:59
+//line templates/http_readers.qtpl:62
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:61
+//line templates/http_readers.qtpl:64
qw422016.N().S(tree)
-//line templates/http_readers.qtpl:61
+//line templates/http_readers.qtpl:64
qw422016.N().S(`
`)
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
}
-//line templates/http_readers.qtpl:64
-func WritePageHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree string) {
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
+func WritePageHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName string, hasAmnt bool) {
+//line templates/http_readers.qtpl:67
qw422016 := qt422016.AcquireWriter(qq422016)
-//line templates/http_readers.qtpl:64
- StreamPageHTML(qw422016, rq, hyphaName, naviTitle, contents, tree)
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
+ StreamPageHTML(qw422016, rq, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName, hasAmnt)
+//line templates/http_readers.qtpl:67
qt422016.ReleaseWriter(qw422016)
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
}
-//line templates/http_readers.qtpl:64
-func PageHTML(rq *http.Request, hyphaName, naviTitle, contents, tree string) string {
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
+func PageHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName string, hasAmnt bool) string {
+//line templates/http_readers.qtpl:67
qb422016 := qt422016.AcquireByteBuffer()
-//line templates/http_readers.qtpl:64
- WritePageHTML(qb422016, rq, hyphaName, naviTitle, contents, tree)
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
+ WritePageHTML(qb422016, rq, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName, hasAmnt)
+//line templates/http_readers.qtpl:67
qs422016 := string(qb422016.B)
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
qt422016.ReleaseByteBuffer(qb422016)
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
return qs422016
-//line templates/http_readers.qtpl:64
+//line templates/http_readers.qtpl:67
}
diff --git a/templates/http_stuff.qtpl b/templates/http_stuff.qtpl
index 9bacf51..8723860 100644
--- a/templates/http_stuff.qtpl
+++ b/templates/http_stuff.qtpl
@@ -1,12 +1,26 @@
-{% func BaseHTML(title, body string) %}
+{% import "github.com/bouncepaw/mycorrhiza/util" %}
+{% import "github.com/bouncepaw/mycorrhiza/user" %}
+
+{% func BaseHTML(title, body string, u *user.User, headElements ...string) %}
{%s title %}
+ {% for _, el := range headElements %}{%s= el %}{% endfor %}
+
{%s= body %}
@@ -40,3 +54,25 @@
{% endif %}
{% endfunc %}
+
+{% func AboutHTML() %}
+
+
+ About {%s util.SiteName %}
+
+ MycorrhizaWiki version: β 0.12 indev
+ {%- if user.AuthUsed -%}
+ User count: {%d user.Count() %}
+ Home page: {%s util.HomePage %}
+ Administrators: {%- for i, username := range user.ListUsersWithGroup("admin") -%}
+ {%- if i > 0 -%},
+ {%- endif -%}
+ {%s username %} {%- endfor -%}
+ {%- else -%}
+ This wiki does not use authorization
+ {%- endif -%}
+
+ See /list for information about hyphae on this wiki.
+
+
+{% endfunc %}
diff --git a/templates/http_stuff.qtpl.go b/templates/http_stuff.qtpl.go
index 8fa0096..2de5906 100644
--- a/templates/http_stuff.qtpl.go
+++ b/templates/http_stuff.qtpl.go
@@ -5,21 +5,27 @@
package templates
//line templates/http_stuff.qtpl:1
+import "github.com/bouncepaw/mycorrhiza/util"
+
+//line templates/http_stuff.qtpl:2
+import "github.com/bouncepaw/mycorrhiza/user"
+
+//line templates/http_stuff.qtpl:4
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
-//line templates/http_stuff.qtpl:1
+//line templates/http_stuff.qtpl:4
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
+//line templates/http_stuff.qtpl:4
+func StreamBaseHTML(qw422016 *qt422016.Writer, title, body string, u *user.User, headElements ...string) {
+//line templates/http_stuff.qtpl:4
qw422016.N().S(`
@@ -27,59 +33,96 @@ func StreamBaseHTML(qw422016 *qt422016.Writer, title, body string) {
`)
-//line templates/http_stuff.qtpl:7
+//line templates/http_stuff.qtpl:10
qw422016.E().S(title)
-//line templates/http_stuff.qtpl:7
+//line templates/http_stuff.qtpl:10
qw422016.N().S(`
+ `)
+//line templates/http_stuff.qtpl:11
+ for _, el := range headElements {
+//line templates/http_stuff.qtpl:11
+ qw422016.N().S(el)
+//line templates/http_stuff.qtpl:11
+ }
+//line templates/http_stuff.qtpl:11
+ qw422016.N().S(`
+
`)
-//line templates/http_stuff.qtpl:10
+//line templates/http_stuff.qtpl:24
qw422016.N().S(body)
-//line templates/http_stuff.qtpl:10
+//line templates/http_stuff.qtpl:24
qw422016.N().S(`