From 673c2b1836163647ea960796ad9fc02567071b50 Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Wed, 5 Aug 2020 20:08:59 +0500 Subject: [PATCH] Replace with 0.7 version in a primitive way --- .gitmodules | 3 + Makefile | 4 +- README.md | 14 +- cfg/config.go | 96 ----- fs/data.go | 86 ----- fs/fs.go | 56 --- fs/genealogy.go | 96 ----- fs/html.go | 38 -- fs/hypha.go | 341 ------------------ fs/revision.go | 42 --- gemtext/lexer.go | 173 +++++++++ gemtext/lexer_test.go | 57 +++ gemtext/parser.go | 31 ++ gemtext/testdata/test.gmi | 24 ++ gemtext/utils.go | 23 ++ gemtext/xclusion.go | 106 ++++++ gemtext/xclusion_test.go | 22 ++ go.mod | 12 +- go.sum | 40 +- handlers.go | 68 ---- http_mutators.go | 151 ++++++++ http_readers.go | 86 +++++ hypha.go | 124 +++++++ main.go | 191 ++++++---- metarrhiza | 1 + mime.go | 108 ++++++ mime_test.go | 19 + mycelium/mycelium.go | 107 ------ name.go | 41 +++ plugin/lang/en.go | 31 -- plugin/parser/creole.go | 11 - plugin/parser/gemini.go | 134 ------- plugin/parser/markdown.go | 10 - plugin/plugin.go | 20 - render/render.go | 149 -------- util/url.go | 44 --- util/util.go | 90 ----- util/util_test.go | 25 -- wiki/README.md | 1 - wiki/config.json | 30 -- wiki/favicon.ico | Bin 173902 -> 0 bytes wiki/main/README.md | 1 - wiki/main/doc/1.markdown | 1 - wiki/main/doc/meta.json | 21 -- wiki/main/doc/mycelia/1.markdown | 85 ----- wiki/main/doc/mycelia/meta.json | 21 -- wiki/main/doc/plugin/1.markdown | 29 -- wiki/main/doc/plugin/2.markdown | 30 -- wiki/main/doc/plugin/3.markdown | 27 -- wiki/main/doc/plugin/4.markdown | 27 -- wiki/main/doc/plugin/5.markdown | 32 -- wiki/main/doc/plugin/6.markdown | 32 -- wiki/main/doc/plugin/meta.json | 84 ----- wiki/main/doc/user/1.txt | 7 - wiki/main/doc/user/2.txt | 9 - wiki/main/doc/user/meta.json | 32 -- wiki/main/doc/wikilink/1.markdown | 46 --- wiki/main/doc/wikilink/2.markdown | 49 --- wiki/main/doc/wikilink/3.markdown | 51 --- wiki/main/doc/wikilink/meta.json | 45 --- wiki/main/fruit/1.md | 1 - wiki/main/fruit/2.md | 2 - wiki/main/fruit/3.markdown | 4 - wiki/main/fruit/4.markdown | 5 - wiki/main/fruit/5.markdown | 10 - wiki/main/fruit/apple/1.jpg | Bin 44237 -> 0 bytes wiki/main/fruit/apple/1.txt | 3 - wiki/main/fruit/apple/2.jpg | Bin 35292 -> 0 bytes wiki/main/fruit/apple/2.txt | 3 - wiki/main/fruit/apple/3.txt | 6 - wiki/main/fruit/apple/4.jpg | Bin 437226 -> 0 bytes wiki/main/fruit/apple/4.txt | 2 - wiki/main/fruit/apple/meta.json | 52 --- wiki/main/fruit/meta.json | 73 ---- wiki/main/fruit/pear/1.markdown | 3 - wiki/main/fruit/pear/2.markdown | 5 - wiki/main/fruit/pear/meta.json | 30 -- wiki/main/fruit/pineapple/1.html | 1 - wiki/main/fruit/pineapple/meta.json | 19 - wiki/main/fungus/1.markdown | 1 - wiki/main/fungus/2.markdown | 3 - wiki/main/fungus/amanita_muscaria/1.markdown | 3 - wiki/main/fungus/amanita_muscaria/2.markdown | 4 - wiki/main/fungus/amanita_muscaria/3.markdown | 5 - wiki/main/fungus/amanita_muscaria/4.markdown | 6 - wiki/main/fungus/amanita_muscaria/meta.json | 58 --- wiki/main/fungus/amanita_regalis/1.jpeg | Bin 622081 -> 0 bytes wiki/main/fungus/amanita_regalis/1.markdown | 7 - wiki/main/fungus/amanita_regalis/meta.json | 19 - wiki/main/fungus/meta.json | 32 -- wiki/main/test/1.markdown | 1 - wiki/main/test/meta.json | 19 - wiki/main/testcreole/1.txt | 135 ------- wiki/main/testcreole/2.txt | 135 ------- wiki/main/testcreole/meta.json | 32 -- wiki/main/testgemini/1.txt | 50 --- wiki/main/testgemini/meta.json | 17 - wiki/main/testmd/1.markdown | 232 ------------ wiki/main/testmd/meta.json | 21 -- wiki/spec/README.md | 0 wiki/sys/theme/1.markdown | 1 - wiki/sys/theme/2.markdown | 11 - wiki/sys/theme/3.markdown | 11 - wiki/sys/theme/default-light/base.html/1.html | 24 -- .../theme/default-light/base.html/meta.json | 17 - .../default-light/edit/index.html/1.html | 45 --- .../default-light/edit/index.html/meta.json | 17 - wiki/sys/theme/default-light/main.css/1.css | 59 --- .../theme/default-light/main.css/meta.json | 19 - wiki/sys/theme/default-light/main.js/1.mjs | 11 - .../sys/theme/default-light/main.js/meta.json | 17 - .../theme/default-light/update_ok.html/1.html | 9 - .../default-light/update_ok.html/meta.json | 17 - .../theme/default-light/view/404.html/1.html | 18 - .../default-light/view/404.html/meta.json | 17 - .../default-light/view/index.html/1.html | 14 - .../default-light/view/index.html/meta.json | 17 - wiki/sys/theme/meta.json | 45 --- wiki/tag/README.md | 0 wiki/user/README.md | 0 120 files changed, 1102 insertions(+), 3600 deletions(-) create mode 100644 .gitmodules delete mode 100644 cfg/config.go delete mode 100644 fs/data.go delete mode 100644 fs/fs.go delete mode 100644 fs/genealogy.go delete mode 100644 fs/html.go delete mode 100644 fs/hypha.go delete mode 100644 fs/revision.go create mode 100644 gemtext/lexer.go create mode 100644 gemtext/lexer_test.go create mode 100644 gemtext/parser.go create mode 100644 gemtext/testdata/test.gmi create mode 100644 gemtext/utils.go create mode 100644 gemtext/xclusion.go create mode 100644 gemtext/xclusion_test.go delete mode 100644 handlers.go create mode 100644 http_mutators.go create mode 100644 http_readers.go create mode 100644 hypha.go create mode 160000 metarrhiza create mode 100644 mime.go create mode 100644 mime_test.go delete mode 100644 mycelium/mycelium.go create mode 100644 name.go delete mode 100644 plugin/lang/en.go delete mode 100644 plugin/parser/creole.go delete mode 100644 plugin/parser/gemini.go delete mode 100644 plugin/parser/markdown.go delete mode 100644 plugin/plugin.go delete mode 100644 render/render.go delete mode 100644 util/url.go delete mode 100644 util/util.go delete mode 100644 util/util_test.go delete mode 100644 wiki/README.md delete mode 100644 wiki/config.json delete mode 100644 wiki/favicon.ico delete mode 100644 wiki/main/README.md delete mode 100644 wiki/main/doc/1.markdown delete mode 100644 wiki/main/doc/meta.json delete mode 100644 wiki/main/doc/mycelia/1.markdown delete mode 100644 wiki/main/doc/mycelia/meta.json delete mode 100644 wiki/main/doc/plugin/1.markdown delete mode 100644 wiki/main/doc/plugin/2.markdown delete mode 100644 wiki/main/doc/plugin/3.markdown delete mode 100644 wiki/main/doc/plugin/4.markdown delete mode 100644 wiki/main/doc/plugin/5.markdown delete mode 100644 wiki/main/doc/plugin/6.markdown delete mode 100644 wiki/main/doc/plugin/meta.json delete mode 100644 wiki/main/doc/user/1.txt delete mode 100644 wiki/main/doc/user/2.txt delete mode 100644 wiki/main/doc/user/meta.json delete mode 100644 wiki/main/doc/wikilink/1.markdown delete mode 100644 wiki/main/doc/wikilink/2.markdown delete mode 100644 wiki/main/doc/wikilink/3.markdown delete mode 100644 wiki/main/doc/wikilink/meta.json delete mode 100644 wiki/main/fruit/1.md delete mode 100644 wiki/main/fruit/2.md delete mode 100644 wiki/main/fruit/3.markdown delete mode 100644 wiki/main/fruit/4.markdown delete mode 100644 wiki/main/fruit/5.markdown delete mode 100644 wiki/main/fruit/apple/1.jpg delete mode 100644 wiki/main/fruit/apple/1.txt delete mode 100644 wiki/main/fruit/apple/2.jpg delete mode 100644 wiki/main/fruit/apple/2.txt delete mode 100644 wiki/main/fruit/apple/3.txt delete mode 100644 wiki/main/fruit/apple/4.jpg delete mode 100644 wiki/main/fruit/apple/4.txt delete mode 100644 wiki/main/fruit/apple/meta.json delete mode 100644 wiki/main/fruit/meta.json delete mode 100644 wiki/main/fruit/pear/1.markdown delete mode 100644 wiki/main/fruit/pear/2.markdown delete mode 100644 wiki/main/fruit/pear/meta.json delete mode 100644 wiki/main/fruit/pineapple/1.html delete mode 100644 wiki/main/fruit/pineapple/meta.json delete mode 100644 wiki/main/fungus/1.markdown delete mode 100644 wiki/main/fungus/2.markdown delete mode 100644 wiki/main/fungus/amanita_muscaria/1.markdown delete mode 100644 wiki/main/fungus/amanita_muscaria/2.markdown delete mode 100644 wiki/main/fungus/amanita_muscaria/3.markdown delete mode 100644 wiki/main/fungus/amanita_muscaria/4.markdown delete mode 100644 wiki/main/fungus/amanita_muscaria/meta.json delete mode 100644 wiki/main/fungus/amanita_regalis/1.jpeg delete mode 100644 wiki/main/fungus/amanita_regalis/1.markdown delete mode 100644 wiki/main/fungus/amanita_regalis/meta.json delete mode 100644 wiki/main/fungus/meta.json delete mode 100644 wiki/main/test/1.markdown delete mode 100644 wiki/main/test/meta.json delete mode 100644 wiki/main/testcreole/1.txt delete mode 100644 wiki/main/testcreole/2.txt delete mode 100644 wiki/main/testcreole/meta.json delete mode 100644 wiki/main/testgemini/1.txt delete mode 100644 wiki/main/testgemini/meta.json delete mode 100644 wiki/main/testmd/1.markdown delete mode 100644 wiki/main/testmd/meta.json delete mode 100644 wiki/spec/README.md delete mode 100644 wiki/sys/theme/1.markdown delete mode 100644 wiki/sys/theme/2.markdown delete mode 100644 wiki/sys/theme/3.markdown delete mode 100644 wiki/sys/theme/default-light/base.html/1.html delete mode 100644 wiki/sys/theme/default-light/base.html/meta.json delete mode 100644 wiki/sys/theme/default-light/edit/index.html/1.html delete mode 100644 wiki/sys/theme/default-light/edit/index.html/meta.json delete mode 100644 wiki/sys/theme/default-light/main.css/1.css delete mode 100644 wiki/sys/theme/default-light/main.css/meta.json delete mode 100644 wiki/sys/theme/default-light/main.js/1.mjs delete mode 100644 wiki/sys/theme/default-light/main.js/meta.json delete mode 100644 wiki/sys/theme/default-light/update_ok.html/1.html delete mode 100644 wiki/sys/theme/default-light/update_ok.html/meta.json delete mode 100644 wiki/sys/theme/default-light/view/404.html/1.html delete mode 100644 wiki/sys/theme/default-light/view/404.html/meta.json delete mode 100644 wiki/sys/theme/default-light/view/index.html/1.html delete mode 100644 wiki/sys/theme/default-light/view/index.html/meta.json delete mode 100644 wiki/sys/theme/meta.json delete mode 100644 wiki/tag/README.md delete mode 100644 wiki/user/README.md diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ffd5f5a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "metarrhiza"] + path = metarrhiza + url = https://github.com/bouncepaw/metarrhiza.git diff --git a/Makefile b/Makefile index 1573ad2..49dc584 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ run: build - ./mycorrhiza wiki + ./mycorrhiza metarrhiza build: go build . test: - go test ./util + go test . help: echo "Read the Makefile to see what it can do. It is simple." diff --git a/README.md b/README.md index 93047a7..64dc545 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,13 @@ # mycorrhiza wiki A wiki engine inspired by fungi. Not production-ready. -Current version: 0.5 (or more?) +Current version: 0.7 (or more?) ## Current features * Edit pages through html forms * Responsive design -* Theme support (no themes other than the default one implemented though) * Works in text browsers -* Pages (called hyphae) can be written in markdown, creole or geminitext. -* Two types of plugins: parsers (for supporting more markup languages) and languages (for i18n) -* Change history is saved and can be viewed (no nice way to do this yet, it can be done by changing url only) -* Namespaces called mycelia +* Pages (called hyphae) can be in gemtext. * Everything is stored as simple files, no database required ## Future features @@ -23,9 +19,3 @@ Current version: 0.5 (or more?) ## Installation I guess you can just clone this repo and run `make` to play around with the default wiki. -## License -Contents of `wiki` directory are under CC BY-SA 4.0. - -File `wiki/favicon.icon` is from [here](https://thenounproject.com/search/?q=mushroom&i=990340). - -The license for the rest of files is not decided yet, so let's consider it to be CC BY-SA 4.0 too. diff --git a/cfg/config.go b/cfg/config.go deleted file mode 100644 index d47ca38..0000000 --- a/cfg/config.go +++ /dev/null @@ -1,96 +0,0 @@ -package cfg - -import ( - "encoding/json" - "io/ioutil" - "log" - "os" - "path/filepath" - - "github.com/bouncepaw/mycorrhiza/plugin/lang" -) - -type MyceliumConfig struct { - Names []string `json:"names"` - Type string `json:"type"` -} - -const ( - HyphaPattern = `[^\s\d:/?&\\][^:?&\\]*` - HyphaUrl = `/{hypha:` + HyphaPattern + `}` - RevisionPattern = `[\d]+` - RevQuery = `{rev:` + RevisionPattern + `}` - MyceliumPattern = `[^\s\d:/?&\\][^:?&\\/]*` - MyceliumUrl = `/:{mycelium:` + MyceliumPattern + `}` -) - -var ( - Locale map[string]string - WikiDir string - configJsonPath string - - // Default values that can be overriden in config.json - Address = "0.0.0.0:80" - LocaleName = "en" - SiteTitle = `MycorrhizaWiki` - Theme = `default-light` - HomePage = `/Home` - BinaryLimit int64 = 10 << 20 - Mycelia = []MyceliumConfig{ - {[]string{"main"}, "main"}, - {[]string{"sys", "system"}, "system"}, - } -) - -func InitConfig(wd string) bool { - log.Println("WikiDir is", wd) - WikiDir = wd - configJsonPath = filepath.Join(WikiDir, "config.json") - - if _, err := os.Stat(configJsonPath); os.IsNotExist(err) { - log.Println("config.json not found, using default values") - return false - } - log.Println("config.json found, overriding default values...") - return readConfig() -} - -func readConfig() bool { - configJsonContents, err := ioutil.ReadFile(configJsonPath) - if err != nil { - log.Fatal("Error when reading config.json:", err) - return false - } - - cfg := struct { - Address string `json:"address"` - Theme string `json:"theme"` - SiteTitle string `json:"site-title"` - HomePage string `json:"home-page"` - BinaryLimitMB int64 `json:"binary-limit-mb"` - LocaleName string `json:"locale"` - Mycelia []MyceliumConfig `json:"mycelia"` - }{} - - err = json.Unmarshal(configJsonContents, &cfg) - if err != nil { - log.Fatal("Error when parsing config.json:", err) - return false - } - - Address = cfg.Address - Theme = cfg.Theme - SiteTitle = cfg.SiteTitle - HomePage = "/" + cfg.HomePage - BinaryLimit = 1024 * cfg.BinaryLimitMB - Mycelia = cfg.Mycelia - - switch cfg.LocaleName { - case "en": - Locale = lang.EnglishMap - default: - Locale = lang.EnglishMap - } - - return true -} diff --git a/fs/data.go b/fs/data.go deleted file mode 100644 index 15599cf..0000000 --- a/fs/data.go +++ /dev/null @@ -1,86 +0,0 @@ -// 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" - "github.com/bouncepaw/mycorrhiza/util" -) - -func (h *Hypha) MetaJsonPath() string { - return filepath.Join(h.Path(), "meta.json") -} - -func (h *Hypha) CanonicalName() string { - return util.UrlToCanonical(h.FullName) -} - -func (h *Hypha) Path() string { - return filepath.Join(cfg.WikiDir, h.CanonicalName()) -} - -func (h *Hypha) TextPath() string { - return h.actual.TextPath -} - -func (h *Hypha) parentName() string { - return filepath.Dir(util.UrlToCanonical(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.Locale["edit/box/help pattern"], h.FullName) -} diff --git a/fs/fs.go b/fs/fs.go deleted file mode 100644 index 3d1488a..0000000 --- a/fs/fs.go +++ /dev/null @@ -1,56 +0,0 @@ -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 - } - } -} diff --git a/fs/genealogy.go b/fs/genealogy.go deleted file mode 100644 index 5d63c37..0000000 --- a/fs/genealogy.go +++ /dev/null @@ -1,96 +0,0 @@ -package fs - -import ( - "fmt" - "log" - "path/filepath" - "sort" - "strings" - - "github.com/bouncepaw/mycorrhiza/util" -) - -func (s *Storage) RenderHypha(h *Hypha) { -} - -// If Name == "", the tree is empty. -type Tree struct { - Name string - Ancestors []string - Siblings []string - Descendants []*Tree - Root bool -} - -// 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`. -// In `root` is `false`, siblings will not be fetched. -func (s *Storage) GetTree(name string, root bool) *Tree { - name = util.UrlToCanonical(name) - t := &Tree{Name: name, Root: root} - for hyphaName, _ := range s.paths { - s.compareNamesAndAppend(t, hyphaName) - } - sort.Slice(t.Ancestors, func(i, j int) bool { - return strings.Count(t.Ancestors[i], "/") < strings.Count(t.Ancestors[j], "/") - }) - sort.Strings(t.Siblings) - sort.Slice(t.Descendants, func(i, j int) bool { - a := t.Descendants[i].Name - b := t.Descendants[j].Name - return len(a) < len(b) - }) - log.Printf("Generate tree for %v: %v %v\n", t.Name, t.Ancestors, t.Siblings) - return t -} - -// Compares names appends name2 to an array of `t`: -func (s *Storage) compareNamesAndAppend(t *Tree, name2 string) { - switch { - case t.Name == name2: - case strings.HasPrefix(t.Name, name2): - t.Ancestors = append(t.Ancestors, name2) - case t.Root && (strings.Count(t.Name, "/") == strings.Count(name2, "/") && - (filepath.Dir(t.Name) == filepath.Dir(name2))): - t.Siblings = append(t.Siblings, name2) - case strings.HasPrefix(name2, t.Name): - t.Descendants = append(t.Descendants, s.GetTree(name2, false)) - } -} - -// asHtml returns HTML representation of a tree. -// It recursively itself on the tree's children. -// TODO: redo with templates. I'm not in mood for it now. -func (t *Tree) AsHtml() (html string) { - if t.Name == "" { - return "" - } - html += `` - return html -} - -// navitreeEntry is a small utility function that makes generating html easier. -// Someone please redo it in templates. -func navitreeEntry(name, class string) string { - return fmt.Sprintf(`
  • - %s -
  • -`, class, ":"+util.DisplayToCanonical(name), filepath.Base(name)) -} diff --git a/fs/html.go b/fs/html.go deleted file mode 100644 index cae647c..0000000 --- a/fs/html.go +++ /dev/null @@ -1,38 +0,0 @@ -package fs - -import ( - "fmt" - "io/ioutil" - "log" - - "github.com/bouncepaw/mycorrhiza/plugin" - "github.com/bouncepaw/mycorrhiza/util" -) - -func (h *Hypha) asHtml() (string, error) { - rev := h.actual - ret := `
    -

    ` + rev.FullName + `

    -` - // What about using
    ? - // TODO: support other things - if h.hasBinaryData() { - ret += fmt.Sprintf(``, util.DisplayToCanonical(rev.FullName), rev.Id) - } - - contents, err := ioutil.ReadFile(rev.TextPath) - if err != nil { - log.Println("Failed to read contents of", rev.FullName, ":", err) - return "", err - } - - // TODO: support more markups. - // TODO: support mycorrhiza extensions like transclusion. - parser := plugin.ParserForMime(rev.TextMime) - ret += parser(contents) - - ret += ` -
    ` - - return ret, nil -} diff --git a/fs/hypha.go b/fs/hypha.go deleted file mode 100644 index 589c100..0000000 --- a/fs/hypha.go +++ /dev/null @@ -1,341 +0,0 @@ -package fs - -import ( - "encoding/json" - "errors" - "io/ioutil" - "log" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/bouncepaw/mycorrhiza/cfg" - "github.com/bouncepaw/mycorrhiza/mycelium" - "github.com/bouncepaw/mycorrhiza/util" -) - -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 `json:"-"` - Err error `json:"-"` -} - -func (h *Hypha) Invalidate(err error) *Hypha { - h.Invalid = true - h.Err = err - return h -} - -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, - FullName: util.CanonicalToDisplay(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 - } - 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) - } - 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 - } - 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) - } - 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) []byte) *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(renderExists(h.FullName, html)) - } else { - w.Write(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(h.Path(), 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(cfg.BinaryLimit) - // 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.CanonicalName()] = h.Path() - } - return h -} diff --git a/fs/revision.go b/fs/revision.go deleted file mode 100644 index 69ae0c6..0000000 --- a/fs/revision.go +++ /dev/null @@ -1,42 +0,0 @@ -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] - } -} diff --git a/gemtext/lexer.go b/gemtext/lexer.go new file mode 100644 index 0000000..507808b --- /dev/null +++ b/gemtext/lexer.go @@ -0,0 +1,173 @@ +package gemtext + +import ( + "fmt" + "html" + "path" + "strings" +) + +// HyphaExists holds function that checks that a hypha is present. +var HyphaExists func(string) bool + +// HyphaAccess holds function that accesses a hypha by its name. +var HyphaAccess func(string) (rawText, binaryHtml string, err error) + +// GemLexerState is used by gemtext parser to remember what is going on. +type GemLexerState struct { + // Name of hypha being parsed + name string + where string // "", "list", "pre" + // Line id + id int + buf string +} + +// GeminiToHtml converts gemtext `content` of hypha `name` to html string. +func GeminiToHtml(name, content string) string { + return "TODO: do" +} + +type Line struct { + id int + // interface{} may be bad. What I need is a sum of string and Transclusion + contents interface{} +} + +// Parse gemtext line starting with "=>" according to wikilink rules. +// See http://localhost:1737/page/wikilink +func wikilink(src string, state *GemLexerState) (href, text, class string) { + src = strings.TrimSpace(remover("=>")(src)) + if src == "" { + return + } + // Href is text after => till first whitespace + href = strings.Fields(src)[0] + // Text is everything after whitespace. + // If there's no text, make it same as href + if text = strings.TrimPrefix(src, href); text == "" { + text = href + } + + class = "wikilink_internal" + + switch { + case strings.HasPrefix(href, "./"): + hyphaName := canonicalName(path.Join( + state.name, strings.TrimPrefix(href, "./"))) + if !HyphaExists(hyphaName) { + class = "wikilink_new" + } + href = path.Join("/page", hyphaName) + case strings.HasPrefix(href, "../"): + hyphaName := canonicalName(path.Join( + path.Dir(state.name), strings.TrimPrefix(href, "../"))) + if !HyphaExists(hyphaName) { + class = "wikilink_new" + } + href = path.Join("/page", hyphaName) + case strings.HasPrefix(href, "/"): + case strings.ContainsRune(href, ':'): + class = "wikilink_external" + default: + href = path.Join("/page", href) + } + return href, strings.TrimSpace(text), class +} + +func lex(name, content string) (ast []Line) { + var state = GemLexerState{name: name} + + for _, line := range strings.Split(content, "\n") { + geminiLineToAST(line, &state, &ast) + } + return ast +} + +// Lex `line` in gemtext and save it to `ast` using `state`. +func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) { + if "" == strings.TrimSpace(line) { + return + } + + startsWith := func(token string) bool { + return strings.HasPrefix(line, token) + } + addLine := func(text interface{}) { + *ast = append(*ast, Line{id: state.id, contents: text}) + } + + // Beware! Usage of goto. Some may say it is considered evil but in this case it helped to make a better-structured code. + switch state.where { + case "pre": + goto preformattedState + case "list": + goto listState + default: + goto normalState + } + +preformattedState: + switch { + case startsWith("```"): + state.where = "" + state.buf = strings.TrimSuffix(state.buf, "\n") + addLine(state.buf + "") + state.buf = "" + default: + state.buf += html.EscapeString(line) + "\n" + } + return + +listState: + switch { + case startsWith("*"): + state.buf += fmt.Sprintf("\t
  • %s
  • \n", remover("*")(line)) + case startsWith("```"): + state.where = "pre" + addLine(state.buf + "") + state.id++ + state.buf = fmt.Sprintf("
    ", state.id, strings.TrimPrefix(line, "```"))
    +	default:
    +		state.where = ""
    +		addLine(state.buf + "")
    +		goto normalState
    +	}
    +	return
    +
    +normalState:
    +	state.id++
    +	switch {
    +
    +	case startsWith("```"):
    +		state.where = "pre"
    +		state.buf = fmt.Sprintf("
    ", state.id, strings.TrimPrefix(line, "```"))
    +	case startsWith("*"):
    +		state.where = "list"
    +		state.buf = fmt.Sprintf("