Merge pull request #14 from bouncepaw/13mycelia

13mycelia
This commit is contained in:
Timur Ismagilov 2020-07-04 00:22:19 +05:00 committed by GitHub
commit 283dd49d8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 665 additions and 249 deletions

View File

@ -1,2 +1,6 @@
# mycorrhiza wiki # mycorrhiza wiki
Simple wiki engine inspired by fungi. A wiki engine inspired by fungi.
Current version: 0.5.
Not production-ready.

View File

@ -8,17 +8,23 @@ import (
"path/filepath" "path/filepath"
) )
type MyceliumConfig struct {
Names []string `json:"names"`
Type string `json:"type"`
}
const ( const (
HyphaPattern = `[^\s\d:/?&\\][^:?&\\]*` HyphaPattern = `[^\s\d:/?&\\][^:?&\\]*`
HyphaUrl = `/{hypha:` + HyphaPattern + `}` HyphaUrl = `/{hypha:` + HyphaPattern + `}`
RevisionPattern = `[\d]+` RevisionPattern = `[\d]+`
RevQuery = `{rev:` + RevisionPattern + `}` RevQuery = `{rev:` + RevisionPattern + `}`
MyceliumPattern = `[^\s\d:/?&\\][^:?&\\/]*`
MyceliumUrl = `/:{mycelium:` + MyceliumPattern + `}`
) )
var ( var (
DescribeHyphaHerePattern = "Describe %s here" DescribeHyphaHerePattern = "Describe %s here"
WikiDir string WikiDir string
TemplatesDir string
configJsonPath string configJsonPath string
// Default values that can be overriden in config.json // Default values that can be overriden in config.json
@ -28,22 +34,23 @@ var (
GenericErrorMsg = `<b>Sorry, something went wrong</b>` GenericErrorMsg = `<b>Sorry, something went wrong</b>`
SiteTitle = `MycorrhizaWiki` SiteTitle = `MycorrhizaWiki`
Theme = `default-light` Theme = `default-light`
Mycelia = []MyceliumConfig{
{[]string{"main"}, "main"},
{[]string{"sys", "system"}, "system"},
}
) )
func InitConfig(wd string) bool { func InitConfig(wd string) bool {
log.Println("WikiDir is", wd) log.Println("WikiDir is", wd)
WikiDir = wd WikiDir = wd
TemplatesDir = "Templates" configJsonPath = filepath.Join(WikiDir, "config.json")
configJsonPath = filepath.Join(filepath.Dir(WikiDir), "config.json")
if _, err := os.Stat(configJsonPath); os.IsNotExist(err) { if _, err := os.Stat(configJsonPath); os.IsNotExist(err) {
log.Println("config.json not found, using default values") log.Println("config.json not found, using default values")
} else { return false
log.Println("config.json found, overriding default values...")
return readConfig()
} }
log.Println("config.json found, overriding default values...")
return true return readConfig()
} }
func readConfig() bool { func readConfig() bool {
@ -54,9 +61,10 @@ func readConfig() bool {
} }
cfg := struct { cfg := struct {
Address string `json:"address"` Address string `json:"address"`
Theme string `json:"theme"` Theme string `json:"theme"`
SiteTitle string `json:"site-title"` SiteTitle string `json:"site-title"`
Mycelia []MyceliumConfig `json:"mycelia"`
TitleTemplates struct { TitleTemplates struct {
EditHypha string `json:"edit-hypha"` EditHypha string `json:"edit-hypha"`
ViewHypha string `json:"view-hypha"` ViewHypha string `json:"view-hypha"`
@ -74,6 +82,7 @@ func readConfig() bool {
SiteTitle = cfg.SiteTitle SiteTitle = cfg.SiteTitle
TitleEditTemplate = cfg.TitleTemplates.EditHypha TitleEditTemplate = cfg.TitleTemplates.EditHypha
TitleTemplate = cfg.TitleTemplates.ViewHypha TitleTemplate = cfg.TitleTemplates.ViewHypha
Mycelia = cfg.Mycelia
return true return true
} }

View File

@ -10,14 +10,19 @@ import (
"strings" "strings"
"github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/util"
) )
func (h *Hypha) MetaJsonPath() string { func (h *Hypha) MetaJsonPath() string {
return filepath.Join(h.Path(), "meta.json") return filepath.Join(h.Path(), "meta.json")
} }
func (h *Hypha) CanonicalName() string {
return util.UrlToCanonical(h.FullName)
}
func (h *Hypha) Path() string { func (h *Hypha) Path() string {
return filepath.Join(cfg.WikiDir, h.FullName) return filepath.Join(cfg.WikiDir, h.CanonicalName())
} }
func (h *Hypha) TextPath() string { func (h *Hypha) TextPath() string {
@ -25,7 +30,7 @@ func (h *Hypha) TextPath() string {
} }
func (h *Hypha) parentName() string { func (h *Hypha) parentName() string {
return filepath.Dir(h.FullName) return filepath.Dir(util.UrlToCanonical(h.FullName))
} }
// hasBinaryData returns true if the revision has any binary data associated. // hasBinaryData returns true if the revision has any binary data associated.

View File

@ -54,6 +54,3 @@ func (s *Storage) indexHyphae(path string) {
} }
} }
} }
func (h *Hypha) Close() {
}

View File

