mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-19 07:02:51 +00:00
commit
981f4ae513
@ -8,10 +8,18 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HyphaPattern = `[^\s\d:/?&\\][^:?&\\]*`
|
||||||
|
HyphaUrl = `/{hypha:` + HyphaPattern + `}`
|
||||||
|
RevisionPattern = `[\d]+`
|
||||||
|
RevQuery = `{rev:` + RevisionPattern + `}`
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
WikiDir string
|
DescribeHyphaHerePattern = "Describe %s here"
|
||||||
TemplatesDir string
|
WikiDir string
|
||||||
configJsonPath string
|
TemplatesDir string
|
||||||
|
configJsonPath string
|
||||||
|
|
||||||
// Default values that can be overriden in config.json
|
// Default values that can be overriden in config.json
|
||||||
Address = "127.0.0.1:80"
|
Address = "127.0.0.1:80"
|
||||||
|
81
fs/data.go
Normal file
81
fs/data.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// This file contains methods for Hypha that calculate data about the hypha based on known information.
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Hypha) MetaJsonPath() string {
|
||||||
|
return filepath.Join(h.Path(), "meta.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) Path() string {
|
||||||
|
return filepath.Join(cfg.WikiDir, h.FullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) TextPath() string {
|
||||||
|
return h.actual.TextPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) parentName() string {
|
||||||
|
return filepath.Dir(h.FullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasBinaryData returns true if the revision has any binary data associated.
|
||||||
|
// During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data. (is it?)
|
||||||
|
func (h *Hypha) hasBinaryData() bool {
|
||||||
|
return h.actual.BinaryMime != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) TagsJoined() string {
|
||||||
|
if h.Exists {
|
||||||
|
return strings.Join(h.actual.Tags, ", ")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) TextMime() string {
|
||||||
|
if h.Exists {
|
||||||
|
return h.actual.TextMime
|
||||||
|
}
|
||||||
|
return "text/markdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) mimeTypeForActionRaw() string {
|
||||||
|
// If text mime type is text/html, it is not good as it will be rendered.
|
||||||
|
if h.actual.TextMime == "text/html" {
|
||||||
|
return "text/plain"
|
||||||
|
}
|
||||||
|
return h.actual.TextMime
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewestId finds the largest id among all revisions.
|
||||||
|
func (h *Hypha) NewestId() string {
|
||||||
|
var largest int
|
||||||
|
for k, _ := range h.Revisions {
|
||||||
|
id, _ := strconv.Atoi(k)
|
||||||
|
if id > largest {
|
||||||
|
largest = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strconv.Itoa(largest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) TextContent() string {
|
||||||
|
if h.Exists {
|
||||||
|
contents, err := ioutil.ReadFile(h.TextPath())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Could not read", h.FullName)
|
||||||
|
return "Error: could not hypha text content file. It is recommended to cancel editing. Please contact the wiki admin. If you are the admin, see the logs."
|
||||||
|
}
|
||||||
|
return string(contents)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(cfg.DescribeHyphaHerePattern, h.FullName)
|
||||||
|
}
|
59
fs/fs.go
Normal file
59
fs/fs.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
// hypha name => path
|
||||||
|
paths map[string]string
|
||||||
|
root string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Hs *Storage
|
||||||
|
|
||||||
|
// InitStorage initiates filesystem-based hypha storage. It has to be called after configuration was inited.
|
||||||
|
func InitStorage() {
|
||||||
|
Hs = &Storage{
|
||||||
|
paths: make(map[string]string),
|
||||||
|
root: cfg.WikiDir,
|
||||||
|
}
|
||||||
|
Hs.indexHyphae(Hs.root)
|
||||||
|
log.Printf("Indexed %v hyphae\n", len(Hs.paths))
|
||||||
|
}
|
||||||
|
|
||||||
|
// hyphaName gets name of a hypha by stripping path to the hypha in `fullPath`
|
||||||
|
func hyphaName(fullPath string) string {
|
||||||
|
// {cfg.WikiDir}/{the name}
|
||||||
|
return fullPath[len(cfg.WikiDir)+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexHyphae searches for all hyphae that seem valid in `path` and saves their absolute paths to `s.paths`. This function is recursive.
|
||||||
|
func (s *Storage) indexHyphae(path string) {
|
||||||
|
nodes, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error when checking", path, ":", err, "; skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
matchesHypha, err := regexp.MatchString(cfg.HyphaPattern, node.Name())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error when matching", node.Name(), err, "\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch name := filepath.Join(path, node.Name()); {
|
||||||
|
case matchesHypha && node.IsDir():
|
||||||
|
s.indexHyphae(name)
|
||||||
|
case node.Name() == "meta.json" && !node.IsDir():
|
||||||
|
s.paths[hyphaName(path)] = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) Close() {
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
/* Genealogy is all about relationships between hyphae.*/
|
package fs
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -9,18 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setRelations fills in all children names based on what hyphae call their parents.
|
func (s *Storage) RenderHypha(h *Hypha) {
|
||||||
func setRelations(hyphae map[string]*Hypha) {
|
|
||||||
for name, h := range hyphae {
|
|
||||||
if _, ok := hyphae[h.parentName]; ok && h.parentName != "." {
|
|
||||||
hyphae[h.parentName].ChildrenNames = append(hyphae[h.parentName].ChildrenNames, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddChild adds a name to the list of children names of the hypha.
|
|
||||||
func (h *Hypha) AddChild(childName string) {
|
|
||||||
h.ChildrenNames = append(h.ChildrenNames, childName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Name == "", the tree is empty.
|
// If Name == "", the tree is empty.
|
||||||
@ -35,11 +23,10 @@ type Tree struct {
|
|||||||
// GetTree generates a Tree for the given hypha name.
|
// GetTree generates a Tree for the given hypha name.
|
||||||
// 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.
|
||||||
// Parameter `limit` is unused now but it is meant to limit how many subhyphae can be shown.
|
func (s *Storage) GetTree(name string, root bool) *Tree {
|
||||||
func GetTree(name string, root bool, limit ...int) *Tree {
|
|
||||||
t := &Tree{Name: name, Root: root}
|
t := &Tree{Name: name, Root: root}
|
||||||
for hyphaName, _ := range hyphae {
|
for hyphaName, _ := range s.paths {
|
||||||
t.compareNamesAndAppend(hyphaName)
|
s.compareNamesAndAppend(t, hyphaName)
|
||||||
}
|
}
|
||||||
sort.Slice(t.Ancestors, func(i, j int) bool {
|
sort.Slice(t.Ancestors, func(i, j int) bool {
|
||||||
return strings.Count(t.Ancestors[i], "/") < strings.Count(t.Ancestors[j], "/")
|
return strings.Count(t.Ancestors[i], "/") < strings.Count(t.Ancestors[j], "/")
|
||||||
@ -55,7 +42,7 @@ func GetTree(name string, root bool, limit ...int) *Tree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compares names appends name2 to an array of `t`:
|
// Compares names appends name2 to an array of `t`:
|
||||||
func (t *Tree) compareNamesAndAppend(name2 string) {
|
func (s *Storage) compareNamesAndAppend(t *Tree, name2 string) {
|
||||||
switch {
|
switch {
|
||||||
case t.Name == name2:
|
case t.Name == name2:
|
||||||
case strings.HasPrefix(t.Name, name2):
|
case strings.HasPrefix(t.Name, name2):
|
||||||
@ -64,11 +51,11 @@ func (t *Tree) compareNamesAndAppend(name2 string) {
|
|||||||
(filepath.Dir(t.Name) == filepath.Dir(name2))):
|
(filepath.Dir(t.Name) == filepath.Dir(name2))):
|
||||||
t.Siblings = append(t.Siblings, name2)
|
t.Siblings = append(t.Siblings, name2)
|
||||||
case strings.HasPrefix(name2, t.Name):
|
case strings.HasPrefix(name2, t.Name):
|
||||||
t.Descendants = append(t.Descendants, GetTree(name2, false))
|
t.Descendants = append(t.Descendants, s.GetTree(name2, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsHtml returns HTML representation of a tree.
|
// asHtml returns HTML representation of a tree.
|
||||||
// It recursively itself on the tree's children.
|
// It recursively itself on the tree's children.
|
||||||
// TODO: redo with templates. I'm not in mood for it now.
|
// TODO: redo with templates. I'm not in mood for it now.
|
||||||
func (t *Tree) AsHtml() (html string) {
|
func (t *Tree) AsHtml() (html string) {
|
||||||
@ -78,16 +65,13 @@ 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)
|
html += navitreeEntry(ancestor, "navitree__ancestor")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
html += navitreeEntry(t.Name)
|
|
||||||
|
|
||||||
if t.Root {
|
|
||||||
for _, siblingName := range t.Siblings {
|
for _, siblingName := range t.Siblings {
|
||||||
html += navitreeEntry(siblingName)
|
html += navitreeEntry(siblingName, "navitree__sibling")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
html += navitreeEntry(t.Name, "navitree__name")
|
||||||
|
|
||||||
for _, subtree := range t.Descendants {
|
for _, subtree := range t.Descendants {
|
||||||
html += subtree.AsHtml()
|
html += subtree.AsHtml()
|
||||||
@ -99,9 +83,9 @@ func (t *Tree) AsHtml() (html string) {
|
|||||||
|
|
||||||
// navitreeEntry is a small utility function that makes generating html easier.
|
// navitreeEntry is a small utility function that makes generating html easier.
|
||||||
// Someone please redo it in templates.
|
// Someone please redo it in templates.
|
||||||
func navitreeEntry(name string) string {
|
func navitreeEntry(name, class string) string {
|
||||||
return fmt.Sprintf(`<li class="navitree__entry">
|
return fmt.Sprintf(`<li class="navitree__entry">
|
||||||
<a class="navitree__link" href="/%s">%s</a>
|
<a class="navitree__link %s" href="/%s">%s</a>
|
||||||
</li>
|
</li>
|
||||||
`, name, filepath.Base(name))
|
`, class, name, filepath.Base(name))
|
||||||
}
|
}
|
54
fs/html.go
Normal file
54
fs/html.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gomarkdown/markdown"
|
||||||
|
"github.com/gomarkdown/markdown/html"
|
||||||
|
"github.com/gomarkdown/markdown/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func markdownToHtml(md string) string {
|
||||||
|
extensions := parser.CommonExtensions | parser.AutoHeadingIDs
|
||||||
|
p := parser.NewWithExtensions(extensions)
|
||||||
|
|
||||||
|
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
||||||
|
opts := html.RendererOptions{Flags: htmlFlags}
|
||||||
|
renderer := html.NewRenderer(opts)
|
||||||
|
|
||||||
|
return string(markdown.ToHTML([]byte(md), p, renderer))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) asHtml() (string, error) {
|
||||||
|
rev := h.actual
|
||||||
|
ret := `<article class="page">
|
||||||
|
<h1 class="page__title">` + rev.FullName + `</h1>
|
||||||
|
`
|
||||||
|
// What about using <figure>?
|
||||||
|
if h.hasBinaryData() {
|
||||||
|
ret += fmt.Sprintf(`<img src="/%s?action=binary&rev=%d" class="page__amnt"/>`, rev.FullName, rev.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err := ioutil.ReadFile(rev.TextPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to render", rev.FullName, ":", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: support more markups.
|
||||||
|
// TODO: support mycorrhiza extensions like transclusion.
|
||||||
|
switch rev.TextMime {
|
||||||
|
case "text/markdown":
|
||||||
|
html := markdown.ToHTML(contents, nil, nil)
|
||||||
|
ret += string(html)
|
||||||
|
default:
|
||||||
|
ret += fmt.Sprintf(`<pre>%s</pre>`, contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret += `
|
||||||
|
</article>`
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
319
fs/hypha.go
Normal file
319
fs/hypha.go
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hypha struct {
|
||||||
|
Exists bool `json:"-"`
|
||||||
|
FullName string `json:"-"`
|
||||||
|
ViewCount int `json:"views"`
|
||||||
|
Deleted bool `json:"deleted"`
|
||||||
|
Revisions map[string]*Revision `json:"revisions"`
|
||||||
|
actual *Revision `json:"-"`
|
||||||
|
Invalid bool
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) Invalidate(err error) *Hypha {
|
||||||
|
h.Invalid = true
|
||||||
|
h.Err = err
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Open(name string) *Hypha {
|
||||||
|
h := &Hypha{
|
||||||
|
Exists: true,
|
||||||
|
FullName: name,
|
||||||
|
}
|
||||||
|
path, ok := s.paths[name]
|
||||||
|
// This hypha does not exist yet
|
||||||
|
if !ok {
|
||||||
|
log.Println("Hypha", name, "does not exist")
|
||||||
|
h.Exists = false
|
||||||
|
h.Revisions = make(map[string]*Revision)
|
||||||
|
} else {
|
||||||
|
metaJsonText, err := ioutil.ReadFile(filepath.Join(path, "meta.json"))
|
||||||
|
if err != nil {
|
||||||
|
return h.Invalidate(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(metaJsonText, &h)
|
||||||
|
if err != nil {
|
||||||
|
return h.Invalidate(err)
|
||||||
|
}
|
||||||
|
// fill in rooted paths to content files and full names
|
||||||
|
for idStr, rev := range h.Revisions {
|
||||||
|
rev.FullName = filepath.Join(h.parentName(), rev.ShortName)
|
||||||
|
rev.Id, _ = strconv.Atoi(idStr)
|
||||||
|
if rev.BinaryName != "" {
|
||||||
|
rev.BinaryPath = filepath.Join(path, rev.BinaryName)
|
||||||
|
}
|
||||||
|
rev.TextPath = filepath.Join(path, rev.TextName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.OnRevision("0")
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRevision tries to change to a revision specified by `id`.
|
||||||
|
func (h *Hypha) OnRevision(id string) *Hypha {
|
||||||
|
if h.Invalid || !h.Exists {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
if len(h.Revisions) == 0 {
|
||||||
|
return h.Invalidate(errors.New("This hypha has no revisions"))
|
||||||
|
}
|
||||||
|
if id == "0" {
|
||||||
|
id = h.NewestId()
|
||||||
|
}
|
||||||
|
// Revision must be there, so no error checking
|
||||||
|
if rev, _ := h.Revisions[id]; true {
|
||||||
|
h.actual = rev
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) PlainLog(s string) {
|
||||||
|
if h.Exists {
|
||||||
|
log.Println(h.FullName, h.actual.Id, s)
|
||||||
|
} else {
|
||||||
|
log.Println("nonexistent", h.FullName, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) LogSuccMaybe(succMsg string) *Hypha {
|
||||||
|
if h.Invalid {
|
||||||
|
h.PlainLog(h.Err.Error())
|
||||||
|
} else {
|
||||||
|
h.PlainLog(succMsg)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionRaw is used with `?action=raw`.
|
||||||
|
// It writes text content of the revision without any parsing or rendering.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", h.mimeTypeForActionRaw())
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(fileContents)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionBinary is used with `?action=binary`.
|
||||||
|
// It writes contents of binary content file.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", h.actual.BinaryMime)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(fileContents)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionZen is used with `?action=zen`.
|
||||||
|
// It renders the hypha but without any layout or styles. Pure. Zen.
|
||||||
|
func (h *Hypha) ActionZen(w http.ResponseWriter) *Hypha {
|
||||||
|
if h.Invalid {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
html, err := h.asHtml()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return h.Invalidate(err)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(html))
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionView is used with `?action=view` or no action at all.
|
||||||
|
// It renders the page, the layout and everything else.
|
||||||
|
func (h *Hypha) ActionView(w http.ResponseWriter, renderExists, renderNotExists func(string, string) string) *Hypha {
|
||||||
|
var html string
|
||||||
|
var err error
|
||||||
|
if h.Exists {
|
||||||
|
html, err = h.asHtml()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return h.Invalidate(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
if h.Exists {
|
||||||
|
w.Write([]byte(renderExists(h.FullName, html)))
|
||||||
|
} else {
|
||||||
|
w.Write([]byte(renderNotExists(h.FullName, "")))
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDirIfNeeded creates directory where the hypha must reside if needed.
|
||||||
|
// It is not needed if the dir already exists.
|
||||||
|
func (h *Hypha) CreateDirIfNeeded() *Hypha {
|
||||||
|
if h.Invalid {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
h.Invalidate(err)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant.
|
||||||
|
// Expected output for string above: []string{"foo", "bar", "kek"}
|
||||||
|
func makeTagsSlice(responseTagsString string) (ret []string) {
|
||||||
|
for _, tag := range strings.Split(responseTagsString, ",") {
|
||||||
|
if trimmed := strings.TrimSpace(tag); "" == trimmed {
|
||||||
|
ret = append(ret, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// revisionFromHttpData creates a new revison for hypha `h`. All data is fetched from `rq`, except for BinaryMime and BinaryPath which require additional processing. The revision is inserted for you. You'll have to pop it out if there is an error.
|
||||||
|
func (h *Hypha) AddRevisionFromHttpData(rq *http.Request) *Hypha {
|
||||||
|
if h.Invalid {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
id := 1
|
||||||
|
if h.Exists {
|
||||||
|
id = h.actual.Id + 1
|
||||||
|
}
|
||||||
|
log.Printf("Creating revision %d from http data", id)
|
||||||
|
rev := &Revision{
|
||||||
|
Id: id,
|
||||||
|
FullName: h.FullName,
|
||||||
|
ShortName: filepath.Base(h.FullName),
|
||||||
|
Tags: makeTagsSlice(rq.PostFormValue("tags")),
|
||||||
|
Comment: rq.PostFormValue("comment"),
|
||||||
|
Author: rq.PostFormValue("author"),
|
||||||
|
Time: int(time.Now().Unix()),
|
||||||
|
TextMime: rq.PostFormValue("text_mime"),
|
||||||
|
// Fields left: BinaryMime, BinaryPath, BinaryName, TextName, TextPath
|
||||||
|
}
|
||||||
|
rev.generateTextFilename() // TextName is set now
|
||||||
|
rev.TextPath = filepath.Join(h.Path(), rev.TextName)
|
||||||
|
return h.AddRevision(rev)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hypha) AddRevision(rev *Revision) *Hypha {
|
||||||
|
if h.Invalid {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
h.Revisions[strconv.Itoa(rev.Id)] = rev
|
||||||
|
h.actual = rev
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTextFileFromHttpData tries to fetch text content from `rq` for revision `rev` and write it to a corresponding text file. It used in `HandlerUpdate`.
|
||||||
|
func (h *Hypha) WriteTextFileFromHttpData(rq *http.Request) *Hypha {
|
||||||
|
if h.Invalid {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
data := []byte(rq.PostFormValue("text"))
|
||||||
|
err := ioutil.WriteFile(h.TextPath(), data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write", len(data), "bytes to", h.TextPath())
|
||||||
|
h.Invalidate(err)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBinaryFileFromHttpData tries to fetch binary content from `rq` for revision `newRev` and write it to a corresponding binary file. If there is no content, it is taken from a previous revision, if there is any.
|
||||||
|
func (h *Hypha) WriteBinaryFileFromHttpData(rq *http.Request) *Hypha {
|
||||||
|
if h.Invalid {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
// 10 MB file size limit
|
||||||
|
rq.ParseMultipartForm(10 << 20)
|
||||||
|
// Read file
|
||||||
|
file, handler, err := rq.FormFile("binary")
|
||||||
|
if file != nil {
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
// If file is not passed:
|
||||||
|
if err != nil {
|
||||||
|
// Let's hope there are no other errors 🙏
|
||||||
|
// TODO: actually check if there any other errors
|
||||||
|
log.Println("No binary data passed for", h.FullName)
|
||||||
|
// It is expected there is at least one revision
|
||||||
|
if len(h.Revisions) > 1 {
|
||||||
|
prevRev := h.Revisions[strconv.Itoa(h.actual.Id-1)]
|
||||||
|
h.actual.BinaryMime = prevRev.BinaryMime
|
||||||
|
h.actual.BinaryPath = prevRev.BinaryPath
|
||||||
|
h.actual.BinaryName = prevRev.BinaryName
|
||||||
|
log.Println("Set previous revision's binary data")
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
// If file is passed:
|
||||||
|
h.actual.BinaryMime = handler.Header.Get("Content-Type")
|
||||||
|
h.actual.generateBinaryFilename()
|
||||||
|
h.actual.BinaryPath = filepath.Join(h.Path(), h.actual.BinaryName)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return h.Invalidate(err)
|
||||||
|
}
|
||||||
|
log.Println("Got", len(data), "of binary data for", h.FullName)
|
||||||
|
err = ioutil.WriteFile(h.actual.BinaryPath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return h.Invalidate(err)
|
||||||
|
}
|
||||||
|
log.Println("Written", len(data), "of binary data for", h.FullName)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveJson dumps the hypha's metadata to `meta.json` file.
|
||||||
|
func (h *Hypha) SaveJson() *Hypha {
|
||||||
|
if h.Invalid {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(h, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return h.Invalidate(err)
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(h.MetaJsonPath(), data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return h.Invalidate(err)
|
||||||
|
}
|
||||||
|
log.Println("Saved JSON data of", h.FullName)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store adds `h` to the `Hs` if it is not already there
|
||||||
|
func (h *Hypha) Store() *Hypha {
|
||||||
|
if !h.Invalid {
|
||||||
|
Hs.paths[h.FullName] = h.Path()
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
42
fs/revision.go
Normal file
42
fs/revision.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Revision struct {
|
||||||
|
Id int `json:"-"`
|
||||||
|
FullName string `json:"-"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
ShortName string `json:"name"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Time int `json:"time"`
|
||||||
|
TextMime string `json:"text_mime"`
|
||||||
|
BinaryMime string `json:"binary_mime"`
|
||||||
|
TextPath string `json:"-"`
|
||||||
|
BinaryPath string `json:"-"`
|
||||||
|
TextName string `json:"text_name"`
|
||||||
|
BinaryName string `json:"binary_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: https://github.com/bouncepaw/mycorrhiza/issues/4
|
||||||
|
// Some filenames are wrong?
|
||||||
|
func (rev *Revision) generateTextFilename() {
|
||||||
|
ts, err := mime.ExtensionsByType(rev.TextMime)
|
||||||
|
if err != nil || ts == nil {
|
||||||
|
rev.TextName = strconv.Itoa(rev.Id) + ".txt"
|
||||||
|
} else {
|
||||||
|
rev.TextName = strconv.Itoa(rev.Id) + ts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rev *Revision) generateBinaryFilename() {
|
||||||
|
ts, err := mime.ExtensionsByType(rev.BinaryMime)
|
||||||
|
if err != nil || ts == nil {
|
||||||
|
rev.BinaryName = strconv.Itoa(rev.Id) + ".bin"
|
||||||
|
} else {
|
||||||
|
rev.BinaryName = strconv.Itoa(rev.Id) + ts[0]
|
||||||
|
}
|
||||||
|
}
|
215
handlers.go
215
handlers.go
@ -1,213 +1,68 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
"github.com/bouncepaw/mycorrhiza/fs"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/render"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// There are handlers below. See main() for their usage.
|
// There are handlers below. See main() for their usage.
|
||||||
|
|
||||||
// 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) (Revision, bool) {
|
func HandlerBase(w http.ResponseWriter, rq *http.Request) *fs.Hypha {
|
||||||
vars := mux.Vars(rq)
|
vars := mux.Vars(rq)
|
||||||
revno := RevInMap(vars)
|
return fs.Hs.Open(vars["hypha"]).OnRevision(RevInMap(vars))
|
||||||
return GetRevision(vars["hypha"], revno)
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) {
|
|
||||||
if rev, ok := HandlerBase(w, rq); ok {
|
|
||||||
rev.ActionGetBinary(w)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandlerRaw(w http.ResponseWriter, rq *http.Request) {
|
func HandlerRaw(w http.ResponseWriter, rq *http.Request) {
|
||||||
if rev, ok := HandlerBase(w, rq); ok {
|
log.Println("?action=raw")
|
||||||
rev.ActionRaw(w)
|
HandlerBase(w, rq).ActionRaw(w).LogSuccMaybe("Serving raw text")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandlerBinary(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
log.Println("?action=binary")
|
||||||
|
HandlerBase(w, rq).ActionBinary(w).LogSuccMaybe("Serving binary data")
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandlerZen(w http.ResponseWriter, rq *http.Request) {
|
func HandlerZen(w http.ResponseWriter, rq *http.Request) {
|
||||||
if rev, ok := HandlerBase(w, rq); ok {
|
log.Println("?action=zen")
|
||||||
rev.ActionZen(w)
|
HandlerBase(w, rq).ActionZen(w).LogSuccMaybe("Rendering zen")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandlerView(w http.ResponseWriter, rq *http.Request) {
|
func HandlerView(w http.ResponseWriter, rq *http.Request) {
|
||||||
if rev, ok := HandlerBase(w, rq); ok {
|
log.Println("?action=view")
|
||||||
rev.ActionView(w, HyphaPage)
|
HandlerBase(w, rq).
|
||||||
} else { // Hypha does not exist
|
ActionView(w, render.HyphaPage, render.Hypha404).
|
||||||
log.Println("Hypha does not exist, showing 404")
|
LogSuccMaybe("Rendering hypha view")
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
w.Write([]byte(Hypha404(mux.Vars(rq)["hypha"])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandlerHistory(w http.ResponseWriter, rq *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
log.Println("Attempt to access an unimplemented thing")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandlerEdit(w http.ResponseWriter, rq *http.Request) {
|
func HandlerEdit(w http.ResponseWriter, rq *http.Request) {
|
||||||
vars := mux.Vars(rq)
|
vars := mux.Vars(rq)
|
||||||
ActionEdit(vars["hypha"], w)
|
h := fs.Hs.Open(vars["hypha"]).OnRevision("0")
|
||||||
}
|
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
func HandlerRewind(w http.ResponseWriter, rq *http.Request) {
|
w.Write([]byte(render.HyphaEdit(h)))
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
log.Println("Attempt to access an unimplemented thing")
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandlerDelete(w http.ResponseWriter, rq *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
log.Println("Attempt to access an unimplemented thing")
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandlerRename(w http.ResponseWriter, rq *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
log.Println("Attempt to access an unimplemented thing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant.
|
|
||||||
// Expected output for string above: []string{"foo", "bar", "kek"}
|
|
||||||
func makeTagsSlice(responseTagsString string) (ret []string) {
|
|
||||||
for _, tag := range strings.Split(responseTagsString, ",") {
|
|
||||||
if trimmed := strings.TrimSpace(tag); "" == trimmed {
|
|
||||||
ret = append(ret, trimmed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// getHypha returns an existing hypha if it exists in `hyphae` or creates a new one. If it `isNew`, you'll have to insert it to `hyphae` yourself.
|
|
||||||
func getHypha(name string) (*Hypha, bool) {
|
|
||||||
log.Println("Accessing hypha", name)
|
|
||||||
if h, ok := hyphae[name]; ok {
|
|
||||||
log.Println("Got hypha", name)
|
|
||||||
return h, false
|
|
||||||
}
|
|
||||||
log.Println("Create hypha", name)
|
|
||||||
h := &Hypha{
|
|
||||||
FullName: name,
|
|
||||||
Path: filepath.Join(cfg.WikiDir, name),
|
|
||||||
Revisions: make(map[string]*Revision),
|
|
||||||
parentName: filepath.Dir(name),
|
|
||||||
}
|
|
||||||
return h, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// revisionFromHttpData creates a new revison for hypha `h`. All data is fetched from `rq`, except for BinaryMime and BinaryPath which require additional processing. You'll have te insert the revision to `h` yourself.
|
|
||||||
func revisionFromHttpData(h *Hypha, rq *http.Request) *Revision {
|
|
||||||
idStr := strconv.Itoa(h.NewestRevisionInt() + 1)
|
|
||||||
log.Printf("Creating revision %s from http data", idStr)
|
|
||||||
rev := &Revision{
|
|
||||||
Id: h.NewestRevisionInt() + 1,
|
|
||||||
FullName: h.FullName,
|
|
||||||
ShortName: filepath.Base(h.FullName),
|
|
||||||
Tags: makeTagsSlice(rq.PostFormValue("tags")),
|
|
||||||
Comment: rq.PostFormValue("comment"),
|
|
||||||
Author: rq.PostFormValue("author"),
|
|
||||||
Time: int(time.Now().Unix()),
|
|
||||||
TextMime: rq.PostFormValue("text_mime"),
|
|
||||||
// Fields left: BinaryMime, BinaryPath, BinaryName, TextName, TextPath
|
|
||||||
}
|
|
||||||
rev.desiredTextFilename() // TextName is set now
|
|
||||||
rev.TextPath = filepath.Join(h.Path, rev.TextName)
|
|
||||||
return rev
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeTextFileFromHttpData tries to fetch text content from `rq` for revision `rev` and write it to a corresponding text file. It used in `HandlerUpdate`.
|
|
||||||
func writeTextFileFromHttpData(rev *Revision, rq *http.Request) error {
|
|
||||||
data := []byte(rq.PostFormValue("text"))
|
|
||||||
err := ioutil.WriteFile(rev.TextPath, data, 0644)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to write", len(data), "bytes to", rev.TextPath)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeBinaryFileFromHttpData tries to fetch binary content from `rq` for revision `newRev` and write it to a corresponding binary file. If there is no content, it is taken from `oldRev`.
|
|
||||||
func writeBinaryFileFromHttpData(h *Hypha, oldRev Revision, newRev *Revision, rq *http.Request) error {
|
|
||||||
// 10 MB file size limit
|
|
||||||
rq.ParseMultipartForm(10 << 20)
|
|
||||||
// Read file
|
|
||||||
file, handler, err := rq.FormFile("binary")
|
|
||||||
if file != nil {
|
|
||||||
defer file.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Println("No binary data passed for", newRev.FullName)
|
|
||||||
newRev.BinaryMime = oldRev.BinaryMime
|
|
||||||
newRev.BinaryPath = oldRev.BinaryPath
|
|
||||||
newRev.BinaryName = oldRev.BinaryName
|
|
||||||
log.Println("Set previous revision's binary data")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
newRev.BinaryMime = handler.Header.Get("Content-Type")
|
|
||||||
newRev.BinaryPath = filepath.Join(h.Path, newRev.IdAsStr()+".bin")
|
|
||||||
newRev.BinaryName = newRev.desiredBinaryFilename()
|
|
||||||
data, err := ioutil.ReadAll(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Println("Got", len(data), "of binary data for", newRev.FullName)
|
|
||||||
err = ioutil.WriteFile(newRev.BinaryPath, data, 0644)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to write", len(data), "bytes to", newRev.TextPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Println("Written", len(data), "of binary data for", newRev.FullName)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandlerUpdate(w http.ResponseWriter, rq *http.Request) {
|
func HandlerUpdate(w http.ResponseWriter, rq *http.Request) {
|
||||||
vars := mux.Vars(rq)
|
vars := mux.Vars(rq)
|
||||||
log.Println("Attempt to update hypha", mux.Vars(rq)["hypha"])
|
log.Println("Attempt to update hypha", vars["hypha"])
|
||||||
h, isNew := getHypha(vars["hypha"])
|
h := fs.Hs.
|
||||||
var oldRev Revision
|
Open(vars["hypha"]).
|
||||||
if !isNew {
|
CreateDirIfNeeded().
|
||||||
oldRev = h.GetNewestRevision()
|
AddRevisionFromHttpData(rq).
|
||||||
} else {
|
WriteTextFileFromHttpData(rq).
|
||||||
h = &Hypha{
|
WriteBinaryFileFromHttpData(rq).
|
||||||
FullName: vars["hypha"],
|
SaveJson().
|
||||||
Path: filepath.Join(cfg.WikiDir, vars["hypha"]),
|
Store().
|
||||||
Revisions: make(map[string]*Revision),
|
LogSuccMaybe("Saved changes")
|
||||||
parentName: filepath.Dir(vars["hypha"]),
|
|
||||||
}
|
|
||||||
h.CreateDir()
|
|
||||||
oldRev = Revision{}
|
|
||||||
}
|
|
||||||
newRev := revisionFromHttpData(h, rq)
|
|
||||||
|
|
||||||
err := writeTextFileFromHttpData(newRev, rq)
|
if !h.Invalid {
|
||||||
if err != nil {
|
w.WriteHeader(http.StatusOK)
|
||||||
log.Println(err)
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
return
|
w.Write([]byte(render.HyphaUpdateOk(h)))
|
||||||
}
|
}
|
||||||
err = writeBinaryFileFromHttpData(h, oldRev, newRev, rq)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Revisions[newRev.IdAsStr()] = newRev
|
|
||||||
h.SaveJson()
|
|
||||||
if isNew {
|
|
||||||
hyphae[h.FullName] = h
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Current hyphae storage is", hyphae)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
d := map[string]string{"Name": h.FullName}
|
|
||||||
w.Write([]byte(renderFromMap(d, "updateOk.html")))
|
|
||||||
}
|
}
|
||||||
|
113
hypha.go
113
hypha.go
@ -1,113 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// `Hypha` represents a hypha. It is the thing MycorrhizaWiki generally serves.
|
|
||||||
// Each hypha has 1 or more revisions.
|
|
||||||
type Hypha struct {
|
|
||||||
FullName string `json:"-"`
|
|
||||||
Path string `json:"-"`
|
|
||||||
ViewCount int `json:"views"`
|
|
||||||
Deleted bool `json:"deleted"`
|
|
||||||
Revisions map[string]*Revision `json:"revisions"`
|
|
||||||
ChildrenNames []string `json:"-"`
|
|
||||||
parentName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsHtml returns HTML representation of the hypha.
|
|
||||||
// No layout or navigation are present here. Just the hypha.
|
|
||||||
func (h *Hypha) AsHtml(id string, w http.ResponseWriter) (string, error) {
|
|
||||||
if "0" == id {
|
|
||||||
id = h.NewestRevision()
|
|
||||||
}
|
|
||||||
if rev, ok := h.Revisions[id]; ok {
|
|
||||||
return rev.AsHtml(w)
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNewestRevision returns the most recent Revision.
|
|
||||||
func (h *Hypha) GetNewestRevision() Revision {
|
|
||||||
return *h.Revisions[h.NewestRevision()]
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewestRevision returns the most recent revision's id as a string.
|
|
||||||
func (h *Hypha) NewestRevision() string {
|
|
||||||
return strconv.Itoa(h.NewestRevisionInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewestRevision returns the most recent revision's id as an integer.
|
|
||||||
func (h *Hypha) NewestRevisionInt() (ret int) {
|
|
||||||
for k, _ := range h.Revisions {
|
|
||||||
id, _ := strconv.Atoi(k)
|
|
||||||
if id > ret {
|
|
||||||
ret = id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// MetaJsonPath returns rooted path to the hypha's `meta.json` file.
|
|
||||||
// It is not promised that the file exists.
|
|
||||||
func (h *Hypha) MetaJsonPath() string {
|
|
||||||
return filepath.Join(h.Path, "meta.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDir creates directory where the hypha must reside.
|
|
||||||
// It is meant to be used with new hyphae.
|
|
||||||
func (h *Hypha) CreateDir() error {
|
|
||||||
return os.MkdirAll(h.Path, os.ModePerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveJson dumps the hypha's metadata to `meta.json` file.
|
|
||||||
func (h *Hypha) SaveJson() {
|
|
||||||
data, err := json.MarshalIndent(h, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to create JSON of hypha.", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = ioutil.WriteFile(h.MetaJsonPath(), data, 0644)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to save JSON of hypha.", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("Saved JSON data of", h.FullName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActionEdit is called with `?acton=edit`.
|
|
||||||
// It represents the hypha editor.
|
|
||||||
func ActionEdit(hyphaName string, w http.ResponseWriter) {
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
var initContents, initTextMime, initTags string
|
|
||||||
if h, ok := hyphae[hyphaName]; ok {
|
|
||||||
newestRev := h.GetNewestRevision()
|
|
||||||
contents, err := ioutil.ReadFile(newestRev.TextPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Could not read", newestRev.TextPath)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte(cfg.GenericErrorMsg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initContents = string(contents)
|
|
||||||
initTextMime = newestRev.TextMime
|
|
||||||
initTags = strings.Join(newestRev.Tags, ",")
|
|
||||||
} else {
|
|
||||||
initContents = "Describe " + hyphaName + "here."
|
|
||||||
initTextMime = "text/markdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(EditHyphaPage(hyphaName, initTextMime, initContents, initTags)))
|
|
||||||
}
|
|
77
main.go
77
main.go
@ -9,25 +9,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/fs"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetRevision finds revision with id `id` of `hyphaName` in `hyphae`.
|
|
||||||
// If `id` is `"0"`, it means the last revision.
|
|
||||||
// If no such revision is found, last return value is false.
|
|
||||||
func GetRevision(hyphaName string, id string) (Revision, bool) {
|
|
||||||
log.Println("Getting hypha", hyphaName, id)
|
|
||||||
if hypha, ok := hyphae[hyphaName]; ok {
|
|
||||||
if id == "0" {
|
|
||||||
id = hypha.NewestRevision()
|
|
||||||
}
|
|
||||||
if rev, ok := hypha.Revisions[id]; ok {
|
|
||||||
return *rev, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Revision{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RevInMap finds value of `rev` (the one from URL queries like) in the passed map that is usually got from `mux.Vars(*http.Request)`.
|
// RevInMap finds value of `rev` (the one from URL queries like) in the passed map that is usually got from `mux.Vars(*http.Request)`.
|
||||||
// If there is no `rev`, return "0".
|
// If there is no `rev`, return "0".
|
||||||
func RevInMap(m map[string]string) string {
|
func RevInMap(m map[string]string) string {
|
||||||
@ -37,9 +22,6 @@ func RevInMap(m map[string]string) string {
|
|||||||
return "0"
|
return "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// `hyphae` is a map with all hyphae. Many functions use it.
|
|
||||||
var hyphae map[string]*Hypha
|
|
||||||
|
|
||||||
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")
|
||||||
@ -52,51 +34,42 @@ 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...")
|
||||||
hyphae = recurFindHyphae(wikiDir)
|
fs.InitStorage()
|
||||||
log.Println("Indexed", len(hyphae), "hyphae. Ready to accept requests.")
|
|
||||||
|
|
||||||
// Start server code. See handlers.go for handlers' implementations.
|
// Start server code. See handlers.go for handlers' implementations.
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
r.Queries("action", "getBinary", "rev", revQuery).Path(hyphaUrl).
|
r.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
|
||||||
HandlerFunc(HandlerGetBinary)
|
http.ServeFile(w, rq, filepath.Join(filepath.Dir(cfg.WikiDir), "favicon.ico"))
|
||||||
r.Queries("action", "getBinary").Path(hyphaUrl).
|
})
|
||||||
HandlerFunc(HandlerGetBinary)
|
|
||||||
|
|
||||||
r.Queries("action", "raw", "rev", revQuery).Path(hyphaUrl).
|
r.Queries("action", "binary", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
|
||||||
|
HandlerFunc(HandlerBinary)
|
||||||
|
r.Queries("action", "binary").Path(cfg.HyphaUrl).
|
||||||
|
HandlerFunc(HandlerBinary)
|
||||||
|
|
||||||
|
r.Queries("action", "raw", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
|
||||||
HandlerFunc(HandlerRaw)
|
HandlerFunc(HandlerRaw)
|
||||||
r.Queries("action", "raw").Path(hyphaUrl).
|
r.Queries("action", "raw").Path(cfg.HyphaUrl).
|
||||||
HandlerFunc(HandlerRaw)
|
HandlerFunc(HandlerRaw)
|
||||||
|
|
||||||
r.Queries("action", "zen", "rev", revQuery).Path(hyphaUrl).
|
r.Queries("action", "zen", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
|
||||||
HandlerFunc(HandlerZen)
|
HandlerFunc(HandlerZen)
|
||||||
r.Queries("action", "zen").Path(hyphaUrl).
|
r.Queries("action", "zen").Path(cfg.HyphaUrl).
|
||||||
HandlerFunc(HandlerZen)
|
HandlerFunc(HandlerZen)
|
||||||
|
|
||||||
r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl).
|
r.Queries("action", "view", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
|
||||||
HandlerFunc(HandlerView)
|
HandlerFunc(HandlerView)
|
||||||
r.Queries("action", "view").Path(hyphaUrl).
|
r.Queries("action", "view").Path(cfg.HyphaUrl).
|
||||||
HandlerFunc(HandlerView)
|
HandlerFunc(HandlerView)
|
||||||
|
|
||||||
r.Queries("action", "history").Path(hyphaUrl).
|
r.Queries("action", "edit").Path(cfg.HyphaUrl).
|
||||||
HandlerFunc(HandlerHistory)
|
|
||||||
|
|
||||||
r.Queries("action", "edit").Path(hyphaUrl).
|
|
||||||
HandlerFunc(HandlerEdit)
|
HandlerFunc(HandlerEdit)
|
||||||
|
|
||||||
r.Queries("action", "rewind", "rev", revQuery).Path(hyphaUrl).
|
r.Queries("action", "update").Path(cfg.HyphaUrl).Methods("POST").
|
||||||
HandlerFunc(HandlerRewind)
|
|
||||||
|
|
||||||
r.Queries("action", "delete").Path(hyphaUrl).
|
|
||||||
HandlerFunc(HandlerDelete)
|
|
||||||
|
|
||||||
r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl).
|
|
||||||
HandlerFunc(HandlerRename)
|
|
||||||
|
|
||||||
r.Queries("action", "update").Path(hyphaUrl).Methods("POST").
|
|
||||||
HandlerFunc(HandlerUpdate)
|
HandlerFunc(HandlerUpdate)
|
||||||
|
|
||||||
r.HandleFunc(hyphaUrl, HandlerView)
|
r.HandleFunc(cfg.HyphaUrl, HandlerView)
|
||||||
|
|
||||||
// Debug page that renders all hyphae.
|
// Debug page that renders all hyphae.
|
||||||
// TODO: make it redirect to home page.
|
// TODO: make it redirect to home page.
|
||||||
@ -104,14 +77,10 @@ func main() {
|
|||||||
r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
|
r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
|
||||||
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)
|
||||||
for _, h := range hyphae {
|
fmt.Fprintln(w, `
|
||||||
log.Println("Rendering latest revision of hypha", h.FullName)
|
<p>Check out <a href="/Fruit">Fruit</a> maybe.</p>
|
||||||
html, err := h.AsHtml("0", w)
|
<p><strong>TODO:</strong> make this page usable</p>
|
||||||
if err != nil {
|
`)
|
||||||
fmt.Fprintln(w, err)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(w, html)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
http.Handle("/", r)
|
http.Handle("/", r)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EditHyphaPage returns HTML page of hypha editor.
|
// EditHyphaPage returns HTML page of hypha editor.
|
||||||
@ -25,21 +26,36 @@ func EditHyphaPage(name, textMime, content, tags string) string {
|
|||||||
return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys)
|
return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HyphaEdit(h *fs.Hypha) string {
|
||||||
|
page := map[string]string{
|
||||||
|
"Name": h.FullName,
|
||||||
|
"Tags": h.TagsJoined(),
|
||||||
|
"TextMime": h.TextMime(),
|
||||||
|
"Text": h.TextContent(),
|
||||||
|
}
|
||||||
|
keys := map[string]string{
|
||||||
|
"Title": fmt.Sprintf(cfg.TitleEditTemplate, h.FullName),
|
||||||
|
"Header": renderFromString(h.FullName, "Hypha/edit/header.html"),
|
||||||
|
"Sidebar": renderFromMap(page, "Hypha/edit/sidebar.html"),
|
||||||
|
}
|
||||||
|
return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys)
|
||||||
|
}
|
||||||
|
|
||||||
// Hypha404 returns 404 page for nonexistent page.
|
// Hypha404 returns 404 page for nonexistent page.
|
||||||
func Hypha404(name string) string {
|
func Hypha404(name, _ string) string {
|
||||||
return HyphaGeneric(name, name, "Hypha/view/404.html")
|
return HyphaGeneric(name, name, "Hypha/view/404.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
// HyphaPage returns HTML page of hypha viewer.
|
// HyphaPage returns HTML page of hypha viewer.
|
||||||
func HyphaPage(rev Revision, content string) string {
|
func HyphaPage(name, content string) string {
|
||||||
return HyphaGeneric(rev.FullName, content, "Hypha/view/index.html")
|
return HyphaGeneric(name, content, "Hypha/view/index.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
// HyphaGeneric is used when building renderers for all types of hypha pages
|
// HyphaGeneric is used when building renderers for all types of hypha pages
|
||||||
func HyphaGeneric(name string, content, templatePath string) string {
|
func HyphaGeneric(name string, content, templatePath string) string {
|
||||||
var sidebar string
|
var sidebar string
|
||||||
if aside := renderFromMap(map[string]string{
|
if aside := renderFromMap(map[string]string{
|
||||||
"Tree": GetTree(name, true).AsHtml(),
|
"Tree": fs.Hs.GetTree(name, true).AsHtml(),
|
||||||
}, "Hypha/view/sidebar.html"); aside != "" {
|
}, "Hypha/view/sidebar.html"); aside != "" {
|
||||||
sidebar = aside
|
sidebar = aside
|
||||||
}
|
}
|
||||||
@ -50,6 +66,13 @@ func HyphaGeneric(name string, content, templatePath string) string {
|
|||||||
return renderBase(renderFromString(content, templatePath), keys)
|
return renderBase(renderFromString(content, templatePath), keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HyphaUpdateOk(h *fs.Hypha) string {
|
||||||
|
data := map[string]string{
|
||||||
|
"Name": h.FullName,
|
||||||
|
}
|
||||||
|
return renderFromMap(data, "updateOk.html")
|
||||||
|
}
|
||||||
|
|
||||||
// renderBase collects and renders page from base template
|
// renderBase collects and renders page from base template
|
||||||
// Args:
|
// Args:
|
||||||
// content: string or pre-rendered template
|
// content: string or pre-rendered template
|
||||||
@ -70,8 +93,8 @@ func renderBase(content string, keys map[string]string) string {
|
|||||||
// renderFromMap applies `data` map to template in `templatePath` and returns the result.
|
// renderFromMap applies `data` map to template in `templatePath` and returns the result.
|
||||||
func renderFromMap(data map[string]string, templatePath string) string {
|
func renderFromMap(data map[string]string, templatePath string) string {
|
||||||
hyphPath := path.Join(cfg.TemplatesDir, cfg.Theme, templatePath)
|
hyphPath := path.Join(cfg.TemplatesDir, cfg.Theme, templatePath)
|
||||||
rev, _ := GetRevision(hyphPath, "0")
|
h := fs.Hs.Open(hyphPath).OnRevision("0")
|
||||||
tmpl, err := template.ParseFiles(rev.TextPath)
|
tmpl, err := template.ParseFiles(h.TextPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err.Error()
|
return err.Error()
|
||||||
}
|
}
|
||||||
@ -85,8 +108,8 @@ func renderFromMap(data map[string]string, templatePath string) string {
|
|||||||
// renderFromMap applies `data` string to template in `templatePath` and returns the result.
|
// renderFromMap applies `data` string to template in `templatePath` and returns the result.
|
||||||
func renderFromString(data string, templatePath string) string {
|
func renderFromString(data string, templatePath string) string {
|
||||||
hyphPath := path.Join(cfg.TemplatesDir, cfg.Theme, templatePath)
|
hyphPath := path.Join(cfg.TemplatesDir, cfg.Theme, templatePath)
|
||||||
rev, _ := GetRevision(hyphPath, "0")
|
h := fs.Hs.Open(hyphPath).OnRevision("0")
|
||||||
tmpl, err := template.ParseFiles(rev.TextPath)
|
tmpl, err := template.ParseFiles(h.TextPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err.Error()
|
return err.Error()
|
||||||
}
|
}
|
148
revision.go
148
revision.go
@ -1,148 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gomarkdown/markdown"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Revision represents a revision, duh.
|
|
||||||
// A revision is a version of a hypha at a point in time.
|
|
||||||
type Revision struct {
|
|
||||||
Id int `json:"-"`
|
|
||||||
FullName string `json:"-"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
ShortName string `json:"name"`
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
Time int `json:"time"`
|
|
||||||
TextMime string `json:"text_mime"`
|
|
||||||
BinaryMime string `json:"binary_mime"`
|
|
||||||
TextPath string `json:"-"`
|
|
||||||
BinaryPath string `json:"-"`
|
|
||||||
TextName string `json:"text_name"`
|
|
||||||
BinaryName string `json:"binary_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdAsStr returns revision's id as a string.
|
|
||||||
func (rev *Revision) IdAsStr() string {
|
|
||||||
return strconv.Itoa(rev.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasBinaryData returns true if the revision has any binary data associated.
|
|
||||||
// During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data.
|
|
||||||
func (rev *Revision) hasBinaryData() bool {
|
|
||||||
return rev.BinaryMime != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// desiredBinaryFilename returns string that represents filename to use when saving a binary content file of a new revison.
|
|
||||||
// It also sets the corresponding field in `rev`.
|
|
||||||
func (rev *Revision) desiredBinaryFilename() string {
|
|
||||||
ts, err := mime.ExtensionsByType(rev.BinaryMime)
|
|
||||||
if err != nil || ts == nil {
|
|
||||||
rev.BinaryName = rev.IdAsStr() + ".bin"
|
|
||||||
} else {
|
|
||||||
rev.BinaryName = rev.IdAsStr() + ts[0]
|
|
||||||
}
|
|
||||||
return rev.BinaryName
|
|
||||||
}
|
|
||||||
|
|
||||||
// desiredTextFilename returns string that represents filename to use when saving a text content file of a new revison.
|
|
||||||
// It also sets the corresponding field in `rev`.
|
|
||||||
func (rev *Revision) desiredTextFilename() string {
|
|
||||||
ts, err := mime.ExtensionsByType(rev.TextMime)
|
|
||||||
if err != nil || ts == nil {
|
|
||||||
log.Println("No idea how I should name this one:", rev.TextMime)
|
|
||||||
rev.TextName = rev.IdAsStr() + ".txt"
|
|
||||||
} else {
|
|
||||||
log.Println("A good extension would be one of these:", ts)
|
|
||||||
rev.TextName = rev.IdAsStr() + ts[0]
|
|
||||||
}
|
|
||||||
return rev.TextName
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsHtml returns HTML representation of the revision.
|
|
||||||
// If there is an error, it will be told about it in `w`.
|
|
||||||
func (rev *Revision) AsHtml(w http.ResponseWriter) (ret string, err error) {
|
|
||||||
ret += `<article class="page">
|
|
||||||
<h1 class="page__title">` + rev.FullName + `</h1>
|
|
||||||
`
|
|
||||||
// TODO: support things other than images
|
|
||||||
if rev.hasBinaryData() {
|
|
||||||
ret += fmt.Sprintf(`<img src="/%s?action=getBinary&rev=%d" class="page__amnt"/>`, rev.FullName, rev.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
contents, err := ioutil.ReadFile(rev.TextPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to render", rev.FullName)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: support more markups.
|
|
||||||
// TODO: support mycorrhiza extensions like transclusion.
|
|
||||||
switch rev.TextMime {
|
|
||||||
case "text/markdown":
|
|
||||||
html := markdown.ToHTML(contents, nil, nil)
|
|
||||||
ret += string(html)
|
|
||||||
default:
|
|
||||||
ret += fmt.Sprintf(`<pre>%s</pre>`, contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
ret += `
|
|
||||||
</article>`
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActionGetBinary is used with `?action=getBinary`.
|
|
||||||
// It writes binary data of the revision. It also sets the MIME-type.
|
|
||||||
func (rev *Revision) ActionGetBinary(w http.ResponseWriter) {
|
|
||||||
fileContents, err := ioutil.ReadFile(rev.BinaryPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to load binary data of", rev.FullName, rev.Id)
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", rev.BinaryMime)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(fileContents)
|
|
||||||
log.Println("Serving binary data of", rev.FullName, rev.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActionRaw is used with `?action=raw`.
|
|
||||||
// It writes text content of the revision without any parsing or rendering.
|
|
||||||
func (rev *Revision) ActionRaw(w http.ResponseWriter) {
|
|
||||||
fileContents, err := ioutil.ReadFile(rev.TextPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", rev.TextMime)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(fileContents)
|
|
||||||
log.Println("Serving text data of", rev.FullName, rev.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActionZen is used with `?action=zen`.
|
|
||||||
// It renders the revision without any layout or navigation.
|
|
||||||
func (rev *Revision) ActionZen(w http.ResponseWriter) {
|
|
||||||
html, err := rev.AsHtml(w)
|
|
||||||
if err == nil {
|
|
||||||
fmt.Fprint(w, html)
|
|
||||||
log.Println("Rendering", rev.FullName, "in zen mode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActionView is used with `?action=view` or without any action.
|
|
||||||
// It renders the revision with layout and navigation.
|
|
||||||
func (rev *Revision) ActionView(w http.ResponseWriter, layoutFun func(Revision, string) string) {
|
|
||||||
html, err := rev.AsHtml(w)
|
|
||||||
if err == nil {
|
|
||||||
fmt.Fprint(w, layoutFun(*rev, html))
|
|
||||||
log.Println("Rendering", rev.FullName)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"address": "127.0.0.1:1737",
|
"address": "127.0.0.1:1737",
|
||||||
"theme": "default-dark",
|
"theme": "default-light",
|
||||||
"site-title": "🍄 MycorrhizaWiki",
|
"site-title": "🍄 MycorrhizaWiki",
|
||||||
"title-templates": {
|
"title-templates": {
|
||||||
"edit-hypha": "Edit %s at MycorrhizaWiki",
|
"edit-hypha": "Edit %s at MycorrhizaWiki",
|
||||||
|
BIN
w/favicon.ico
Normal file
BIN
w/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
4
w/m/Fruit/3.markdown
Normal file
4
w/m/Fruit/3.markdown
Normal file
@ -0,0 +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:
|
||||||
|
|
||||||
|
* [Apple](Apple)
|
||||||
|
* [Pear](Pear)
|
1
w/m/Fruit/Pineapple/1.html
Normal file
1
w/m/Fruit/Pineapple/1.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<b>Pineapple is apple from a pine</b>
|
19
w/m/Fruit/Pineapple/meta.json
Normal file
19
w/m/Fruit/Pineapple/meta.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"views": 0,
|
||||||
|
"deleted": false,
|
||||||
|
"revisions": {
|
||||||
|
"1": {
|
||||||
|
"tags": [
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"name": "Pineapple",
|
||||||
|
"comment": "Update Fruit/Pineapple",
|
||||||
|
"author": "",
|
||||||
|
"time": 1592997100,
|
||||||
|
"text_mime": "text/html",
|
||||||
|
"binary_mime": "",
|
||||||
|
"text_name": "1.html",
|
||||||
|
"binary_name": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,47 @@
|
|||||||
{
|
{
|
||||||
"revisions":{
|
"views": 0,
|
||||||
"1":{
|
"deleted": false,
|
||||||
"name": "Fruit",
|
"revisions": {
|
||||||
"time": 1591635559,
|
"1": {
|
||||||
"author": "bouncepaw",
|
"tags": [
|
||||||
"comment": "create Fruit",
|
"fetus",
|
||||||
"tags": ["fetus", "tasty"],
|
"tasty"
|
||||||
"text_mime": "text/markdown",
|
],
|
||||||
"text_name": "1.md"
|
"name": "Fruit",
|
||||||
|
"comment": "create Fruit",
|
||||||
|
"author": "bouncepaw",
|
||||||
|
"time": 1591635559,
|
||||||
|
"text_mime": "text/markdown",
|
||||||
|
"binary_mime": "",
|
||||||
|
"text_name": "1.md",
|
||||||
|
"binary_name": ""
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"tags": [
|
||||||
|
"fetus",
|
||||||
|
"tasty"
|
||||||
|
],
|
||||||
|
"name": "Fruit",
|
||||||
|
"comment": "update Fruit",
|
||||||
|
"author": "fungimaster",
|
||||||
|
"time": 1591636222,
|
||||||
|
"text_mime": "text/markdown",
|
||||||
|
"binary_mime": "",
|
||||||
|
"text_name": "2.md",
|
||||||
|
"binary_name": ""
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"tags": null,
|
||||||
|
"name": "Fruit",
|
||||||
|
"comment": "Update Fruit",
|
||||||
|
"author": "",
|
||||||
|
"time": 1593279957,
|
||||||
|
"text_mime": "text/markdown",
|
||||||
|
"binary_mime": "",
|
||||||
|
"text_name": "3.markdown",
|
||||||
|
"binary_name": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"2":{
|
"Invalid": false,
|
||||||
"name": "Fruit",
|
"Err": null
|
||||||
"time": 1591636222,
|
|
||||||
"author": "fungimaster",
|
|
||||||
"comment": "update Fruit",
|
|
||||||
"tags": ["fetus", "tasty"],
|
|
||||||
"text_mime": "text/markdown",
|
|
||||||
"text_name": "2.md"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
1
w/m/Templates/1.markdown
Normal file
1
w/m/Templates/1.markdown
Normal file
@ -0,0 +1 @@
|
|||||||
|
**TODO:** Reorganize templates.
|
@ -9,8 +9,7 @@
|
|||||||
<h4 class="">Edit box</h4>
|
<h4 class="">Edit box</h4>
|
||||||
<!-- It is important that there is no indent ↓ -->
|
<!-- It is important that there is no indent ↓ -->
|
||||||
<textarea class="edit-box__text" name="text" cols="80" rows="25">
|
<textarea class="edit-box__text" name="text" cols="80" rows="25">
|
||||||
{{ .Text }}
|
{{ .Text }}</textarea>
|
||||||
</textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
19
w/m/Templates/default-light/Hypha/edit/sidebar.html/1.html
Normal file
19
w/m/Templates/default-light/Hypha/edit/sidebar.html/1.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<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"/></p>
|
||||||
|
</div>
|
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"views": 0,
|
||||||
|
"deleted": false,
|
||||||
|
"revisions": {
|
||||||
|
"1": {
|
||||||
|
"tags": [
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"name": "sidebar.html",
|
||||||
|
"comment": "Create Templates/default-dark/Hypha/edit/sidebar.html",
|
||||||
|
"author": "",
|
||||||
|
"time": 1593003792,
|
||||||
|
"text_mime": "text/html",
|
||||||
|
"binary_mime": "",
|
||||||
|
"text_name": "1.html",
|
||||||
|
"binary_name": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
w/m/Templates/meta.json
Normal file
21
w/m/Templates/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Invalid": false,
|
||||||
|
"Err": null
|
||||||
|
}
|
120
walk.go
120
walk.go
@ -1,120 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hyphaPattern = `[^\s\d:/?&\\][^:?&\\]*`
|
|
||||||
hyphaUrl = `/{hypha:` + hyphaPattern + `}`
|
|
||||||
revisionPattern = `[\d]+`
|
|
||||||
revQuery = `{rev:` + revisionPattern + `}`
|
|
||||||
metaJsonPattern = `meta\.json`
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
leadingInt = regexp.MustCompile(`^[-+]?\d+`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// matchNameToEverything matches `name` to all filename patterns and returns 4 boolean results.
|
|
||||||
func matchNameToEverything(name string) (metaJsonM, hyphaM bool) {
|
|
||||||
// simpleMatch reduces boilerplate. Errors are ignored because I trust my regex skills.
|
|
||||||
simpleMatch := func(s string, p string) bool {
|
|
||||||
m, _ := regexp.MatchString(p, s)
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
return simpleMatch(name, metaJsonPattern),
|
|
||||||
simpleMatch(name, hyphaPattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
// scanHyphaDir scans directory at `fullPath` and tells what it has found.
|
|
||||||
func scanHyphaDir(fullPath string) (valid bool, possibleSubhyphae []string, metaJsonPath string, err error) {
|
|
||||||
nodes, err := ioutil.ReadDir(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return // implicit return values
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, node := range nodes {
|
|
||||||
metaJsonM, hyphaM := matchNameToEverything(node.Name())
|
|
||||||
switch {
|
|
||||||
case hyphaM && node.IsDir():
|
|
||||||
possibleSubhyphae = append(possibleSubhyphae, filepath.Join(fullPath, node.Name()))
|
|
||||||
case metaJsonM && !node.IsDir():
|
|
||||||
metaJsonPath = filepath.Join(fullPath, "meta.json")
|
|
||||||
// Other nodes are ignored. It is not promised they will be ignored in future versions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if metaJsonPath != "" {
|
|
||||||
valid = true
|
|
||||||
}
|
|
||||||
return // implicit return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// hyphaName gets name of a hypha by stripping path to the hypha in `fullPath`
|
|
||||||
func hyphaName(fullPath string) string {
|
|
||||||
// {cfg.WikiDir}/{the name}
|
|
||||||
return fullPath[len(cfg.WikiDir)+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// recurFindHyphae recursively searches for hyphae in passed directory path.
|
|
||||||
func recurFindHyphae(fullPath string) map[string]*Hypha {
|
|
||||||
hyphae := make(map[string]*Hypha)
|
|
||||||
valid, possibleSubhyphae, metaJsonPath, err := scanHyphaDir(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return hyphae
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, let's process subhyphae
|
|
||||||
for _, possibleSubhypha := range possibleSubhyphae {
|
|
||||||
for k, v := range recurFindHyphae(possibleSubhypha) {
|
|
||||||
hyphae[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This folder is not a hypha itself, nothing to do here
|
|
||||||
if !valid {
|
|
||||||
return hyphae
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template hypha struct. Other fields are default json values.
|
|
||||||
h := Hypha{
|
|
||||||
FullName: hyphaName(fullPath),
|
|
||||||
Path: fullPath,
|
|
||||||
parentName: filepath.Dir(hyphaName(fullPath)),
|
|
||||||
// Children names are unknown now
|
|
||||||
}
|
|
||||||
|
|
||||||
metaJsonContents, err := ioutil.ReadFile(metaJsonPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error when reading `%s`; skipping", metaJsonPath)
|
|
||||||
return hyphae
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(metaJsonContents, &h)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error when unmarshaling `%s`; skipping", metaJsonPath)
|
|
||||||
log.Println(err)
|
|
||||||
return hyphae
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill in rooted paths to content files and full names
|
|
||||||
for idStr, rev := range h.Revisions {
|
|
||||||
rev.FullName = filepath.Join(h.parentName, rev.ShortName)
|
|
||||||
rev.Id, _ = strconv.Atoi(idStr)
|
|
||||||
if rev.BinaryName != "" {
|
|
||||||
rev.BinaryPath = filepath.Join(fullPath, rev.BinaryName)
|
|
||||||
}
|
|
||||||
rev.TextPath = filepath.Join(fullPath, rev.TextName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now the hypha should be ok, gotta send structs
|
|
||||||
hyphae[h.FullName] = &h
|
|
||||||
return hyphae
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user