1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-12-12 05:20:26 +00:00

Merge pull request #33 from bouncepaw/0.12

Version 0.12
This commit is contained in:
Timur Ismagilov 2021-01-24 13:41:17 +05:00 committed by GitHub
commit 2a5d7d580c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2941 additions and 993 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
hypha
mycorrhiza mycorrhiza

View File

@ -1,7 +1,10 @@
# 🍄 MycorrhizaWiki 0.11 # 🍄 MycorrhizaWiki 0.12
A wiki engine. A wiki engine.
[Main wiki](https://mycorrhiza.lesarbr.es)
## Building ## Building
Also see [detailed instructions](https://mycorrhiza.lesarbr.es/page/deploy) on wiki.
```sh ```sh
git clone --recurse-submodules https://github.com/bouncepaw/mycorrhiza git clone --recurse-submodules https://github.com/bouncepaw/mycorrhiza
cd mycorrhiza cd mycorrhiza
@ -22,36 +25,40 @@ Options:
What auth method to use. Variants: "none", "fixed" (default "none") What auth method to use. Variants: "none", "fixed" (default "none")
-fixed-credentials-path string -fixed-credentials-path string
Used when -auth-method=fixed. Path to file with user credentials. (default "mycocredentials.json") 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 -home string
The home page (default "home") 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 string
Port to serve the wiki at (default "1737") Port to serve the wiki at (default "1737")
-title string -url string
How to call your wiki in the navititle (default "🍄") URL at which your wiki can be found. Used to generate feeds (default "http://0.0.0.0:$port")
-user-tree string -user-hypha string
Hypha which is a superhypha of all user pages (default "u") Hypha which is a superhypha of all user pages (default "u")
``` ```
## Features ## Features
* Edit pages through html forms * Edit pages through html forms, graphical preview
* Responsive design * Responsive design, dark theme (synced with system theme)
* Works in text browsers * Works in text browsers
* Wiki pages (called hyphae) are written in mycomarkup * 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. * 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 * Page trees; links to previous and next pages
* Changes are saved to git * Changes are saved to git
* List of hyphae page * List of hyphae page
* History page * History page
* Random 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 deleted (while still preserving history)
* Hyphae can be renamed (recursive renaming of subhyphae is also supported) * 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 * Authorization with pre-set credentials
## Contributing ## 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 You can view list of all planned features on [our kanban board](https://github.com/bouncepaw/mycorrhiza/projects/1).
* Tagging system
* Better history viewing

23
flag.go
View File

@ -10,12 +10,15 @@ import (
) )
func init() { func init() {
flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at") 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.HomePage, "home", "home", "The home page") flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at using HTTP")
flag.StringVar(&util.SiteTitle, "title", "🍄", "How to call your wiki in the navititle") flag.StringVar(&util.HomePage, "home", "home", "The home page name")
flag.StringVar(&util.UserTree, "user-tree", "u", "Hypha which is a superhypha of all user pages") 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.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.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 // Do the things related to cli args and die maybe
@ -34,19 +37,19 @@ func parseCliArgs() {
log.Fatal(err) log.Fatal(err)
} }
if !isCanonicalName(util.HomePage) { if util.URL == "http://0.0.0.0:$port" {
log.Fatal("Error: you must use a proper name for the homepage") util.URL = "http://0.0.0.0:" + util.ServerPort
} }
if !isCanonicalName(util.UserTree) { util.HomePage = CanonicalName(util.HomePage)
log.Fatal("Error: you must use a proper name for user tree") util.UserHypha = CanonicalName(util.UserHypha)
} util.HeaderLinksHypha = CanonicalName(util.HeaderLinksHypha)
switch util.AuthMethod { switch util.AuthMethod {
case "none": case "none":
case "fixed": case "fixed":
user.AuthUsed = true user.AuthUsed = true
user.PopulateFixedUserStorage() user.ReadUsersFromFilesystem()
default: default:
log.Fatal("Error: unknown auth method:", util.AuthMethod) log.Fatal("Error: unknown auth method:", util.AuthMethod)
} }

2
go.mod
View File

@ -4,5 +4,7 @@ go 1.14
require ( require (
github.com/adrg/xdg v0.2.2 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 github.com/valyala/quicktemplate v1.6.3
) )

12
go.sum
View File

@ -1,11 +1,21 @@
github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo= 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/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/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/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.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.0/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/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/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/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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/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/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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"os/exec" "os/exec"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -13,6 +14,8 @@ import (
"github.com/bouncepaw/mycorrhiza/util" "github.com/bouncepaw/mycorrhiza/util"
) )
var renameMsgPattern = regexp.MustCompile(`^Rename (.*) to .*`)
// Start initializes git credentials. // Start initializes git credentials.
func Start(wikiDir string) { func Start(wikiDir string) {
_, err := gitsh("config", "user.name", "wikimind") _, err := gitsh("config", "user.name", "wikimind")
@ -31,6 +34,42 @@ type Revision struct {
Username string Username string
Time time.Time Time time.Time
Message string 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. // 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. // HyphaeLinks returns a comma-separated list of hyphae that were affected by this revision as HTML string.
func (rev Revision) HyphaeLinks() (html string) { func (rev Revision) HyphaeLinks() (html string) {
// diff-tree --no-commit-id --name-only -r hyphae := rev.hyphaeAffected()
var ( for i, hyphaName := range hyphae {
// List of files affected by this revision, one per line. if i > 0 {
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 += `<span aria-hidden="true">, </span>` html += `<span aria-hidden="true">, </span>`
} }
html += fmt.Sprintf(`<a href="/page/%[1]s">%[1]s</a>`, hyphaName) html += fmt.Sprintf(`<a href="/page/%[1]s">%[1]s</a>`, hyphaName)
} }
}
}
return html return html
} }
func (rev *Revision) descriptionForFeed() (html string) {
return fmt.Sprintf(
`<p>%s</p>
<p><b>Hyphae affected:</b> %s</p>`, rev.Message, rev.HyphaeLinks())
}
// Try and guess what link is the most important by looking at the message.
func (rev *Revision) bestLink() string {
var (
revs = rev.hyphaeAffected()
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
)
switch {
case renameRes != nil:
return "/page/" + renameRes[1]
case len(revs) == 0:
return ""
default:
return "/page/" + revs[0]
}
}
func (rev Revision) RecentChangesEntry() (html string) { func (rev Revision) RecentChangesEntry() (html string) {
if user.AuthUsed && rev.Username != "anon" { if user.AuthUsed && rev.Username != "anon" {
return fmt.Sprintf(` return fmt.Sprintf(`
@ -83,7 +118,7 @@ func (rev Revision) RecentChangesEntry() (html string) {
<li class="rc-entry__hash">%[2]s</li> <li class="rc-entry__hash">%[2]s</li>
<li class="rc-entry__links">%[5]s</li> <li class="rc-entry__links">%[5]s</li>
<li class="rc-entry__msg">%[6]s <span class="rc-entry__author">by <a href="/page/%[3]s/%[4]s" rel="author">%[4]s</a></span></li> <li class="rc-entry__msg">%[6]s <span class="rc-entry__author">by <a href="/page/%[3]s/%[4]s" rel="author">%[4]s</a></span></li>
`, rev.TimeString(), rev.Hash, util.UserTree, rev.Username, rev.HyphaeLinks(), rev.Message) `, rev.TimeString(), rev.Hash, util.UserHypha, rev.Username, rev.HyphaeLinks(), rev.Message)
} }
return fmt.Sprintf(` return fmt.Sprintf(`
<li class="rc-entry__time"><time>%[1]s</time></li> <li class="rc-entry__time"><time>%[1]s</time></li>

View File

@ -7,10 +7,60 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/util"
"github.com/gorilla/feeds"
) )
func recentChangesFeed() *feeds.Feed {
feed := &feeds.Feed{
Title: "Recent changes",
Link: &feeds.Link{Href: util.URL},
Description: "List of 30 recent changes on the wiki",
Author: &feeds.Author{Name: "Wikimind", Email: "wikimind@mycorrhiza"},
Updated: time.Now(),
}
var (
out, err = gitsh(
"log", "--oneline", "--no-merges",
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
"--max-count=30",
)
revs []Revision
)
if err == nil {
for _, line := range strings.Split(out.String(), "\n") {
revs = append(revs, parseRevisionLine(line))
}
}
for _, rev := range revs {
feed.Add(&feeds.Item{
Title: rev.Message,
Author: &feeds.Author{Name: rev.Username},
Id: rev.Hash,
Description: rev.descriptionForFeed(),
Created: rev.Time,
Updated: rev.Time,
Link: &feeds.Link{Href: util.URL + rev.bestLink()},
})
}
return feed
}
func RecentChangesRSS() (string, error) {
return recentChangesFeed().ToRss()
}
func RecentChangesAtom() (string, error) {
return recentChangesFeed().ToAtom()
}
func RecentChangesJSON() (string, error) {
return recentChangesFeed().ToJSON()
}
func RecentChanges(n int) string { func RecentChanges(n int) string {
var ( var (
out, err = gitsh( out, err = gitsh(
@ -32,13 +82,19 @@ func RecentChanges(n int) string {
return templates.RecentChangesHTML(entries, n) return templates.RecentChangesHTML(entries, n)
} }
// FileChanged tells you if the file has been changed.
func FileChanged(path string) bool {
_, err := gitsh("diff", "--exit-code", path)
return err != nil
}
// Revisions returns slice of revisions for the given hypha name. // Revisions returns slice of revisions for the given hypha name.
func Revisions(hyphaName string) ([]Revision, error) { func Revisions(hyphaName string) ([]Revision, error) {
var ( var (
out, err = gitsh( out, err = gitsh(
"log", "--oneline", "--no-merges", "log", "--oneline", "--no-merges",
// Hash, Commiter email, Commiter time, Commit msg separated by tab // Hash, author email, author time, commit msg separated by tab
"--pretty=format:\"%h\t%ce\t%ct\t%s\"", "--pretty=format:\"%h\t%ae\t%at\t%s\"",
"--", hyphaName+".*", "--", hyphaName+".*",
) )
revs []Revision revs []Revision
@ -53,6 +109,59 @@ func Revisions(hyphaName string) ([]Revision, error) {
return revs, err return revs, err
} }
// HistoryWithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
func HistoryWithRevisions(hyphaName string, revs []Revision) (html string) {
var (
currentYear int
currentMonth time.Month
)
for i, rev := range revs {
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
currentYear = rev.Time.Year()
currentMonth = rev.Time.Month()
if i != 0 {
html += `
</ul>
</section>`
}
html += fmt.Sprintf(`
<section class="history__month">
<a href="#%[1]d-%[2]d" class="history__month-anchor">
<h2 id="%[1]d-%[2]d" class="history__month-title">%[3]s</h2>
</a>
<ul class="history__entries">`,
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(`
<span class="history-entry__author">by <a href="/page/%[1]s/%[2]s" rel="author">%[2]s</span>`, util.UserHypha, rev.Username)
}
return fmt.Sprintf(`
<li class="history__entry">
<a class="history-entry" href="/rev/%[3]s/%[1]s">
<time class="history-entry__time">%[2]s</time>
<span class="history-entry__hash">%[3]s</span>
<span class="history-entry__msg">%[4]s</span>
</a>%[5]s
</li>
`, 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. // 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(.*)\"") 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(`
<tr>
<td><time>%s</time></td>
<td><a href="/rev/%s/%s">%s</a></td>
<td>%s</td>
</tr>`, rev.TimeString(), rev.Hash, hyphaName, rev.Hash, rev.Message)
}
// See how the file with `filepath` looked at commit with `hash`. // See how the file with `filepath` looked at commit with `hash`.
func FileAtRevision(filepath, hash string) (string, error) { func FileAtRevision(filepath, hash string) (string, error) {
out, err := gitsh("show", hash+":"+filepath) out, err := gitsh("show", hash+":"+filepath)

View File

@ -24,6 +24,7 @@ const (
TypeEditBinary TypeEditBinary
TypeDeleteHypha TypeDeleteHypha
TypeRenameHypha TypeRenameHypha
TypeUnattachHypha
) )
// HistoryOp is an object representing a history operation. // HistoryOp is an object representing a history operation.
@ -108,6 +109,12 @@ func (hop *HistoryOp) Apply() *HistoryOp {
return hop 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. // 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 { func (hop *HistoryOp) WithMsg(userMsg string) *HistoryOp {
for _, ch := range userMsg { for _, ch := range userMsg {
@ -121,7 +128,7 @@ func (hop *HistoryOp) WithMsg(userMsg string) *HistoryOp {
// WithUser sets a user for the commit. // WithUser sets a user for the commit.
func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp { func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp {
if u.Group != user.UserAnon { if u.Group != "anon" {
hop.name = u.Name hop.name = u.Name
hop.email = u.Name + "@mycorrhiza" hop.email = u.Name + "@mycorrhiza"
} }

View File

@ -28,7 +28,7 @@ func handlerLogout(w http.ResponseWriter, rq *http.Request) {
log.Println("Unknown user tries to log out") log.Println("Unknown user tries to log out")
w.WriteHeader(http.StatusForbidden) 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) { 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) err = user.LoginDataHTTP(w, rq, username, password)
) )
if err != "" { if err != "" {
w.Write([]byte(base(err, templates.LoginErrorHTML(err)))) w.Write([]byte(base(err, templates.LoginErrorHTML(err), user.EmptyUser())))
} else { } else {
http.Redirect(w, rq, "/", http.StatusSeeOther) http.Redirect(w, rq, "/", http.StatusSeeOther)
} }
@ -58,5 +58,5 @@ func handlerLogin(w http.ResponseWriter, rq *http.Request) {
} else { } else {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
} }
w.Write([]byte(base("Login", templates.LoginHTML()))) w.Write([]byte(base("Login", templates.LoginHTML(), user.EmptyUser())))
} }

78
http_history.go Normal file
View File

@ -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")
}

View File

@ -5,6 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util" "github.com/bouncepaw/mycorrhiza/util"
@ -15,11 +16,65 @@ func init() {
http.HandleFunc("/edit/", handlerEdit) http.HandleFunc("/edit/", handlerEdit)
http.HandleFunc("/delete-ask/", handlerDeleteAsk) http.HandleFunc("/delete-ask/", handlerDeleteAsk)
http.HandleFunc("/rename-ask/", handlerRenameAsk) http.HandleFunc("/rename-ask/", handlerRenameAsk)
http.HandleFunc("/unattach-ask/", handlerUnattachAsk)
// And those that do mutate something: // And those that do mutate something:
http.HandleFunc("/upload-binary/", handlerUploadBinary) http.HandleFunc("/upload-binary/", handlerUploadBinary)
http.HandleFunc("/upload-text/", handlerUploadText) http.HandleFunc("/upload-text/", handlerUploadText)
http.HandleFunc("/delete-confirm/", handlerDeleteConfirm) http.HandleFunc("/delete-confirm/", handlerDeleteConfirm)
http.HandleFunc("/rename-confirm/", handlerRenameConfirm) 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: <code>%v</code>", hop.Errs))
return
}
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
} }
func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) { func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) {
@ -27,13 +82,14 @@ func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) {
var ( var (
hyphaName = HyphaNameFromRq(rq, "rename-ask") hyphaName = HyphaNameFromRq(rq, "rename-ask")
_, isOld = HyphaStorage[hyphaName] _, 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.") HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.")
log.Println("Rejected", rq.URL) log.Println("Rejected", rq.URL)
return 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) { 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")) newName = CanonicalName(rq.PostFormValue("new-name"))
_, newNameIsUsed = HyphaStorage[newName] _, newNameIsUsed = HyphaStorage[newName]
recursive = rq.PostFormValue("recursive") == "true" recursive = rq.PostFormValue("recursive") == "true"
u = user.FromRequest(rq).OrAnon() u = user.FromRequest(rq)
) )
switch { switch {
case !u.CanProceed("rename-confirm"): case !u.CanProceed("rename-confirm"):
@ -79,13 +135,14 @@ func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) {
var ( var (
hyphaName = HyphaNameFromRq(rq, "delete-ask") hyphaName = HyphaNameFromRq(rq, "delete-ask")
_, isOld = HyphaStorage[hyphaName] _, 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.") HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
log.Println("Rejected", rq.URL) log.Println("Rejected", rq.URL)
return 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 // handlerDeleteConfirm deletes a hypha for sure
@ -126,8 +183,9 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
warning string warning string
textAreaFill string textAreaFill string
err error 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.") HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
log.Println("Rejected", rq.URL) log.Println("Rejected", rq.URL)
return return
@ -142,7 +200,7 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
} else { } else {
warning = `<p>You are creating a new hypha.</p>` warning = `<p>You are creating a new hypha.</p>`
} }
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. // handlerUploadText uploads a new text part for the hypha.
@ -151,9 +209,10 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
var ( var (
hyphaName = HyphaNameFromRq(rq, "upload-text") hyphaName = HyphaNameFromRq(rq, "upload-text")
textData = rq.PostFormValue("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.") HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
log.Println("Rejected", rq.URL) log.Println("Rejected", rq.URL)
return return
@ -162,7 +221,9 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed") HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed")
return 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()) HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error())
} else { } else {
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)

View File

@ -13,6 +13,7 @@ import (
"github.com/bouncepaw/mycorrhiza/markup" "github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/tree" "github.com/bouncepaw/mycorrhiza/tree"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util" "github.com/bouncepaw/mycorrhiza/util"
) )
@ -20,7 +21,6 @@ func init() {
http.HandleFunc("/page/", handlerPage) http.HandleFunc("/page/", handlerPage)
http.HandleFunc("/text/", handlerText) http.HandleFunc("/text/", handlerText)
http.HandleFunc("/binary/", handlerBinary) http.HandleFunc("/binary/", handlerBinary)
http.HandleFunc("/history/", handlerHistory)
http.HandleFunc("/rev/", handlerRevision) http.HandleFunc("/rev/", handlerRevision)
} }
@ -35,40 +35,23 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) {
contents = fmt.Sprintf(`<p>This hypha had no text at this revision.</p>`) contents = fmt.Sprintf(`<p>This hypha had no text at this revision.</p>`)
textPath = hyphaName + ".myco" textPath = hyphaName + ".myco"
textContents, err = history.FileAtRevision(textPath, revHash) textContents, err = history.FileAtRevision(textPath, revHash)
u = user.FromRequest(rq)
) )
if err == nil { if err == nil {
contents = markup.ToHtml(hyphaName, textContents) contents = markup.Doc(hyphaName, textContents).AsHTML()
} }
treeHTML, _, _ := tree.Tree(hyphaName, IterateHyphaNamesWith)
page := templates.RevisionHTML( page := templates.RevisionHTML(
rq, rq,
hyphaName, hyphaName,
naviTitle(hyphaName), naviTitle(hyphaName),
contents, contents,
tree.TreeAsHtml(hyphaName, IterateHyphaNamesWith), treeHTML,
revHash, revHash,
) )
w.Header().Set("Content-Type", "text/html;charset=utf-8") w.Header().Set("Content-Type", "text/html;charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte(base(hyphaName, page))) w.Write([]byte(base(hyphaName, page, u)))
}
// 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)))
} }
// handlerText serves raw source text of the hypha. // handlerText serves raw source text of the hypha.
@ -99,20 +82,32 @@ func handlerPage(w http.ResponseWriter, rq *http.Request) {
var ( var (
hyphaName = HyphaNameFromRq(rq, "page") hyphaName = HyphaNameFromRq(rq, "page")
data, hyphaExists = HyphaStorage[hyphaName] data, hyphaExists = HyphaStorage[hyphaName]
hasAmnt = hyphaExists && data.binaryPath != ""
contents string contents string
openGraph string
u = user.FromRequest(rq)
) )
if hyphaExists { if hyphaExists {
fileContentsT, errT := ioutil.ReadFile(data.textPath) fileContentsT, errT := ioutil.ReadFile(data.textPath)
_, errB := os.Stat(data.binaryPath) _, errB := os.Stat(data.binaryPath)
if errT == nil { 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) { if !os.IsNotExist(errB) {
contents = binaryHtmlBlock(hyphaName, data) + contents contents = binaryHtmlBlock(hyphaName, data) + contents
} }
} }
util.HTTP200Page(w, base(hyphaName, templates.PageHTML(rq, hyphaName, treeHTML, prevHypha, nextHypha := tree.Tree(hyphaName, IterateHyphaNamesWith)
util.HTTP200Page(w,
templates.BaseHTML(
hyphaName,
templates.PageHTML(rq, hyphaName,
naviTitle(hyphaName), naviTitle(hyphaName),
contents, contents,
tree.TreeAsHtml(hyphaName, IterateHyphaNamesWith)))) treeHTML, prevHypha, nextHypha,
hasAmnt),
u,
openGraph))
} }

View File

@ -8,9 +8,10 @@ import (
"mime/multipart" "mime/multipart"
"os" "os"
"path/filepath" "path/filepath"
"strings" "regexp"
"github.com/bouncepaw/mycorrhiza/history" "github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/markup" "github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util" "github.com/bouncepaw/mycorrhiza/util"
@ -33,6 +34,12 @@ func init() {
return return
} }
markup.HyphaIterate = IterateHyphaNamesWith 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. // 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 // New hyphae must be added to the hypha storage
if !isOld { if !isOld {
HyphaStorage[hyphaName] = hyphaData HyphaStorage[hyphaName] = hyphaData
hyphae.IncrementCount()
} }
*originalFullPath = fullPath *originalFullPath = fullPath
if isOld && hop.Type == history.TypeEditText && !history.FileChanged(fullPath) {
return hop.Abort()
}
return hop.WithFiles(fullPath). return hop.WithFiles(fullPath).
WithUser(u). WithUser(u).
Apply() Apply()
@ -115,6 +126,30 @@ func (hd *HyphaData) DeleteHypha(hyphaName string, u *user.User) *history.Histor
Apply() Apply()
if len(hop.Errs) == 0 { if len(hop.Errs) == 0 {
delete(HyphaStorage, hyphaName) 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 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. // 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 { func RenameHypha(hyphaName, newName string, recursive bool, u *user.User) *history.HistoryOp {
var ( var (
re = regexp.MustCompile(`(?i)` + hyphaName)
replaceName = func(str string) string { replaceName = func(str string) string {
return strings.Replace(str, hyphaName, newName, 1) return re.ReplaceAllString(CanonicalName(str), newName)
} }
hyphaNames = findHyphaeToRename(hyphaName, recursive) hyphaNames = findHyphaeToRename(hyphaName, recursive)
renameMap, err = renamingPairs(hyphaNames, replaceName) renameMap, err = renamingPairs(hyphaNames, replaceName)
@ -212,7 +248,7 @@ func binaryHtmlBlock(hyphaName string, hd *HyphaData) string {
default: default:
return fmt.Sprintf(` return fmt.Sprintf(`
<div class="binary-container binary-container_with-nothing"> <div class="binary-container binary-container_with-nothing">
<p>This hypha's media cannot be rendered. Access it <a href="/binary/%s">directly</a></p> <p>This hypha's media cannot be rendered. <a href="/binary/%s">Download it</a></p>
</div> </div>
`, hyphaName) `, hyphaName)
} }
@ -244,6 +280,7 @@ func Index(path string) {
} else { } else {
hyphaData = &HyphaData{} hyphaData = &HyphaData{}
HyphaStorage[hyphaName] = hyphaData HyphaStorage[hyphaName] = hyphaData
hyphae.IncrementCount()
} }
if isText { if isText {
hyphaData.textPath = hyphaPartPath hyphaData.textPath = hyphaPartPath
@ -276,3 +313,17 @@ func FetchTextPart(d *HyphaData) (string, error) {
} }
return string(text), nil 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)
}
}
}

38
hyphae/count.go Normal file
View File

@ -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
}

21
hyphae/hypha.go Normal file
View File

@ -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) {
}

113
main.go
View File

@ -4,16 +4,17 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/bouncepaw/mycorrhiza/history" "github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util" "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) log.Println(errMsg, "for", name)
w.Header().Set("Content-Type", "text/html;charset=utf-8") w.Header().Set("Content-Type", "text/html;charset=utf-8")
w.WriteHeader(status) w.WriteHeader(status)
fmt.Fprint(w, base(title, fmt.Sprintf( fmt.Fprint(
w,
base(
title,
fmt.Sprintf(
`<main><p>%s. <a href="/page/%s">Go back to the hypha.<a></p></main>`, `<main><p>%s. <a href="/page/%s">Go back to the hypha.<a></p></main>`,
errMsg, name))) errMsg,
name,
),
user.EmptyUser(),
),
)
} }
// Show all hyphae // Show all hyphae
@ -50,12 +60,13 @@ func handlerList(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL) log.Println(rq.URL)
var ( var (
tbody string tbody string
pageCount = len(HyphaStorage) pageCount = hyphae.Count()
u = user.FromRequest(rq)
) )
for hyphaName, data := range HyphaStorage { for hyphaName, data := range HyphaStorage {
tbody += templates.HyphaListRowHTML(hyphaName, ExtensionToMime(filepath.Ext(data.binaryPath)), data.binaryPath != "") 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. // 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) log.Println("Rejected", rq.URL)
return return
} }
hyphae.ResetCount()
HyphaStorage = make(map[string]*HyphaData) HyphaStorage = make(map[string]*HyphaData)
log.Println("Wiki storage directory is", WikiDir) log.Println("Wiki storage directory is", WikiDir)
log.Println("Start indexing hyphae...") log.Println("Start indexing hyphae...")
Index(WikiDir) 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. // Redirect to a random hypha.
func handlerRandom(w http.ResponseWriter, rq *http.Request) { func handlerRandom(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL) log.Println(rq.URL)
var randomHyphaName string var randomHyphaName string
i := rand.Intn(len(HyphaStorage)) i := rand.Intn(hyphae.Count())
for hyphaName := range HyphaStorage { for hyphaName := range HyphaStorage {
if i == 0 { if i == 0 {
randomHyphaName = hyphaName randomHyphaName = hyphaName
@ -91,20 +116,6 @@ func handlerRandom(w http.ResponseWriter, rq *http.Request) {
http.Redirect(w, rq, "/page/"+randomHyphaName, http.StatusSeeOther) 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) { func handlerStyle(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL) log.Println(rq.URL)
if _, err := os.Stat(WikiDir + "/static/common.css"); err == nil { 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() { func main() {
log.Println("Running MycorrhizaWiki β") log.Println("Running MycorrhizaWiki β")
parseCliArgs() parseCliArgs()
@ -122,26 +177,30 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
log.Println("Wiki storage directory is", WikiDir) log.Println("Wiki storage directory is", WikiDir)
log.Println("Start indexing hyphae...")
Index(WikiDir) Index(WikiDir)
log.Println("Indexed", len(HyphaStorage), "hyphae") log.Println("Indexed", hyphae.Count(), "hyphae")
history.Start(WikiDir) history.Start(WikiDir)
setHeaderLinks()
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static")))) // See http_readers.go for /page/, /text/, /binary/
// See http_readers.go for /page/, /text/, /binary/, /history/. // See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/, /rename-ask/, /rename-confirm/, /unattach-ask/, /unattach-confirm/
// See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/, /rename-ask/, /rename-confirm/.
// See http_auth.go for /login, /login-data, /logout, /logout-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("/list", handlerList)
http.HandleFunc("/reindex", handlerReindex) http.HandleFunc("/reindex", handlerReindex)
http.HandleFunc("/update-header-links", handlerUpdateHeaderLinks)
http.HandleFunc("/random", handlerRandom) 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.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
http.ServeFile(w, rq, WikiDir+"/static/favicon.ico") http.ServeFile(w, rq, WikiDir+"/static/favicon.ico")
}) })
http.HandleFunc("/static/common.css", handlerStyle) http.HandleFunc("/static/common.css", handlerStyle)
http.HandleFunc("/static/icon/", handlerIcon)
http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
http.Redirect(w, rq, "/page/"+util.HomePage, http.StatusSeeOther) 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)) log.Fatal(http.ListenAndServe("0.0.0.0:"+util.ServerPort, nil))
} }

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"github.com/bouncepaw/mycorrhiza/util"
) )
var imgRe = regexp.MustCompile(`^img\s+{`) 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 { func (img *Img) pagePathFor(path string) string {
path = strings.TrimSpace(path) path = strings.TrimSpace(path)
if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 { if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 {
@ -218,7 +228,7 @@ func (img *Img) checkLinks() map[string]bool {
} }
HyphaIterate(func(hyphaName string) { HyphaIterate(func(hyphaName string) {
for _, entry := range img.entries { for _, entry := range img.entries {
if hyphaName == entry.trimmedPath { if hyphaName == xclCanonicalName(img.hyphaName, entry.trimmedPath) {
m[entry.trimmedPath] = true m[entry.trimmedPath] = true
} }
} }

View File

@ -9,6 +9,9 @@ import (
// HyphaExists holds function that checks that a hypha is present. // HyphaExists holds function that checks that a hypha is present.
var HyphaExists func(string) bool var HyphaExists func(string) bool
//
var HyphaImageForOG func(string) string
// HyphaAccess holds function that accesses a hypha by its name. // HyphaAccess holds function that accesses a hypha by its name.
var HyphaAccess func(string) (rawText, binaryHtml string, err error) var HyphaAccess func(string) (rawText, binaryHtml string, err error)
@ -25,28 +28,36 @@ type GemLexerState struct {
buf string buf string
// Temporaries // Temporaries
img *Img img *Img
table *Table
} }
type Line struct { type Line struct {
id int 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{} contents interface{}
} }
func lex(name, content string) (ast []Line) { func (md *MycoDoc) lex() (ast []Line) {
var state = GemLexerState{name: name} var state = GemLexerState{name: md.hyphaName}
for _, line := range append(strings.Split(content, "\n"), "") { for _, line := range append(strings.Split(md.contents, "\n"), "") {
geminiLineToAST(line, &state, &ast) lineToAST(line, &state, &ast)
} }
return ast return ast
} }
// Lex `line` in markup and save it to `ast` using `state`. // 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{}) { addLine := func(text interface{}) {
*ast = append(*ast, Line{id: state.id, contents: text}) *ast = append(*ast, Line{id: state.id, contents: text})
} }
addParagraphIfNeeded := func() {
if state.where == "p" {
state.where = ""
addLine(fmt.Sprintf("<p id='%d'>%s</p>", state.id, strings.ReplaceAll(ParagraphToHtml(state.name, state.buf), "\n", "<br>")))
state.buf = ""
}
}
// Process empty lines depending on the current state // Process empty lines depending on the current state
if "" == strings.TrimSpace(line) { if "" == strings.TrimSpace(line) {
@ -59,6 +70,11 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
addLine(state.buf + "</ol>") addLine(state.buf + "</ol>")
case "pre": case "pre":
state.buf += "\n" state.buf += "\n"
case "launchpad":
state.where = ""
addLine(state.buf + "</ul>")
case "p":
addParagraphIfNeeded()
} }
return return
} }
@ -74,13 +90,17 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
switch state.where { switch state.where {
case "img": case "img":
goto imgState goto imgState
case "table":
goto tableState
case "pre": case "pre":
goto preformattedState goto preformattedState
case "list": case "list":
goto listState goto listState
case "number": case "number":
goto numberState goto numberState
default: case "launchpad":
goto launchpadState
default: // "p" or ""
goto normalState goto normalState
} }
@ -91,6 +111,13 @@ imgState:
} }
return return
tableState:
if shouldGoBackToNormal := state.table.Process(line); shouldGoBackToNormal {
state.where = ""
addLine(*state.table)
}
return
preformattedState: preformattedState:
switch { switch {
case startsWith("```"): case startsWith("```"):
@ -135,48 +162,84 @@ numberState:
} }
return return
launchpadState:
switch {
case startsWith("=>"):
href, text, class := Rocketlink(line, state.name)
state.buf += fmt.Sprintf(` <li class="launchpad__entry"><a class="rocketlink %s" href="%s">%s</a></li>`, class, href, text)
case startsWith("```"):
state.where = "pre"
addLine(state.buf + "</ul>")
state.id++
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
default:
state.where = ""
addLine(state.buf + "</ul>")
goto normalState
}
return
normalState: normalState:
state.id++ state.id++
switch { switch {
case startsWith("```"): case startsWith("```"):
addParagraphIfNeeded()
state.where = "pre" state.where = "pre"
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```")) state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
case startsWith("* "): case startsWith("* "):
addParagraphIfNeeded()
state.where = "list" state.where = "list"
state.buf = fmt.Sprintf("<ul id='%d'>\n", state.id) state.buf = fmt.Sprintf("<ul id='%d'>\n", state.id)
goto listState goto listState
case startsWith("*. "): case startsWith("*. "):
addParagraphIfNeeded()
state.where = "number" state.where = "number"
state.buf = fmt.Sprintf("<ol id='%d'>\n", state.id) state.buf = fmt.Sprintf("<ol id='%d'>\n", state.id)
goto numberState goto numberState
case startsWith("###### "): case startsWith("###### "):
addParagraphIfNeeded()
addHeading(6) addHeading(6)
case startsWith("##### "): case startsWith("##### "):
addParagraphIfNeeded()
addHeading(5) addHeading(5)
case startsWith("#### "): case startsWith("#### "):
addParagraphIfNeeded()
addHeading(4) addHeading(4)
case startsWith("### "): case startsWith("### "):
addParagraphIfNeeded()
addHeading(3) addHeading(3)
case startsWith("## "): case startsWith("## "):
addParagraphIfNeeded()
addHeading(2) addHeading(2)
case startsWith("# "): case startsWith("# "):
addParagraphIfNeeded()
addHeading(1) addHeading(1)
case startsWith(">"): case startsWith(">"):
addLine(fmt.Sprintf( addParagraphIfNeeded()
"<blockquote id='%d'>%s</blockquote>", state.id, remover(">")(line))) addLine(
fmt.Sprintf(
"<blockquote id='%d'>%s</blockquote>",
state.id,
ParagraphToHtml(state.name, remover(">")(line)),
),
)
case startsWith("=>"): case startsWith("=>"):
href, text, class := Rocketlink(line, state.name) addParagraphIfNeeded()
addLine(fmt.Sprintf( state.where = "launchpad"
`<p><a id='%d' class='rocketlink %s' href="%s">%s</a></p>`, state.id, class, href, text)) state.buf = fmt.Sprintf("<ul class='launchpad' id='%d'>\n", state.id)
goto launchpadState
case startsWith("<="): case startsWith("<="):
addParagraphIfNeeded()
addLine(parseTransclusion(line, state.name)) addLine(parseTransclusion(line, state.name))
case line == "----": case line == "----":
addParagraphIfNeeded()
*ast = append(*ast, Line{id: -1, contents: "<hr/>"}) *ast = append(*ast, Line{id: -1, contents: "<hr/>"})
case MatchesImg(line): case MatchesImg(line):
addParagraphIfNeeded()
img, shouldGoBackToNormal := ImgFromFirstLine(line, state.name) img, shouldGoBackToNormal := ImgFromFirstLine(line, state.name)
if shouldGoBackToNormal { if shouldGoBackToNormal {
addLine(*img) addLine(*img)
@ -184,7 +247,15 @@ normalState:
state.where = "img" state.where = "img"
state.img = 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: default:
addLine(fmt.Sprintf("<p id='%d'>%s</p>", state.id, ParagraphToHtml(state.name, line))) state.where = "p"
state.buf = line
} }
} }

View File

@ -1,6 +1,7 @@
package markup package markup
import ( import (
"fmt"
"path" "path"
"strings" "strings"
) )
@ -15,11 +16,19 @@ func LinkParts(addr, display, hyphaName string) (href, text, class string) {
} else { } else {
text = strings.TrimSpace(display) text = strings.TrimSpace(display)
} }
class = "wikilink_internal" class = "wikilink wikilink_internal"
switch { switch {
case strings.ContainsRune(addr, ':'): 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, "/"): case strings.HasPrefix(addr, "/"):
return addr, text, class return addr, text, class
case strings.HasPrefix(addr, "./"): case strings.HasPrefix(addr, "./"):

View File

@ -2,8 +2,12 @@
package markup package markup
import ( import (
"fmt"
"html" "html"
"regexp"
"strings" "strings"
"github.com/bouncepaw/mycorrhiza/util"
) )
// A Mycomarkup-formatted document // A Mycomarkup-formatted document
@ -11,26 +15,79 @@ type MycoDoc struct {
// data // data
hyphaName string hyphaName string
contents string contents string
// indicators
// state parsedAlready bool
recursionDepth int
// results // results
ast []Line
html string
firstImageURL string
description string
} }
// Constructor // Constructor
func Doc(hyphaName, contents string) *MycoDoc { func Doc(hyphaName, contents string) *MycoDoc {
return &MycoDoc{ md := &MycoDoc{
hyphaName: hyphaName, hyphaName: hyphaName,
contents: contents, 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 // AsHtml returns an html representation of the document
func (md *MycoDoc) AsHtml() string { func (md *MycoDoc) AsHTML() string {
return "" 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(`<meta property="og:%s" content="%s"/>`, property, content)
}
/* The rest of this file is currently unused. TODO: use it I guess */
type BlockType int type BlockType int
const ( const (

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"html" "html"
"strings" "strings"
"unicode"
) )
type spanTokenType int 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 { func getLinkNode(input *bytes.Buffer, hyphaName string, isBracketedLink bool) string {
input.Next(2) if isBracketedLink {
input.Next(2) // drop those [[
}
var ( var (
escaping = false escaping = false
addrBuf = bytes.Buffer{} addrBuf = bytes.Buffer{}
@ -47,11 +50,13 @@ func getLinkNode(input *bytes.Buffer, hyphaName string) string {
if escaping { if escaping {
currBuf.WriteByte(b) currBuf.WriteByte(b)
escaping = false escaping = false
} else if b == '|' && currBuf == &addrBuf { } else if isBracketedLink && b == '|' && currBuf == &addrBuf {
currBuf = &displayBuf currBuf = &displayBuf
} else if b == ']' && bytes.HasPrefix(input.Bytes(), []byte{']'}) { } else if isBracketedLink && b == ']' && bytes.HasPrefix(input.Bytes(), []byte{']'}) {
input.Next(1) input.Next(1)
break break
} else if !isBracketedLink && unicode.IsSpace(rune(b)) {
break
} else { } else {
currBuf.WriteByte(b) currBuf.WriteByte(b)
} }
@ -65,6 +70,12 @@ func getTextNode(input *bytes.Buffer) string {
var ( var (
textNodeBuffer = bytes.Buffer{} textNodeBuffer = bytes.Buffer{}
escaping = false 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) // Always read the first byte in advance to avoid endless loops that kill computers (sad experience)
if input.Len() != 0 { if input.Len() != 0 {
@ -82,6 +93,9 @@ func getTextNode(input *bytes.Buffer) string {
} else if strings.IndexByte("/*`^,![~", b) >= 0 { } else if strings.IndexByte("/*`^,![~", b) >= 0 {
input.UnreadByte() input.UnreadByte()
break break
} else if couldBeLinkStart() {
textNodeBuffer.WriteByte(b)
break
} else { } else {
textNodeBuffer.WriteByte(b) textNodeBuffer.WriteByte(b)
} }
@ -106,6 +120,9 @@ func ParagraphToHtml(hyphaName, input string) string {
startsWith = func(t string) bool { startsWith = func(t string) bool {
return bytes.HasPrefix(p.Bytes(), []byte(t)) 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 { for p.Len() != 0 {
@ -132,7 +149,9 @@ func ParagraphToHtml(hyphaName, input string) string {
ret.WriteString(tagFromState(spanMark, tagState, "s", "~~")) ret.WriteString(tagFromState(spanMark, tagState, "s", "~~"))
p.Next(2) p.Next(2)
case startsWith("[["): 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: default:
ret.WriteString(html.EscapeString(getTextNode(p))) ret.WriteString(html.EscapeString(getTextNode(p)))
} }

View File

@ -1,35 +1,26 @@
package markup package markup
import ()
const maxRecursionLevel = 3 const maxRecursionLevel = 3
type GemParserState struct { func Parse(ast []Line, from, to int, recursionLevel int) (html string) {
recursionLevel int if recursionLevel > maxRecursionLevel {
}
func Parse(ast []Line, from, to int, state GemParserState) (html string) {
if state.recursionLevel > maxRecursionLevel {
return "Transclusion depth limit" return "Transclusion depth limit"
} }
for _, line := range ast { for _, line := range ast {
if line.id >= from && (line.id <= to || to == 0) || line.id == -1 { if line.id >= from && (line.id <= to || to == 0) || line.id == -1 {
switch v := line.contents.(type) { switch v := line.contents.(type) {
case Transclusion: case Transclusion:
html += Transclude(v, state) html += Transclude(v, recursionLevel)
case Img: case Img:
html += v.ToHtml() html += v.ToHtml()
case Table:
html += v.asHtml()
case string: case string:
html += v html += v
default: default:
html += "Unknown" html += "<b class='error'>Unknown element.</b>"
} }
} }
} }
return html return html
} }
func ToHtml(name, text string) string {
state := GemParserState{}
return Parse(lex(name, text), 0, 0, state)
}

231
markup/table.go Normal file
View File

@ -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("<caption>%s</caption>", t.caption)
}
if len(t.rows) > 0 && t.rows[0].looksLikeThead() {
html += fmt.Sprintf("<thead>%s</thead>", t.rows[0].asHtml(t.hyphaName))
t.rows = t.rows[1:]
}
html += "\n<tbody>\n"
for _, tr := range t.rows {
html += tr.asHtml(t.hyphaName)
}
return fmt.Sprintf(`<table>%s</tbody></table>`, 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("<tr>%s</tr>\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 += `<br>`
}
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"
}
}

View File

@ -17,14 +17,14 @@ type Transclusion struct {
} }
// Transclude transcludes `xcl` and returns html representation. // Transclude transcludes `xcl` and returns html representation.
func Transclude(xcl Transclusion, state GemParserState) (html string) { func Transclude(xcl Transclusion, recursionLevel int) (html string) {
state.recursionLevel++ recursionLevel++
tmptOk := `<section class="transclusion transclusion_ok"> tmptOk := `<section class="transclusion transclusion_ok">
<a class="transclusion__link" href="/page/%s">%s</a> <a class="transclusion__link" href="/page/%s">%s</a>
<div class="transclusion__content">%s</div> <div class="transclusion__content">%s</div>
</section>` </section>`
tmptFailed := `<section class="transclusion transclusion_failed"> tmptFailed := `<section class="transclusion transclusion_failed">
<p>Failed to transclude <a href="/page/%s">%s</a></p> <p class="error">Hypha <a class="wikilink_new" href="/page/%s">%s</a> does not exist</p>
</section>` </section>`
if xcl.from == xclError || xcl.to == xclError || xcl.from > xcl.to { if xcl.from == xclError || xcl.to == xclError || xcl.from > xcl.to {
return fmt.Sprintf(tmptFailed, xcl.name, xcl.name) return fmt.Sprintf(tmptFailed, xcl.name, xcl.name)
@ -34,7 +34,8 @@ func Transclude(xcl Transclusion, state GemParserState) (html string) {
if err != nil { if err != nil {
return fmt.Sprintf(tmptFailed, xcl.name, xcl.name) 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) return fmt.Sprintf(tmptOk, xcl.name, xcl.name, binaryHtml+xclText)
} }

@ -1 +1 @@
Subproject commit 7828352598c19afe5f2e13df0219656ac7b44c9c Subproject commit be5b922e9b564551601d21ed45bf7d9ced65c6bb

20
name.go
View File

@ -23,16 +23,24 @@ func CanonicalName(name string) string {
func naviTitle(canonicalName string) string { func naviTitle(canonicalName string) string {
var ( var (
html = fmt.Sprintf(`<h1 class="navi-title" id="navi-title"> html = fmt.Sprintf(`<h1 class="navi-title" id="navi-title">
<a href="/page/%s">%s</a>`, util.HomePage, util.SiteTitle) <a href="/page/%s">%s</a><span aria-hidden="true" class="navi-title__colon">:</span>`, util.HomePage, util.SiteNavIcon)
prevAcc = `/page/` prevAcc = `/page/`
parts = strings.Split(canonicalName, "/") parts = strings.Split(canonicalName, "/")
rel = "up"
) )
for _, part := range parts { for i, part := range parts {
html += fmt.Sprintf(` if i > 0 {
<span aria-hidden="true">/</span> html += `<span aria-hidden="true" class="navi-title__separator">/</span>`
<a href="%s">%s</a>`, }
if i == len(parts)-1 {
rel = "bookmark"
}
html += fmt.Sprintf(
`<a href="%s" rel="%s">%s</a>`,
prevAcc+part, prevAcc+part,
strings.Title(part)) rel,
util.BeautifulName(part),
)
prevAcc += part + "/" prevAcc += part + "/"
} }
return html + "</h1>" return html + "</h1>"

21
templates/asset.qtpl Normal file
View File

@ -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 %}

414
templates/asset.qtpl.go Normal file
View File

@ -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(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-2.29-2.333A17.9 17.9 0 0 1 8.027 13H4.062a8.008 8.008 0 0 0 5.648 6.667zM10.03 13c.151 2.439.848 4.73 1.97 6.752A15.905 15.905 0 0 0 13.97 13h-3.94zm9.908 0h-3.965a17.9 17.9 0 0 1-1.683 6.667A8.008 8.008 0 0 0 19.938 13zM4.062 11h3.965A17.9 17.9 0 0 1 9.71 4.333 8.008 8.008 0 0 0 4.062 11zm5.969 0h3.938A15.905 15.905 0 0 0 12 4.248 15.905 15.905 0 0 0 10.03 11zm4.259-6.667A17.9 17.9 0 0 1 15.973 11h3.965a8.008 8.008 0 0 0-5.648-6.667z"/></svg>
`)
//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(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M15.502 20A6.523 6.523 0 0 1 12 23.502 6.523 6.523 0 0 1 8.498 20h2.26c.326.489.747.912 1.242 1.243.495-.33.916-.754 1.243-1.243h2.259zM18 14.805l2 2.268V19H4v-1.927l2-2.268V9c0-3.483 2.504-6.447 6-7.545C15.496 2.553 18 5.517 18 9v5.805zM17.27 17L16 15.56V9c0-2.318-1.57-4.43-4-5.42C9.57 4.57 8 6.681 8 9v6.56L6.73 17h10.54zM12 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>
`)
//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(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm17 4.238l-7.928 7.1L4 7.216V19h16V7.238zM4.511 5l7.55 6.662L19.502 5H4.511z"/></svg>
`)
//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(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16">
<path fill="#999" d="M447.238,204.944v-70.459c0-8.836-7.164-16-16-16c-34.051,0-64.414,21.118-75.079,55.286
C226.094,41.594,0,133.882,0,319.435c0,0.071,0.01,0.14,0.011,0.21c0.116,44.591,36.423,80.833,81.04,80.833h171.203
c8.836,0,16-7.164,16-16c0-8.836-7.164-16-16-16H81.051c-21.441,0-39.7-13.836-46.351-33.044H496c8.836,0,16-7.164,16-16
C512,271.82,486.82,228.692,447.238,204.944z M415.238,153.216v37.805c-10.318-2.946-19.556-4.305-29.342-4.937
C390.355,168.611,402.006,157.881,415.238,153.216z M295.484,303.435L295.484,303.435c-7.562-41.495-43.948-73.062-87.593-73.062
c-8.836,0-16,7.164-16,16c0,8.836,7.164,16,16,16c25.909,0,47.826,17.364,54.76,41.062H32.722
c14.415-159.15,218.064-217.856,315.136-90.512c3.545,4.649,9.345,6.995,15.124,6.118
c55.425-8.382,107.014,29.269,115.759,84.394H295.484z"/>
<circle fill="#999" cx="415.238" cy="260.05" r="21.166"/>
</svg>
`)
//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
}

View File

@ -6,7 +6,7 @@
{% if user.AuthUsed %} {% if user.AuthUsed %}
<h1>Login</h1> <h1>Login</h1>
<form method="post" action="/login-data" id="login-form" enctype="multipart/form-data"> <form method="post" action="/login-data" id="login-form" enctype="multipart/form-data">
<p>Use the data you were given by the administrator.</p> <p>Use the data you were given by an administrator.</p>
<fieldset> <fieldset>
<legend>Username</legend> <legend>Username</legend>
<input type="text" required autofocus name="username" autocomplete="on"> <input type="text" required autofocus name="username" autocomplete="on">
@ -15,7 +15,7 @@
<legend>Password</legend> <legend>Password</legend>
<input type="password" required name="password" autocomplete="on"> <input type="password" required name="password" autocomplete="on">
</fieldset> </fieldset>
<p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.</p> <p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you. You will stay logged in until you log out.</p>
<input type="submit"> <input type="submit">
<a href="/">Cancel</a> <a href="/">Cancel</a>
</form> </form>

View File

@ -33,7 +33,7 @@ func StreamLoginHTML(qw422016 *qt422016.Writer) {
qw422016.N().S(` qw422016.N().S(`
<h1>Login</h1> <h1>Login</h1>
<form method="post" action="/login-data" id="login-form" enctype="multipart/form-data"> <form method="post" action="/login-data" id="login-form" enctype="multipart/form-data">
<p>Use the data you were given by the administrator.</p> <p>Use the data you were given by an administrator.</p>
<fieldset> <fieldset>
<legend>Username</legend> <legend>Username</legend>
<input type="text" required autofocus name="username" autocomplete="on"> <input type="text" required autofocus name="username" autocomplete="on">
@ -42,7 +42,7 @@ func StreamLoginHTML(qw422016 *qt422016.Writer) {
<legend>Password</legend> <legend>Password</legend>
<input type="password" required name="password" autocomplete="on"> <input type="password" required name="password" autocomplete="on">
</fieldset> </fieldset>
<p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.</p> <p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you. You will stay logged in until you log out.</p>
<input type="submit"> <input type="submit">
<a href="/">Cancel</a> <a href="/">Cancel</a>
</form> </form>

View File

@ -21,31 +21,39 @@ var navEntries = []navEntry{
{% func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) %} {% func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) %}
{% code {% code
u := user.FromRequest(rq).OrAnon() u := user.FromRequest(rq)
%} %}
<nav class="navlinks">
<ul> <nav class="hypha-tabs">
<ul class="hypha-tabs__flex">
{%- for _, entry := range navEntries -%} {%- for _, entry := range navEntries -%}
{%- if navType == "revision" && entry.path == "revision" -%} {%- if navType == "revision" && entry.path == "revision" -%}
<li><b>{%s revisionHash[0] %}</b></li> <li class="hypha-tabs__tab hypha-tabs__tab_active">
{%s revisionHash[0] %}
</li>
{%- elseif navType == entry.path -%} {%- elseif navType == entry.path -%}
<li><b>{%s entry.title %}</b></li> <li class="hypha-tabs__tab hypha-tabs__tab_active">
{%- elseif entry.path != "revision" && u.Group.CanAccessRoute(entry.path) -%} {%s entry.title %}
<li><a href="/{%s entry.path %}/{%s hyphaName %}">{%s entry.title %}</a></li> </li>
{%- elseif entry.path != "revision" && u.CanProceed(entry.path) -%}
<li class="hypha-tabs__tab">
<a href="/{%s entry.path %}/{%s hyphaName %}">{%s entry.title %}</a>
</li>
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor -%}
{%s= userMenuHTML(u) %}
</ul> </ul>
</nav> </nav>
{% endfunc %} {% endfunc %}
{% func userMenuHTML(u *user.User) %} {% func userMenuHTML(u *user.User) %}
<li class="navlinks__user"> {% if user.AuthUsed %}
{% if u.Group == user.UserAnon %} <li class="header-links__entry header-links__entry_user">
<a href="/login">Login</a> {% if u.Group == "anon" %}
<a href="/login" class="header-links__link">Login</a>
{% else %} {% else %}
<a href="/page/{%s util.UserTree %}/{%s u.Name %}">{%s u.Name %}</a> <a href="/page/{%s util.UserHypha %}/{%s u.Name %}" class="header-links__link">{%s u.Name %}</a>
{% endif %} {% endif %}
</li> </li>
{% endif %}
{% endfunc %} {% endfunc %}

View File

@ -50,153 +50,165 @@ func streamnavHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, navTy
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/common.qtpl:24 //line templates/common.qtpl:24
u := user.FromRequest(rq).OrAnon() u := user.FromRequest(rq)
//line templates/common.qtpl:25 //line templates/common.qtpl:25
qw422016.N().S(` qw422016.N().S(`
<nav class="navlinks">
<ul> <nav class="hypha-tabs">
<ul class="hypha-tabs__flex">
`) `)
//line templates/common.qtpl:28 //line templates/common.qtpl:29
for _, entry := range navEntries { for _, entry := range navEntries {
//line templates/common.qtpl:29 //line templates/common.qtpl:30
if navType == "revision" && entry.path == "revision" { if navType == "revision" && entry.path == "revision" {
//line templates/common.qtpl:29
qw422016.N().S(` <li><b>`)
//line templates/common.qtpl:30 //line templates/common.qtpl:30
qw422016.N().S(` <li class="hypha-tabs__tab hypha-tabs__tab_active">
`)
//line templates/common.qtpl:32
qw422016.E().S(revisionHash[0]) qw422016.E().S(revisionHash[0])
//line templates/common.qtpl:30
qw422016.N().S(`</b></li>
`)
//line templates/common.qtpl:31
} else if navType == entry.path {
//line templates/common.qtpl:31
qw422016.N().S(` <li><b>`)
//line templates/common.qtpl:32 //line templates/common.qtpl:32
qw422016.E().S(entry.title)
//line templates/common.qtpl:32
qw422016.N().S(`</b></li>
`)
//line templates/common.qtpl:33
} else if entry.path != "revision" && u.Group.CanAccessRoute(entry.path) {
//line templates/common.qtpl:33
qw422016.N().S(` <li><a href="/`)
//line templates/common.qtpl:34
qw422016.E().S(entry.path)
//line templates/common.qtpl:34
qw422016.N().S(`/`)
//line templates/common.qtpl:34
qw422016.E().S(hyphaName)
//line templates/common.qtpl:34
qw422016.N().S(`">`)
//line templates/common.qtpl:34
qw422016.E().S(entry.title)
//line templates/common.qtpl:34
qw422016.N().S(`</a></li>
`)
//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(`
</ul>
</nav>
`)
//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(`
<li class="navlinks__user">
`)
//line templates/common.qtpl:44
if u.Group == user.UserAnon {
//line templates/common.qtpl:44
qw422016.N().S(`
<a href="/login">Login</a>
`)
//line templates/common.qtpl:46
} else {
//line templates/common.qtpl:46
qw422016.N().S(`
<a href="/page/`)
//line templates/common.qtpl:47
qw422016.E().S(util.UserTree)
//line templates/common.qtpl:47
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:47
qw422016.E().S(u.Name)
//line templates/common.qtpl:47
qw422016.N().S(`</a>
`)
//line templates/common.qtpl:48
}
//line templates/common.qtpl:48
qw422016.N().S(` qw422016.N().S(`
</li> </li>
`) `)
//line templates/common.qtpl:50 //line templates/common.qtpl:34
} else if navType == entry.path {
//line templates/common.qtpl:34
qw422016.N().S(` <li class="hypha-tabs__tab hypha-tabs__tab_active">
`)
//line templates/common.qtpl:36
qw422016.E().S(entry.title)
//line templates/common.qtpl:36
qw422016.N().S(`
</li>
`)
//line templates/common.qtpl:38
} else if entry.path != "revision" && u.CanProceed(entry.path) {
//line templates/common.qtpl:38
qw422016.N().S(` <li class="hypha-tabs__tab">
<a href="/`)
//line templates/common.qtpl:40
qw422016.E().S(entry.path)
//line templates/common.qtpl:40
qw422016.N().S(`/`)
//line templates/common.qtpl:40
qw422016.E().S(hyphaName)
//line templates/common.qtpl:40
qw422016.N().S(`">`)
//line templates/common.qtpl:40
qw422016.E().S(entry.title)
//line templates/common.qtpl:40
qw422016.N().S(`</a>
</li>
`)
//line templates/common.qtpl:42
}
//line templates/common.qtpl:43
}
//line templates/common.qtpl:43
qw422016.N().S(` </ul>
</nav>
`)
//line templates/common.qtpl:46
} }
//line templates/common.qtpl:50 //line templates/common.qtpl:46
func writeuserMenuHTML(qq422016 qtio422016.Writer, u *user.User) { func writenavHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, navType string, revisionHash ...string) {
//line templates/common.qtpl:50 //line templates/common.qtpl:46
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/common.qtpl:50 //line templates/common.qtpl:46
streamuserMenuHTML(qw422016, u) streamnavHTML(qw422016, rq, hyphaName, navType, revisionHash...)
//line templates/common.qtpl:50 //line templates/common.qtpl:46
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/common.qtpl:50 //line templates/common.qtpl:46
} }
//line templates/common.qtpl:50 //line templates/common.qtpl:46
func userMenuHTML(u *user.User) string { func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) string {
//line templates/common.qtpl:50 //line templates/common.qtpl:46
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/common.qtpl:50 //line templates/common.qtpl:46
writeuserMenuHTML(qb422016, u) writenavHTML(qb422016, rq, hyphaName, navType, revisionHash...)
//line templates/common.qtpl:50 //line templates/common.qtpl:46
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/common.qtpl:50 //line templates/common.qtpl:46
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/common.qtpl:50 //line templates/common.qtpl:46
return qs422016 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(`
<li class="header-links__entry header-links__entry_user">
`)
//line templates/common.qtpl:51
if u.Group == "anon" {
//line templates/common.qtpl:51
qw422016.N().S(`
<a href="/login" class="header-links__link">Login</a>
`)
//line templates/common.qtpl:53
} else {
//line templates/common.qtpl:53
qw422016.N().S(`
<a href="/page/`)
//line templates/common.qtpl:54
qw422016.E().S(util.UserHypha)
//line templates/common.qtpl:54
qw422016.N().S(`/`)
//line templates/common.qtpl:54
qw422016.E().S(u.Name)
//line templates/common.qtpl:54
qw422016.N().S(`" class="header-links__link">`)
//line templates/common.qtpl:54
qw422016.E().S(u.Name)
//line templates/common.qtpl:54
qw422016.N().S(`</a>
`)
//line templates/common.qtpl:55
}
//line templates/common.qtpl:55
qw422016.N().S(`
</li>
`)
//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
} }

View File

@ -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 %}

View File

@ -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
}

183
templates/default.css Normal file
View File

@ -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; }
}
}

View File

@ -2,8 +2,8 @@
This dialog is to be shown to a user when they try to delete a hypha. 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) %} {% func DeleteAskHTML(rq *http.Request, hyphaName string, isOld bool) %}
<main>
{%= navHTML(rq, hyphaName, "delete-ask") %} {%= navHTML(rq, hyphaName, "delete-ask") %}
<main>
{% if isOld %} {% if isOld %}
<section> <section>
<h1>Delete {%s hyphaName %}?</h1> <h1>Delete {%s hyphaName %}?</h1>

View File

@ -26,12 +26,12 @@ var (
func StreamDeleteAskHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName string, isOld bool) { func StreamDeleteAskHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
//line templates/delete.qtpl:4 //line templates/delete.qtpl:4
qw422016.N().S(` qw422016.N().S(`
<main>
`) `)
//line templates/delete.qtpl:6 //line templates/delete.qtpl:5
streamnavHTML(qw422016, rq, hyphaName, "delete-ask") streamnavHTML(qw422016, rq, hyphaName, "delete-ask")
//line templates/delete.qtpl:6 //line templates/delete.qtpl:5
qw422016.N().S(` qw422016.N().S(`
<main>
`) `)
//line templates/delete.qtpl:7 //line templates/delete.qtpl:7
if isOld { if isOld {

View File

@ -1,16 +1,35 @@
{% import "net/http" %} {% import "net/http" %}
{% func EditHTML(rq *http.Request, hyphaName, textAreaFill, warning string) %} {% func EditHTML(rq *http.Request, hyphaName, textAreaFill, warning string) %}
<main class="edit">
{%s= navHTML(rq, hyphaName, "edit") %} {%s= navHTML(rq, hyphaName, "edit") %}
<main class="edit edit_no-preview">
<h1>Edit {%s hyphaName %}</h1> <h1>Edit {%s hyphaName %}</h1>
{%s= warning %} {%s= warning %}
<form method="post" class="edit-form" <form method="post" class="edit-form"
action="/upload-text/{%s hyphaName %}"> action="/upload-text/{%s hyphaName %}">
<textarea name="text">{%s textAreaFill %}</textarea> <textarea name="text">{%s textAreaFill %}</textarea>
<br/> <br/>
<input type="submit"/> <input type="submit" name="action" value="Save" class="edit-form__save"/>
<a href="/page/{%s hyphaName %}">Cancel</a> <input type="submit" name="action" value="Preview" class="edit-form__preview">
<a href="/page/{%s hyphaName %}" class="edit-form__cancel">Cancel</a>
</form> </form>
</main> </main>
{% endfunc %} {% endfunc %}
{% func PreviewHTML(rq *http.Request, hyphaName, textAreaFill, warning string, renderedPage string) %}
{%s= navHTML(rq, hyphaName, "edit") %}
<main class="edit edit_with-preview">
<h1>Edit {%s hyphaName %} (preview)</h1>
{%s= warning %}
<form method="post" class="edit-form"
action="/upload-text/{%s hyphaName %}">
<textarea name="text">{%s textAreaFill %}</textarea>
<br/>
<input type="submit" name="action" value="Save" class="edit-form__save"/>
<input type="submit" name="action" value="Preview" class="edit-form__preview">
<a href="/page/{%s hyphaName %}" class="edit-form__cancel">Cancel</a>
</form>
<p class="warning">Note that the hypha is not saved yet. You can preview the changes ↓</p>
<section class="edit__preview">{%s= renderedPage %}</section>
</main>
{% endfunc %}

View File

@ -24,12 +24,12 @@ var (
func StreamEditHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, textAreaFill, warning string) { func StreamEditHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, textAreaFill, warning string) {
//line templates/http_mutators.qtpl:3 //line templates/http_mutators.qtpl:3
qw422016.N().S(` qw422016.N().S(`
<main class="edit">
`) `)
//line templates/http_mutators.qtpl:5 //line templates/http_mutators.qtpl:4
qw422016.N().S(navHTML(rq, hyphaName, "edit")) qw422016.N().S(navHTML(rq, hyphaName, "edit"))
//line templates/http_mutators.qtpl:5 //line templates/http_mutators.qtpl:4
qw422016.N().S(` qw422016.N().S(`
<main class="edit edit_no-preview">
<h1>Edit `) <h1>Edit `)
//line templates/http_mutators.qtpl:6 //line templates/http_mutators.qtpl:6
qw422016.E().S(hyphaName) qw422016.E().S(hyphaName)
@ -52,40 +52,118 @@ func StreamEditHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, text
//line templates/http_mutators.qtpl:10 //line templates/http_mutators.qtpl:10
qw422016.N().S(`</textarea> qw422016.N().S(`</textarea>
<br/> <br/>
<input type="submit"/> <input type="submit" name="action" value="Save" class="edit-form__save"/>
<input type="submit" name="action" value="Preview" class="edit-form__preview">
<a href="/page/`) <a href="/page/`)
//line templates/http_mutators.qtpl:13 //line templates/http_mutators.qtpl:14
qw422016.E().S(hyphaName) qw422016.E().S(hyphaName)
//line templates/http_mutators.qtpl:13 //line templates/http_mutators.qtpl:14
qw422016.N().S(`">Cancel</a> qw422016.N().S(`" class="edit-form__cancel">Cancel</a>
</form> </form>
</main> </main>
`) `)
//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) { 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) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/http_mutators.qtpl:16 //line templates/http_mutators.qtpl:17
StreamEditHTML(qw422016, rq, hyphaName, textAreaFill, warning) StreamEditHTML(qw422016, rq, hyphaName, textAreaFill, warning)
//line templates/http_mutators.qtpl:16 //line templates/http_mutators.qtpl:17
qt422016.ReleaseWriter(qw422016) 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 { 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() qb422016 := qt422016.AcquireByteBuffer()
//line templates/http_mutators.qtpl:16 //line templates/http_mutators.qtpl:17
WriteEditHTML(qb422016, rq, hyphaName, textAreaFill, warning) WriteEditHTML(qb422016, rq, hyphaName, textAreaFill, warning)
//line templates/http_mutators.qtpl:16 //line templates/http_mutators.qtpl:17
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/http_mutators.qtpl:16 //line templates/http_mutators.qtpl:17
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/http_mutators.qtpl:16 //line templates/http_mutators.qtpl:17
return qs422016 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(`
<main class="edit edit_with-preview">
<h1>Edit `)
//line templates/http_mutators.qtpl:22
qw422016.E().S(hyphaName)
//line templates/http_mutators.qtpl:22
qw422016.N().S(` (preview)</h1>
`)
//line templates/http_mutators.qtpl:23
qw422016.N().S(warning)
//line templates/http_mutators.qtpl:23
qw422016.N().S(`
<form method="post" class="edit-form"
action="/upload-text/`)
//line templates/http_mutators.qtpl:25
qw422016.E().S(hyphaName)
//line templates/http_mutators.qtpl:25
qw422016.N().S(`">
<textarea name="text">`)
//line templates/http_mutators.qtpl:26
qw422016.E().S(textAreaFill)
//line templates/http_mutators.qtpl:26
qw422016.N().S(`</textarea>
<br/>
<input type="submit" name="action" value="Save" class="edit-form__save"/>
<input type="submit" name="action" value="Preview" class="edit-form__preview">
<a href="/page/`)
//line templates/http_mutators.qtpl:30
qw422016.E().S(hyphaName)
//line templates/http_mutators.qtpl:30
qw422016.N().S(`" class="edit-form__cancel">Cancel</a>
</form>
<p class="warning">Note that the hypha is not saved yet. You can preview the changes </p>
<section class="edit__preview">`)
//line templates/http_mutators.qtpl:33
qw422016.N().S(renderedPage)
//line templates/http_mutators.qtpl:33
qw422016.N().S(`</section>
</main>
`)
//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
} }

View File

@ -1,27 +1,20 @@
{% import "net/http" %} {% import "net/http" %}
{% import "path" %}
{% import "github.com/bouncepaw/mycorrhiza/user" %} {% import "github.com/bouncepaw/mycorrhiza/user" %}
{% func HistoryHTML(rq *http.Request, hyphaName, tbody string) %} {% func HistoryHTML(rq *http.Request, hyphaName, list string) %}
<main>
{%= navHTML(rq, hyphaName, "history") %} {%= navHTML(rq, hyphaName, "history") %}
<table> <main>
<thead> <article class="history">
<tr> <h1>History of {%s hyphaName %}</h1>
<th>Time</th> {%s= list %}
<th>Hash</th> </article>
<th>Message</th>
</tr>
</thead>
<tbody>
{%s= tbody %}
</tbody>
</table>
</main> </main>
{% endfunc %} {% endfunc %}
{% func RevisionHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) %} {% func RevisionHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, revHash string) %}
<main>
{%= navHTML(rq, hyphaName, "revision", revHash) %} {%= navHTML(rq, hyphaName, "revision", revHash) %}
<main>
<article> <article>
<p>Please note that viewing binary parts of hyphae is not supported in history for now.</p> <p>Please note that viewing binary parts of hyphae is not supported in history for now.</p>
{%s= naviTitle %} {%s= naviTitle %}
@ -35,9 +28,9 @@
{% endfunc %} {% endfunc %}
If `contents` == "", a helpful message is shown instead. 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) %}
<main>
{%= navHTML(rq, hyphaName, "page") %} {%= navHTML(rq, hyphaName, "page") %}
<main>
<article> <article>
{%s= naviTitle %} {%s= naviTitle %}
{% if contents == "" %} {% if contents == "" %}
@ -46,16 +39,26 @@ If `contents` == "", a helpful message is shown instead.
{%s= contents %} {%s= contents %}
{% endif %} {% endif %}
</article> </article>
<hr/> <section class="prevnext">
{% if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon %} {% if prevHyphaName != "" %}
<a class="prevnext__el prevnext__prev" href="/page/{%s prevHyphaName %}" rel="prev">← {%s path.Base(prevHyphaName) %}</a>
{% endif %}
{% if nextHyphaName != "" %}
<a class="prevnext__el prevnext__next" href="/page/{%s nextHyphaName %}" rel="next">{%s path.Base(nextHyphaName) %} →</a>
{% endif %}
</section>
{% if u := user.FromRequest(rq); !user.AuthUsed || u.Group != "anon" %}
<form action="/upload-binary/{%s hyphaName %}" <form action="/upload-binary/{%s hyphaName %}"
method="post" enctype="multipart/form-data"> method="post" enctype="multipart/form-data"
class="upload-amnt">
{% if hasAmnt %}
<a class="upload-amnt__unattach" href="/unattach-ask/{%s hyphaName %}">Unattach current attachment?</a>
{% endif %}
<label for="upload-binary__input">Upload a new attachment</label> <label for="upload-binary__input">Upload a new attachment</label>
<br> <br>
<input type="file" id="upload-binary__input" name="binary"/> <input type="file" id="upload-binary__input" name="binary"/>
<input type="submit"/> <input type="submit"/>
</form> </form>
<hr/>
{% endif %} {% endif %}
<aside> <aside>
{%s= tree %} {%s= tree %}

View File

@ -8,188 +8,226 @@ package templates
import "net/http" import "net/http"
//line templates/http_readers.qtpl:2 //line templates/http_readers.qtpl:2
import "path"
//line templates/http_readers.qtpl:3
import "github.com/bouncepaw/mycorrhiza/user" import "github.com/bouncepaw/mycorrhiza/user"
//line templates/http_readers.qtpl:4 //line templates/http_readers.qtpl:5
import ( import (
qtio422016 "io" qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate" qt422016 "github.com/valyala/quicktemplate"
) )
//line templates/http_readers.qtpl:4 //line templates/http_readers.qtpl:5
var ( var (
_ = qtio422016.Copy _ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer _ = qt422016.AcquireByteBuffer
) )
//line templates/http_readers.qtpl:4 //line templates/http_readers.qtpl:5
func StreamHistoryHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, tbody string) { func StreamHistoryHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, list string) {
//line templates/http_readers.qtpl:4 //line templates/http_readers.qtpl:5
qw422016.N().S(` qw422016.N().S(`
<main>
`) `)
//line templates/http_readers.qtpl:6 //line templates/http_readers.qtpl:6
streamnavHTML(qw422016, rq, hyphaName, "history") streamnavHTML(qw422016, rq, hyphaName, "history")
//line templates/http_readers.qtpl:6 //line templates/http_readers.qtpl:6
qw422016.N().S(` qw422016.N().S(`
<table> <main>
<thead> <article class="history">
<tr> <h1>History of `)
<th>Time</th> //line templates/http_readers.qtpl:9
<th>Hash</th> qw422016.E().S(hyphaName)
<th>Message</th> //line templates/http_readers.qtpl:9
</tr> qw422016.N().S(`</h1>
</thead>
<tbody>
`) `)
//line templates/http_readers.qtpl:16 //line templates/http_readers.qtpl:10
qw422016.N().S(tbody) qw422016.N().S(list)
//line templates/http_readers.qtpl:16 //line templates/http_readers.qtpl:10
qw422016.N().S(` qw422016.N().S(`
</tbody> </article>
</table>
</main> </main>
`) `)
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
} }
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
func WriteHistoryHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, tbody string) { func WriteHistoryHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, list string) {
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
StreamHistoryHTML(qw422016, rq, hyphaName, tbody) StreamHistoryHTML(qw422016, rq, hyphaName, list)
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
} }
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
func HistoryHTML(rq *http.Request, hyphaName, tbody string) string { func HistoryHTML(rq *http.Request, hyphaName, list string) string {
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
WriteHistoryHTML(qb422016, rq, hyphaName, tbody) WriteHistoryHTML(qb422016, rq, hyphaName, list)
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/http_readers.qtpl:20 //line templates/http_readers.qtpl:13
return qs422016 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) { 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(` qw422016.N().S(`
<main> <main>
`)
//line templates/http_readers.qtpl:24
streamnavHTML(qw422016, rq, hyphaName, "revision", revHash)
//line templates/http_readers.qtpl:24
qw422016.N().S(`
<article> <article>
<p>Please note that viewing binary parts of hyphae is not supported in history for now.</p> <p>Please note that viewing binary parts of hyphae is not supported in history for now.</p>
`) `)
//line templates/http_readers.qtpl:27 //line templates/http_readers.qtpl:20
qw422016.N().S(naviTitle) qw422016.N().S(naviTitle)
//line templates/http_readers.qtpl:27 //line templates/http_readers.qtpl:20
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/http_readers.qtpl:28 //line templates/http_readers.qtpl:21
qw422016.N().S(contents) qw422016.N().S(contents)
//line templates/http_readers.qtpl:28 //line templates/http_readers.qtpl:21
qw422016.N().S(` qw422016.N().S(`
</article> </article>
<hr/> <hr/>
<aside> <aside>
`) `)
//line templates/http_readers.qtpl:32 //line templates/http_readers.qtpl:25
qw422016.N().S(tree) qw422016.N().S(tree)
//line templates/http_readers.qtpl:32 //line templates/http_readers.qtpl:25
qw422016.N().S(` qw422016.N().S(`
</aside> </aside>
</main> </main>
`) `)
//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) { 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) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/http_readers.qtpl:35 //line templates/http_readers.qtpl:28
StreamRevisionHTML(qw422016, rq, hyphaName, naviTitle, contents, tree, revHash) StreamRevisionHTML(qw422016, rq, hyphaName, naviTitle, contents, tree, revHash)
//line templates/http_readers.qtpl:35 //line templates/http_readers.qtpl:28
qt422016.ReleaseWriter(qw422016) 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 { 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() qb422016 := qt422016.AcquireByteBuffer()
//line templates/http_readers.qtpl:35 //line templates/http_readers.qtpl:28
WriteRevisionHTML(qb422016, rq, hyphaName, naviTitle, contents, tree, revHash) WriteRevisionHTML(qb422016, rq, hyphaName, naviTitle, contents, tree, revHash)
//line templates/http_readers.qtpl:35 //line templates/http_readers.qtpl:28
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/http_readers.qtpl:35 //line templates/http_readers.qtpl:28
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/http_readers.qtpl:35 //line templates/http_readers.qtpl:28
return qs422016 return qs422016
//line templates/http_readers.qtpl:35 //line templates/http_readers.qtpl:28
} }
// If `contents` == "", a helpful message is shown instead. // If `contents` == "", a helpful message is shown instead.
//line templates/http_readers.qtpl:38 //line templates/http_readers.qtpl:31
func StreamPageHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree string) { func StreamPageHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName string, hasAmnt bool) {
//line templates/http_readers.qtpl:38 //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(` qw422016.N().S(`
<main> <main>
`)
//line templates/http_readers.qtpl:40
streamnavHTML(qw422016, rq, hyphaName, "page")
//line templates/http_readers.qtpl:40
qw422016.N().S(`
<article> <article>
`) `)
//line templates/http_readers.qtpl:42 //line templates/http_readers.qtpl:35
qw422016.N().S(naviTitle) qw422016.N().S(naviTitle)
//line templates/http_readers.qtpl:42 //line templates/http_readers.qtpl:35
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/http_readers.qtpl:43 //line templates/http_readers.qtpl:36
if contents == "" { if contents == "" {
//line templates/http_readers.qtpl:43 //line templates/http_readers.qtpl:36
qw422016.N().S(` qw422016.N().S(`
<p>This hypha has no text. Why not <a href="/edit/`) <p>This hypha has no text. Why not <a href="/edit/`)
//line templates/http_readers.qtpl:44 //line templates/http_readers.qtpl:37
qw422016.E().S(hyphaName) qw422016.E().S(hyphaName)
//line templates/http_readers.qtpl:44 //line templates/http_readers.qtpl:37
qw422016.N().S(`">create it</a>?</p> qw422016.N().S(`">create it</a>?</p>
`) `)
//line templates/http_readers.qtpl:45 //line templates/http_readers.qtpl:38
} else { } else {
//line templates/http_readers.qtpl:45 //line templates/http_readers.qtpl:38
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/http_readers.qtpl:46 //line templates/http_readers.qtpl:39
qw422016.N().S(contents) qw422016.N().S(contents)
//line templates/http_readers.qtpl:46 //line templates/http_readers.qtpl:39
qw422016.N().S(` 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(` qw422016.N().S(`
</article> </article>
<hr/> <section class="prevnext">
`)
//line templates/http_readers.qtpl:43
if prevHyphaName != "" {
//line templates/http_readers.qtpl:43
qw422016.N().S(`
<a class="prevnext__el prevnext__prev" href="/page/`)
//line templates/http_readers.qtpl:44
qw422016.E().S(prevHyphaName)
//line templates/http_readers.qtpl:44
qw422016.N().S(`" rel="prev">← `)
//line templates/http_readers.qtpl:44
qw422016.E().S(path.Base(prevHyphaName))
//line templates/http_readers.qtpl:44
qw422016.N().S(`</a>
`)
//line templates/http_readers.qtpl:45
}
//line templates/http_readers.qtpl:45
qw422016.N().S(`
`)
//line templates/http_readers.qtpl:46
if nextHyphaName != "" {
//line templates/http_readers.qtpl:46
qw422016.N().S(`
<a class="prevnext__el prevnext__next" href="/page/`)
//line templates/http_readers.qtpl:47
qw422016.E().S(nextHyphaName)
//line templates/http_readers.qtpl:47
qw422016.N().S(`" rel="next">`)
//line templates/http_readers.qtpl:47
qw422016.E().S(path.Base(nextHyphaName))
//line templates/http_readers.qtpl:47
qw422016.N().S(` </a>
`)
//line templates/http_readers.qtpl:48
}
//line templates/http_readers.qtpl:48
qw422016.N().S(`
</section>
`) `)
//line templates/http_readers.qtpl:50 //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 //line templates/http_readers.qtpl:50
qw422016.N().S(` qw422016.N().S(`
<form action="/upload-binary/`) <form action="/upload-binary/`)
@ -197,52 +235,67 @@ func StreamPageHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, navi
qw422016.E().S(hyphaName) qw422016.E().S(hyphaName)
//line templates/http_readers.qtpl:51 //line templates/http_readers.qtpl:51
qw422016.N().S(`" qw422016.N().S(`"
method="post" enctype="multipart/form-data"> 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(`
<a class="upload-amnt__unattach" href="/unattach-ask/`)
//line templates/http_readers.qtpl:55
qw422016.E().S(hyphaName)
//line templates/http_readers.qtpl:55
qw422016.N().S(`">Unattach current attachment?</a>
`)
//line templates/http_readers.qtpl:56
}
//line templates/http_readers.qtpl:56
qw422016.N().S(`
<label for="upload-binary__input">Upload a new attachment</label> <label for="upload-binary__input">Upload a new attachment</label>
<br> <br>
<input type="file" id="upload-binary__input" name="binary"/> <input type="file" id="upload-binary__input" name="binary"/>
<input type="submit"/> <input type="submit"/>
</form> </form>
<hr/>
`) `)
//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(` qw422016.N().S(`
<aside> <aside>
`) `)
//line templates/http_readers.qtpl:61 //line templates/http_readers.qtpl:64
qw422016.N().S(tree) qw422016.N().S(tree)
//line templates/http_readers.qtpl:61 //line templates/http_readers.qtpl:64
qw422016.N().S(` qw422016.N().S(`
</aside> </aside>
</main> </main>
`) `)
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
} }
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
func WritePageHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree string) { func WritePageHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName string, hasAmnt bool) {
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
StreamPageHTML(qw422016, rq, hyphaName, naviTitle, contents, tree) StreamPageHTML(qw422016, rq, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName, hasAmnt)
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
} }
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
func PageHTML(rq *http.Request, hyphaName, naviTitle, contents, tree string) string { func PageHTML(rq *http.Request, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName string, hasAmnt bool) string {
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
WritePageHTML(qb422016, rq, hyphaName, naviTitle, contents, tree) WritePageHTML(qb422016, rq, hyphaName, naviTitle, contents, tree, prevHyphaName, nextHyphaName, hasAmnt)
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
return qs422016 return qs422016
//line templates/http_readers.qtpl:64 //line templates/http_readers.qtpl:67
} }

View File

@ -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) %}
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/static/common.css"> <link rel="stylesheet" type="text/css" href="/static/common.css">
<title>{%s title %}</title> <title>{%s title %}</title>
{% for _, el := range headElements %}{%s= el %}{% endfor %}
</head> </head>
<body> <body>
<header>
<nav class="header-links">
<ul class="header-links__list">
{%- for _, link := range util.HeaderLinks -%}
<li class="header-links__entry"><a class="header-links__link" href="{%s link.Href %}">{%s link.Display %}</a></li>
{%- endfor -%}
{%s= userMenuHTML(u) %}
</ul>
</nav>
</header>
{%s= body %} {%s= body %}
</body> </body>
</html> </html>
@ -40,3 +54,25 @@
{% endif %} {% endif %}
</tr> </tr>
{% endfunc %} {% endfunc %}
{% func AboutHTML() %}
<main>
<section>
<h1>About {%s util.SiteName %}</h1>
<ul>
<li><b><a href="https://mycorrhiza.lesarbr.es">MycorrhizaWiki</a> version:</b> β 0.12 indev</li>
{%- if user.AuthUsed -%}
<li><b>User count:</b> {%d user.Count() %}</li>
<li><b>Home page:</b> <a href="/">{%s util.HomePage %}</a></li>
<li><b>Administrators:</b> {%- for i, username := range user.ListUsersWithGroup("admin") -%}
{%- if i > 0 -%}<span aria-hidden="true">, </span>
{%- endif -%}
<a href="/page/{%s util.UserHypha %}/{%s username %}">{%s username %}</a>{%- endfor -%}</li>
{%- else -%}
<li>This wiki does not use authorization</li>
{%- endif -%}
</ul>
<p>See <a href="/list">/list</a> for information about hyphae on this wiki.</p>
</section>
</main>
{% endfunc %}

View File

@ -5,21 +5,27 @@
package templates package templates
//line templates/http_stuff.qtpl:1 //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 ( import (
qtio422016 "io" qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate" qt422016 "github.com/valyala/quicktemplate"
) )
//line templates/http_stuff.qtpl:1 //line templates/http_stuff.qtpl:4
var ( var (
_ = qtio422016.Copy _ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer _ = qt422016.AcquireByteBuffer
) )
//line templates/http_stuff.qtpl:1 //line templates/http_stuff.qtpl:4
func StreamBaseHTML(qw422016 *qt422016.Writer, title, body string) { func StreamBaseHTML(qw422016 *qt422016.Writer, title, body string, u *user.User, headElements ...string) {
//line templates/http_stuff.qtpl:1 //line templates/http_stuff.qtpl:4
qw422016.N().S(` qw422016.N().S(`
<!doctype html> <!doctype html>
<html> <html>
@ -27,59 +33,96 @@ func StreamBaseHTML(qw422016 *qt422016.Writer, title, body string) {
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/static/common.css"> <link rel="stylesheet" type="text/css" href="/static/common.css">
<title>`) <title>`)
//line templates/http_stuff.qtpl:7 //line templates/http_stuff.qtpl:10
qw422016.E().S(title) qw422016.E().S(title)
//line templates/http_stuff.qtpl:7 //line templates/http_stuff.qtpl:10
qw422016.N().S(`</title> qw422016.N().S(`</title>
`)
//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(`
</head> </head>
<body> <body>
<header>
<nav class="header-links">
<ul class="header-links__list">
`)
//line templates/http_stuff.qtpl:17
for _, link := range util.HeaderLinks {
//line templates/http_stuff.qtpl:17
qw422016.N().S(` <li class="header-links__entry"><a class="header-links__link" href="`)
//line templates/http_stuff.qtpl:18
qw422016.E().S(link.Href)
//line templates/http_stuff.qtpl:18
qw422016.N().S(`">`)
//line templates/http_stuff.qtpl:18
qw422016.E().S(link.Display)
//line templates/http_stuff.qtpl:18
qw422016.N().S(`</a></li>
`)
//line templates/http_stuff.qtpl:19
}
//line templates/http_stuff.qtpl:19
qw422016.N().S(` `)
//line templates/http_stuff.qtpl:20
qw422016.N().S(userMenuHTML(u))
//line templates/http_stuff.qtpl:20
qw422016.N().S(`
</ul>
</nav>
</header>
`) `)
//line templates/http_stuff.qtpl:10 //line templates/http_stuff.qtpl:24
qw422016.N().S(body) qw422016.N().S(body)
//line templates/http_stuff.qtpl:10 //line templates/http_stuff.qtpl:24
qw422016.N().S(` qw422016.N().S(`
</body> </body>
</html> </html>
`) `)
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
} }
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
func WriteBaseHTML(qq422016 qtio422016.Writer, title, body string) { func WriteBaseHTML(qq422016 qtio422016.Writer, title, body string, u *user.User, headElements ...string) {
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
StreamBaseHTML(qw422016, title, body) StreamBaseHTML(qw422016, title, body, u, headElements...)
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
} }
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
func BaseHTML(title, body string) string { func BaseHTML(title, body string, u *user.User, headElements ...string) string {
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
WriteBaseHTML(qb422016, title, body) WriteBaseHTML(qb422016, title, body, u, headElements...)
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
return qs422016 return qs422016
//line templates/http_stuff.qtpl:13 //line templates/http_stuff.qtpl:27
} }
//line templates/http_stuff.qtpl:15 //line templates/http_stuff.qtpl:29
func StreamHyphaListHTML(qw422016 *qt422016.Writer, tbody string, pageCount int) { func StreamHyphaListHTML(qw422016 *qt422016.Writer, tbody string, pageCount int) {
//line templates/http_stuff.qtpl:15 //line templates/http_stuff.qtpl:29
qw422016.N().S(` qw422016.N().S(`
<main> <main>
<h1>List of hyphae</h1> <h1>List of hyphae</h1>
<p>This wiki has `) <p>This wiki has `)
//line templates/http_stuff.qtpl:18 //line templates/http_stuff.qtpl:32
qw422016.N().D(pageCount) qw422016.N().D(pageCount)
//line templates/http_stuff.qtpl:18 //line templates/http_stuff.qtpl:32
qw422016.N().S(` hyphae.</p> qw422016.N().S(` hyphae.</p>
<table> <table>
<thead> <thead>
@ -90,105 +133,203 @@ func StreamHyphaListHTML(qw422016 *qt422016.Writer, tbody string, pageCount int)
</thead> </thead>
<tbody> <tbody>
`) `)
//line templates/http_stuff.qtpl:27 //line templates/http_stuff.qtpl:41
qw422016.N().S(tbody) qw422016.N().S(tbody)
//line templates/http_stuff.qtpl:27 //line templates/http_stuff.qtpl:41
qw422016.N().S(` qw422016.N().S(`
</tbody> </tbody>
</table> </table>
</main> </main>
`) `)
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
} }
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
func WriteHyphaListHTML(qq422016 qtio422016.Writer, tbody string, pageCount int) { func WriteHyphaListHTML(qq422016 qtio422016.Writer, tbody string, pageCount int) {
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
StreamHyphaListHTML(qw422016, tbody, pageCount) StreamHyphaListHTML(qw422016, tbody, pageCount)
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
} }
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
func HyphaListHTML(tbody string, pageCount int) string { func HyphaListHTML(tbody string, pageCount int) string {
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
WriteHyphaListHTML(qb422016, tbody, pageCount) WriteHyphaListHTML(qb422016, tbody, pageCount)
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
return qs422016 return qs422016
//line templates/http_stuff.qtpl:31 //line templates/http_stuff.qtpl:45
} }
//line templates/http_stuff.qtpl:33 //line templates/http_stuff.qtpl:47
func StreamHyphaListRowHTML(qw422016 *qt422016.Writer, hyphaName, binaryMime string, binaryPresent bool) { func StreamHyphaListRowHTML(qw422016 *qt422016.Writer, hyphaName, binaryMime string, binaryPresent bool) {
//line templates/http_stuff.qtpl:33 //line templates/http_stuff.qtpl:47
qw422016.N().S(` qw422016.N().S(`
<tr> <tr>
<td><a href="/page/`) <td><a href="/page/`)
//line templates/http_stuff.qtpl:35 //line templates/http_stuff.qtpl:49
qw422016.E().S(hyphaName) qw422016.E().S(hyphaName)
//line templates/http_stuff.qtpl:35 //line templates/http_stuff.qtpl:49
qw422016.N().S(`">`) qw422016.N().S(`">`)
//line templates/http_stuff.qtpl:35 //line templates/http_stuff.qtpl:49
qw422016.E().S(hyphaName) qw422016.E().S(hyphaName)
//line templates/http_stuff.qtpl:35 //line templates/http_stuff.qtpl:49
qw422016.N().S(`</a></td> qw422016.N().S(`</a></td>
`) `)
//line templates/http_stuff.qtpl:36 //line templates/http_stuff.qtpl:50
if binaryPresent { if binaryPresent {
//line templates/http_stuff.qtpl:36 //line templates/http_stuff.qtpl:50
qw422016.N().S(` qw422016.N().S(`
<td>`) <td>`)
//line templates/http_stuff.qtpl:37 //line templates/http_stuff.qtpl:51
qw422016.E().S(binaryMime) qw422016.E().S(binaryMime)
//line templates/http_stuff.qtpl:37 //line templates/http_stuff.qtpl:51
qw422016.N().S(`</td> qw422016.N().S(`</td>
`) `)
//line templates/http_stuff.qtpl:38 //line templates/http_stuff.qtpl:52
} else { } else {
//line templates/http_stuff.qtpl:38 //line templates/http_stuff.qtpl:52
qw422016.N().S(` qw422016.N().S(`
<td></td> <td></td>
`) `)
//line templates/http_stuff.qtpl:40 //line templates/http_stuff.qtpl:54
} }
//line templates/http_stuff.qtpl:40 //line templates/http_stuff.qtpl:54
qw422016.N().S(` qw422016.N().S(`
</tr> </tr>
`) `)
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
} }
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
func WriteHyphaListRowHTML(qq422016 qtio422016.Writer, hyphaName, binaryMime string, binaryPresent bool) { func WriteHyphaListRowHTML(qq422016 qtio422016.Writer, hyphaName, binaryMime string, binaryPresent bool) {
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
StreamHyphaListRowHTML(qw422016, hyphaName, binaryMime, binaryPresent) StreamHyphaListRowHTML(qw422016, hyphaName, binaryMime, binaryPresent)
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
} }
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
func HyphaListRowHTML(hyphaName, binaryMime string, binaryPresent bool) string { func HyphaListRowHTML(hyphaName, binaryMime string, binaryPresent bool) string {
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
WriteHyphaListRowHTML(qb422016, hyphaName, binaryMime, binaryPresent) WriteHyphaListRowHTML(qb422016, hyphaName, binaryMime, binaryPresent)
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
return qs422016 return qs422016
//line templates/http_stuff.qtpl:42 //line templates/http_stuff.qtpl:56
}
//line templates/http_stuff.qtpl:58
func StreamAboutHTML(qw422016 *qt422016.Writer) {
//line templates/http_stuff.qtpl:58
qw422016.N().S(`
<main>
<section>
<h1>About `)
//line templates/http_stuff.qtpl:61
qw422016.E().S(util.SiteName)
//line templates/http_stuff.qtpl:61
qw422016.N().S(`</h1>
<ul>
<li><b><a href="https://mycorrhiza.lesarbr.es">MycorrhizaWiki</a> version:</b> β 0.12 indev</li>
`)
//line templates/http_stuff.qtpl:64
if user.AuthUsed {
//line templates/http_stuff.qtpl:64
qw422016.N().S(` <li><b>User count:</b> `)
//line templates/http_stuff.qtpl:65
qw422016.N().D(user.Count())
//line templates/http_stuff.qtpl:65
qw422016.N().S(`</li>
<li><b>Home page:</b> <a href="/">`)
//line templates/http_stuff.qtpl:66
qw422016.E().S(util.HomePage)
//line templates/http_stuff.qtpl:66
qw422016.N().S(`</a></li>
<li><b>Administrators:</b>`)
//line templates/http_stuff.qtpl:67
for i, username := range user.ListUsersWithGroup("admin") {
//line templates/http_stuff.qtpl:68
if i > 0 {
//line templates/http_stuff.qtpl:68
qw422016.N().S(`<span aria-hidden="true">, </span>
`)
//line templates/http_stuff.qtpl:69
}
//line templates/http_stuff.qtpl:69
qw422016.N().S(` <a href="/page/`)
//line templates/http_stuff.qtpl:70
qw422016.E().S(util.UserHypha)
//line templates/http_stuff.qtpl:70
qw422016.N().S(`/`)
//line templates/http_stuff.qtpl:70
qw422016.E().S(username)
//line templates/http_stuff.qtpl:70
qw422016.N().S(`">`)
//line templates/http_stuff.qtpl:70
qw422016.E().S(username)
//line templates/http_stuff.qtpl:70
qw422016.N().S(`</a>`)
//line templates/http_stuff.qtpl:70
}
//line templates/http_stuff.qtpl:70
qw422016.N().S(`</li>
`)
//line templates/http_stuff.qtpl:71
} else {
//line templates/http_stuff.qtpl:71
qw422016.N().S(` <li>This wiki does not use authorization</li>
`)
//line templates/http_stuff.qtpl:73
}
//line templates/http_stuff.qtpl:73
qw422016.N().S(` </ul>
<p>See <a href="/list">/list</a> for information about hyphae on this wiki.</p>
</section>
</main>
`)
//line templates/http_stuff.qtpl:78
}
//line templates/http_stuff.qtpl:78
func WriteAboutHTML(qq422016 qtio422016.Writer) {
//line templates/http_stuff.qtpl:78
qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/http_stuff.qtpl:78
StreamAboutHTML(qw422016)
//line templates/http_stuff.qtpl:78
qt422016.ReleaseWriter(qw422016)
//line templates/http_stuff.qtpl:78
}
//line templates/http_stuff.qtpl:78
func AboutHTML() string {
//line templates/http_stuff.qtpl:78
qb422016 := qt422016.AcquireByteBuffer()
//line templates/http_stuff.qtpl:78
WriteAboutHTML(qb422016)
//line templates/http_stuff.qtpl:78
qs422016 := string(qb422016.B)
//line templates/http_stuff.qtpl:78
qt422016.ReleaseByteBuffer(qb422016)
//line templates/http_stuff.qtpl:78
return qs422016
//line templates/http_stuff.qtpl:78
} }

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M15.502 20A6.523 6.523 0 0 1 12 23.502 6.523 6.523 0 0 1 8.498 20h2.26c.326.489.747.912 1.242 1.243.495-.33.916-.754 1.243-1.243h2.259zM18 14.805l2 2.268V19H4v-1.927l2-2.268V9c0-3.483 2.504-6.447 6-7.545C15.496 2.553 18 5.517 18 9v5.805zM17.27 17L16 15.56V9c0-2.318-1.57-4.43-4-5.42C9.57 4.57 8 6.681 8 9v6.56L6.73 17h10.54zM12 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16">
<path fill="#999" d="M447.238,204.944v-70.459c0-8.836-7.164-16-16-16c-34.051,0-64.414,21.118-75.079,55.286
C226.094,41.594,0,133.882,0,319.435c0,0.071,0.01,0.14,0.011,0.21c0.116,44.591,36.423,80.833,81.04,80.833h171.203
c8.836,0,16-7.164,16-16c0-8.836-7.164-16-16-16H81.051c-21.441,0-39.7-13.836-46.351-33.044H496c8.836,0,16-7.164,16-16
C512,271.82,486.82,228.692,447.238,204.944z M415.238,153.216v37.805c-10.318-2.946-19.556-4.305-29.342-4.937
C390.355,168.611,402.006,157.881,415.238,153.216z M295.484,303.435L295.484,303.435c-7.562-41.495-43.948-73.062-87.593-73.062
c-8.836,0-16,7.164-16,16c0,8.836,7.164,16,16,16c25.909,0,47.826,17.364,54.76,41.062H32.722
c14.415-159.15,218.064-217.856,315.136-90.512c3.545,4.649,9.345,6.995,15.124,6.118
c55.425-8.382,107.014,29.269,115.759,84.394H295.484z"/>
<circle fill="#999" cx="415.238" cy="260.05" r="21.166"/>
</svg>

After

Width:  |  Height:  |  Size: 951 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-2.29-2.333A17.9 17.9 0 0 1 8.027 13H4.062a8.008 8.008 0 0 0 5.648 6.667zM10.03 13c.151 2.439.848 4.73 1.97 6.752A15.905 15.905 0 0 0 13.97 13h-3.94zm9.908 0h-3.965a17.9 17.9 0 0 1-1.683 6.667A8.008 8.008 0 0 0 19.938 13zM4.062 11h3.965A17.9 17.9 0 0 1 9.71 4.333 8.008 8.008 0 0 0 4.062 11zm5.969 0h3.938A15.905 15.905 0 0 0 12 4.248 15.905 15.905 0 0 0 10.03 11zm4.259-6.667A17.9 17.9 0 0 1 15.973 11h3.965a8.008 8.008 0 0 0-5.648-6.667z"/></svg>

After

Width:  |  Height:  |  Size: 627 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm17 4.238l-7.928 7.1L4 7.216V19h16V7.238zM4.511 5l7.55 6.662L19.502 5H4.511z"/></svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@ -18,6 +18,8 @@
recent changes recent changes
</nav> </nav>
<p><img class="icon" width="20" height="20" src="https://upload.wikimedia.org/wikipedia/commons/4/46/Generic_Feed-icon.svg">Subscribe via <a href="/recent-changes-rss">RSS</a>, <a href="/recent-changes-atom">Atom</a> or <a href="/recent-changes-json">JSON feed</a>.</p>
{% comment %} {% comment %}
Here I am, willing to add some accesibility using ARIA. Turns out, Here I am, willing to add some accesibility using ARIA. Turns out,
role="feed" is not supported in any screen reader as of September role="feed" is not supported in any screen reader as of September

View File

@ -77,81 +77,83 @@ func StreamRecentChangesHTML(qw422016 *qt422016.Writer, changes []string, n int)
recent changes recent changes
</nav> </nav>
<p><img class="icon" width="20" height="20" src="https://upload.wikimedia.org/wikipedia/commons/4/46/Generic_Feed-icon.svg">Subscribe via <a href="/recent-changes-rss">RSS</a>, <a href="/recent-changes-atom">Atom</a> or <a href="/recent-changes-json">JSON feed</a>.</p>
`) `)
//line templates/recent_changes.qtpl:26 //line templates/recent_changes.qtpl:28
qw422016.N().S(` qw422016.N().S(`
<section class="recent-changes__list" role="feed"> <section class="recent-changes__list" role="feed">
`) `)
//line templates/recent_changes.qtpl:29 //line templates/recent_changes.qtpl:31
if len(changes) == 0 { if len(changes) == 0 {
//line templates/recent_changes.qtpl:29 //line templates/recent_changes.qtpl:31
qw422016.N().S(` qw422016.N().S(`
<p>Could not find any recent changes.</p> <p>Could not find any recent changes.</p>
`) `)
//line templates/recent_changes.qtpl:31 //line templates/recent_changes.qtpl:33
} else { } else {
//line templates/recent_changes.qtpl:31 //line templates/recent_changes.qtpl:33
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/recent_changes.qtpl:32 //line templates/recent_changes.qtpl:34
for i, entry := range changes { for i, entry := range changes {
//line templates/recent_changes.qtpl:32 //line templates/recent_changes.qtpl:34
qw422016.N().S(` qw422016.N().S(`
<ul class="recent-changes__entry rc-entry" role="article" <ul class="recent-changes__entry rc-entry" role="article"
aria-setsize="`) aria-setsize="`)
//line templates/recent_changes.qtpl:34 //line templates/recent_changes.qtpl:36
qw422016.N().D(n) qw422016.N().D(n)
//line templates/recent_changes.qtpl:34 //line templates/recent_changes.qtpl:36
qw422016.N().S(`" aria-posinset="`) qw422016.N().S(`" aria-posinset="`)
//line templates/recent_changes.qtpl:34 //line templates/recent_changes.qtpl:36
qw422016.N().D(i) qw422016.N().D(i)
//line templates/recent_changes.qtpl:34 //line templates/recent_changes.qtpl:36
qw422016.N().S(`"> qw422016.N().S(`">
`) `)
//line templates/recent_changes.qtpl:35 //line templates/recent_changes.qtpl:37
qw422016.N().S(entry) qw422016.N().S(entry)
//line templates/recent_changes.qtpl:35 //line templates/recent_changes.qtpl:37
qw422016.N().S(` qw422016.N().S(`
</ul> </ul>
`) `)
//line templates/recent_changes.qtpl:37 //line templates/recent_changes.qtpl:39
} }
//line templates/recent_changes.qtpl:37 //line templates/recent_changes.qtpl:39
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/recent_changes.qtpl:38 //line templates/recent_changes.qtpl:40
} }
//line templates/recent_changes.qtpl:38 //line templates/recent_changes.qtpl:40
qw422016.N().S(` qw422016.N().S(`
</section> </section>
</main> </main>
`) `)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
} }
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
func WriteRecentChangesHTML(qq422016 qtio422016.Writer, changes []string, n int) { func WriteRecentChangesHTML(qq422016 qtio422016.Writer, changes []string, n int) {
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
StreamRecentChangesHTML(qw422016, changes, n) StreamRecentChangesHTML(qw422016, changes, n)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
} }
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
func RecentChangesHTML(changes []string, n int) string { func RecentChangesHTML(changes []string, n int) string {
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
WriteRecentChangesHTML(qb422016, changes, n) WriteRecentChangesHTML(qb422016, changes, n)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
return qs422016 return qs422016
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
} }

View File

@ -1,8 +1,8 @@
{% import "net/http" %} {% import "net/http" %}
This dialog is to be shown to a user when they try to rename a hypha. This dialog is to be shown to a user when they try to rename a hypha.
{% func RenameAskHTML(rq *http.Request, hyphaName string, isOld bool) %} {% func RenameAskHTML(rq *http.Request, hyphaName string, isOld bool) %}
<main>
{%= navHTML(rq, hyphaName, "rename-ask") %} {%= navHTML(rq, hyphaName, "rename-ask") %}
<main>
{%- if isOld -%} {%- if isOld -%}
<section> <section>
<h1>Rename {%s hyphaName %}</h1> <h1>Rename {%s hyphaName %}</h1>

View File

@ -26,12 +26,12 @@ var (
func StreamRenameAskHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName string, isOld bool) { func StreamRenameAskHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
//line templates/rename.qtpl:3 //line templates/rename.qtpl:3
qw422016.N().S(` qw422016.N().S(`
<main>
`) `)
//line templates/rename.qtpl:5 //line templates/rename.qtpl:4
streamnavHTML(qw422016, rq, hyphaName, "rename-ask") streamnavHTML(qw422016, rq, hyphaName, "rename-ask")
//line templates/rename.qtpl:5 //line templates/rename.qtpl:4
qw422016.N().S(` qw422016.N().S(`
<main>
`) `)
//line templates/rename.qtpl:6 //line templates/rename.qtpl:6
if isOld { if isOld {

24
templates/unattach.qtpl Normal file
View File

@ -0,0 +1,24 @@
{% import "net/http" %}
{% func UnattachAskHTML(rq *http.Request, hyphaName string, isOld bool) %}
<main>
{%= navHTML(rq, hyphaName, "unattach-ask") %}
{%- if isOld -%}
<section>
<h1>Unattach {%s hyphaName %}?</h1>
<p>Do you really want to unattach hypha <em>{%s hyphaName %}</em>?</p>
<p><a href="/unattach-confirm/{%s hyphaName %}"><strong>Confirm</strong></a></p>
<p><a href="/page/{%s hyphaName %}">Cancel</a></p>
</section>
{%- else -%}
{%= cannotUnattachDueToNonExistence(hyphaName) %}
{%- endif -%}
</main>
{% endfunc %}
{% func cannotUnattachDueToNonExistence(hyphaName string) %}
<section>
<h1>Cannot unattach {%s hyphaName %}</h1>
<p>This hypha does not exist.</p>
<p><a href="/page/{%s hyphaName %}">Go back</a></p>
</section>
{% endfunc %}

148
templates/unattach.qtpl.go Normal file
View File

@ -0,0 +1,148 @@
// Code generated by qtc from "unattach.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line templates/unattach.qtpl:1
package templates
//line templates/unattach.qtpl:1
import "net/http"
//line templates/unattach.qtpl:2
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line templates/unattach.qtpl:2
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line templates/unattach.qtpl:2
func StreamUnattachAskHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
//line templates/unattach.qtpl:2
qw422016.N().S(`
<main>
`)
//line templates/unattach.qtpl:4
streamnavHTML(qw422016, rq, hyphaName, "unattach-ask")
//line templates/unattach.qtpl:4
qw422016.N().S(`
`)
//line templates/unattach.qtpl:5
if isOld {
//line templates/unattach.qtpl:5
qw422016.N().S(` <section>
<h1>Unattach `)
//line templates/unattach.qtpl:7
qw422016.E().S(hyphaName)
//line templates/unattach.qtpl:7
qw422016.N().S(`?</h1>
<p>Do you really want to unattach hypha <em>`)
//line templates/unattach.qtpl:8
qw422016.E().S(hyphaName)
//line templates/unattach.qtpl:8
qw422016.N().S(`</em>?</p>
<p><a href="/unattach-confirm/`)
//line templates/unattach.qtpl:9
qw422016.E().S(hyphaName)
//line templates/unattach.qtpl:9
qw422016.N().S(`"><strong>Confirm</strong></a></p>
<p><a href="/page/`)
//line templates/unattach.qtpl:10
qw422016.E().S(hyphaName)
//line templates/unattach.qtpl:10
qw422016.N().S(`">Cancel</a></p>
</section>
`)
//line templates/unattach.qtpl:12
} else {
//line templates/unattach.qtpl:12
qw422016.N().S(` `)
//line templates/unattach.qtpl:13
streamcannotUnattachDueToNonExistence(qw422016, hyphaName)
//line templates/unattach.qtpl:13
qw422016.N().S(`
`)
//line templates/unattach.qtpl:14
}
//line templates/unattach.qtpl:14
qw422016.N().S(`</main>
`)
//line templates/unattach.qtpl:16
}
//line templates/unattach.qtpl:16
func WriteUnattachAskHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName string, isOld bool) {
//line templates/unattach.qtpl:16
qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/unattach.qtpl:16
StreamUnattachAskHTML(qw422016, rq, hyphaName, isOld)
//line templates/unattach.qtpl:16
qt422016.ReleaseWriter(qw422016)
//line templates/unattach.qtpl:16
}
//line templates/unattach.qtpl:16
func UnattachAskHTML(rq *http.Request, hyphaName string, isOld bool) string {
//line templates/unattach.qtpl:16
qb422016 := qt422016.AcquireByteBuffer()
//line templates/unattach.qtpl:16
WriteUnattachAskHTML(qb422016, rq, hyphaName, isOld)
//line templates/unattach.qtpl:16
qs422016 := string(qb422016.B)
//line templates/unattach.qtpl:16
qt422016.ReleaseByteBuffer(qb422016)
//line templates/unattach.qtpl:16
return qs422016
//line templates/unattach.qtpl:16
}
//line templates/unattach.qtpl:18
func streamcannotUnattachDueToNonExistence(qw422016 *qt422016.Writer, hyphaName string) {
//line templates/unattach.qtpl:18
qw422016.N().S(`
<section>
<h1>Cannot unattach `)
//line templates/unattach.qtpl:20
qw422016.E().S(hyphaName)
//line templates/unattach.qtpl:20
qw422016.N().S(`</h1>
<p>This hypha does not exist.</p>
<p><a href="/page/`)
//line templates/unattach.qtpl:22
qw422016.E().S(hyphaName)
//line templates/unattach.qtpl:22
qw422016.N().S(`">Go back</a></p>
</section>
`)
//line templates/unattach.qtpl:24
}
//line templates/unattach.qtpl:24
func writecannotUnattachDueToNonExistence(qq422016 qtio422016.Writer, hyphaName string) {
//line templates/unattach.qtpl:24
qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/unattach.qtpl:24
streamcannotUnattachDueToNonExistence(qw422016, hyphaName)
//line templates/unattach.qtpl:24
qt422016.ReleaseWriter(qw422016)
//line templates/unattach.qtpl:24
}
//line templates/unattach.qtpl:24
func cannotUnattachDueToNonExistence(hyphaName string) string {
//line templates/unattach.qtpl:24
qb422016 := qt422016.AcquireByteBuffer()
//line templates/unattach.qtpl:24
writecannotUnattachDueToNonExistence(qb422016, hyphaName)
//line templates/unattach.qtpl:24
qs422016 := string(qb422016.B)
//line templates/unattach.qtpl:24
qt422016.ReleaseByteBuffer(qb422016)
//line templates/unattach.qtpl:24
return qs422016
//line templates/unattach.qtpl:24
}

View File

@ -5,22 +5,27 @@ import (
"path" "path"
"sort" "sort"
"strings" "strings"
"github.com/bouncepaw/mycorrhiza/util"
) )
// If Name == "", the tree is empty. // If Name == "", the tree is empty.
type tree struct { type tree struct {
name string name string
exists bool
prevSibling string
nextSibling string
siblings []string siblings []string
descendants []*tree descendants []*tree
root bool root bool
hyphaIterator func(func(string)) hyphaIterator func(func(string))
} }
// TreeAsHtml generates a tree for `hyphaName`. `hyphaStorage` has this type because package `tree` has no access to `HyphaData` data type. One day it shall have it, I guess. // Tree generates a tree for `hyphaName` as html and returns next and previous hyphae if any.
func TreeAsHtml(hyphaName string, hyphaIterator func(func(string))) string { func Tree(hyphaName string, hyphaIterator func(func(string))) (html, prev, next string) {
t := &tree{name: hyphaName, root: true, hyphaIterator: hyphaIterator} t := &tree{name: hyphaName, root: true, hyphaIterator: hyphaIterator}
t.fill() t.fill()
return t.asHtml() return t.asHtml(), util.BeautifulName(t.prevSibling), util.BeautifulName(t.nextSibling)
} }
// subtree adds a descendant tree to `t` and returns that tree. // subtree adds a descendant tree to `t` and returns that tree.
@ -34,11 +39,22 @@ func (t *tree) fork(descendantName string) *tree {
return subt return subt
} }
// Compare current prev next hyphae and decide if any of them should be set to `name2`.
func (t *tree) prevNextDetermine(name2 string) {
if name2 < t.name && (name2 > t.prevSibling || t.prevSibling == "") {
t.prevSibling = name2
} else if name2 > t.name && (name2 < t.nextSibling || t.nextSibling == "") {
t.nextSibling = name2
}
}
// Compares names and does something with them, may generate a subtree. // Compares names and does something with them, may generate a subtree.
func (t *tree) compareNamesAndAppend(name2 string) { func (t *tree) compareNamesAndAppend(name2 string) {
switch { switch {
case t.name == name2: case t.name == name2:
t.exists = true
case t.root && path.Dir(t.name) == path.Dir(name2): case t.root && path.Dir(t.name) == path.Dir(name2):
t.prevNextDetermine(name2)
t.siblings = append(t.siblings, name2) t.siblings = append(t.siblings, name2)
case t.name == path.Dir(name2): case t.name == path.Dir(name2):
t.fork(name2).fill() t.fork(name2).fill()

View File

@ -10,53 +10,55 @@ import (
"github.com/bouncepaw/mycorrhiza/util" "github.com/bouncepaw/mycorrhiza/util"
) )
func PopulateFixedUserStorage() { // ReadUsersFromFilesystem reads all user information from filesystem and stores it internally. Call it during initialization.
func ReadUsersFromFilesystem() {
rememberUsers(usersFromFixedCredentials())
readTokensToUsers()
}
func usersFromFixedCredentials() []*User {
var users []*User
contents, err := ioutil.ReadFile(util.FixedCredentialsPath) contents, err := ioutil.ReadFile(util.FixedCredentialsPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
err = json.Unmarshal(contents, &UserStorage.Users) err = json.Unmarshal(contents, &users)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for _, user := range UserStorage.Users { log.Println("Found", len(users), "fixed users")
user.Group = groupFromString(user.GroupString) return users
} }
log.Println("Found", len(UserStorage.Users), "fixed users")
contents, err = ioutil.ReadFile(tokenStoragePath()) func rememberUsers(uu []*User) {
// uu is used to not shadow the `users` in `users.go`.
for _, user := range uu {
users.Store(user.Name, user)
}
}
func readTokensToUsers() {
contents, err := ioutil.ReadFile(tokenStoragePath())
if os.IsNotExist(err) { if os.IsNotExist(err) {
return return
} }
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var tmp map[string]string var tmp map[string]string
err = json.Unmarshal(contents, &tmp) err = json.Unmarshal(contents, &tmp)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for token, username := range tmp { for token, username := range tmp {
user := UserStorage.userByName(username) commenceSession(username, token)
UserStorage.Tokens[token] = user
} }
log.Println("Found", len(tmp), "active sessions") log.Println("Found", len(tmp), "active sessions")
} }
func dumpTokens() { // Return path to tokens.json. Creates folders if needed.
tmp := make(map[string]string)
for token, user := range UserStorage.Tokens {
tmp[token] = user.Name
}
blob, err := json.Marshal(tmp)
if err != nil {
log.Println(err)
} else {
ioutil.WriteFile(tokenStoragePath(), blob, 0644)
}
}
// Return path to tokens.json.
func tokenStoragePath() string { func tokenStoragePath() string {
dir, err := xdg.DataFile("mycorrhiza/tokens.json") dir, err := xdg.DataFile("mycorrhiza/tokens.json")
if err != nil { if err != nil {
@ -67,3 +69,21 @@ func tokenStoragePath() string {
} }
return dir return dir
} }
func dumpTokens() {
tmp := make(map[string]string)
tokens.Range(func(k, v interface{}) bool {
token := k.(string)
username := v.(string)
tmp[token] = username
return true
})
blob, err := json.Marshal(tmp)
if err != nil {
log.Println(err)
} else {
ioutil.WriteFile(tokenStoragePath(), blob, 0644)
}
}

View File

@ -1,70 +0,0 @@
package user
import (
"log"
"net/http"
)
func groupFromString(s string) UserGroup {
switch s {
case "admin":
return UserAdmin
case "moderator":
return UserModerator
case "trusted":
return UserTrusted
case "editor":
return UserEditor
default:
log.Fatal("Unknown user group", s)
return UserAnon
}
}
// UserGroup represents a group that a user is part of.
type UserGroup int
const (
// UserAnon is the default user group which all unauthorized visitors have.
UserAnon UserGroup = iota
// UserEditor is a user who can edit and upload stuff.
UserEditor
// UserTrusted is a trusted editor who can also rename stuff.
UserTrusted
// UserModerator is a moderator who can also delete stuff.
UserModerator
// UserAdmin can do everything.
UserAdmin
)
var minimalRights = map[string]UserGroup{
"edit": UserEditor,
"upload-binary": UserEditor,
"upload-text": UserEditor,
"rename-ask": UserTrusted,
"rename-confirm": UserTrusted,
"delete-ask": UserModerator,
"delete-confirm": UserModerator,
"reindex": UserAdmin,
}
func (ug UserGroup) CanAccessRoute(route string) bool {
if !AuthUsed {
return true
}
if minimalRight, ok := minimalRights[route]; ok {
if ug >= minimalRight {
return true
}
return false
}
return true
}
func CanProceed(rq *http.Request, route string) bool {
return FromRequest(rq).OrAnon().CanProceed(route)
}
func (u *User) CanProceed(route string) bool {
return u.Group.CanAccessRoute(route)
}

75
user/net.go Normal file
View File

@ -0,0 +1,75 @@
package user
import (
"log"
"net/http"
"time"
"github.com/bouncepaw/mycorrhiza/util"
)
// CanProceed returns `true` if the user in `rq` has enough rights to access `route`.
func CanProceed(rq *http.Request, route string) bool {
return FromRequest(rq).CanProceed(route)
}
// FromRequest returns user from `rq`. If there is no user, an anon user is returned instead.
func FromRequest(rq *http.Request) *User {
cookie, err := rq.Cookie("mycorrhiza_token")
if err != nil {
return EmptyUser()
}
return userByToken(cookie.Value)
}
// LogoutFromRequest logs the user in `rq` out and rewrites the cookie in `w`.
func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) {
cookieFromUser, err := rq.Cookie("mycorrhiza_token")
if err == nil {
http.SetCookie(w, cookie("token", "", time.Unix(0, 0)))
terminateSession(cookieFromUser.Value)
}
}
// LoginDataHTTP logs such user in and returns string representation of an error if there is any.
func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string {
w.Header().Set("Content-Type", "text/html;charset=utf-8")
if !HasUsername(username) {
w.WriteHeader(http.StatusBadRequest)
log.Println("Unknown username", username, "was entered")
return "unknown username"
}
if !CredentialsOK(username, password) {
w.WriteHeader(http.StatusBadRequest)
log.Println("A wrong password was entered for username", username)
return "wrong password"
}
token, err := AddSession(username)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return err.Error()
}
http.SetCookie(w, cookie("token", token, time.Now().Add(365*24*time.Hour)))
return ""
}
// AddSession saves a session for `username` and returns a token to use.
func AddSession(username string) (string, error) {
token, err := util.RandomString(16)
if err == nil {
commenceSession(username, token)
log.Println("New token for", username, "is", token)
}
return token, err
}
// A handy cookie constructor
func cookie(name_suffix, val string, t time.Time) *http.Cookie {
return &http.Cookie{
Name: "mycorrhiza_" + name_suffix,
Value: val,
Expires: t,
Path: "/",
}
}

View File

@ -1,138 +1,69 @@
package user package user
import ( import (
"log" "sync"
"net/http"
"time"
"github.com/bouncepaw/mycorrhiza/util"
) )
func (u *User) OrAnon() *User {
if u == nil {
return &User{}
}
return u
}
func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) {
cookieFromUser, err := rq.Cookie("mycorrhiza_token")
if err == nil {
http.SetCookie(w, cookie("token", "", time.Unix(0, 0)))
terminateSession(cookieFromUser.Value)
}
}
func (us *FixedUserStorage) userByToken(token string) *User {
if user, ok := us.Tokens[token]; ok {
return user
}
return nil
}
func (us *FixedUserStorage) userByName(username string) *User {
for _, user := range us.Users {
if user.Name == username {
return user
}
}
return nil
}
func FromRequest(rq *http.Request) *User {
cookie, err := rq.Cookie("mycorrhiza_token")
if err != nil {
return nil
}
return UserStorage.userByToken(cookie.Value).OrAnon()
}
func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string {
w.Header().Set("Content-Type", "text/html;charset=utf-8")
if !HasUsername(username) {
w.WriteHeader(http.StatusBadRequest)
log.Println("Unknown username", username, "was entered")
return "unknown username"
}
if !CredentialsOK(username, password) {
w.WriteHeader(http.StatusBadRequest)
log.Println("A wrong password was entered for username", username)
return "wrong password"
}
token, err := AddSession(username)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return err.Error()
}
http.SetCookie(w, cookie("token", token, time.Now().Add(14*24*time.Hour)))
return ""
}
// AddSession saves a session for `username` and returns a token to use.
func AddSession(username string) (string, error) {
token, err := util.RandomString(16)
if err == nil {
for _, user := range UserStorage.Users {
if user.Name == username {
UserStorage.Tokens[token] = user
go dumpTokens()
}
}
log.Println("New token for", username, "is", token)
}
return token, err
}
func terminateSession(token string) {
delete(UserStorage.Tokens, token)
go dumpTokens()
}
func HasUsername(username string) bool {
for _, user := range UserStorage.Users {
if user.Name == username {
return true
}
}
return false
}
func CredentialsOK(username, password string) bool {
for _, user := range UserStorage.Users {
if user.Name == username && user.Password == password {
return true
}
}
return false
}
type FixedUserStorage struct {
Users []*User
Tokens map[string]*User
}
var UserStorage = FixedUserStorage{Tokens: make(map[string]*User)}
// AuthUsed shows if a method of authentication is used. You should set it by yourself.
var AuthUsed bool
// User is a user. // User is a user.
type User struct { type User struct {
// Name is a username. It must follow hypha naming rules. // Name is a username. It must follow hypha naming rules.
Name string `json:"name"` Name string `json:"name"`
// Group the user is part of. Group string `json:"group"`
Group UserGroup `json:"-"`
GroupString string `json:"group"`
Password string `json:"password"` Password string `json:"password"`
sync.RWMutex
} }
// A handy cookie constructor // Route — Right (more is more right)
func cookie(name_suffix, val string, t time.Time) *http.Cookie { var minimalRights = map[string]int{
return &http.Cookie{ "edit": 1,
Name: "mycorrhiza_" + name_suffix, "upload-binary": 1,
Value: val, "upload-text": 1,
Expires: t, "rename-ask": 2,
Path: "/", "rename-confirm": 2,
"unattach-ask": 2,
"unattach-confirm": 2,
"update-header-links": 3,
"delete-ask": 3,
"delete-confirm": 3,
"reindex": 4,
}
// Group — Right
var groupRight = map[string]int{
"anon": 0,
"editor": 1,
"trusted": 2,
"moderator": 3,
"admin": 4,
}
func EmptyUser() *User {
return &User{
Name: "anon",
Group: "anon",
Password: "",
} }
} }
func (user *User) CanProceed(route string) bool {
if !AuthUsed {
return true
}
user.RLock()
defer user.RUnlock()
right, _ := groupRight[user.Group]
minimalRight, _ := minimalRights[route]
if right >= minimalRight {
return true
}
return false
}
func (user *User) isCorrectPassword(password string) bool {
user.RLock()
defer user.RUnlock()
return password == user.Password
}

66
user/users.go Normal file
View File

@ -0,0 +1,66 @@
package user
import (
"sync"
)
var AuthUsed bool
var users sync.Map
var tokens sync.Map
func ListUsersWithGroup(group string) []string {
usersWithTheGroup := []string{}
users.Range(func(_, v interface{}) bool {
userobj := v.(*User)
if userobj.Group == group {
usersWithTheGroup = append(usersWithTheGroup, userobj.Name)
}
return true
})
return usersWithTheGroup
}
func Count() int {
i := 0
users.Range(func(k, v interface{}) bool {
i++
return true
})
return i
}
func HasUsername(username string) bool {
_, has := users.Load(username)
return has
}
func CredentialsOK(username, password string) bool {
return userByName(username).isCorrectPassword(password)
}
func userByToken(token string) *User {
if usernameUntyped, ok := tokens.Load(token); ok {
username := usernameUntyped.(string)
return userByName(username)
}
return EmptyUser()
}
func userByName(username string) *User {
if userUntyped, ok := users.Load(username); ok {
user := userUntyped.(*User)
return user
}
return EmptyUser()
}
func commenceSession(username, token string) {
tokens.Store(token, username)
go dumpTokens()
}
func terminateSession(token string) {
tokens.Delete(token)
go dumpTokens()
}

35
util/header_links.go Normal file
View File

@ -0,0 +1,35 @@
package util
import (
"strings"
)
func SetDefaultHeaderLinks() {
HeaderLinks = []HeaderLink{
{"/", SiteName},
{"/recent-changes", "Recent changes"},
{"/list", "All hyphae"},
{"/random", "Random"},
}
}
// rocketlinkλ is markup.Rocketlink. You have to pass it like that to avoid cyclical dependency.
func ParseHeaderLinks(text string, rocketlinkλ func(string, string) (string, string, string)) {
HeaderLinks = []HeaderLink{}
for _, line := range strings.Split(text, "\n") {
if strings.HasPrefix(line, "=>") {
href, text, _ := rocketlinkλ(line, HeaderLinksHypha)
HeaderLinks = append(HeaderLinks, HeaderLink{
Href: href,
Display: text,
})
}
}
}
type HeaderLink struct {
Href string
Display string
}
var HeaderLinks []HeaderLink

View File

@ -8,11 +8,14 @@ import (
) )
var ( var (
URL string
ServerPort string ServerPort string
HomePage string HomePage string
SiteTitle string SiteNavIcon string
SiteName string
WikiDir string WikiDir string
UserTree string UserHypha string
HeaderLinksHypha string
AuthMethod string AuthMethod string
FixedCredentialsPath string FixedCredentialsPath string
) )
@ -54,3 +57,11 @@ func RandomString(n int) (string, error) {
} }
return hex.EncodeToString(bytes), nil return hex.EncodeToString(bytes), nil
} }
// Strip hypha name from all ancestor names, replace _ with spaces, title case
func BeautifulName(uglyName string) string {
if uglyName == "" {
return uglyName
}
return strings.Title(strings.ReplaceAll(uglyName, "_", " "))
}