diff --git a/cfg/config.go b/cfg/config.go index 88ea94a..0a40da1 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -18,8 +18,8 @@ const ( HyphaUrl = `/{hypha:` + HyphaPattern + `}` RevisionPattern = `[\d]+` RevQuery = `{rev:` + RevisionPattern + `}` - MyceliumPattern = `:` + HyphaPattern - MyceliumUrl = `/{mycelium:` + MyceliumPattern + `}` + MyceliumPattern = `[^\s\d:/?&\\][^:?&\\/]*` + MyceliumUrl = `/:{mycelium:` + MyceliumPattern + `}` ) var ( diff --git a/fs/genealogy.go b/fs/genealogy.go index 96da25a..5d63c37 100644 --- a/fs/genealogy.go +++ b/fs/genealogy.go @@ -92,5 +92,5 @@ func navitreeEntry(name, class string) string { return fmt.Sprintf(`
  • %s
  • -`, class, util.DisplayToCanonical(name), filepath.Base(name)) +`, class, ":"+util.DisplayToCanonical(name), filepath.Base(name)) } diff --git a/fs/html.go b/fs/html.go index 0289988..264473d 100644 --- a/fs/html.go +++ b/fs/html.go @@ -21,7 +21,7 @@ func (h *Hypha) asHtml() (string, error) { ` // What about using
    ? if h.hasBinaryData() { - ret += fmt.Sprintf(``, util.DisplayToCanonical(rev.FullName), rev.Id) + ret += fmt.Sprintf(``, util.DisplayToCanonical(rev.FullName), rev.Id) } contents, err := ioutil.ReadFile(rev.TextPath) diff --git a/fs/hypha.go b/fs/hypha.go index 762955a..4d2a974 100644 --- a/fs/hypha.go +++ b/fs/hypha.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/bouncepaw/mycorrhiza/mycelium" "github.com/bouncepaw/mycorrhiza/util" ) @@ -32,7 +33,16 @@ func (h *Hypha) Invalidate(err error) *Hypha { 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{ Exists: true, @@ -110,13 +120,18 @@ func (h *Hypha) ActionRaw(w http.ResponseWriter) *Hypha { if h.Invalid { return h } - fileContents, err := ioutil.ReadFile(h.actual.TextPath) - if err != nil { - return h.Invalidate(err) + if h.Exists { + fileContents, err := ioutil.ReadFile(h.actual.TextPath) + 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 } @@ -126,13 +141,18 @@ func (h *Hypha) ActionBinary(w http.ResponseWriter) *Hypha { if h.Invalid { return h } - fileContents, err := ioutil.ReadFile(h.actual.BinaryPath) - if err != nil { - return h.Invalidate(err) + if h.Exists { + fileContents, err := ioutil.ReadFile(h.actual.BinaryPath) + 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 } diff --git a/fs/mycelium.go b/fs/mycelium.go deleted file mode 100644 index 7f87960..0000000 --- a/fs/mycelium.go +++ /dev/null @@ -1,56 +0,0 @@ -package fs - -import ( - "io/ioutil" - "log" - - "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 -} - -func VerifyMycelia() { - var ( - 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]) - } - } - 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") -} diff --git a/handlers.go b/handlers.go index 1272121..3580eff 100644 --- a/handlers.go +++ b/handlers.go @@ -14,7 +14,7 @@ import ( // Boilerplate code present in many handlers. Good to have it. func HandlerBase(w http.ResponseWriter, rq *http.Request) *fs.Hypha { 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) { @@ -41,7 +41,7 @@ func HandlerView(w http.ResponseWriter, rq *http.Request) { func HandlerEdit(w http.ResponseWriter, rq *http.Request) { 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.WriteHeader(http.StatusOK) w.Write(render.HyphaEdit(h)) @@ -51,7 +51,7 @@ func HandlerUpdate(w http.ResponseWriter, rq *http.Request) { vars := mux.Vars(rq) log.Println("Attempt to update hypha", vars["hypha"]) h := fs.Hs. - Open(vars["hypha"]). + OpenFromMap(vars). CreateDirIfNeeded(). AddRevisionFromHttpData(rq). WriteTextFileFromHttpData(rq). diff --git a/main.go b/main.go index baf04d9..ea108dc 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/fs" + "github.com/bouncepaw/mycorrhiza/mycelium" "github.com/gorilla/mux" ) @@ -53,7 +54,7 @@ func main() { log.Println("Welcome to MycorrhizaWiki α") cfg.InitConfig(wikiDir) log.Println("Indexing hyphae...") - fs.VerifyMycelia() + mycelium.Init() fs.InitStorage() // Start server code. See handlers.go for handlers' implementations. @@ -78,6 +79,7 @@ func main() { r.Queries("action", "update").Path(cfg.HyphaUrl). Methods("POST").HandlerFunc(HandlerUpdate) + r.HandleFunc(cfg.MyceliumUrl+cfg.HyphaUrl, HandlerView) r.HandleFunc(cfg.HyphaUrl, HandlerView) // Debug page that renders all hyphae. diff --git a/mycelium/mycelium.go b/mycelium/mycelium.go new file mode 100644 index 0000000..94d9e8b --- /dev/null +++ b/mycelium/mycelium.go @@ -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:" +} diff --git a/render/render.go b/render/render.go index 4e33f38..a70f9f8 100644 --- a/render/render.go +++ b/render/render.go @@ -8,6 +8,7 @@ import ( "github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/fs" + "github.com/bouncepaw/mycorrhiza/mycelium" ) // HyphaEdit renders hypha editor. @@ -85,7 +86,12 @@ type Layout struct { } func layout(name string) *Layout { - h := fs.Hs.Open(path.Join(fs.SystemMycelium, "theme", 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 { return &Layout{nil, nil, true, h.Err} } diff --git a/util/util.go b/util/util.go index de78835..7d07b11 100644 --- a/util/util.go +++ b/util/util.go @@ -5,12 +5,28 @@ import ( "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 strings.ToLower(strings.ReplaceAll(name, " ", "_")) + return removeColonPerhaps( + strings.ToLower(strings.ReplaceAll(name, " ", "_"))) } func DisplayToCanonical(name string) string { - return strings.ToLower(strings.ReplaceAll(name, " ", "_")) + return removeColonPerhaps( + strings.ToLower(strings.ReplaceAll(name, " ", "_"))) } func CanonicalToDisplay(name string) (res string) { @@ -29,5 +45,5 @@ func CanonicalToDisplay(name string) (res string) { } res += string(ch) } - return res + return addColonPerhaps(res) } diff --git a/wiki/sys/theme/2.markdown b/wiki/sys/theme/2.markdown new file mode 100644 index 0000000..7ddb176 --- /dev/null +++ b/wiki/sys/theme/2.markdown @@ -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. diff --git a/wiki/sys/theme/3.markdown b/wiki/sys/theme/3.markdown new file mode 100644 index 0000000..275da3d --- /dev/null +++ b/wiki/sys/theme/3.markdown @@ -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. diff --git a/wiki/sys/theme/default-light/base.html/1.html b/wiki/sys/theme/default-light/base.html/1.html index 5c6165b..54bc2f7 100644 --- a/wiki/sys/theme/default-light/base.html/1.html +++ b/wiki/sys/theme/default-light/base.html/1.html @@ -1,7 +1,7 @@ {{ .Title }} - +
    @@ -20,6 +20,6 @@ - + diff --git a/wiki/sys/theme/default-light/main.css/1.css b/wiki/sys/theme/default-light/main.css/1.css index 5df7308..5b212a8 100644 --- a/wiki/sys/theme/default-light/main.css/1.css +++ b/wiki/sys/theme/default-light/main.css/1.css @@ -15,10 +15,10 @@ h1, h2, h3, h4, h5, h6 { margin: 0.5em 0 0.25em; } code { background-color: #f4f4f4; } .page { line-height: 1.666; max-width: 40rem; hyphens: auto; } .page img { max-width:100%; } -.page pre { white-space: break-spaces; } +.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 { padding: 1rem 0; font-size: .8rem; } footer a, footer a:visited { color: black; } /* Sidebar section */ .sidebar { padding: 1rem 0; background: #f4f4f4; } diff --git a/wiki/sys/theme/meta.json b/wiki/sys/theme/meta.json index 45204e2..53e4e9c 100644 --- a/wiki/sys/theme/meta.json +++ b/wiki/sys/theme/meta.json @@ -14,8 +14,32 @@ "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": "" } - }, - "Invalid": false, - "Err": null + } } \ No newline at end of file