@ -6,6 +6,8 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"github.com/bouncepaw/mycorrhiza/util"
) )
func (s *Storage) RenderHypha(h *Hypha) { func (s *Storage) RenderHypha(h *Hypha) {
@ -24,6 +26,7 @@ type Tree struct {
// It can also generate trees for non-existent hyphae, that's why we use `name string` instead of making it a method on `Hypha`. // It can also generate trees for non-existent hyphae, that's why we use `name string` instead of making it a method on `Hypha`.
// In `root` is `false`, siblings will not be fetched. // In `root` is `false`, siblings will not be fetched.
func (s *Storage) GetTree(name string, root bool) *Tree { func (s *Storage) GetTree(name string, root bool) *Tree {
name = util.UrlToCanonical(name)
t := &Tree{Name: name, Root: root} t := &Tree{Name: name, Root: root}
for hyphaName, _ := range s.paths { for hyphaName, _ := range s.paths {
s.compareNamesAndAppend(t, hyphaName) s.compareNamesAndAppend(t, hyphaName)
@ -65,14 +68,14 @@ func (t *Tree) AsHtml() (html string) {
html += `<ul class="navitree__node">` html += `<ul class="navitree__node">`
if t.Root { if t.Root {
for _, ancestor := range t.Ancestors { for _, ancestor := range t.Ancestors {
html += navitreeEntry(ancestor, "navitree__ancestor") html += navitreeEntry(util.CanonicalToDisplay(ancestor), "navitree__ancestor")
} }
for _, siblingName := range t.Siblings { for _, siblingName := range t.Siblings {
html += navitreeEntry(siblingName, "navitree__sibling") html += navitreeEntry(util.CanonicalToDisplay(siblingName), "navitree__sibling")
} }
html += navitreeEntry(t.Name, "navitree__pagename") html += navitreeEntry(util.CanonicalToDisplay(t.Name), "navitree__pagename")
} else { } else {
html += navitreeEntry(t.Name, "navitree__name") html += navitreeEntry(util.CanonicalToDisplay(t.Name), "navitree__name")
} }
for _, subtree := range t.Descendants { for _, subtree := range t.Descendants {
@ -89,5 +92,5 @@ func navitreeEntry(name, class string) string {
return fmt.Sprintf(`<li class="navitree__entry %s"> return fmt.Sprintf(`<li class="navitree__entry %s">
<a class="navitree__link" href="/%s">%s</a> <a class="navitree__link" href="/%s">%s</a>
</li> </li>
`, class, name, filepath.Base(name)) `, class, ":"+util.DisplayToCanonical(name), filepath.Base(name))
} }

View File

@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"github.com/bouncepaw/mycorrhiza/util"
"gopkg.in/russross/blackfriday.v2" "gopkg.in/russross/blackfriday.v2"
) )
@ -20,7 +21,7 @@ func (h *Hypha) asHtml() (string, error) {
` `
// What about using <figure>? // What about using <figure>?
if h.hasBinaryData() { if h.hasBinaryData() {
ret += fmt.Sprintf(`<img src="/%s?action=binary&rev=%d" class="page__amnt"/>`, rev.FullName, rev.Id) ret += fmt.Sprintf(`<img src="/:%s?action=binary&rev=%d" class="page__amnt"/>`, util.DisplayToCanonical(rev.FullName), rev.Id)
} }
contents, err := ioutil.ReadFile(rev.TextPath) contents, err := ioutil.ReadFile(rev.TextPath)

View File

@ -12,7 +12,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/mycelium"
"github.com/bouncepaw/mycorrhiza/util"
) )
type Hypha struct { type Hypha struct {
@ -22,8 +23,8 @@ type Hypha struct {
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
Revisions map[string]*Revision `json:"revisions"` Revisions map[string]*Revision `json:"revisions"`
actual *Revision `json:"-"` actual *Revision `json:"-"`
Invalid bool Invalid bool `json:"-"`
Err error Err error `json:"-"`
} }
func (h *Hypha) Invalidate(err error) *Hypha { func (h *Hypha) Invalidate(err error) *Hypha {
@ -32,10 +33,20 @@ func (h *Hypha) Invalidate(err error) *Hypha {
return h return h
} }
func (s *Storage) Open(name string) *Hypha { func (s *Storage) OpenFromMap(m map[string]string) *Hypha {
name := mycelium.NameWithMyceliumInMap(m)
h := s.open(name)
if rev, ok := m["rev"]; ok {
return h.OnRevision(rev)
}
return h
}
func (s *Storage) open(name string) *Hypha {
name = util.UrlToCanonical(name)
h := &Hypha{ h := &Hypha{
Exists: true, Exists: true,
FullName: name, FullName: util.CanonicalToDisplay(name),
} }
path, ok := s.paths[name] path, ok := s.paths[name]
// This hypha does not exist yet // This hypha does not exist yet
@ -109,13 +120,18 @@ func (h *Hypha) ActionRaw(w http.ResponseWriter) *Hypha {
if h.Invalid { if h.Invalid {
return h return h
} }
fileContents, err := ioutil.ReadFile(h.actual.TextPath) if h.Exists {
if err != nil { fileContents, err := ioutil.ReadFile(h.actual.TextPath)
return h.Invalidate(err) if err != nil {
return h.Invalidate(err)
}
w.Header().Set("Content-Type", h.mimeTypeForActionRaw())
w.WriteHeader(http.StatusOK)
w.Write(fileContents)
} else {
log.Println("Hypha", h.FullName, "has no actual revision")
w.WriteHeader(http.StatusNotFound)
} }
w.Header().Set("Content-Type", h.mimeTypeForActionRaw())
w.WriteHeader(http.StatusOK)
w.Write(fileContents)
return h return h
} }
@ -125,13 +141,18 @@ func (h *Hypha) ActionBinary(w http.ResponseWriter) *Hypha {
if h.Invalid { if h.Invalid {
return h return h
} }
fileContents, err := ioutil.ReadFile(h.actual.BinaryPath) if h.Exists {
if err != nil { fileContents, err := ioutil.ReadFile(h.actual.BinaryPath)
return h.Invalidate(err) if err != nil {
return h.Invalidate(err)
}
w.Header().Set("Content-Type", h.actual.BinaryMime)
w.WriteHeader(http.StatusOK)
w.Write(fileContents)
} else {
log.Println("Hypha", h.FullName, "has no actual revision")
w.WriteHeader(http.StatusNotFound)
} }
w.Header().Set("Content-Type", h.actual.BinaryMime)
w.WriteHeader(http.StatusOK)
w.Write(fileContents)
return h return h
} }
@ -181,7 +202,7 @@ func (h *Hypha) CreateDirIfNeeded() *Hypha {
return h return h
} }
// os.MkdirAll created dir if it is not there. Basically, checks it for us. // os.MkdirAll created dir if it is not there. Basically, checks it for us.
err := os.MkdirAll(filepath.Join(cfg.WikiDir, h.FullName), os.ModePerm) err := os.MkdirAll(h.Path(), os.ModePerm)
if err != nil { if err != nil {
h.Invalidate(err) h.Invalidate(err)
} }
@ -313,7 +334,7 @@ func (h *Hypha) SaveJson() *Hypha {
// Store adds `h` to the `Hs` if it is not already there // Store adds `h` to the `Hs` if it is not already there
func (h *Hypha) Store() *Hypha { func (h *Hypha) Store() *Hypha {
if !h.Invalid { if !h.Invalid {
Hs.paths[h.FullName] = h.Path() Hs.paths[h.CanonicalName()] = h.Path()
} }
return h return h
} }

View File

@ -14,7 +14,7 @@ import (
// Boilerplate code present in many handlers. Good to have it. // Boilerplate code present in many handlers. Good to have it.
func HandlerBase(w http.ResponseWriter, rq *http.Request) *fs.Hypha { func HandlerBase(w http.ResponseWriter, rq *http.Request) *fs.Hypha {
vars := mux.Vars(rq) vars := mux.Vars(rq)
return fs.Hs.Open(vars["hypha"]).OnRevision(RevInMap(vars)) return fs.Hs.OpenFromMap(vars).OnRevision(RevInMap(vars))
} }
func HandlerRaw(w http.ResponseWriter, rq *http.Request) { func HandlerRaw(w http.ResponseWriter, rq *http.Request) {
@ -41,7 +41,7 @@ func HandlerView(w http.ResponseWriter, rq *http.Request) {
func HandlerEdit(w http.ResponseWriter, rq *http.Request) { func HandlerEdit(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(rq) vars := mux.Vars(rq)
h := fs.Hs.Open(vars["hypha"]).OnRevision("0") h := fs.Hs.OpenFromMap(vars).OnRevision("0")
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(render.HyphaEdit(h)) w.Write(render.HyphaEdit(h))
@ -51,7 +51,7 @@ func HandlerUpdate(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(rq) vars := mux.Vars(rq)
log.Println("Attempt to update hypha", vars["hypha"]) log.Println("Attempt to update hypha", vars["hypha"])
h := fs.Hs. h := fs.Hs.
Open(vars["hypha"]). OpenFromMap(vars).
CreateDirIfNeeded(). CreateDirIfNeeded().
AddRevisionFromHttpData(rq). AddRevisionFromHttpData(rq).
WriteTextFileFromHttpData(rq). WriteTextFileFromHttpData(rq).

53
main.go
View File

@ -10,6 +10,7 @@ import (
"github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/fs" "github.com/bouncepaw/mycorrhiza/fs"
"github.com/bouncepaw/mycorrhiza/mycelium"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -22,6 +23,25 @@ func RevInMap(m map[string]string) string {
return "0" return "0"
} }
func IdempotentRouterBoiler(router *mux.Router, action string, handler func(w http.ResponseWriter, rq *http.Request)) {
router.
Queries("action", action, "rev", cfg.RevQuery).
Path(cfg.MyceliumUrl + cfg.HyphaUrl).
HandlerFunc(handler)
router.
Queries("action", action).
Path(cfg.MyceliumUrl + cfg.HyphaUrl).
HandlerFunc(handler)
router.
Queries("action", action, "rev", cfg.RevQuery).
Path(cfg.HyphaUrl).
HandlerFunc(handler)
router.
Queries("action", action).
Path(cfg.HyphaUrl).
HandlerFunc(handler)
}
func main() { func main() {
if len(os.Args) == 1 { if len(os.Args) == 1 {
panic("Expected a root wiki pages directory") panic("Expected a root wiki pages directory")
@ -34,6 +54,7 @@ func main() {
log.Println("Welcome to MycorrhizaWiki α") log.Println("Welcome to MycorrhizaWiki α")
cfg.InitConfig(wikiDir) cfg.InitConfig(wikiDir)
log.Println("Indexing hyphae...") log.Println("Indexing hyphae...")
mycelium.Init()
fs.InitStorage() fs.InitStorage()
// Start server code. See handlers.go for handlers' implementations. // Start server code. See handlers.go for handlers' implementations.
@ -43,32 +64,22 @@ func main() {
http.ServeFile(w, rq, filepath.Join(filepath.Dir(cfg.WikiDir), "favicon.ico")) http.ServeFile(w, rq, filepath.Join(filepath.Dir(cfg.WikiDir), "favicon.ico"))
}) })
r.Queries("action", "binary", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). IdempotentRouterBoiler(r, "binary", HandlerBinary)
HandlerFunc(HandlerBinary) IdempotentRouterBoiler(r, "raw", HandlerRaw)
r.Queries("action", "binary").Path(cfg.HyphaUrl). IdempotentRouterBoiler(r, "zen", HandlerZen)
HandlerFunc(HandlerBinary) IdempotentRouterBoiler(r, "view", HandlerView)
r.Queries("action", "raw", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
HandlerFunc(HandlerRaw)
r.Queries("action", "raw").Path(cfg.HyphaUrl).
HandlerFunc(HandlerRaw)
r.Queries("action", "zen", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
HandlerFunc(HandlerZen)
r.Queries("action", "zen").Path(cfg.HyphaUrl).
HandlerFunc(HandlerZen)
r.Queries("action", "view", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
HandlerFunc(HandlerView)
r.Queries("action", "view").Path(cfg.HyphaUrl).
HandlerFunc(HandlerView)
r.Queries("action", "edit").Path(cfg.MyceliumUrl + cfg.HyphaUrl).
HandlerFunc(HandlerEdit)
r.Queries("action", "edit").Path(cfg.HyphaUrl). r.Queries("action", "edit").Path(cfg.HyphaUrl).
HandlerFunc(HandlerEdit) HandlerFunc(HandlerEdit)
r.Queries("action", "update").Path(cfg.HyphaUrl).Methods("POST"). r.Queries("action", "update").Path(cfg.MyceliumUrl + cfg.HyphaUrl).
HandlerFunc(HandlerUpdate) Methods("POST").HandlerFunc(HandlerUpdate)
r.Queries("action", "update").Path(cfg.HyphaUrl).
Methods("POST").HandlerFunc(HandlerUpdate)
r.HandleFunc(cfg.MyceliumUrl+cfg.HyphaUrl, HandlerView)
r.HandleFunc(cfg.HyphaUrl, HandlerView) r.HandleFunc(cfg.HyphaUrl, HandlerView)
// Debug page that renders all hyphae. // Debug page that renders all hyphae.

107
mycelium/mycelium.go Normal file
View File

@ -0,0 +1,107 @@
package mycelium
import (
"io/ioutil"
"log"
"strings"
"github.com/bouncepaw/mycorrhiza/cfg"
)
var (
MainMycelium string
SystemMycelium string
)
func gatherDirNames(path string) map[string]struct{} {
res := make(map[string]struct{})
nodes, err := ioutil.ReadDir(path)
if err != nil {
log.Fatal(err)
}
for _, node := range nodes {
if node.IsDir() {
res[node.Name()] = struct{}{}
}
}
return res
}
// Add values to the set. If a value is already there, return false.
func addInUniqueSet(set map[string]struct{}, names []string) bool {
ok := true
for _, name := range names {
if _, present := set[name]; present {
ok = false
}
set[name] = struct{}{}
}
return ok
}
func Init() {
var (
// Used to check if there are no duplicates
foundNames = make(map[string]struct{})
dirs = gatherDirNames(cfg.WikiDir)
mainPresent bool
systemPresent bool
)
for _, mycelium := range cfg.Mycelia {
switch mycelium.Type {
case "main":
mainPresent = true
MainMycelium = mycelium.Names[0]
case "system":
systemPresent = true
SystemMycelium = mycelium.Names[0]
}
// Check if there is a dir corresponding to the mycelium
if _, ok := dirs[mycelium.Names[0]]; !ok {
log.Fatal("No directory found for mycelium " + mycelium.Names[0])
}
// Confirm uniqueness of names
if ok := addInUniqueSet(foundNames, mycelium.Names); !ok {
log.Fatal("At least one name was used more than once for mycelia")
}
}
if !mainPresent {
log.Fatal("No `main` mycelium given in config.json")
}
if !systemPresent {
log.Fatal("No `system` mycelium given in config.json")
}
log.Println("Mycelial dirs are present")
}
func NameWithMyceliumInMap(m map[string]string) (res string) {
var (
hyphaName, okH = m["hypha"]
mycelName, okM = m["mycelium"]
)
log.Println(m)
if !okH {
// It will result in an error when trying to open a hypha with such name
return ":::"
}
if okM {
res = canonicalMycelium(mycelName)
} else {
res = MainMycelium
}
return res + "/" + hyphaName
}
func canonicalMycelium(name string) string {
log.Println("Determining canonical mycelial name for", name)
name = strings.ToLower(name)
for _, mycel := range cfg.Mycelia {
for _, mycelName := range mycel.Names {
if mycelName == name {
return mycel.Names[0]
}
}
}
// This is a nonexistent mycelium. Return a name that will trigger an error
return ":error:"
}

View File

@ -8,6 +8,7 @@ import (
"github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/fs" "github.com/bouncepaw/mycorrhiza/fs"
"github.com/bouncepaw/mycorrhiza/mycelium"
) )
// HyphaEdit renders hypha editor. // HyphaEdit renders hypha editor.
@ -29,7 +30,7 @@ func HyphaEdit(h *fs.Hypha) []byte { //
// HyphaUpdateOk is used to inform that update was successful. // HyphaUpdateOk is used to inform that update was successful.
func HyphaUpdateOk(h *fs.Hypha) []byte { // func HyphaUpdateOk(h *fs.Hypha) []byte { //
return layout("updateOk"). return layout("update_ok").
withMap(map[string]string{"Name": h.FullName}). withMap(map[string]string{"Name": h.FullName}).
Bytes() Bytes()
} }
@ -56,6 +57,9 @@ func hyphaGeneric(name, content, templateName string) []byte {
// wrapInBase is used to wrap layouts in things that are present on all pages. // wrapInBase is used to wrap layouts in things that are present on all pages.
func (lyt *Layout) wrapInBase(keys map[string]string) []byte { func (lyt *Layout) wrapInBase(keys map[string]string) []byte {
if lyt.invalid {
return lyt.Bytes()
}
page := map[string]string{ page := map[string]string{
"Title": cfg.SiteTitle, "Title": cfg.SiteTitle,
"Main": "", "Main": "",
@ -82,7 +86,12 @@ type Layout struct {
} }
func layout(name string) *Layout { func layout(name string) *Layout {
h := fs.Hs.Open(path.Join(cfg.TemplatesDir, cfg.Theme, name+".html")).OnRevision("0") lytName := path.Join("theme", cfg.Theme, name+".html")
h := fs.Hs.OpenFromMap(map[string]string{
"mycelium": mycelium.SystemMycelium,
"hypha": lytName,
"rev": "0",
})
if h.Invalid { if h.Invalid {
return &Layout{nil, nil, true, h.Err} return &Layout{nil, nil, true, h.Err}
} }

49
util/util.go Normal file
View File

@ -0,0 +1,49 @@
package util
import (
"strings"
"unicode"
)
func addColonPerhaps(name string) string {
if strings.HasPrefix(name, ":") {
return name
}
return ":" + name
}
func removeColonPerhaps(name string) string {
if strings.HasPrefix(name, ":") {
return name[1:]
}
return name
}
func UrlToCanonical(name string) string {
return removeColonPerhaps(
strings.ToLower(strings.ReplaceAll(name, " ", "_")))
}
func DisplayToCanonical(name string) string {
return removeColonPerhaps(
strings.ToLower(strings.ReplaceAll(name, " ", "_")))
}
func CanonicalToDisplay(name string) (res string) {
tmp := strings.Title(name)
var afterPoint bool
for _, ch := range tmp {
if afterPoint {
afterPoint = false
ch = unicode.ToLower(ch)
}
switch ch {
case '.':
afterPoint = true
case '_':
ch = ' '
}
res += string(ch)
}
return addColonPerhaps(res)
}

View File

@ -1,9 +0,0 @@
{
"address": "127.0.0.1:1737",
"theme": "default-light",
"site-title": "🍄 MycorrhizaWiki",
"title-templates": {
"edit-hypha": "Edit %s at MycorrhizaWiki",
"view-hypha": "%s at MycorrhizaWiki"
}
}

View File

@ -1,11 +0,0 @@
<div class="naviwrapper">
<form class="naviwrapper__edit edit-box"
method="POST"
enctype="multipart/form-data"
action="?action=update">
<h4>Edit box</h4>
<!-- It is important that there is no indent ↓ -->
<textarea class="edit-box__text" name="text">
{{ .Text }}</textarea>
</form>
</div>

View File

@ -1,21 +0,0 @@
<div style=""><h4>Text MIME-type</h4>
<p>Good types are <code>text/markdown</code> and <code>text/plain</code></p>
<input type="text" name="text_mime" value="{{ .TextMime }}" form="edit-form"/>
<h4>Revision comment</h4>
<p>Please make your comment helpful</p>
<input type="text" name="comment" value="Update {{ .Name }}" form="edit-form"/>
<h4>Edit tags</h4>
<p>Tags are separated by commas, whitespace is ignored</p>
<input type="text" name="tags" value="{{ .Tags }}" form="edit-form"/>
<h4>Upload file</h4>
<p>If this hypha has a file like that, the text above is meant to be a description of it</p>
<input type="file" name="binary" form="edit-form"/>
<p>
<input type="submit" value="update" form="edit-form"/>
<a href="?">Cancel</a>
</p>
</div>

View File

@ -1,4 +0,0 @@
<h1 class="page__title">{{ . }}</h1>
<p class="msg_hypha-does-not-exist msg">
The hypha you are trying to access does not exist yet. Why not <a href="?action=edit">create</a> it?
</p>

31
wiki/config.json Normal file
View File

@ -0,0 +1,31 @@
{
"address": "127.0.0.1:1737",
"theme": "default-light",
"site-title": "🍄 MycorrhizaWiki",
"title-templates": {
"edit-hypha": "Edit %s at MycorrhizaWiki",
"view-hypha": "%s at MycorrhizaWiki"
},
"mycelia": [
{
"names": ["main"],
"type": "main"
},
{
"names": ["sys", "system"],
"type": "system"
},
{
"names": ["spec", "special"],
"type": "special"
},
{
"names": ["user","u"],
"type": "user"
},
{
"names": ["tag", "t"],
"type": "tag"
}
]
}

View File

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

@ -1,4 +1,4 @@
According to real *scientists*, fruit is a type of fetus. Most of them are tasty and cool, though some of them are really sour and depressing. Be careful when choosing fruit. Best ones are: According to real *scientists*, fruit is a type of fetus. Most of them are tasty and cool, though some of them are really sour and depressing. Be careful when choosing fruit. Best ones are:
* [Apple](Apple) * [Apple](Apple)
* [Pear](Pear) * [Pear](Pear)

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,3 +1,3 @@
Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности. Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности.
Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg

View File

@ -1,6 +1,6 @@
Mycorrhiza is pure happiness Mycorrhiza is pure happiness
Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности. Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности.
Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg

View File

Before

Width:  |  Height:  |  Size: 427 KiB

After

Width:  |  Height:  |  Size: 427 KiB

View File

@ -1,2 +1,2 @@
Я́блоко — сочный плод яблони, который употребляется в пищу в свежем виде, служит сырьём в кулинарии и для приготовления напитков. Наибольшее распространение получила яблоня домашняя, реже выращивают яблоню сливолистную. Размер красных, зелёных или жёлтых шаровидных плодов 5—13 см в диаметре. Происходит из Центральной Азии, где до сих пор произрастает дикорастущий предок яблони домашней — яблоня Сиверса. На сегодняшний день существует множество сортов этого вида яблони, произрастающих в различных климатических условиях. По времени созревания отличают летние, осенние и зимние сорта, более поздние сорта отличаются хорошей стойкостью. Я́блоко — сочный плод яблони, который употребляется в пищу в свежем виде, служит сырьём в кулинарии и для приготовления напитков. Наибольшее распространение получила яблоня домашняя, реже выращивают яблоню сливолистную. Размер красных, зелёных или жёлтых шаровидных плодов 5—13 см в диаметре. Происходит из Центральной Азии, где до сих пор произрастает дикорастущий предок яблони домашней — яблоня Сиверса. На сегодняшний день существует множество сортов этого вида яблони, произрастающих в различных климатических условиях. По времени созревания отличают летние, осенние и зимние сорта, более поздние сорта отличаются хорошей стойкостью.

View File

@ -1,3 +1,3 @@
A **pear** is a sweet fruit, usually with a green skin and a lot of juice, that has a round base and is slightly pointed towards the stem. A **pear** is a sweet fruit, usually with a green skin and a lot of juice, that has a round base and is slightly pointed towards the stem.
Source of info: [cambridge dict](https://dictionary.cambridge.org/ru/словарь/английский/pear) Source of info: [cambridge dict](https://dictionary.cambridge.org/ru/словарь/английский/pear)

View File

@ -1,3 +1,3 @@
Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees. Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees.
It is not an [](Apple)! It is not an [](Apple)!

View File

@ -1,4 +1,4 @@
Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees. Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees.
It is not an [Apple](Apple)! It is not an [Apple](Apple)!

View File

@ -1,5 +1,5 @@
Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees. Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees.
It is not an [Apple](/Apple)! It is not an [Apple](/Apple)!

View File

@ -1,6 +1,6 @@
Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees. Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees.
It is not an [Apple](/Fruit/Apple)! It is not an [Apple](/Fruit/Apple)!

View File

Before

Width:  |  Height:  |  Size: 608 KiB

After

Width:  |  Height:  |  Size: 608 KiB

View File

@ -1,7 +1,7 @@
Шляпка 7—16(20) см в диаметре, полушаровидная, раскрывающаяся до выпуклой и почти плоской со слабо вдавленным центром, с радиально разлинованным краем. Окраска тёмно-умброво-коричневая, оливково-охристая, охристо-коричневая, иногда серо-жёлтая, в центре более интенсивная. Общее покрывало на молодых грибах опушённое, ярко-жёлтое, затем остаётся в виде легко смываемых обрывков, на солнце белеющих, а к старости иногда становящихся серо-жёлтыми. Шляпка 7—16(20) см в диаметре, полушаровидная, раскрывающаяся до выпуклой и почти плоской со слабо вдавленным центром, с радиально разлинованным краем. Окраска тёмно-умброво-коричневая, оливково-охристая, охристо-коричневая, иногда серо-жёлтая, в центре более интенсивная. Общее покрывало на молодых грибах опушённое, ярко-жёлтое, затем остаётся в виде легко смываемых обрывков, на солнце белеющих, а к старости иногда становящихся серо-жёлтыми.
Пластинки частые, сначала узко-приросшие к ножке, затем свободные от неё, кремовые, с многочисленными пластиночками разной длины. Пластинки частые, сначала узко-приросшие к ножке, затем свободные от неё, кремовые, с многочисленными пластиночками разной длины.
Ножка достигает 9—20 см в высоту и 1—2,5 см в поперечнике, утончающаяся кверху, в основании с яйцевидным или шаровидным утолщением. Поверхность ножки волокнисто-бархатистая, белая или беловатая, при прикосновении иногда слабо буреющая. Кольцо в верхней части ножки, беловатое, перепончатое, не разлинованное. Остатки общего покрывала в виде нескольких поясков желтоватых бородавчатых хлопьев на утолщении ножки. Ножка достигает 9—20 см в высоту и 1—2,5 см в поперечнике, утончающаяся кверху, в основании с яйцевидным или шаровидным утолщением. Поверхность ножки волокнисто-бархатистая, белая или беловатая, при прикосновении иногда слабо буреющая. Кольцо в верхней части ножки, беловатое, перепончатое, не разлинованное. Остатки общего покрывала в виде нескольких поясков желтоватых бородавчатых хлопьев на утолщении ножки.

View File

@ -0,0 +1 @@
# Help

View File

@ -6,10 +6,10 @@
"tags": [ "tags": [
"" ""
], ],
"name": "Templates", "name": "Help",
"comment": "Update Templates", "comment": "Update Help",
"author": "", "author": "",
"time": 1593194769, "time": 1593540573,
"text_mime": "text/markdown", "text_mime": "text/markdown",
"binary_mime": "", "binary_mime": "",
"text_name": "1.markdown", "text_name": "1.markdown",

View File

@ -0,0 +1,85 @@
In MycorrhizaWiki **mycelia** are used to organize [hyphae](hyphae) in related namespaces.
## Mycelium traits
- Every mycelium has any number (0..∞) of hyphae.
- Every hypha is part of one mycelium.
- Every mycelium has one canonical name and any number of synonyms.
- Every wiki has one main mycelium.
- They are named by the same scheme as hyphae but they have a colon prefix.
## Mycelium URL
- Address a hypha in a particular mycelium: `/:sys/about`.
- Address subpage of the hypha above: `/:sys/about/more`
- Address a hypha in the main mycelium: `/How/does it work`.
- Address a hypha in the main mycelium explicitly: `/:main/How/does it work`.
## Mycelium configuration
In your `config.json`, in `"mycelia"` field there is an array of objects.
```
{
...
"mycelia": [
{
"names": ["main"],
"type": "main"
},
{
"names": ["sys", "system"],
"type": "system"
},
{
"names": ["spec", "special"],
"type": "special"
},
{
"names": ["user","u"],
"type": "user"
},
{
"names": ["tag", "t"],
"type": "tag"
}
]
...
}
```
Each object reprents a mycelium. You can set all their names there. First name in each `"names"` array is a canonical name for the mycelium.
Field `"type"` sets the mycelium's type. There are such types:
| **Type** | **Description** |
| `main` | The main mycelium. There must be exactly one such mycelium in a wiki. |
| `system` | Things like scripts, styles and templates go here. There must be exactly one such mycelium in a wiki. |
| `special` | Things like utility hyphae and plugin pages go here. It is optional because there are no hyphae or plugins now. |
| `user` | Userpages. It is optional because there are no users now. |
| `tag` | Pages describing tags. It is optional because there are no tags now. |
| `other` | Mycelia without any additional meaning added by the engine. There can be any number of them. |
## How are they stored in the filesystem.
For example, `wiki` is your wiki directory and you have configured the mycelia like in the example above. You should have structure like that:
```
wiki/
config.json ← your configuration
favicon.ico ← your site icon
main/ ← :main, <empty prefix>
...most of content goes here
sys/ ← :sys
...themes go here
spec/ ← :spec
...something goes here
user/ ← :user, :u
...user pages go here
tag/ ← :tag, :t
...pages describing tags go here
```
There are usual hypha directories inside those mycelial directories.
## Code
- Things related to reading the `config.json` go to the `cfg` module.
- Most of code related to mycelia is in the `fs` module.
- And also check out `handlers.go` and `main.go` for routing of mycelia.

View File

@ -0,0 +1,21 @@
{
"views": 0,
"deleted": false,
"revisions": {
"1": {
"tags": [
""
],
"name": "Mycelia",
"comment": "Update Help/Mycelia",
"author": "",
"time": 1593541268,
"text_mime": "text/markdown",
"binary_mime": "",
"text_name": "1.markdown",
"binary_name": ""
}
},
"Invalid": false,
"Err": null
}

11
wiki/sys/theme/2.markdown Normal file
View File

@ -0,0 +1,11 @@
# Themes
In MycorrhizaWiki, themes are used to specify what is shown to a user, the front-end. So, a theme consists of:
- **HTML templates.** Data is inserted into them.
- **CSS.**
- **JS.**
Your HTML template structure must be identical to the one. You can include any CSS and JS in your themes.
See [default-light](:sys/theme/default-light) theme for more information. This is the only theme that is guaranteed to work.

11
wiki/sys/theme/3.markdown Normal file
View File

@ -0,0 +1,11 @@
# Themes
In MycorrhizaWiki, themes are used to specify what is shown to a user, the front-end. So, a theme consists of:
- **HTML templates.** Data is inserted into them.
- **CSS.**
- **JS.**
Your HTML template structure must be identical to the one. You can include any CSS and JS in your themes.
See [default-light](/:sys/theme/default-light) theme for more information. This is the only theme that is guaranteed to work.

View File

@ -1,25 +1,25 @@
<html> <html>
<head> <head>
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<link rel="stylesheet" href="/Templates/default-light/main.css?action=raw"> <link rel="stylesheet" href="/:sys/theme/default-light/main.css?action=raw">
</head> </head>
<body> <body>
<header class="header"> <header class="header">
<!-- Site title is fetched from your config.json. Set your title in "site-title" field. You can add more things to the header here. --> <!-- Site title is fetched from your config.json. Set your title in "site-title" field. You can add more things to the header here. -->
<h1 class="header__site-title"> <h1 class="header__site-title">
<a href="/">{{ .SiteTitle }}</a> <a href="/">{{ .SiteTitle }}</a>
</h1> </h1>
<button class="sidebar-controller" id="shroomburger"> <button class="sidebar-controller" id="shroomburger">
</button> </button>
</header> </header>
<aside class="sidebar hidden_mobile" id="sidebar"> <aside class="sidebar hidden_mobile" id="sidebar">
{{ .Sidebar }} {{ .Sidebar }}
</aside> </aside>
<main class="main">{{ .Main }}</main> <main class="main">{{ .Main }}</main>
<footer> <footer>
<p>This website runs <a href='https://github.com/bouncepaw/mycorrhiza'>MycorrhizaWiki</a></p> <p>This website runs <a href='https://github.com/bouncepaw/mycorrhiza'>MycorrhizaWiki</a></p>
</footer> </footer>
<script src="/Templates/default-light/main.js?action=raw"></script> <script src="/:sys/theme/default-light/main.js?action=raw"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,11 @@
<form class="edit-box"
method="POST"
enctype="multipart/form-data"
action="?action=update"
id="edit-form">
<h4>Edit box</h4>
<!-- It is important that there is no indent ↓ -->
<textarea class="edit-box__text" name="text">
{{ .Text }}</textarea>
</form>

View File

@ -0,0 +1,22 @@
<div>
<h4>Text MIME-type</h4>
<p>Good types are <code>text/markdown</code> and <code>text/plain</code></p>
<input type="text" name="text_mime" value="{{ .TextMime }}" form="edit-form"/>
<h4>Revision comment</h4>
<p>Please make your comment helpful</p>
<input type="text" name="comment" value="Update {{ .Name }}" form="edit-form"/>
<h4>Edit tags</h4>
<p>Tags are separated by commas, whitespace is ignored</p>
<input type="text" name="tags" value="{{ .Tags }}" form="edit-form"/>
<h4>Upload file</h4>
<p>If this hypha has a file like that, the text above is meant to be a description of it</p>
<input type="file" name="binary" form="edit-form"/>
<p>
<input type="submit" value="update" form="edit-form"/>
<a href="?">Cancel</a>
</p>
</div>

View File

@ -1,53 +1,54 @@
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
html { height: 100%; } html { height: 100%; }
body { font: 15px/1.5 'PT Sans', system-ui, sans-serif; body { font: 15px/1.5 'PT Sans', system-ui, sans-serif;
min-height: 100%; padding: 0; margin:0; } min-height: 100%; padding: 0; margin:0; }
.msg { background-color: #f4f4f4; padding: 1rem; border-radius: 1rem; } .msg { background-color: #f4f4f4; padding: 1rem; border-radius: 1rem; }
a { color: #44e; } a { color: #44e; }
a:visited { color: #44a; } a:visited { color: #44a; }
header { margin: 0 2rem; } header { margin: 0 2rem; }
header * { display: inline; } header * { display: inline; }
header h1 { margin: 0; font-size: 1rem; } header h1 { margin: 0; font-size: 1rem; }
header a, header a:visited { color: black; text-decoration:none; } header a, header a:visited { color: black; text-decoration:none; }
header a:active, header a:hover { color: #005f87; } header a:active, header a:hover { color: #005f87; }
h1, h2, h3, h4, h5, h6 { margin: 0.5em 0 0.25em; } h1, h2, h3, h4, h5, h6 { margin: 0.5em 0 0.25em; }
.page { line-height: 1.666; max-width: 40rem; hyphens: auto; } code { background-color: #f4f4f4; }
.page img { max-width:100%; } .page { line-height: 1.666; max-width: 40rem; hyphens: auto; }
.page pre { white-space: break-spaces; } .page img { max-width:100%; }
.page__title { font-size: 2rem; margin: 0; } .page pre { white-space: break-spaces; background-color: #f4f4f4; }
.page__title { font-size: 2rem; margin: 0; }
footer { padding: 1rem 0; font-size: .8rem; bottom: 0; position: absolute; }
footer a, footer a:visited { color: black; } footer { padding: 1rem 0; font-size: .8rem; }
/* Sidebar section */ footer a, footer a:visited { color: black; }
.sidebar { padding: 1rem 0; background: #f4f4f4; } /* Sidebar section */
.sidebar div { margin-left: 1rem; } .sidebar { padding: 1rem 0; background: #f4f4f4; }
.sidebar-controller { font: inherit; padding: .25rem 1rem; .sidebar div { margin-left: 1rem; }
font-size: 2rem; float: right; } .sidebar-controller { font: inherit; padding: .25rem 1rem;
font-size: 2rem; float: right; }
.hypha-actions ul { margin: 0; padding: 0; }
.hypha-actions li { list-style: none; } .hypha-actions ul { margin: 0; padding: 0; }
.hypha-actions a { display: block; padding: .25rem 1rem; font: inherit; .hypha-actions li { list-style: none; }
text-decoration: none; color: black; } .hypha-actions a { display: block; padding: .25rem 1rem; font: inherit;
.hypha-actions a:hover { background: #eaeaea; } text-decoration: none; color: black; }
.hypha-actions a:hover { background: #eaeaea; }
.navitree__node { padding-left: 2rem; }
.navitree__entry { margin-bottom: .5rem; } .navitree__node { padding-left: 2rem; }
.navitree__link, .navitree__link:visited { color:black; text-decoration:none; } .navitree__entry { margin-bottom: .5rem; }
.navitree__link:hover, .navitree__link:active { text-decoration:underline; } .navitree__link, .navitree__link:visited { color:black; text-decoration:none; }
.navitree__ancestor { list-style: none; margin-left: -1rem; } .navitree__link:hover, .navitree__link:active { text-decoration:underline; }
.navitree__pagename a { font-weight: bold; } .navitree__ancestor { list-style: none; margin-left: -1rem; }
.navitree__pagename a { font-weight: bold; }
@media (max-width: 950px) {
.hidden_mobile { display: none; } @media (max-width: 950px) {
aside { height: 100%; } .hidden_mobile { display: none; }
main, footer, header { margin: 0 1rem; } aside { height: 100%; }
header, header * { display:inline; } main, footer, header { margin: 0 1rem; }
.edit-box__text { width: 100%; height: 70%; } header, header * { display:inline; }
} .edit-box__text { width: 100%; height: 70%; }
@media (min-width: 950px) { }
.sidebar-controller { display: none; } @media (min-width: 950px) {
aside { float:right; width: 300px; padding: 0; } .sidebar-controller { display: none; }
main, footer { margin: 0 0 auto 2rem; } aside { float:right; width: 300px; padding: 0; }
.edit-box__text { min-width: 600px; height: 70%; } main, footer { margin: 0 0 auto 2rem; }
} .edit-box__text { min-width: 600px; height: 70%; }
}

View File

@ -1,11 +1,11 @@
var isOpen = false var isOpen = false
var sidebar = document.getElementById('sidebar') var sidebar = document.getElementById('sidebar')
var btn = document.getElementById('shroomburger') var btn = document.getElementById('shroomburger')
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
if (isOpen) { if (isOpen) {
sidebar.classList.add('hidden_mobile') sidebar.classList.add('hidden_mobile')
} else { } else {
sidebar.classList.remove('hidden_mobile') sidebar.classList.remove('hidden_mobile')
} }
isOpen = !isOpen isOpen = !isOpen
}) })

View File

@ -1,8 +1,8 @@
<html> <html>
<head> <head>
<title>Saved {{ .Name }}</title> <title>Saved {{ .Name }}</title>
</head> </head>
<body> <body>
<p>Saved successfully. <a href="/{{ .Name }}">Go back</a></p> <p>Saved successfully. <a href="/{{ .Name }}">Go back</a></p>
</body> </body>
</html> </html>

View File

@ -4,7 +4,7 @@
"revisions": { "revisions": {
"1": { "1": {
"tags": null, "tags": null,
"name": "updateOk.html", "name": "update_ok.html",
"comment": "Create Templates/default-light/updateOk.html", "comment": "Create Templates/default-light/updateOk.html",
"author": "", "author": "",
"time": 1592996644, "time": 1592996644,
@ -14,4 +14,4 @@
"binary_name": "" "binary_name": ""
} }
} }
} }

View File

@ -0,0 +1,6 @@
<article class="page page404">
<h1 class="page__title">{{ . }}</h1>
<p class="msg_hypha-does-not-exist msg">
The hypha you are trying to access does not exist yet. Why not <a href="?action=edit">create</a> it?
</p>
</article>

View File

@ -1,7 +1,7 @@
<div class="hypha-actions"> <div class="hypha-actions">
<ul> <ul>
<li><a href="?action=edit">Edit</a></li> <li><a href="?action=edit">Edit</a></li>
<li><a href="?action=getBinary">Download</a></li> <li><a href="?action=binary">Download</a></li>
<li><a href="?action=zen">Zen mode</a></li> <li><a href="?action=zen">Zen mode</a></li>
<li><a href="?action=raw">View raw</a></li> <li><a href="?action=raw">View raw</a></li>
</ul> </ul>

45
wiki/sys/theme/meta.json Normal file
View File

@ -0,0 +1,45 @@
{
"views": 0,
"deleted": false,
"revisions": {
"1": {
"tags": [
""
],
"name": "Templates",
"comment": "Update Templates",
"author": "",
"time": 1593194769,
"text_mime": "text/markdown",
"binary_mime": "",
"text_name": "1.markdown",
"binary_name": ""
},
"2": {
"tags": [
""
],
"name": "Theme",
"comment": "Update Sys/Theme",
"author": "",
"time": 1593802668,
"text_mime": "text/markdown",
"binary_mime": "",
"text_name": "2.markdown",
"binary_name": ""
},
"3": {
"tags": [
""
],
"name": "Theme",
"comment": "Update Sys/Theme",
"author": "",
"time": 1593802769,
"text_mime": "text/markdown",
"binary_mime": "",
"text_name": "3.markdown",
"binary_name": ""
}
}
}