mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-05 17:40:26 +00:00
Replace with 0.7 version in a primitive way
This commit is contained in:
parent
24d11c04c2
commit
673c2b1836
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "metarrhiza"]
|
||||
path = metarrhiza
|
||||
url = https://github.com/bouncepaw/metarrhiza.git
|
4
Makefile
4
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."
|
||||
|
14
README.md
14
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.
|
||||
|
@ -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
|
||||
}
|
86
fs/data.go
86
fs/data.go
@ -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)
|
||||
}
|
56
fs/fs.go
56
fs/fs.go
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 += `<ul class="navitree__node">`
|
||||
if t.Root {
|
||||
for _, ancestor := range t.Ancestors {
|
||||
html += navitreeEntry(util.CanonicalToDisplay(ancestor), "navitree__ancestor")
|
||||
}
|
||||
for _, siblingName := range t.Siblings {
|
||||
html += navitreeEntry(util.CanonicalToDisplay(siblingName), "navitree__sibling")
|
||||
}
|
||||
html += navitreeEntry(util.CanonicalToDisplay(t.Name), "navitree__pagename")
|
||||
} else {
|
||||
html += navitreeEntry(util.CanonicalToDisplay(t.Name), "navitree__name")
|
||||
}
|
||||
|
||||
for _, subtree := range t.Descendants {
|
||||
html += subtree.AsHtml()
|
||||
}
|
||||
|
||||
html += `</ul>`
|
||||
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(`<li class="navitree__entry %s">
|
||||
<a class="navitree__link" href="/%s">%s</a>
|
||||
</li>
|
||||
`, class, ":"+util.DisplayToCanonical(name), filepath.Base(name))
|
||||
}
|
38
fs/html.go
38
fs/html.go
@ -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 := `<article class="page">
|
||||
<h1 class="page__title">` + rev.FullName + `</h1>
|
||||
`
|
||||
// What about using <figure>?
|
||||
// TODO: support other things
|
||||
if h.hasBinaryData() {
|
||||
ret += fmt.Sprintf(`<img src="/:%s?action=binary&rev=%d" class="page__amnt"/>`, util.DisplayToCanonical(rev.FullName), rev.Id)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(rev.TextPath)
|
||||
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 += `
|
||||
</article>`
|
||||
|
||||
return ret, nil
|
||||
}
|
341
fs/hypha.go
341
fs/hypha.go
@ -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
|
||||
}
|
@ -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]
|
||||
}
|
||||
}
|
173
gemtext/lexer.go
Normal file
173
gemtext/lexer.go
Normal file
@ -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 + "</code></pre>")
|
||||
state.buf = ""
|
||||
default:
|
||||
state.buf += html.EscapeString(line) + "\n"
|
||||
}
|
||||
return
|
||||
|
||||
listState:
|
||||
switch {
|
||||
case startsWith("*"):
|
||||
state.buf += fmt.Sprintf("\t<li>%s</li>\n", remover("*")(line))
|
||||
case startsWith("```"):
|
||||
state.where = "pre"
|
||||
addLine(state.buf + "</ul>")
|
||||
state.id++
|
||||
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
|
||||
default:
|
||||
state.where = ""
|
||||
addLine(state.buf + "</ul>")
|
||||
goto normalState
|
||||
}
|
||||
return
|
||||
|
||||
normalState:
|
||||
state.id++
|
||||
switch {
|
||||
|
||||
case startsWith("```"):
|
||||
state.where = "pre"
|
||||
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
|
||||
case startsWith("*"):
|
||||
state.where = "list"
|
||||
state.buf = fmt.Sprintf("<ul id='%d'>\n", state.id)
|
||||
goto listState
|
||||
|
||||
case startsWith("###"):
|
||||
addLine(fmt.Sprintf(
|
||||
"<h3 id='%d'>%s</h3>", state.id, removeHeadingOctothorps(line)))
|
||||
case startsWith("##"):
|
||||
addLine(fmt.Sprintf(
|
||||
"<h2 id='%d'>%s</h2>", state.id, removeHeadingOctothorps(line)))
|
||||
case startsWith("#"):
|
||||
addLine(fmt.Sprintf(
|
||||
"<h1 id='%d'>%s</h1>", state.id, removeHeadingOctothorps(line)))
|
||||
|
||||
case startsWith(">"):
|
||||
addLine(fmt.Sprintf(
|
||||
"<blockquote id='%d'>%s</blockquote>", state.id, remover(">")(line)))
|
||||
case startsWith("=>"):
|
||||
source, content, class := wikilink(line, state)
|
||||
addLine(fmt.Sprintf(
|
||||
`<p><a id='%d' class='%s' href="%s">%s</a></p>`, state.id, class, source, content))
|
||||
|
||||
case startsWith("<="):
|
||||
addLine(parseTransclusion(line, state.name))
|
||||
default:
|
||||
addLine(fmt.Sprintf("<p id='%d'>%s</p>", state.id, line))
|
||||
}
|
||||
}
|
57
gemtext/lexer_test.go
Normal file
57
gemtext/lexer_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package gemtext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TODO: move test gemtext docs to files, perhaps? These strings sure are ugly
|
||||
func TestLex(t *testing.T) {
|
||||
check := func(name, content string, expectedAst []Line) {
|
||||
if ast := lex(name, content); !reflect.DeepEqual(ast, expectedAst) {
|
||||
if len(ast) != len(expectedAst) {
|
||||
t.Error("Expected and generated AST length of", name, "do not match. Printed generated AST.")
|
||||
for _, l := range ast {
|
||||
fmt.Printf("%d: %s\n", l.id, l.contents)
|
||||
}
|
||||
return
|
||||
}
|
||||
for i, e := range ast {
|
||||
if e != expectedAst[i] {
|
||||
t.Error("Mismatch when lexing", name, "\nExpected:", expectedAst[i], "\nGot:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
contentsB, err := ioutil.ReadFile("testdata/test.gmi")
|
||||
if err != nil {
|
||||
t.Error("Could not read test gemtext file!")
|
||||
}
|
||||
contents := string(contentsB)
|
||||
check("Apple", contents, []Line{
|
||||
{1, "<h1 id='1'>1</h1>"},
|
||||
{2, "<h2 id='2'>2</h2>"},
|
||||
{3, "<h3 id='3'>3</h3>"},
|
||||
{4, "<blockquote id='4'>quote</blockquote>"},
|
||||
{5, `<ul id='5'>
|
||||
<li>li 1</li>
|
||||
<li>li 2</li>
|
||||
</ul>`},
|
||||
{6, "<p id='6'>text</p>"},
|
||||
{7, "<p id='7'>more text</p>"},
|
||||
{8, `<p><a id='8' class='wikilink_internal' href="/page/Pear">some link</a></p>`},
|
||||
{9, `<ul id='9'>
|
||||
<li>li\n"+</li>
|
||||
</ul>`},
|
||||
{10, `<pre id='10' alt='alt text goes here' class='codeblock'><code>=> preformatted text
|
||||
where gemtext is not lexed</code></pre>`},
|
||||
{11, `<p><a id='11' class='wikilink_internal' href="/page/linking">linking</a></p>`},
|
||||
{12, "<p id='12'>text</p>"},
|
||||
{13, `<pre id='13' alt='' class='codeblock'><code>()
|
||||
/\</code></pre>`},
|
||||
// More thorough testing of xclusions is done in xclusion_test.go
|
||||
{14, Transclusion{"apple", 1, 3}},
|
||||
})
|
||||
}
|
31
gemtext/parser.go
Normal file
31
gemtext/parser.go
Normal file
@ -0,0 +1,31 @@
|
||||
package gemtext
|
||||
|
||||
import ()
|
||||
|
||||
const maxRecursionLevel = 3
|
||||
|
||||
type GemParserState struct {
|
||||
recursionLevel int
|
||||
}
|
||||
|
||||
func Parse(ast []Line, from, to int, state GemParserState) (html string) {
|
||||
if state.recursionLevel > maxRecursionLevel {
|
||||
return "Transclusion depth limit"
|
||||
}
|
||||
for _, line := range ast {
|
||||
if line.id >= from && (line.id <= to || to == 0) {
|
||||
switch v := line.contents.(type) {
|
||||
case Transclusion:
|
||||
html += Transclude(v, state)
|
||||
case string:
|
||||
html += v
|
||||
}
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
func ToHtml(name, text string) string {
|
||||
state := GemParserState{}
|
||||
return Parse(lex(name, text), 0, 0, state)
|
||||
}
|
24
gemtext/testdata/test.gmi
vendored
Normal file
24
gemtext/testdata/test.gmi
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# 1
|
||||
## 2
|
||||
### 3
|
||||
> quote
|
||||
|
||||
* li 1
|
||||
* li 2
|
||||
text
|
||||
more text
|
||||
=> Pear some link
|
||||
|
||||
* li\n"+
|
||||
```alt text goes here
|
||||
=> preformatted text
|
||||
where gemtext is not lexed
|
||||
```it ends here"
|
||||
=>linking
|
||||
|
||||
text
|
||||
```
|
||||
()
|
||||
/\
|
||||
```
|
||||
<= Apple : 1..3
|
23
gemtext/utils.go
Normal file
23
gemtext/utils.go
Normal file
@ -0,0 +1,23 @@
|
||||
package gemtext
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Function that returns a function that can strip `prefix` and trim whitespace when called.
|
||||
func remover(prefix string) func(string) string {
|
||||
return func(l string) string {
|
||||
return strings.TrimSpace(strings.TrimPrefix(l, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove #, ## or ### from beginning of `line`.
|
||||
func removeHeadingOctothorps(line string) string {
|
||||
f := remover("#")
|
||||
return f(f(f(line)))
|
||||
}
|
||||
|
||||
// Return a canonical representation of a hypha `name`.
|
||||
func canonicalName(name string) string {
|
||||
return strings.ToLower(strings.ReplaceAll(strings.TrimSpace(name), " ", "_"))
|
||||
}
|
106
gemtext/xclusion.go
Normal file
106
gemtext/xclusion.go
Normal file
@ -0,0 +1,106 @@
|
||||
package gemtext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const xclError = -9
|
||||
|
||||
// Transclusion is used by gemtext parser to remember what hyphae shall be transcluded.
|
||||
type Transclusion struct {
|
||||
name string
|
||||
from int // inclusive
|
||||
to int // inclusive
|
||||
}
|
||||
|
||||
// Transclude transcludes `xcl` and returns html representation.
|
||||
func Transclude(xcl Transclusion, state GemParserState) (html string) {
|
||||
state.recursionLevel++
|
||||
tmptOk := `<section class="transclusion transclusion_ok">
|
||||
<a class="transclusion__link" href="/page/%s">%s</a>
|
||||
<div class="transclusion__content">%s</div>
|
||||
</section>`
|
||||
tmptFailed := `<section class="transclusion transclusion_failed">
|
||||
<p>Failed to transclude <a href="/page/%s">%s</a></p>
|
||||
</section>`
|
||||
if xcl.from == xclError || xcl.to == xclError || xcl.from > xcl.to {
|
||||
return fmt.Sprintf(tmptFailed, xcl.name, xcl.name)
|
||||
}
|
||||
|
||||
rawText, binaryHtml, err := HyphaAccess(xcl.name)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(tmptFailed, xcl.name, xcl.name)
|
||||
}
|
||||
xclText := Parse(lex(xcl.name, rawText), xcl.from, xcl.to, state)
|
||||
return fmt.Sprintf(tmptOk, xcl.name, xcl.name, binaryHtml+xclText)
|
||||
}
|
||||
|
||||
/* Grammar from hypha ‘transclusion’:
|
||||
transclusion_line ::= transclusion_token hypha_name LWS* [":" LWS* range LWS*]
|
||||
transclusion_token ::= "<=" LWS+
|
||||
hypha_name ::= canonical_name | noncanonical_name
|
||||
range ::= id | (from_id two_dots to_id) | (from_id two_dots) | (two_dots to_id)
|
||||
two_dots ::= ".."
|
||||
*/
|
||||
|
||||
func parseTransclusion(line, hyphaName string) (xclusion Transclusion) {
|
||||
line = strings.TrimSpace(remover("<=")(line))
|
||||
if line == "" {
|
||||
return Transclusion{"", xclError, xclError}
|
||||
}
|
||||
|
||||
if strings.ContainsRune(line, ':') {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
xclusion.name = xclCanonicalName(hyphaName, strings.TrimSpace(parts[0]))
|
||||
selector := strings.TrimSpace(parts[1])
|
||||
xclusion.from, xclusion.to = parseSelector(selector)
|
||||
} else {
|
||||
xclusion.name = xclCanonicalName(hyphaName, strings.TrimSpace(line))
|
||||
}
|
||||
return xclusion
|
||||
}
|
||||
|
||||
func xclCanonicalName(hyphaName, xclName string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(xclName, "./"):
|
||||
return canonicalName(path.Join(hyphaName, strings.TrimPrefix(xclName, "./")))
|
||||
case strings.HasPrefix(xclName, "../"):
|
||||
return canonicalName(path.Join(path.Dir(hyphaName), strings.TrimPrefix(xclName, "../")))
|
||||
default:
|
||||
return canonicalName(xclName)
|
||||
}
|
||||
}
|
||||
|
||||
// At this point:
|
||||
// selector ::= id
|
||||
// | from ".."
|
||||
// | from ".." to
|
||||
// | ".." to
|
||||
// If it is not, return (xclError, xclError).
|
||||
func parseSelector(selector string) (from, to int) {
|
||||
if selector == "" {
|
||||
return 0, 0
|
||||
}
|
||||
if strings.Contains(selector, "..") {
|
||||
parts := strings.Split(selector, "..")
|
||||
|
||||
var (
|
||||
fromStr = strings.TrimSpace(parts[0])
|
||||
from, fromErr = strconv.Atoi(fromStr)
|
||||
toStr = strings.TrimSpace(parts[1])
|
||||
to, toErr = strconv.Atoi(toStr)
|
||||
)
|
||||
if fromStr == "" && toStr == "" {
|
||||
return 0, 0
|
||||
}
|
||||
if fromErr == nil || toErr == nil {
|
||||
return from, to
|
||||
}
|
||||
} else if id, err := strconv.Atoi(selector); err == nil {
|
||||
return id, id
|
||||
}
|
||||
return xclError, xclError
|
||||
}
|
22
gemtext/xclusion_test.go
Normal file
22
gemtext/xclusion_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package gemtext
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseTransclusion(t *testing.T) {
|
||||
check := func(line string, expectedXclusion Transclusion) {
|
||||
if xcl := parseTransclusion(line); xcl != expectedXclusion {
|
||||
t.Error(line, "; got:", xcl, "wanted:", expectedXclusion)
|
||||
}
|
||||
}
|
||||
check("<= ", Transclusion{"", -9, -9})
|
||||
check("<=hypha", Transclusion{"hypha", 0, 0})
|
||||
check("<= hypha\t", Transclusion{"hypha", 0, 0})
|
||||
check("<= hypha :", Transclusion{"hypha", 0, 0})
|
||||
check("<= hypha : ..", Transclusion{"hypha", 0, 0})
|
||||
check("<= hypha : 3", Transclusion{"hypha", 3, 3})
|
||||
check("<= hypha : 3..", Transclusion{"hypha", 3, 0})
|
||||
check("<= hypha : ..3", Transclusion{"hypha", 0, 3})
|
||||
check("<= hypha : 3..4", Transclusion{"hypha", 3, 4})
|
||||
}
|
12
go.mod
12
go.mod
@ -3,14 +3,6 @@ module github.com/bouncepaw/mycorrhiza
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.7.4
|
||||
mvdan.cc/gogrep v0.0.0-20200420132841-24e8804e5b3c // indirect
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
|
||||
golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/m4tty/cajun v0.0.0-20150303030909-35de273cc87b
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
gopkg.in/russross/blackfriday.v2 v2.0.1
|
||||
)
|
||||
|
||||
replace gopkg.in/russross/blackfriday.v2 => github.com/russross/blackfriday/v2 v2.0.1
|
||||
|
40
go.sum
40
go.sum
@ -1,35 +1,25 @@
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gomarkdown/markdown v0.0.0-20200609195525-3f9352745725 h1:X6sZdr+t2E2jwajTy/FfXbmAKPFTYxEq9hiFgzMiuPQ=
|
||||
github.com/gomarkdown/markdown v0.0.0-20200609195525-3f9352745725/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/m4tty/cajun v0.0.0-20150303030909-35de273cc87b h1:aY3LtSBlkQahoWaPTytHcIFsDbeXFYMc4noRQ/N5Q+A=
|
||||
github.com/m4tty/cajun v0.0.0-20150303030909-35de273cc87b/go.mod h1:zFXkL7I5vIwKg4dxEA9025SLdIHu9qFX/cYTdUcusHc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20191223235410-3721262b3e7c h1:PeFrxQ8YTAKg53UR8aP/nxa82lQYIdb+pd1bfg3dBDM=
|
||||
golang.org/x/tools v0.0.0-20191223235410-3721262b3e7c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6 h1:qKpj8TpV+LEhel7H/fR788J+KvhWZ3o3V6N2fU/iuLU=
|
||||
golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
mvdan.cc/gogrep v0.0.0-20200420132841-24e8804e5b3c h1:bz7/KkVXLQ6AWDoNX/hXfcAcNbLwQVAKNGt2I5vZKEE=
|
||||
mvdan.cc/gogrep v0.0.0-20200420132841-24e8804e5b3c/go.mod h1:LBbI8cEsbrMdWjW4Lcs806EWonhTiZbaBCCbsalF+6c=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
68
handlers.go
68
handlers.go
@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/fs"
|
||||
"github.com/bouncepaw/mycorrhiza/render"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// There are handlers below. See main() for their usage.
|
||||
|
||||
// Boilerplate code present in many handlers. Good to have it.
|
||||
func HandlerBase(w http.ResponseWriter, rq *http.Request) *fs.Hypha {
|
||||
vars := mux.Vars(rq)
|
||||
return fs.Hs.OpenFromMap(vars).OnRevision(RevInMap(vars))
|
||||
}
|
||||
|
||||
func HandlerRaw(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println("?action=raw")
|
||||
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) {
|
||||
log.Println("?action=zen")
|
||||
HandlerBase(w, rq).ActionZen(w).LogSuccMaybe("Rendering zen")
|
||||
}
|
||||
|
||||
func HandlerView(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println("?action=view")
|
||||
HandlerBase(w, rq).
|
||||
ActionView(w, render.HyphaPage, render.Hypha404).
|
||||
LogSuccMaybe("Rendering hypha view")
|
||||
}
|
||||
|
||||
func HandlerEdit(w http.ResponseWriter, rq *http.Request) {
|
||||
vars := mux.Vars(rq)
|
||||
h := fs.Hs.OpenFromMap(vars).OnRevision("0")
|
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(render.HyphaEdit(h))
|
||||
}
|
||||
|
||||
func HandlerUpdate(w http.ResponseWriter, rq *http.Request) {
|
||||
vars := mux.Vars(rq)
|
||||
log.Println("Attempt to update hypha", vars["hypha"])
|
||||
h := fs.Hs.
|
||||
OpenFromMap(vars).
|
||||
CreateDirIfNeeded().
|
||||
AddRevisionFromHttpData(rq).
|
||||
WriteTextFileFromHttpData(rq).
|
||||
WriteBinaryFileFromHttpData(rq).
|
||||
SaveJson().
|
||||
Store().
|
||||
LogSuccMaybe("Saved changes")
|
||||
|
||||
if !h.Invalid {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write(render.HyphaUpdateOk(h))
|
||||
}
|
||||
}
|
151
http_mutators.go
Normal file
151
http_mutators.go
Normal file
@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func init() {
|
||||
http.HandleFunc("/upload-binary/", handlerUploadBinary)
|
||||
http.HandleFunc("/upload-text/", handlerUploadText)
|
||||
http.HandleFunc("/edit/", handlerEdit)
|
||||
}
|
||||
|
||||
// handlerEdit shows the edit form. It doesn't edit anything actually.
|
||||
func handlerEdit(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
var (
|
||||
hyphaName = HyphaNameFromRq(rq, "edit")
|
||||
hyphaData, isOld = HyphaStorage[hyphaName]
|
||||
warning string
|
||||
textAreaFill string
|
||||
err error
|
||||
)
|
||||
if isOld {
|
||||
textAreaFill, err = FetchTextPart(hyphaData)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error",
|
||||
"Could not fetch text data")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
warning = `<p>You are creating a new hypha.</p>`
|
||||
}
|
||||
form := fmt.Sprintf(`
|
||||
<main>
|
||||
<h1>Edit %[1]s</h1>
|
||||
%[3]s
|
||||
<form method="post" class="upload-text-form"
|
||||
action="/upload-text/%[1]s">
|
||||
<textarea name="text">%[2]s</textarea>
|
||||
<br/>
|
||||
<input type="submit"/>
|
||||
<a href="/page/%[1]s">Cancel</a>
|
||||
</form>
|
||||
</main>
|
||||
`, hyphaName, textAreaFill, warning)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(base(
|
||||
"Edit "+hyphaName, form)))
|
||||
}
|
||||
|
||||
// handlerUploadText uploads a new text part for the hypha.
|
||||
func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
var (
|
||||
hyphaName = HyphaNameFromRq(rq, "upload-text")
|
||||
hyphaData, isOld = HyphaStorage[hyphaName]
|
||||
textData = rq.PostFormValue("text")
|
||||
textDataBytes = []byte(textData)
|
||||
fullPath = filepath.Join(WikiDir, hyphaName+"&.gmi")
|
||||
)
|
||||
if textData == "" {
|
||||
HttpErr(w, http.StatusBadRequest, hyphaName, "Error",
|
||||
"No text data passed")
|
||||
return
|
||||
}
|
||||
// For some reason, only 0777 works. Why?
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(fullPath, textDataBytes, 0644); err != nil {
|
||||
log.Println(err)
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error",
|
||||
fmt.Sprintf("Failed to write %d bytes to %s",
|
||||
len(textDataBytes), fullPath))
|
||||
return
|
||||
}
|
||||
if !isOld {
|
||||
HyphaStorage[hyphaName] = &HyphaData{
|
||||
textType: TextGemini,
|
||||
textPath: fullPath,
|
||||
}
|
||||
} else {
|
||||
hyphaData.textType = TextGemini
|
||||
hyphaData.textPath = fullPath
|
||||
}
|
||||
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handlerUploadBinary uploads a new binary part for the hypha.
|
||||
func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
hyphaName := HyphaNameFromRq(rq, "upload-binary")
|
||||
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 {
|
||||
HttpErr(w, http.StatusBadRequest, hyphaName, "Error",
|
||||
"No binary data passed")
|
||||
return
|
||||
}
|
||||
// If file is passed:
|
||||
var (
|
||||
hyphaData, isOld = HyphaStorage[hyphaName]
|
||||
mimeType = MimeToBinaryType(handler.Header.Get("Content-Type"))
|
||||
ext = mimeType.Extension()
|
||||
fullPath = filepath.Join(WikiDir, hyphaName+"&"+ext)
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error",
|
||||
"Could not read passed data")
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err = ioutil.WriteFile(fullPath, data, 0644); err != nil {
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error",
|
||||
"Could not save passed data")
|
||||
return
|
||||
}
|
||||
if !isOld {
|
||||
HyphaStorage[hyphaName] = &HyphaData{
|
||||
binaryPath: fullPath,
|
||||
binaryType: mimeType,
|
||||
}
|
||||
} else {
|
||||
if hyphaData.binaryPath != fullPath {
|
||||
if err := os.Remove(hyphaData.binaryPath); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
hyphaData.binaryPath = fullPath
|
||||
hyphaData.binaryType = mimeType
|
||||
}
|
||||
log.Println("Written", len(data), "of binary data for", hyphaName, "to path", fullPath)
|
||||
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
|
||||
}
|
86
http_readers.go
Normal file
86
http_readers.go
Normal file
@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/gemtext"
|
||||
)
|
||||
|
||||
func init() {
|
||||
http.HandleFunc("/page/", handlerPage)
|
||||
http.HandleFunc("/text/", handlerText)
|
||||
http.HandleFunc("/binary/", handlerBinary)
|
||||
}
|
||||
|
||||
// handlerText serves raw source text of the hypha.
|
||||
func handlerText(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
hyphaName := HyphaNameFromRq(rq, "text")
|
||||
if data, ok := HyphaStorage[hyphaName]; ok {
|
||||
log.Println("Serving", data.textPath)
|
||||
w.Header().Set("Content-Type", data.textType.Mime())
|
||||
http.ServeFile(w, rq, data.textPath)
|
||||
}
|
||||
}
|
||||
|
||||
// handlerBinary serves binary part of the hypha.
|
||||
func handlerBinary(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
hyphaName := HyphaNameFromRq(rq, "binary")
|
||||
if data, ok := HyphaStorage[hyphaName]; ok {
|
||||
log.Println("Serving", data.binaryPath)
|
||||
w.Header().Set("Content-Type", data.binaryType.Mime())
|
||||
http.ServeFile(w, rq, data.binaryPath)
|
||||
}
|
||||
}
|
||||
|
||||
// handlerPage is the main hypha action that displays the hypha and the binary upload form along with some navigation.
|
||||
func handlerPage(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
var (
|
||||
hyphaName = HyphaNameFromRq(rq, "page")
|
||||
contents = fmt.Sprintf(`<p>This hypha has no text. Why not <a href="/edit/%s">create it</a>?</p>`, hyphaName)
|
||||
data, hyphaExists = HyphaStorage[hyphaName]
|
||||
)
|
||||
if hyphaExists {
|
||||
fileContentsT, errT := ioutil.ReadFile(data.textPath)
|
||||
_, errB := os.Stat(data.binaryPath)
|
||||
if errT == nil {
|
||||
contents = gemtext.ToHtml(hyphaName, string(fileContentsT))
|
||||
}
|
||||
if !os.IsNotExist(errB) {
|
||||
contents = binaryHtmlBlock(hyphaName, data) + contents
|
||||
}
|
||||
}
|
||||
form := fmt.Sprintf(`
|
||||
<main>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/edit/%[1]s">Edit</a></li>
|
||||
<li><a href="/text/%[1]s">Raw text</a></li>
|
||||
<li><a href="/binary/%[1]s">Binary part</a></li>
|
||||
<li><a href="/history/%[1]s">History</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<article>
|
||||
%[2]s
|
||||
%[3]s
|
||||
</article>
|
||||
<hr>
|
||||
<form action="/upload-binary/%[1]s"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<label for="upload-binary__input">Upload new binary part</label>
|
||||
<br>
|
||||
<input type="file" id="upload-binary__input" name="binary"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
</main>
|
||||
`, hyphaName, naviTitle(hyphaName), contents)
|
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(base(hyphaName, form)))
|
||||
}
|
124
hypha.go
Normal file
124
hypha.go
Normal file
@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/gemtext"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gemtext.HyphaExists = func(hyphaName string) bool {
|
||||
_, hyphaExists := HyphaStorage[hyphaName]
|
||||
return hyphaExists
|
||||
}
|
||||
gemtext.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) {
|
||||
if hyphaData, ok := HyphaStorage[hyphaName]; ok {
|
||||
rawText, err = FetchTextPart(hyphaData)
|
||||
if hyphaData.binaryPath != "" {
|
||||
binaryBlock = binaryHtmlBlock(hyphaName, hyphaData)
|
||||
}
|
||||
} else {
|
||||
err = errors.New("Hypha " + hyphaName + " does not exist")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// HyphaData represents a hypha's meta information: binary and text parts rooted paths and content types.
|
||||
type HyphaData struct {
|
||||
textPath string
|
||||
textType TextType
|
||||
binaryPath string
|
||||
binaryType BinaryType
|
||||
}
|
||||
|
||||
// binaryHtmlBlock creates an html block for binary part of the hypha.
|
||||
func binaryHtmlBlock(hyphaName string, d *HyphaData) string {
|
||||
switch d.binaryType {
|
||||
case BinaryJpeg, BinaryGif, BinaryPng, BinaryWebp, BinarySvg, BinaryIco:
|
||||
return fmt.Sprintf(`
|
||||
<div class="binary-container binary-container_with-img">
|
||||
<img src="/binary/%s"/>
|
||||
</div>`, hyphaName)
|
||||
case BinaryOgg, BinaryWebm, BinaryMp4:
|
||||
return fmt.Sprintf(`
|
||||
<div class="binary-container binary-container_with-video">
|
||||
<video>
|
||||
<source src="/binary/%[1]s"/>
|
||||
<p>Your browser does not support video. See video's <a href="/binary/%[1]s">direct url</a></p>
|
||||
</video>
|
||||
`, hyphaName)
|
||||
case BinaryMp3:
|
||||
return fmt.Sprintf(`
|
||||
<div class="binary-container binary-container_with-audio">
|
||||
<audio>
|
||||
<source src="/binary/%[1]s"/>
|
||||
<p>Your browser does not support audio. See audio's <a href="/binary/%[1]s">direct url</a></p>
|
||||
</audio>
|
||||
`, hyphaName)
|
||||
default:
|
||||
return fmt.Sprintf(`
|
||||
<div class="binary-container binary-container_with-nothing">
|
||||
<p>This hypha's media cannot be rendered. Access it <a href="/binary/%s">directly</a></p>
|
||||
</div>
|
||||
`, hyphaName)
|
||||
}
|
||||
}
|
||||
|
||||
// Index finds all hypha files in the full `path` and saves them to HyphaStorage. This function is recursive.
|
||||
func Index(path string) {
|
||||
nodes, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
// If this hypha looks like it can be a hypha path, go deeper
|
||||
if node.IsDir() && isCanonicalName(node.Name()) {
|
||||
Index(filepath.Join(path, node.Name()))
|
||||
}
|
||||
|
||||
hyphaPartFilename := filepath.Join(path, node.Name())
|
||||
skip, hyphaName, isText, mimeId := DataFromFilename(hyphaPartFilename)
|
||||
if !skip {
|
||||
var (
|
||||
hyphaData *HyphaData
|
||||
ok bool
|
||||
)
|
||||
if hyphaData, ok = HyphaStorage[hyphaName]; !ok {
|
||||
hyphaData = &HyphaData{}
|
||||
HyphaStorage[hyphaName] = hyphaData
|
||||
}
|
||||
if isText {
|
||||
hyphaData.textPath = hyphaPartFilename
|
||||
hyphaData.textType = TextType(mimeId)
|
||||
} else {
|
||||
hyphaData.binaryPath = hyphaPartFilename
|
||||
hyphaData.binaryType = BinaryType(mimeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FetchTextPart tries to read text file in the `d`. If there is no file, empty string is returned.
|
||||
func FetchTextPart(d *HyphaData) (string, error) {
|
||||
if d.textPath == "" {
|
||||
return "", nil
|
||||
}
|
||||
_, err := os.Stat(d.textPath)
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text, err := ioutil.ReadFile(d.textPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(text), nil
|
||||
}
|
191
main.go
191
main.go
@ -1,99 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
"github.com/bouncepaw/mycorrhiza/fs"
|
||||
"github.com/bouncepaw/mycorrhiza/mycelium"
|
||||
"github.com/gorilla/mux"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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".
|
||||
func RevInMap(m map[string]string) string {
|
||||
if id, ok := m["rev"]; ok {
|
||||
return id
|
||||
}
|
||||
return "0"
|
||||
// WikiDir is a rooted path to the wiki storage directory.
|
||||
var WikiDir string
|
||||
|
||||
// HyphaPattern is a pattern which all hyphae must match. Not used currently.
|
||||
var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%]+`)
|
||||
|
||||
// HyphaStorage is a mapping between canonical hypha names and their meta information.
|
||||
var HyphaStorage = make(map[string]*HyphaData)
|
||||
|
||||
// HttpErr is used by many handlers to signal errors in a compact way.
|
||||
func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) {
|
||||
log.Println(errMsg, "for", name)
|
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprint(w, base(title, fmt.Sprintf(
|
||||
`<p>%s. <a href="/page/%s">Go back to the hypha.<a></p>`,
|
||||
errMsg, name)))
|
||||
}
|
||||
|
||||
func IdempotentRouterBoiler(router *mux.Router, action string, handler func(w http.ResponseWriter, rq *http.Request)) {
|
||||
router.
|
||||
Queries("action", action, "rev", cfg.RevQuery).
|
||||
Path(cfg.MyceliumUrl + cfg.HyphaUrl).
|
||||
HandlerFunc(handler)
|
||||
router.
|
||||
Queries("action", action).
|
||||
Path(cfg.MyceliumUrl + cfg.HyphaUrl).
|
||||
HandlerFunc(handler)
|
||||
router.
|
||||
Queries("action", action, "rev", cfg.RevQuery).
|
||||
Path(cfg.HyphaUrl).
|
||||
HandlerFunc(handler)
|
||||
router.
|
||||
Queries("action", action).
|
||||
Path(cfg.HyphaUrl).
|
||||
HandlerFunc(handler)
|
||||
// shorterPath is used by handlerList to display shorter path to the files. It simply strips WikiDir.
|
||||
func shorterPath(fullPath string) string {
|
||||
tmp := strings.TrimPrefix(fullPath, WikiDir)
|
||||
if tmp == "" {
|
||||
return ""
|
||||
}
|
||||
return tmp[1:]
|
||||
}
|
||||
|
||||
// Show all hyphae
|
||||
func handlerList(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
buf := `
|
||||
<h1>List of pages</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Text path</th>
|
||||
<th>Text type</th>
|
||||
<th>Binary path</th>
|
||||
<th>Binary type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`
|
||||
for name, data := range HyphaStorage {
|
||||
buf += fmt.Sprintf(`
|
||||
<tr>
|
||||
<td><a href="/page/%s">%s</a></td>
|
||||
<td>%s</td>
|
||||
<td>%d</td>
|
||||
<td>%s</td>
|
||||
<td>%d</td>
|
||||
</tr>`,
|
||||
name, name,
|
||||
shorterPath(data.textPath), data.textType,
|
||||
shorterPath(data.binaryPath), data.binaryType,
|
||||
)
|
||||
}
|
||||
buf += `
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
w.Write([]byte(base("List of pages", buf)))
|
||||
}
|
||||
|
||||
// This part is present in all html documents.
|
||||
func base(title, body string) string {
|
||||
return fmt.Sprintf(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/static/common.css">
|
||||
<title>%s</title>
|
||||
</head>
|
||||
<body>
|
||||
%s
|
||||
</body>
|
||||
</html>
|
||||
`, title, body)
|
||||
}
|
||||
|
||||
// Reindex all hyphae by checking the wiki storage directory anew.
|
||||
func handlerReindex(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
HyphaStorage = make(map[string]*HyphaData)
|
||||
log.Println("Wiki storage directory is", WikiDir)
|
||||
log.Println("Start indexing hyphae...")
|
||||
Index(WikiDir)
|
||||
log.Println("Indexed", len(HyphaStorage), "hyphae")
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
panic("Expected a root wiki pages directory")
|
||||
}
|
||||
wikiDir, err := filepath.Abs(os.Args[1])
|
||||
log.Println("Running MycorrhizaWiki β")
|
||||
|
||||
var err error
|
||||
WikiDir, err = filepath.Abs(os.Args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Wiki storage directory is", WikiDir)
|
||||
log.Println("Start indexing hyphae...")
|
||||
Index(WikiDir)
|
||||
log.Println("Indexed", len(HyphaStorage), "hyphae")
|
||||
|
||||
log.Println("Welcome to MycorrhizaWiki α")
|
||||
cfg.InitConfig(wikiDir)
|
||||
log.Println("Indexing hyphae...")
|
||||
mycelium.Init()
|
||||
fs.InitStorage()
|
||||
|
||||
// Start server code. See handlers.go for handlers' implementations.
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
|
||||
http.ServeFile(w, rq, filepath.Join(cfg.WikiDir, "favicon.ico"))
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static"))))
|
||||
// See http_readers.go for /page/, /text/, /binary/.
|
||||
// See http_mutators.go for /upload-binary/, /upload-text/, /edit/.
|
||||
http.HandleFunc("/list", handlerList)
|
||||
http.HandleFunc("/reindex", handlerReindex)
|
||||
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
|
||||
http.ServeFile(w, rq, WikiDir+"/static/favicon.ico")
|
||||
})
|
||||
|
||||
IdempotentRouterBoiler(r, "binary", HandlerBinary)
|
||||
IdempotentRouterBoiler(r, "raw", HandlerRaw)
|
||||
IdempotentRouterBoiler(r, "zen", HandlerZen)
|
||||
IdempotentRouterBoiler(r, "view", HandlerView)
|
||||
|
||||
r.Queries("action", "edit").Path(cfg.MyceliumUrl + cfg.HyphaUrl).
|
||||
HandlerFunc(HandlerEdit)
|
||||
r.Queries("action", "edit").Path(cfg.HyphaUrl).
|
||||
HandlerFunc(HandlerEdit)
|
||||
|
||||
r.Queries("action", "update").Path(cfg.MyceliumUrl + cfg.HyphaUrl).
|
||||
Methods("POST").HandlerFunc(HandlerUpdate)
|
||||
r.Queries("action", "update").Path(cfg.HyphaUrl).
|
||||
Methods("POST").HandlerFunc(HandlerUpdate)
|
||||
|
||||
r.HandleFunc(cfg.MyceliumUrl+cfg.HyphaUrl, HandlerView)
|
||||
r.HandleFunc(cfg.HyphaUrl, HandlerView)
|
||||
|
||||
r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
|
||||
http.Redirect(w, rq, cfg.HomePage, http.StatusSeeOther)
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
|
||||
http.Redirect(w, rq, "/page/home", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.Handle("/", r)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: cfg.Address,
|
||||
// Good practice: enforce timeouts for servers you create!
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
log.Fatal(http.ListenAndServe("0.0.0.0:1737", nil))
|
||||
}
|
||||
|
1
metarrhiza
Submodule
1
metarrhiza
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit a22fcac89f10ad1e1db77d765788dfd8966cbb36
|
108
mime.go
Normal file
108
mime.go
Normal file
@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TextType is content type of text part of a hypha.
|
||||
type TextType int
|
||||
|
||||
const (
|
||||
// TextPlain is default text content type.
|
||||
TextPlain TextType = iota
|
||||
// TextGemini is content type for MycorrhizaWiki's dialect of gemtext.
|
||||
TextGemini
|
||||
)
|
||||
|
||||
// Mime returns mime type representation of `t`.
|
||||
func (t TextType) Mime() string {
|
||||
return [...]string{"text/plain", "text/gemini"}[t]
|
||||
}
|
||||
|
||||
// Extension returns extension (with dot) to be used for files with content type `t`.
|
||||
func (t TextType) Extension() string {
|
||||
return [...]string{".txt", ".gmi"}[t]
|
||||
}
|
||||
|
||||
// BinaryType is content type of binary part of a hypha.
|
||||
type BinaryType int
|
||||
|
||||
// Supported binary content types
|
||||
const (
|
||||
// BinaryOctet is default binary content type.
|
||||
BinaryOctet BinaryType = iota
|
||||
BinaryJpeg
|
||||
BinaryGif
|
||||
BinaryPng
|
||||
BinaryWebp
|
||||
BinarySvg
|
||||
BinaryIco
|
||||
BinaryOgg
|
||||
BinaryWebm
|
||||
BinaryMp3
|
||||
BinaryMp4
|
||||
)
|
||||
|
||||
var binaryMimes = [...]string{
|
||||
"application/octet-stream",
|
||||
"image/jpeg", "image/gif", "image/png", "image/webp",
|
||||
"image/svg+xml", "image/x-icon",
|
||||
"application/ogg", "video/webm", "audio/mp3", "video/mp4",
|
||||
}
|
||||
|
||||
// Mime returns mime type representation of `t`.
|
||||
func (t BinaryType) Mime() string {
|
||||
return binaryMimes[t]
|
||||
}
|
||||
|
||||
var binaryExtensions = [...]string{
|
||||
".bin", ".jpg", ".gif", ".png", ".webp", ".svg", ".ico",
|
||||
".ogg", ".webm", ".mp3", ".mp4",
|
||||
}
|
||||
|
||||
// Extension returns extension (with dot) to be used for files with content type `t`.
|
||||
func (t BinaryType) Extension() string {
|
||||
return binaryExtensions[t]
|
||||
}
|
||||
|
||||
// MimeToBinaryType converts mime type to BinaryType. If the mime type is not supported, BinaryOctet is returned as a fallback type.
|
||||
func MimeToBinaryType(mime string) BinaryType {
|
||||
for i, binaryMime := range binaryMimes {
|
||||
if binaryMime == mime {
|
||||
return BinaryType(i)
|
||||
}
|
||||
}
|
||||
return BinaryOctet
|
||||
}
|
||||
|
||||
// DataFromFilename fetches all meta information from hypha content file with path `fullPath`. If it is not a content file, `skip` is true, and you are expected to ignore this file when indexing hyphae. `name` is name of the hypha to which this file relates. `isText` is true when the content file is text, false when is binary. `mimeId` is an integer representation of content type. Cast it to TextType if `isText == true`, cast it to BinaryType if `isText == false`.
|
||||
func DataFromFilename(fullPath string) (skip bool, name string, isText bool, mimeId int) {
|
||||
shortPath := strings.TrimPrefix(fullPath, WikiDir)[1:]
|
||||
// Special files start with &
|
||||
// &. is used in normal hypha part names
|
||||
if shortPath[0] == '&' || strings.LastIndex(shortPath, "&.") < 0 {
|
||||
skip = true
|
||||
return
|
||||
}
|
||||
ext := filepath.Ext(shortPath)
|
||||
name = strings.TrimSuffix(shortPath, "&"+ext)
|
||||
isText, mimeId = mimeData(ext)
|
||||
return
|
||||
}
|
||||
|
||||
// mimeData determines what content type file has judging by its `ext`ension. `itText` and `mimeId` are the same as in DataFromFilename.
|
||||
func mimeData(ext string) (isText bool, mimeId int) {
|
||||
switch ext {
|
||||
case ".txt":
|
||||
return true, int(TextPlain)
|
||||
case ".gmi":
|
||||
return true, int(TextGemini)
|
||||
}
|
||||
for i, binExt := range binaryExtensions {
|
||||
if ext == binExt {
|
||||
return false, i
|
||||
}
|
||||
}
|
||||
return false, 0
|
||||
}
|
19
mime_test.go
Normal file
19
mime_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMimeData(t *testing.T) {
|
||||
check := func(ext string, expectedIsText bool, expectedMimeId int) {
|
||||
isText, mimeId := mimeData(ext)
|
||||
if isText != expectedIsText || mimeId != expectedMimeId {
|
||||
t.Error(ext, isText, mimeId)
|
||||
}
|
||||
}
|
||||
check(".txt", true, int(TextPlain))
|
||||
check(".gmi", true, int(TextGemini))
|
||||
check(".bin", false, int(BinaryOctet))
|
||||
check(".jpg", false, int(BinaryJpeg))
|
||||
check(".bin", false, int(BinaryOctet))
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package mycelium
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
)
|
||||
|
||||
var (
|
||||
MainMycelium string
|
||||
SystemMycelium string
|
||||
)
|
||||
|
||||
func gatherDirNames(path string) map[string]struct{} {
|
||||
res := make(map[string]struct{})
|
||||
nodes, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.IsDir() {
|
||||
res[node.Name()] = struct{}{}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Add values to the set. If a value is already there, return false.
|
||||
func addInUniqueSet(set map[string]struct{}, names []string) bool {
|
||||
ok := true
|
||||
for _, name := range names {
|
||||
if _, present := set[name]; present {
|
||||
ok = false
|
||||
}
|
||||
set[name] = struct{}{}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func Init() {
|
||||
var (
|
||||
// Used to check if there are no duplicates
|
||||
foundNames = make(map[string]struct{})
|
||||
dirs = gatherDirNames(cfg.WikiDir)
|
||||
mainPresent bool
|
||||
systemPresent bool
|
||||
)
|
||||
for _, mycelium := range cfg.Mycelia {
|
||||
switch mycelium.Type {
|
||||
case "main":
|
||||
mainPresent = true
|
||||
MainMycelium = mycelium.Names[0]
|
||||
case "system":
|
||||
systemPresent = true
|
||||
SystemMycelium = mycelium.Names[0]
|
||||
}
|
||||
// Check if there is a dir corresponding to the mycelium
|
||||
if _, ok := dirs[mycelium.Names[0]]; !ok {
|
||||
log.Fatal("No directory found for mycelium " + mycelium.Names[0])
|
||||
}
|
||||
// Confirm uniqueness of names
|
||||
if ok := addInUniqueSet(foundNames, mycelium.Names); !ok {
|
||||
log.Fatal("At least one name was used more than once for mycelia")
|
||||
}
|
||||
}
|
||||
if !mainPresent {
|
||||
log.Fatal("No `main` mycelium given in config.json")
|
||||
}
|
||||
if !systemPresent {
|
||||
log.Fatal("No `system` mycelium given in config.json")
|
||||
}
|
||||
log.Println("Mycelial dirs are present")
|
||||
}
|
||||
|
||||
func NameWithMyceliumInMap(m map[string]string) (res string) {
|
||||
var (
|
||||
hyphaName, okH = m["hypha"]
|
||||
mycelName, okM = m["mycelium"]
|
||||
)
|
||||
log.Println(m)
|
||||
if !okH {
|
||||
// It will result in an error when trying to open a hypha with such name
|
||||
return ":::"
|
||||
}
|
||||
if okM {
|
||||
res = canonicalMycelium(mycelName)
|
||||
} else {
|
||||
res = MainMycelium
|
||||
}
|
||||
return res + "/" + hyphaName
|
||||
}
|
||||
|
||||
func canonicalMycelium(name string) string {
|
||||
log.Println("Determining canonical mycelial name for", name)
|
||||
name = strings.ToLower(name)
|
||||
for _, mycel := range cfg.Mycelia {
|
||||
for _, mycelName := range mycel.Names {
|
||||
if mycelName == name {
|
||||
return mycel.Names[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
// This is a nonexistent mycelium. Return a name that will trigger an error
|
||||
return ":error:"
|
||||
}
|
41
name.go
Normal file
41
name.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isCanonicalName checks if the `name` is canonical.
|
||||
func isCanonicalName(name string) bool {
|
||||
return HyphaPattern.MatchString(name)
|
||||
}
|
||||
|
||||
// CanonicalName makes sure the `name` is canonical. A name is canonical if it is lowercase and all spaces are replaced with underscores.
|
||||
func CanonicalName(name string) string {
|
||||
return strings.ToLower(strings.ReplaceAll(name, " ", "_"))
|
||||
}
|
||||
|
||||
// naviTitle turns `canonicalName` into html string with each hypha path parts higlighted as links.
|
||||
func naviTitle(canonicalName string) string {
|
||||
var (
|
||||
html = `<h1 class="navi-title" id="0">
|
||||
<a href="/">🍄</a>`
|
||||
prevAcc = `/page/`
|
||||
parts = strings.Split(canonicalName, "/")
|
||||
)
|
||||
for _, part := range parts {
|
||||
html += fmt.Sprintf(`
|
||||
<span>/</span>
|
||||
<a href="%s">%s</a>`,
|
||||
prevAcc+part,
|
||||
strings.Title(part))
|
||||
prevAcc += part + "/"
|
||||
}
|
||||
return html + "</h1>"
|
||||
}
|
||||
|
||||
// HyphaNameFromRq extracts hypha name from http request. You have to also pass the action which is embedded in the url. For url /page/hypha, the action would be "page".
|
||||
func HyphaNameFromRq(rq *http.Request, action string) string {
|
||||
return CanonicalName(strings.TrimPrefix(rq.URL.Path, "/"+action+"/"))
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package lang
|
||||
|
||||
var EnglishMap = map[string]string{
|
||||
"edit hypha title template": "Edit %s at MycorrhizaWiki",
|
||||
"view hypha title template": "%s at MycorrhizaWiki",
|
||||
"this site runs myco wiki": `<p>This website runs <a href="https://github.com/bouncepaw/mycorrhiza">MycorrhizaWiki</a></p>`,
|
||||
"generic error msg": `<b>Sorry, something went wrong</b>`,
|
||||
|
||||
"edit/text mime type": "Text MIME-type",
|
||||
"edit/text mime type/tip": "We support <code>text/markdown</code>, <code>text/creole</code> and <code>text/gemini</code>",
|
||||
|
||||
"edit/revision comment": "Revision comment",
|
||||
"edit/revision comment/tip": "Please make your comment helpful",
|
||||
"edit/revision comment/new": "Create %s",
|
||||
"edit/revision comment/old": "Update %s",
|
||||
|
||||
"edit/tags": "Edit tags",
|
||||
"edit/tags/tip": "Tags are separated by commas, whitespace is ignored",
|
||||
|
||||
"edit/upload file": "Upload file",
|
||||
"edit/upload file/tip": "Only images are fully supported for now",
|
||||
|
||||
"edit/box": "Edit box",
|
||||
"edit/box/title": "Edit %s",
|
||||
"edit/box/help pattern": "Describe %s here",
|
||||
|
||||
"edit/cancel": "Cancel",
|
||||
|
||||
"update ok/title": "Saved %s",
|
||||
"update ok/msg": "Saved successfully. <a href='/%s'>Go back</a>",
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
"github.com/m4tty/cajun"
|
||||
)
|
||||
|
||||
func CreoleToHtml(creole []byte) string {
|
||||
out, _ := cajun.Transform(string(util.NormalizeEOL(creole)))
|
||||
return out
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
)
|
||||
|
||||
const (
|
||||
linkToken = "=>"
|
||||
headerToken = "#"
|
||||
quoteToken = ">"
|
||||
preformattedToken = "```"
|
||||
listItemToken = "*"
|
||||
)
|
||||
|
||||
var preState bool
|
||||
var listState bool
|
||||
|
||||
func GeminiToHtml(gemini []byte) string {
|
||||
lines, _ := StringToLines(string(util.NormalizeEOL(gemini)))
|
||||
var html []string
|
||||
|
||||
for _, line := range lines {
|
||||
html = append(html, geminiLineToHtml(line))
|
||||
}
|
||||
|
||||
buffer := bytes.Buffer{}
|
||||
for _, line := range html {
|
||||
buffer.WriteString(line)
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func geminiLineToHtml(line string) (res string) {
|
||||
arr := strings.Fields(line)
|
||||
token := checkLineType(arr)
|
||||
|
||||
switch token {
|
||||
case headerToken:
|
||||
level, content := makeOutHeader(arr)
|
||||
res = fmt.Sprintf("<h%v>%v</h%v>", level, content, level)
|
||||
case linkToken:
|
||||
source, content := makeOutLink(arr[1:])
|
||||
res = fmt.Sprintf(`<a href="%v">%v</a>`, source, content)
|
||||
case quoteToken:
|
||||
res = "<blockquote>" + LinesToString(arr[1:], " ") + "</blockquote>"
|
||||
case preformattedToken:
|
||||
preState = true
|
||||
res = fmt.Sprintf(`<pre alt="%v">`, LinesToString(arr[1:], " "))
|
||||
case "pre/empty":
|
||||
res = "\n"
|
||||
case "pre/text":
|
||||
res = line + "\n"
|
||||
case "pre/end":
|
||||
preState = false
|
||||
res = "</pre>"
|
||||
case "list/begin":
|
||||
res = "<ul><li>" + LinesToString(arr[1:], " ") + "</li>"
|
||||
case listItemToken:
|
||||
res = "<li>" + LinesToString(arr[1:], " ") + "</li>"
|
||||
case "list/end":
|
||||
listState = false
|
||||
res = "</ul>" + geminiLineToHtml(line)
|
||||
case "linebreak":
|
||||
res = "<br>"
|
||||
default:
|
||||
res = "<p>" + line + "</p>"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeOutLink(arr []string) (source, content string) {
|
||||
switch len(arr) {
|
||||
case 0:
|
||||
return "", ""
|
||||
case 1:
|
||||
return arr[0], arr[0]
|
||||
default:
|
||||
return arr[0], LinesToString(arr[1:], " ")
|
||||
}
|
||||
}
|
||||
|
||||
func makeOutHeader(arr []string) (level int, content string) {
|
||||
level = len(arr[0])
|
||||
content = LinesToString(arr[1:], " ")
|
||||
return
|
||||
}
|
||||
|
||||
func checkLineType(arr []string) (res string) {
|
||||
isEmpty := len(arr) == 0
|
||||
if preState {
|
||||
if isEmpty {
|
||||
res = "pre/empty"
|
||||
} else if arr[0] == preformattedToken {
|
||||
res = "pre/end"
|
||||
} else {
|
||||
res = "pre/text"
|
||||
}
|
||||
} else if listState {
|
||||
if arr[0] == listItemToken {
|
||||
res = listItemToken
|
||||
} else {
|
||||
res = "list/end"
|
||||
}
|
||||
} else if isEmpty {
|
||||
res = "linebreak"
|
||||
} else if arr[0][0] == headerToken[0] {
|
||||
res = headerToken
|
||||
} else {
|
||||
return arr[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func StringToLines(s string) (lines []string, err error) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(s))
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
err = scanner.Err()
|
||||
return
|
||||
}
|
||||
|
||||
func LinesToString(lines []string, separator string) string {
|
||||
buffer := bytes.Buffer{}
|
||||
for _, line := range lines {
|
||||
buffer.WriteString(line + separator)
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
"gopkg.in/russross/blackfriday.v2"
|
||||
)
|
||||
|
||||
func MarkdownToHtml(md []byte) string {
|
||||
return string(blackfriday.Run(util.NormalizeEOL(md)))
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bouncepaw/mycorrhiza/plugin/parser"
|
||||
)
|
||||
|
||||
func ParserForMime(mime string) func([]byte) string {
|
||||
parsers := map[string]func([]byte) string{
|
||||
"text/markdown": parser.MarkdownToHtml,
|
||||
"text/creole": parser.CreoleToHtml,
|
||||
"text/gemini": parser.GeminiToHtml,
|
||||
}
|
||||
if parserFunc, ok := parsers[mime]; ok {
|
||||
return parserFunc
|
||||
}
|
||||
return func(contents []byte) string {
|
||||
return fmt.Sprintf(`<pre><code>%s</code></pre>`, contents)
|
||||
}
|
||||
}
|
149
render/render.go
149
render/render.go
@ -1,149 +0,0 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"text/template"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
"github.com/bouncepaw/mycorrhiza/fs"
|
||||
"github.com/bouncepaw/mycorrhiza/mycelium"
|
||||
)
|
||||
|
||||
func titleTemplateView(name string) string {
|
||||
return fmt.Sprintf(cfg.Locale["view hypha title template"], name)
|
||||
}
|
||||
|
||||
// HyphaEdit renders hypha editor.
|
||||
func HyphaEdit(h *fs.Hypha) []byte { //
|
||||
hyphaData := map[string]interface{}{
|
||||
"Name": h.FullName,
|
||||
"Tags": h.TagsJoined(),
|
||||
"TextMime": h.TextMime(),
|
||||
"Text": h.TextContent(),
|
||||
"Locale": cfg.Locale,
|
||||
}
|
||||
return layout("edit/index").
|
||||
withMap(hyphaData).
|
||||
wrapInBase(map[string]string{
|
||||
"Title": fmt.Sprintf(cfg.Locale["edit hypha title template"], h.FullName),
|
||||
})
|
||||
}
|
||||
|
||||
// HyphaUpdateOk is used to inform that update was successful.
|
||||
func HyphaUpdateOk(h *fs.Hypha) []byte { //
|
||||
return layout("update_ok").
|
||||
withMap(map[string]interface{}{
|
||||
"Name": h.FullName,
|
||||
"Locale": cfg.Locale,
|
||||
}).
|
||||
Bytes()
|
||||
}
|
||||
|
||||
// Hypha404 renders 404 page for nonexistent page.
|
||||
func Hypha404(name, _ string) []byte {
|
||||
return layout("view/404").
|
||||
withMap(map[string]interface{}{
|
||||
"PageTitle": name,
|
||||
"Tree": hyphaTree(name),
|
||||
"Locale": cfg.Locale,
|
||||
}).
|
||||
wrapInBase(map[string]string{
|
||||
"Title": titleTemplateView(name),
|
||||
})
|
||||
}
|
||||
|
||||
// HyphaPage renders hypha viewer.
|
||||
func HyphaPage(name, content string) []byte {
|
||||
return layout("view/index").
|
||||
withMap(map[string]interface{}{
|
||||
"Content": content,
|
||||
"Tree": hyphaTree(name),
|
||||
"Locale": cfg.Locale,
|
||||
}).
|
||||
wrapInBase(map[string]string{
|
||||
"Title": titleTemplateView(name),
|
||||
})
|
||||
}
|
||||
|
||||
// wrapInBase is used to wrap layouts in things that are present on all pages.
|
||||
func (lyt *Layout) wrapInBase(keys map[string]string) []byte {
|
||||
if lyt.invalid {
|
||||
return lyt.Bytes()
|
||||
}
|
||||
page := map[string]interface{}{
|
||||
"Content": lyt.String(),
|
||||
"Locale": cfg.Locale,
|
||||
"Title": cfg.SiteTitle,
|
||||
"SiteTitle": cfg.SiteTitle,
|
||||
}
|
||||
for key, val := range keys {
|
||||
page[key] = val
|
||||
}
|
||||
return layout("base").withMap(page).Bytes()
|
||||
}
|
||||
|
||||
func hyphaTree(name string) string {
|
||||
return fs.Hs.GetTree(name, true).AsHtml()
|
||||
}
|
||||
|
||||
type Layout struct {
|
||||
tmpl *template.Template
|
||||
buf *bytes.Buffer
|
||||
invalid bool
|
||||
err error
|
||||
}
|
||||
|
||||
func layout(name string) *Layout {
|
||||
lytName := path.Join("theme", cfg.Theme, name+".html")
|
||||
h := fs.Hs.OpenFromMap(map[string]string{
|
||||
"mycelium": mycelium.SystemMycelium,
|
||||
"hypha": lytName,
|
||||
"rev": "0",
|
||||
})
|
||||
if h.Invalid {
|
||||
return &Layout{nil, nil, true, h.Err}
|
||||
}
|
||||
tmpl, err := template.ParseFiles(h.TextPath())
|
||||
if err != nil {
|
||||
return &Layout{nil, nil, true, err}
|
||||
}
|
||||
return &Layout{tmpl, new(bytes.Buffer), false, nil}
|
||||
}
|
||||
|
||||
func (lyt *Layout) withString(data string) *Layout {
|
||||
if lyt.invalid {
|
||||
return lyt
|
||||
}
|
||||
if err := lyt.tmpl.Execute(lyt.buf, data); err != nil {
|
||||
lyt.invalid = true
|
||||
lyt.err = err
|
||||
}
|
||||
return lyt
|
||||
}
|
||||
|
||||
func (lyt *Layout) withMap(data map[string]interface{}) *Layout {
|
||||
if lyt.invalid {
|
||||
return lyt
|
||||
}
|
||||
if err := lyt.tmpl.Execute(lyt.buf, data); err != nil {
|
||||
lyt.invalid = true
|
||||
lyt.err = err
|
||||
}
|
||||
return lyt
|
||||
}
|
||||
|
||||
func (lyt *Layout) Bytes() []byte {
|
||||
if lyt.invalid {
|
||||
return []byte(lyt.err.Error())
|
||||
}
|
||||
return lyt.buf.Bytes()
|
||||
}
|
||||
|
||||
func (lyt *Layout) String() string {
|
||||
if lyt.invalid {
|
||||
return lyt.err.Error()
|
||||
}
|
||||
return lyt.buf.String()
|
||||
}
|
44
util/url.go
44
util/url.go
@ -1,44 +0,0 @@
|
||||
/* This file implements things defined by Wikilink RFC. See :main/help/wikilink
|
||||
*/
|
||||
package util
|
||||
|
||||
import (
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// `name` must be non-empty.
|
||||
func sections(name string) (mycel, hyphaName string) {
|
||||
mycelRe := regexp.MustCompile(`^:.*/`)
|
||||
loc := mycelRe.FindIndex([]byte(name))
|
||||
if loc != nil { // if has mycel
|
||||
mycel = name[:loc[1]]
|
||||
name = name[loc[1]:]
|
||||
}
|
||||
return mycel, name
|
||||
}
|
||||
|
||||
// Wikilink processes `link` as defined by :main/help/wikilink assuming that `atHypha` is current hypha name.
|
||||
func Wikilink(link, atHypha string) string {
|
||||
mycel, hyphaName := sections(atHypha)
|
||||
urlProtocolRe := regexp.MustCompile(`^[a-zA-Z]+:`)
|
||||
switch {
|
||||
case strings.HasPrefix(link, "::"):
|
||||
return "/" + mycel + link[2:]
|
||||
case strings.HasPrefix(link, ":"):
|
||||
return "/" + link
|
||||
case strings.HasPrefix(link, "../") && strings.Count(hyphaName, "/") > 0:
|
||||
return "/" + path.Dir(atHypha) + "/" + link[3:]
|
||||
case strings.HasPrefix(link, "../"):
|
||||
return "/" + mycel + link[3:]
|
||||
case strings.HasPrefix(link, "/"):
|
||||
return "/" + atHypha + link
|
||||
case strings.HasPrefix(link, "./"):
|
||||
return "/" + atHypha + link[1:]
|
||||
case urlProtocolRe.MatchString(link):
|
||||
return link
|
||||
default:
|
||||
return "/" + link
|
||||
}
|
||||
}
|
90
util/util.go
90
util/util.go
@ -1,90 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func addColonPerhaps(name string) string {
|
||||
if strings.HasPrefix(name, ":") {
|
||||
return name
|
||||
}
|
||||
return ":" + name
|
||||
}
|
||||
|
||||
func removeColonPerhaps(name string) string {
|
||||
if strings.HasPrefix(name, ":") {
|
||||
return name[1:]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func UrlToCanonical(name string) string {
|
||||
return removeColonPerhaps(
|
||||
strings.ToLower(strings.ReplaceAll(name, " ", "_")))
|
||||
}
|
||||
|
||||
func DisplayToCanonical(name string) string {
|
||||
return removeColonPerhaps(
|
||||
strings.ToLower(strings.ReplaceAll(name, " ", "_")))
|
||||
}
|
||||
|
||||
func CanonicalToDisplay(name string) (res string) {
|
||||
tmp := strings.Title(name)
|
||||
var afterPoint bool
|
||||
for _, ch := range tmp {
|
||||
if afterPoint {
|
||||
afterPoint = false
|
||||
ch = unicode.ToLower(ch)
|
||||
}
|
||||
switch ch {
|
||||
case '.':
|
||||
afterPoint = true
|
||||
case '_':
|
||||
ch = ' '
|
||||
}
|
||||
res += string(ch)
|
||||
}
|
||||
return addColonPerhaps(res)
|
||||
}
|
||||
|
||||
// NormalizeEOL will convert Windows (CRLF) and Mac (CR) EOLs to UNIX (LF)
|
||||
// Code taken from here: https://github.com/go-gitea/gitea/blob/dc8036dcc680abab52b342d18181a5ee42f40318/modules/util/util.go#L68-L102
|
||||
// Gitea has MIT License
|
||||
//
|
||||
// We use it because md parser does not handle CRLF correctly. I don't know why, but CRLF appears sometimes.
|
||||
func NormalizeEOL(input []byte) []byte {
|
||||
var right, left, pos int
|
||||
if right = bytes.IndexByte(input, '\r'); right == -1 {
|
||||
return input
|
||||
}
|
||||
length := len(input)
|
||||
tmp := make([]byte, length)
|
||||
|
||||
// We know that left < length because otherwise right would be -1 from IndexByte.
|
||||
copy(tmp[pos:pos+right], input[left:left+right])
|
||||
pos += right
|
||||
tmp[pos] = '\n'
|
||||
left += right + 1
|
||||
pos++
|
||||
|
||||
for left < length {
|
||||
if input[left] == '\n' {
|
||||
left++
|
||||
}
|
||||
|
||||
right = bytes.IndexByte(input[left:], '\r')
|
||||
if right == -1 {
|
||||
copy(tmp[pos:], input[left:])
|
||||
pos += length - left
|
||||
break
|
||||
}
|
||||
copy(tmp[pos:pos+right], input[left:left+right])
|
||||
pos += right
|
||||
tmp[pos] = '\n'
|
||||
left += right + 1
|
||||
pos++
|
||||
}
|
||||
return tmp[:pos]
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWikilink(t *testing.T) {
|
||||
atHypha := ":example/test"
|
||||
results := map[string]string{
|
||||
"foo": "/foo",
|
||||
"::foo": "/:example/foo",
|
||||
":bar/foo": "/:bar/foo",
|
||||
"/baz": "/:example/test/baz",
|
||||
"./baz": "/:example/test/baz",
|
||||
"../qux": "/:example/qux",
|
||||
"http://example.org": "http://example.org",
|
||||
"gemini://example.org": "gemini://example.org",
|
||||
"mailto:me@example.org": "mailto:me@example.org",
|
||||
}
|
||||
for link, expect := range results {
|
||||
if res := Wikilink(link, atHypha); expect != res {
|
||||
t.Errorf("%s → %s; expected %s", link, res, expect)
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
This is root wiki directory.
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"address": "0.0.0.0:1737",
|
||||
"theme": "default-light",
|
||||
"site-title": "🍄 MycorrhizaWiki",
|
||||
"home-page": "Home",
|
||||
"binary-limit-mb": 10,
|
||||
"locale": "en",
|
||||
"mycelia": [
|
||||
{
|
||||
"names": ["main"],
|
||||
"type": "main"
|
||||
},
|
||||
{
|
||||
"names": ["sys", "system"],
|
||||
"type": "system"
|
||||
},
|
||||
{
|
||||
"names": ["spec", "special"],
|
||||
"type": "special"
|
||||
},
|
||||
{
|
||||
"names": ["user","u"],
|
||||
"type": "user"
|
||||
},
|
||||
{
|
||||
"names": ["tag", "t"],
|
||||
"type": "tag"
|
||||
}
|
||||
]
|
||||
}
|
BIN
wiki/favicon.ico
BIN
wiki/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 170 KiB |
@ -1 +0,0 @@
|
||||
This is directory with hyphae
|
@ -1 +0,0 @@
|
||||
# Help
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Help",
|
||||
"comment": "Update Help",
|
||||
"author": "",
|
||||
"time": 1593540573,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
},
|
||||
"Invalid": false,
|
||||
"Err": null
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
In MycorrhizaWiki **mycelia** are used to organize [hyphae](hyphae) in related namespaces.
|
||||
|
||||
## Mycelium traits
|
||||
- Every mycelium has any number (0..∞) of hyphae.
|
||||
- Every hypha is part of one mycelium.
|
||||
- Every mycelium has one canonical name and any number of synonyms.
|
||||
- Every wiki has one main mycelium.
|
||||
- They are named by the same scheme as hyphae but they have a colon prefix.
|
||||
|
||||
## Mycelium URL
|
||||
- Address a hypha in a particular mycelium: `/:sys/about`.
|
||||
- Address subpage of the hypha above: `/:sys/about/more`
|
||||
- Address a hypha in the main mycelium: `/How/does it work`.
|
||||
- Address a hypha in the main mycelium explicitly: `/:main/How/does it work`.
|
||||
|
||||
## Mycelium configuration
|
||||
In your `config.json`, in `"mycelia"` field there is an array of objects.
|
||||
|
||||
```
|
||||
{
|
||||
...
|
||||
"mycelia": [
|
||||
{
|
||||
"names": ["main"],
|
||||
"type": "main"
|
||||
},
|
||||
{
|
||||
"names": ["sys", "system"],
|
||||
"type": "system"
|
||||
},
|
||||
{
|
||||
"names": ["spec", "special"],
|
||||
"type": "special"
|
||||
},
|
||||
{
|
||||
"names": ["user","u"],
|
||||
"type": "user"
|
||||
},
|
||||
{
|
||||
"names": ["tag", "t"],
|
||||
"type": "tag"
|
||||
}
|
||||
]
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Each object reprents a mycelium. You can set all their names there. First name in each `"names"` array is a canonical name for the mycelium.
|
||||
|
||||
Field `"type"` sets the mycelium's type. There are such types:
|
||||
|
||||
| **Type** | **Description** |
|
||||
| `main` | The main mycelium. There must be exactly one such mycelium in a wiki. |
|
||||
| `system` | Things like scripts, styles and templates go here. There must be exactly one such mycelium in a wiki. |
|
||||
| `special` | Things like utility hyphae and plugin pages go here. It is optional because there are no hyphae or plugins now. |
|
||||
| `user` | Userpages. It is optional because there are no users now. |
|
||||
| `tag` | Pages describing tags. It is optional because there are no tags now. |
|
||||
| `other` | Mycelia without any additional meaning added by the engine. There can be any number of them. |
|
||||
|
||||
## How are they stored in the filesystem.
|
||||
For example, `wiki` is your wiki directory and you have configured the mycelia like in the example above. You should have structure like that:
|
||||
|
||||
```
|
||||
wiki/
|
||||
config.json ← your configuration
|
||||
favicon.ico ← your site icon
|
||||
main/ ← :main, <empty prefix>
|
||||
...most of content goes here
|
||||
sys/ ← :sys
|
||||
...themes go here
|
||||
spec/ ← :spec
|
||||
...something goes here
|
||||
user/ ← :user, :u
|
||||
...user pages go here
|
||||
tag/ ← :tag, :t
|
||||
...pages describing tags go here
|
||||
```
|
||||
|
||||
There are usual hypha directories inside those mycelial directories.
|
||||
|
||||
## Code
|
||||
- Things related to reading the `config.json` go to the `cfg` module.
|
||||
- Most of code related to mycelia is in the `fs` module.
|
||||
- And also check out `handlers.go` and `main.go` for routing of mycelia.
|
||||
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Mycelia",
|
||||
"comment": "Update Help/Mycelia",
|
||||
"author": "",
|
||||
"time": 1593541268,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
},
|
||||
"Invalid": false,
|
||||
"Err": null
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
# Plugin system RFC
|
||||
MycorrhizaWiki engine does not provide all the functionality a wiki may need and not need. Instead, it relies on the system of plugins.
|
||||
|
||||
## Types of plugins
|
||||
- **Parser.** They add support for displaying different MIME-types.
|
||||
- **Utilities.** They add hyphae to the `:spec` mycelium. These hyphae provide administrative functionality.
|
||||
- **Macros.** Something like [moinmoin ones](http://moinmo.in/HelpOnMacros), I guess.
|
||||
|
||||
## Default plugins
|
||||
Default MycorrhizaWiki distributive is shipped with several plugins installed.
|
||||
|
||||
- **parser/markdown.** Support for `text/markdown`.
|
||||
- **parser/gemini.** Support for `text/gemini`.
|
||||
- *what other markups to ship? I don't want to have markdown as the main one. Textile? ReST? Asciidoc is too complex, so let's not choose it.*
|
||||
- **utility/rename.** Renaming of non-user hyphae.
|
||||
- *what else?*
|
||||
- **macro/toc.** Table of contents.
|
||||
- *what else?*
|
||||
|
||||
## Plugin implementation
|
||||
All plugins are written in Go and are compiled together with MycorrhizaWiki. If a wiki's admin decides to add a plugin, they shall recompile the engine with the plugin.
|
||||
|
||||
> Reminds of [something](http://suckless.org/), right?
|
||||
|
||||
*But compiling the engine just to add a plugin is stupid!!* Not really. Also, it makes the architecture more simple and secure.
|
||||
|
||||
*What if an admin doesn't know how to program?* Plugin installation is basically limited to putting some files into a folder, editing the config and running a shell command. No programming required to install a plugin.
|
||||
|
||||
|
@ -1,30 +0,0 @@
|
||||
# Plugin system RFC
|
||||
MycorrhizaWiki engine does not provide all the functionality a wiki may need and not need. Instead, it relies on the system of plugins.
|
||||
|
||||
## Types of plugins
|
||||
- **Parser.** They add support for displaying different MIME-types.
|
||||
- **Utilities.** They add hyphae to the `:spec` mycelium. These hyphae provide administrative functionality.
|
||||
- **Macros.** Something like [moinmoin ones](http://moinmo.in/HelpOnMacros), I guess.
|
||||
|
||||
## Default plugins
|
||||
Default MycorrhizaWiki distributive is shipped with several plugins installed.
|
||||
|
||||
- **parser/markdown.** Support for `text/markdown`. This parser is powered by [russross/blackfriday](https://github.com/russross/blackfriday); this parser is ok, I guess.
|
||||
- **parser/creole.** Support for `text/creole`. *Note:* there is no standard Creole MIME type. This parser is powered by [m4tty/cajun](https://github.com/m4tty/cajun); this library is somewhat outdated. Perhaps we'll reimplement it.
|
||||
- **parser/gemini.** Support for `text/gemini`. *Not implemented yet.*
|
||||
- *what about shipping BBcode? lol*
|
||||
- **utility/rename.** Renaming of non-user hyphae. *Not implemented yet.*
|
||||
- *what else?*
|
||||
- **macro/toc.** Table of contents. *Not implemented yet.*
|
||||
- *what else?*
|
||||
|
||||
## Plugin implementation
|
||||
All plugins are written in Go and are compiled together with MycorrhizaWiki. If a wiki's admin decides to add a plugin, they shall recompile the engine with the plugin.
|
||||
|
||||
> Reminds of [something](http://suckless.org/), right?
|
||||
|
||||
*But compiling the engine just to add a plugin is stupid!!* Not really. Also, it makes the architecture more simple and secure.
|
||||
|
||||
*What if an admin doesn't know how to program?* Plugin installation is basically limited to putting some files into a folder, editing the config and running a shell command. No programming required to install a plugin.
|
||||
|
||||
See `plugin` directory at the root of the repo to get inspired by the present parsers.
|
@ -1,27 +0,0 @@
|
||||
# Plugin system RFC
|
||||
MycorrhizaWiki engine does not provide all the functionality a wiki may need and not need. Instead, it relies on the system of plugins.
|
||||
|
||||
This document is up-to-date.
|
||||
|
||||
## Types of plugins
|
||||
- **Parser.** They add support for displaying different MIME-types.
|
||||
- **Language.** They provide i18n support.
|
||||
|
||||
## Default plugins
|
||||
Default MycorrhizaWiki distributive is shipped with several plugins installed.
|
||||
|
||||
- **parser/markdown.** Support for `text/markdown`. This parser is powered by [russross/blackfriday](https://github.com/russross/blackfriday); this parser is ok, I guess.
|
||||
- **parser/creole.** Support for `text/creole`. *Note:* there is no standard Creole MIME type. This parser is powered by [m4tty/cajun](https://github.com/m4tty/cajun); this library is somewhat outdated. Perhaps we'll reimplement it.
|
||||
- **parser/gemini.** Support for `text/gemini`.
|
||||
- **language/en.** Support for English language.
|
||||
|
||||
## Plugin implementation
|
||||
All plugins are written in Go and are compiled together with MycorrhizaWiki. If a wiki's admin decides to add a plugin, they shall recompile the engine with the plugin.
|
||||
|
||||
> Reminds of [something](http://suckless.org/), right?
|
||||
|
||||
*But compiling the engine just to add a plugin is stupid!!* Not really. Also, it makes the architecture more simple and secure.
|
||||
|
||||
*What if an admin doesn't know how to program?* Plugin installation is basically limited to putting some files into a folder, editing the config and running a shell command. No programming required to install a plugin.
|
||||
|
||||
See `plugin` directory at the root of the repo to get inspired by the present parsers.
|
@ -1,27 +0,0 @@
|
||||
# Plugin system RFC
|
||||
MycorrhizaWiki engine does not provide all the functionality a wiki may need and not need. Instead, it relies on the system of plugins.
|
||||
|
||||
This document is up-to-date.
|
||||
|
||||
## Types of plugins
|
||||
- **Parser.** They add support for displaying different MIME-types.
|
||||
- **Language.** They provide i18n support.
|
||||
|
||||
## Default plugins
|
||||
Default MycorrhizaWiki distributive is shipped with several plugins installed.
|
||||
|
||||
- **parser/markdown.** Support for `text/markdown`. This parser is powered by [russross/blackfriday](https://github.com/russross/blackfriday); this parser is ok, I guess.
|
||||
- **parser/creole.** Support for `text/creole`. *Note:* there is no standard Creole MIME type. This parser is powered by [m4tty/cajun](https://github.com/m4tty/cajun); this library is somewhat outdated. Perhaps we'll reimplement it.
|
||||
- **parser/gemini.** Support for `text/gemini`.
|
||||
- **lang/en.** Support for English language.
|
||||
|
||||
## Plugin implementation
|
||||
All plugins are written in Go and are compiled together with MycorrhizaWiki. If a wiki's admin decides to add a plugin, they shall recompile the engine with the plugin.
|
||||
|
||||
> Reminds of [something](http://suckless.org/), right?
|
||||
|
||||
*But compiling the engine just to add a plugin is stupid!!* Not really. Also, it makes the architecture more simple and secure.
|
||||
|
||||
*What if an admin doesn't know how to program?* Plugin installation is basically limited to putting some files into a folder, editing the config and running a shell command. No programming required to install a plugin.
|
||||
|
||||
See `plugin` directory at the root of the repo to get inspired by the present parsers.
|
@ -1,32 +0,0 @@
|
||||
# Plugin system RFC
|
||||
MycorrhizaWiki engine does not provide all the functionality a wiki may need and not need. Instead, it relies on the system of plugins.
|
||||
|
||||
This document is up-to-date.
|
||||
|
||||
## Types of plugins
|
||||
- **Parser.** They add support for displaying different MIME-types.
|
||||
- **Language.** They provide i18n support.
|
||||
|
||||
## Default plugins
|
||||
Default MycorrhizaWiki distributive is shipped with several plugins installed.
|
||||
|
||||
- **parser/markdown.** Support for `text/markdown`. This parser is powered by [russross/blackfriday](https://github.com/russross/blackfriday); this parser is ok, I guess.
|
||||
- **parser/creole.** Support for `text/creole`. *Note:* there is no standard Creole MIME type. This parser is powered by [m4tty/cajun](https://github.com/m4tty/cajun); this library is somewhat outdated. Perhaps we'll reimplement it.
|
||||
- **parser/gemini.** Support for `text/gemini`.
|
||||
- **lang/en.** Support for English language.
|
||||
|
||||
## Plugin implementation
|
||||
All plugins are written in Go and are compiled together with MycorrhizaWiki. If a wiki's admin decides to add a plugin, they shall recompile the engine with the plugin.
|
||||
|
||||
> Reminds of [something](http://suckless.org/), right?
|
||||
|
||||
*But compiling the engine just to add a plugin is stupid!!* Not really. Also, it makes the architecture more simple and secure.
|
||||
|
||||
*What if an admin doesn't know how to program?* Plugin installation is basically limited to putting some files into a folder, editing the config and running a shell command. No programming required to install a plugin.
|
||||
|
||||
See `plugin` directory at the root of the repo to get inspired by the present parsers.
|
||||
|
||||
## Possible future plugins
|
||||
- **Utilites.** Plugins that can do different things and provide their own hypha interface in `:spec` mycelium.
|
||||
- **Macros.** Dynamic content parts that are generated when page is accessed: TOC, meta information accessors, etc.
|
||||
|
@ -1,32 +0,0 @@
|
||||
# Plugin system RFC
|
||||
MycorrhizaWiki engine does not provide all the functionality a wiki may need and not need. Instead, it relies on the system of plugins.
|
||||
|
||||
This document is up-to-date.
|
||||
|
||||
## Types of plugins
|
||||
- **Parser.** They add support for displaying different MIME-types.
|
||||
- **Language.** They provide i18n support.
|
||||
|
||||
## Default plugins
|
||||
Default MycorrhizaWiki distributive is shipped with several plugins installed.
|
||||
|
||||
- **parser/markdown.** Support for `text/markdown`. This parser is powered by [russross/blackfriday](https://github.com/russross/blackfriday); this parser is ok, I guess.
|
||||
- **parser/creole.** Support for `text/creole`. *Note:* there is no standard Creole MIME type. This parser is powered by [m4tty/cajun](https://github.com/m4tty/cajun); this library is somewhat outdated. Perhaps we'll reimplement it.
|
||||
- **parser/gemini.** Support for `text/gemini`.
|
||||
- **lang/en.** Support for English language.
|
||||
|
||||
## Plugin implementation
|
||||
All plugins are written in Go and are compiled together with MycorrhizaWiki. If a wiki's admin decides to add a plugin, they shall recompile the engine with the plugin.
|
||||
|
||||
> Reminds of [something](http://suckless.org/), right?
|
||||
|
||||
*But compiling the engine just to add a plugin is stupid!!* Not really. Also, it makes the architecture more simple and secure.
|
||||
|
||||
*What if an admin doesn't know how to program?* Plugin installation is basically limited to putting some files into a folder, editing the config and running a shell command. No programming required to install a plugin.
|
||||
|
||||
See `plugin` directory at the root of the repo to get inspired by the present parsers.
|
||||
|
||||
## Possible future plugins
|
||||
- **Utilites.** Plugins that can do different things and provide their own hypha interface in `:spec` mycelium.
|
||||
- **Macros.** Dynamic content parts that are generated when page is accessed: TOC, meta information accessors, etc.
|
||||
|
@ -1,84 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Plugin",
|
||||
"comment": "Update :Main/Help/Plugin",
|
||||
"author": "",
|
||||
"time": 1593806875,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"2": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Plugin",
|
||||
"comment": "Update :Main/Doc/Plugin",
|
||||
"author": "",
|
||||
"time": 1593882606,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "2.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"3": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Plugin",
|
||||
"comment": "Update :Main/Doc/Plugin",
|
||||
"author": "",
|
||||
"time": 1594999571,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "3.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"4": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Plugin",
|
||||
"comment": "Update :Main/Doc/Plugin",
|
||||
"author": "",
|
||||
"time": 1595001530,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "4.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"5": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Plugin",
|
||||
"comment": "Update :Main/Doc/Plugin",
|
||||
"author": "",
|
||||
"time": 1595092102,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "5.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"6": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Plugin",
|
||||
"comment": "Update :Main/Doc/Plugin",
|
||||
"author": "",
|
||||
"time": 1595092219,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "6.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
Unlike earliest wiki engines, MycorrhizaWiki supports authorization system and non-authorized edits are disabled by default. Anyone can get an account on a MycorrhizaWiki wiki by signing up. There must be a form for that.
|
||||
|
||||
* Each user is part of one or more user groups.
|
||||
* Each user has a username. Usernames follow the same naming conventions as hyphae but additionally the forward slash / character is prohibited in usernames. Please note that usernames are case-insensitive, thus //shroom// and //Shroom// mean the same; spaces and underscores are also the same, //amanita muscaria// = //amanita_muscaria//.
|
||||
* Password chosen by the user is not directly stored on the server. Only its hash is salted so even if a server is hacked, a hacker won't get the passwords. There are no restrictions on passwords. Server administrator doesn't have access to the password as well.
|
||||
* Each user gets their own hypha in the //:user// mycelium. The hypha has the same name as the user. Subhyphae can also be added.
|
||||
* If an authenticated user makes an edit, the fact that they have made the edit is stored in the revision history.
|
@ -1,9 +0,0 @@
|
||||
This document is not up-to-date. Information here will be true once we finish its development :)
|
||||
|
||||
Unlike earliest wiki engines, MycorrhizaWiki supports authorization system and non-authorized edits are disabled by default. Anyone can get an account on a MycorrhizaWiki wiki by signing up. There must be a form for that.
|
||||
|
||||
* Each user is part of one or more user groups.
|
||||
* Each user has a username. Usernames follow the same naming conventions as hyphae but additionally the forward slash / character is prohibited in usernames. Please note that usernames are case-insensitive, thus //shroom// and //Shroom// mean the same; spaces and underscores are also the same, //amanita muscaria// = //amanita_muscaria//.
|
||||
* Password chosen by the user is not directly stored on the server. Only its hash is salted so even if a server is hacked, a hacker won't get the passwords. There are no restrictions on passwords. Server administrator doesn't have access to the password as well.
|
||||
* Each user gets their own hypha in the //:user// mycelium. The hypha has the same name as the user. Subhyphae can also be added.
|
||||
* If an authenticated user makes an edit, the fact that they have made the edit is stored in the revision history.
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "User",
|
||||
"comment": "Update :Main/Doc/User",
|
||||
"author": "",
|
||||
"time": 1594749111,
|
||||
"text_mime": "text/creole",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.txt",
|
||||
"binary_name": ""
|
||||
},
|
||||
"2": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "User",
|
||||
"comment": "Update :Main/Doc/User",
|
||||
"author": "",
|
||||
"time": 1595092543,
|
||||
"text_mime": "text/creole",
|
||||
"binary_mime": "",
|
||||
"text_name": "2.txt",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
# Wikilink RFC
|
||||
|
||||
All parsers for MycorrhizaWiki provide hyperlink support. Usually, they follow HTML convention.
|
||||
|
||||
- `http://example.org/absolute-path`
|
||||
- `/rooted-path`
|
||||
- `same-folder-path`
|
||||
- `../parent-folder-path`
|
||||
|
||||
This is not really convenient for wikis where most of links are either rooted or links to children!
|
||||
|
||||
All parsers of MycorrhizaWiki are expected to support these types of links and convert them to rooted paths.
|
||||
|
||||
- `http://example.org/absolute-path`
|
||||
- `hypha in main mycelium`
|
||||
- `::hypha in the same mycelium`
|
||||
- `:mycelium/hypha in an explicit mycelium`
|
||||
- `/subhypha`
|
||||
- `./subhypha`
|
||||
- `../sibling-hypha`
|
||||
|
||||
**TODO:** create a package that implements this thing. NB: to generate a correct link, it is required to know full name of hypha where the link is used.
|
||||
|
||||
## Markdown extension
|
||||
|
||||
> This is an extension to markdown's syntax that is used in MycorrhizaWiki and nowhere else.
|
||||
|
||||
Text wrapped in `[[` and `]]` is a link that has same text and url. *For some reason it's not possible in Markdown without duplicating url*
|
||||
|
||||
```
|
||||
[[x]] == [x](x)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
All examples assume that `:example/test` is the current hypha.
|
||||
|
||||
```
|
||||
wikilink actual path
|
||||
foo == /foo
|
||||
::foo == /:example/foo
|
||||
:bar/foo == /:bar/foo
|
||||
/baz == /:example/test/baz
|
||||
./baz == /:example/test/baz
|
||||
../qux == /:example/qux
|
||||
```
|
@ -1,49 +0,0 @@
|
||||
# Wikilink RFC
|
||||
|
||||
All parsers for MycorrhizaWiki provide hyperlink support. Usually, they follow HTML convention.
|
||||
|
||||
- `http://example.org/absolute-path`
|
||||
- `/rooted-path`
|
||||
- `same-folder-path`
|
||||
- `../parent-folder-path`
|
||||
|
||||
This is not really convenient for wikis where most of links are either rooted or links to children!
|
||||
|
||||
All parsers of MycorrhizaWiki are expected to support these types of links and convert them to rooted paths.
|
||||
|
||||
- `http://example.org/absolute-path`
|
||||
- `hypha in main mycelium`
|
||||
- `::hypha in the same mycelium`
|
||||
- `:mycelium/hypha in an explicit mycelium`
|
||||
- `/subhypha`
|
||||
- `./subhypha`
|
||||
- `../sibling-hypha`
|
||||
|
||||
**TODO:** create a package that implements this thing. NB: to generate a correct link, it is required to know full name of hypha where the link is used.
|
||||
|
||||
## Markdown extension
|
||||
|
||||
> This is an extension to markdown's syntax that is used in MycorrhizaWiki and nowhere else.
|
||||
|
||||
Text wrapped in `[[` and `]]` is a link that has same text and url. *For some reason it's not possible in Markdown without duplicating url*
|
||||
|
||||
```
|
||||
[[x]] == [x](x)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
All examples assume that `:example/test` is the current hypha.
|
||||
|
||||
```
|
||||
wikilink actual path
|
||||
foo == /foo
|
||||
::foo == /:example/foo
|
||||
:bar/foo == /:bar/foo
|
||||
/baz == /:example/test/baz
|
||||
./baz == /:example/test/baz
|
||||
../qux == /:example/qux
|
||||
http://example.org == http://example.org
|
||||
gemini://example.org == gemini://example.org
|
||||
mailto:me@example.org == mailto:me@example.org
|
||||
```
|
@ -1,51 +0,0 @@
|
||||
# Wikilink RFC
|
||||
|
||||
*This page is not up-to-date. One day, features defined here shall be implemented.*
|
||||
|
||||
All parsers for MycorrhizaWiki provide hyperlink support. Usually, they follow HTML convention.
|
||||
|
||||
- `http://example.org/absolute-path`
|
||||
- `/rooted-path`
|
||||
- `same-folder-path`
|
||||
- `../parent-folder-path`
|
||||
|
||||
This is not really convenient for wikis where most of links are either rooted or links to children!
|
||||
|
||||
All parsers of MycorrhizaWiki are expected to support these types of links and convert them to rooted paths.
|
||||
|
||||
- `http://example.org/absolute-path`
|
||||
- `hypha in main mycelium`
|
||||
- `::hypha in the same mycelium`
|
||||
- `:mycelium/hypha in an explicit mycelium`
|
||||
- `/subhypha`
|
||||
- `./subhypha`
|
||||
- `../sibling-hypha`
|
||||
|
||||
**TODO:** create a package that implements this thing. NB: to generate a correct link, it is required to know full name of hypha where the link is used.
|
||||
|
||||
## Markdown extension
|
||||
|
||||
> This is an extension to markdown's syntax that is used in MycorrhizaWiki and nowhere else.
|
||||
|
||||
Text wrapped in `[[` and `]]` is a link that has same text and url. *For some reason it's not possible in Markdown without duplicating url*
|
||||
|
||||
```
|
||||
[[x]] == [x](x)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
All examples assume that `:example/test` is the current hypha.
|
||||
|
||||
```
|
||||
wikilink actual path
|
||||
foo == /foo
|
||||
::foo == /:example/foo
|
||||
:bar/foo == /:bar/foo
|
||||
/baz == /:example/test/baz
|
||||
./baz == /:example/test/baz
|
||||
../qux == /:example/qux
|
||||
http://example.org == http://example.org
|
||||
gemini://example.org == gemini://example.org
|
||||
mailto:me@example.org == mailto:me@example.org
|
||||
```
|
@ -1,45 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Wikilink",
|
||||
"comment": "Update :Main/Help/Wikilink",
|
||||
"author": "",
|
||||
"time": 1593807908,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"2": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Wikilink",
|
||||
"comment": "Update :Main/Doc/Wikilink",
|
||||
"author": "",
|
||||
"time": 1593875778,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "2.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"3": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Wikilink",
|
||||
"comment": "Update :Main/Doc/Wikilink",
|
||||
"author": "",
|
||||
"time": 1595092265,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "3.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
Fruit is a type of fetus. Is tasty so cool coo l ha i love fwriotwsn
|
@ -1,2 +0,0 @@
|
||||
Many people ask the question: what is fruit exactly?
|
||||
Fruit is a type of fetus. Is tasty so cool coo l ha i love fwriotwsn
|
@ -1,4 +0,0 @@
|
||||
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,5 +0,0 @@
|
||||
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,10 +0,0 @@
|
||||
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](Fruit/Apple)
|
||||
- [Pear](Fruit/Pear)
|
||||
|
||||
```
|
||||
фрукты полезны для здоровья!!
|
||||
```
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 43 KiB |
@ -1,3 +0,0 @@
|
||||
A honeycrisp apple from an organic food farm co-op.
|
||||
|
||||
Source: https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D0%B9%D0%BB:Honeycrisp-Apple.jpg
|
Binary file not shown.
Before Width: | Height: | Size: 34 KiB |
@ -1,3 +0,0 @@
|
||||
Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности.
|
||||
|
||||
Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg
|
@ -1,6 +0,0 @@
|
||||
Mycorrhiza is pure happiness
|
||||
|
||||
Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности.
|
||||
|
||||
Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 427 KiB |
@ -1,2 +0,0 @@
|
||||
Я́блоко — сочный плод яблони, который употребляется в пищу в свежем виде, служит сырьём в кулинарии и для приготовления напитков. Наибольшее распространение получила яблоня домашняя, реже выращивают яблоню сливолистную. Размер красных, зелёных или жёлтых шаровидных плодов 5—13 см в диаметре. Происходит из Центральной Азии, где до сих пор произрастает дикорастущий предок яблони домашней — яблоня Сиверса. На сегодняшний день существует множество сортов этого вида яблони, произрастающих в различных климатических условиях. По времени созревания отличают летние, осенние и зимние сорта, более поздние сорта отличаются хорошей стойкостью.
|
||||
|
@ -1,52 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
"img"
|
||||
],
|
||||
"name": "Apple",
|
||||
"comment": "add apple pic hehehe",
|
||||
"author": "bouncepaw",
|
||||
"time": 1591639464,
|
||||
"text_mime": "text/plain",
|
||||
"binary_mime": "image/jpeg",
|
||||
"text_name": "1.txt",
|
||||
"binary_name": "1.jpg"
|
||||
},
|
||||
"2": {
|
||||
"tags": null,
|
||||
"name": "Apple",
|
||||
"comment": "Update Fruit/Apple",
|
||||
"author": "",
|
||||
"time": 1592570366,
|
||||
"text_mime": "text/plain",
|
||||
"binary_mime": "image/jpeg",
|
||||
"text_name": "2.txt",
|
||||
"binary_name": "2.jpg"
|
||||
},
|
||||
"3": {
|
||||
"tags": null,
|
||||
"name": "Apple",
|
||||
"comment": "Test fs dumping",
|
||||
"author": "",
|
||||
"time": 1592570926,
|
||||
"text_mime": "text/plain",
|
||||
"binary_mime": "image/jpeg",
|
||||
"text_name": "3.txt",
|
||||
"binary_name": "2.jpg"
|
||||
},
|
||||
"4": {
|
||||
"tags": null,
|
||||
"name": "Apple",
|
||||
"comment": "copypaste from wikipedia",
|
||||
"author": "",
|
||||
"time": 1592663126,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "image/jpeg",
|
||||
"text_name": "4.txt",
|
||||
"binary_name": "4.jpg"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
"fetus",
|
||||
"tasty"
|
||||
],
|
||||
"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": ""
|
||||
},
|
||||
"4": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Fruit",
|
||||
"comment": "Update Fruit",
|
||||
"author": "",
|
||||
"time": 1593338963,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "4.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"5": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Fruit",
|
||||
"comment": "Update Fruit",
|
||||
"author": "",
|
||||
"time": 1593339050,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "5.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
},
|
||||
"Invalid": false,
|
||||
"Err": null
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
A **pear** is a sweet fruit, usually with a green skin and a lot of juice, that has a round base and is slightly pointed towards the stem.
|
||||
|
||||
Source of info: [cambridge dict](https://dictionary.cambridge.org/ru/словарь/английский/pear)
|
@ -1,5 +0,0 @@
|
||||
A **pear** is a sweet fruit, usually with a green skin and a lot of juice, that has a round base and is slightly pointed towards the stem.
|
||||
|
||||
Source of info: [cambridge dict](https://dictionary.cambridge.org/ru/словарь/английский/pear)
|
||||
|
||||
По-русски: груша
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Pear",
|
||||
"comment": "Create Fruit/Pear",
|
||||
"author": "",
|
||||
"time": 1592834186,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"2": {
|
||||
"tags": null,
|
||||
"name": "Pear",
|
||||
"comment": "Update :Main/Fruit/Pear",
|
||||
"author": "",
|
||||
"time": 1593949655,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "2.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
<b>Pineapple is apple from a pine</b>
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"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 +0,0 @@
|
||||
A **fungus** is any member of the group of eukaryotic organisms that includes microorganisms such as yeasts and molds, as well as the more familiar mushrooms. These organisms are classified as a kingdom, which is separate from the other eukaryotic life kingdoms of plants and animals.
|
@ -1,3 +0,0 @@
|
||||
A **fungus** is any member of the group of eukaryotic organisms that includes microorganisms such as yeasts and molds, as well as the more familiar mushrooms. These organisms are classified as a kingdom, which is separate from the other eukaryotic life kingdoms of plants and animals.
|
||||
|
||||
По-русски: грибы.
|
@ -1,3 +0,0 @@
|
||||
Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees.
|
||||
|
||||
It is not an [](Apple)!
|
@ -1,4 +0,0 @@
|
||||
Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees.
|
||||
|
||||
It is not an [Apple](Apple)!
|
||||
|
@ -1,5 +0,0 @@
|
||||
Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees.
|
||||
|
||||
It is not an [Apple](/Apple)!
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
Amanita muscaria, commonly known as the fly agaric or fly amanita, is a basidiomycete of the genus Amanita. It is also a muscimol mushroom. Native throughout the temperate and boreal regions of the Northern Hemisphere, Amanita muscaria has been unintentionally introduced to many countries in the Southern Hemisphere, generally as a symbiont with pine and birch plantations, and is now a true cosmopolitan species. It associates with various deciduous and coniferous trees.
|
||||
|
||||
It is not an [Apple](/Fruit/Apple)!
|
||||
|
||||
|
||||
|
@ -1,58 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Amanita Muscaria",
|
||||
"comment": "Update Fungus/Amanita Muscaria",
|
||||
"author": "",
|
||||
"time": 1592836062,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"2": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Amanita Muscaria",
|
||||
"comment": "Update Fungus/Amanita Muscaria",
|
||||
"author": "",
|
||||
"time": 1592836073,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "2.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"3": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Amanita Muscaria",
|
||||
"comment": "Update Fungus/Amanita Muscaria",
|
||||
"author": "",
|
||||
"time": 1592836084,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "3.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"4": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Amanita Muscaria",
|
||||
"comment": "Update Fungus/Amanita Muscaria",
|
||||
"author": "",
|
||||
"time": 1592836099,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "4.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 608 KiB |
@ -1,7 +0,0 @@
|
||||
Шляпка 7—16(20) см в диаметре, полушаровидная, раскрывающаяся до выпуклой и почти плоской со слабо вдавленным центром, с радиально разлинованным краем. Окраска тёмно-умброво-коричневая, оливково-охристая, охристо-коричневая, иногда серо-жёлтая, в центре более интенсивная. Общее покрывало на молодых грибах опушённое, ярко-жёлтое, затем остаётся в виде легко смываемых обрывков, на солнце белеющих, а к старости иногда становящихся серо-жёлтыми.
|
||||
|
||||
|
||||
Пластинки частые, сначала узко-приросшие к ножке, затем свободные от неё, кремовые, с многочисленными пластиночками разной длины.
|
||||
|
||||
|
||||
Ножка достигает 9—20 см в высоту и 1—2,5 см в поперечнике, утончающаяся кверху, в основании с яйцевидным или шаровидным утолщением. Поверхность ножки волокнисто-бархатистая, белая или беловатая, при прикосновении иногда слабо буреющая. Кольцо в верхней части ножки, беловатое, перепончатое, не разлинованное. Остатки общего покрывала в виде нескольких поясков желтоватых бородавчатых хлопьев на утолщении ножки.
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Amanita Regalis",
|
||||
"comment": "Update Fungus/Amanita Regalis",
|
||||
"author": "",
|
||||
"time": 1592838045,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "image/jpeg",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": "1.jpeg"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Fungus",
|
||||
"comment": "Update Fungus",
|
||||
"author": "",
|
||||
"time": 1592835944,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
},
|
||||
"2": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Fungus",
|
||||
"comment": "Update :Main/Fungus",
|
||||
"author": "",
|
||||
"time": 1593951453,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "2.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
This is a testing page to test that editing works, etc
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Test",
|
||||
"comment": "Update :Main/Test",
|
||||
"author": "",
|
||||
"time": 1593943985,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
//This test was taken from http://www.wikicreole.org/wiki/JSPWikiTestCases?skin=raw//
|
||||
|
||||
The following is a test of WikiCreole:
|
||||
|
||||
= Top-level heading (1)
|
||||
== This a test for creole 0.1 (2)
|
||||
=== This is a Subheading (3)
|
||||
==== Subsub (4)
|
||||
===== Subsubsub (5)
|
||||
|
||||
The ending equal signs should not be displayed:
|
||||
|
||||
= Top-level heading (1) =
|
||||
== This a test for creole 0.1 (2) ==
|
||||
=== This is a Subheading (3) ===
|
||||
==== Subsub (4) ====
|
||||
===== Subsubsub (5) =====
|
||||
|
||||
|
||||
You can make things **bold** or //italic// or **//both//** or //**both**//.
|
||||
|
||||
Character formatting extends across line breaks: **bold,
|
||||
this is still bold. This line deliberately does not end in star-star.
|
||||
|
||||
Not bold. Character formatting does not cross paragraph boundaries.
|
||||
|
||||
You can use [[internal links]] or [[http://www.wikicreole.org|external links]],
|
||||
give the link a [[internal links|different]] name.
|
||||
|
||||
Here's another sentence: This wisdom is taken from [[Ward Cunningham's]]
|
||||
[[http://www.c2.com/doc/wikisym/WikiSym2006.pdf|Presentation at the Wikisym 06]].
|
||||
|
||||
Here's a external link without a description: [[http://www.wikicreole.org]]
|
||||
|
||||
Free links without braces should be rendered as well, like http://www.wikicreole.org/ and http://www.wikicreole.org/users/~example.
|
||||
|
||||
Creole1.0 specifies that http://bar and ftp://bar should not render italic,
|
||||
something like foo://bar should render as italic.
|
||||
|
||||
You can use this to draw a line to separate the page:
|
||||
----
|
||||
|
||||
You can use lists, start it at the first column for now, please...
|
||||
|
||||
unnumbered lists are like
|
||||
* item a
|
||||
* item b
|
||||
* **bold item c**
|
||||
|
||||
blank space is also permitted before lists like:
|
||||
* item a
|
||||
* item b
|
||||
* item c
|
||||
** item c.a
|
||||
|
||||
or you can number them
|
||||
# [[item 1]]
|
||||
# item 2
|
||||
# // italic item 3 //
|
||||
## item 3.1
|
||||
## item 3.2
|
||||
|
||||
up to five levels
|
||||
* 1
|
||||
** 2
|
||||
*** 3
|
||||
**** 4
|
||||
***** 5
|
||||
|
||||
* You can have
|
||||
multiline list items
|
||||
* this is a second multiline
|
||||
list item
|
||||
|
||||
You can use nowiki syntax if you would like do stuff like this:
|
||||
|
||||
{{{
|
||||
Guitar Chord C:
|
||||
|
||||
||---|---|---|
|
||||
||-1-|---|---|
|
||||
||---|---|---|
|
||||
||---|-2-|---|
|
||||
||---|---|-3-|
|
||||
||---|---|---|
|
||||
~}}}
|
||||
|
||||
Note: if you look at the source code of the above, you see the escape char (tilde, ~ )
|
||||
being used to escape the closing triple curly braces. This is to do nesting because
|
||||
all this text is enclosed in nowiki markup.
|
||||
}}}
|
||||
|
||||
You can also use it inline nowiki {{{ in a sentence }}} like this.
|
||||
|
||||
= Escapes =
|
||||
Normal Link: http://wikicreole.org/ - now same link, but escaped: ~http://wikicreole.org/
|
||||
|
||||
Normal asterisks: ~**not bold~**
|
||||
|
||||
a tilde alone: ~
|
||||
|
||||
a tilde escapes itself: ~~xxx
|
||||
|
||||
=== Creole 0.2 ===
|
||||
|
||||
This should be a flower with the ALT text "this is a flower" if your wiki supports ALT text on images:
|
||||
|
||||
{{Red_Flower.jpg|here is a red flower}}
|
||||
|
||||
=== Creole 0.4 ===
|
||||
|
||||
Tables are done like this:
|
||||
|
||||
|=header col1|=header col2|
|
||||
|col1|col2|
|
||||
|you |can |
|
||||
|also |align\\ it. |
|
||||
|links |[[http://www.wikicreole.org/|Should Work]]|
|
||||
|
||||
You can format an address by simply forcing linebreaks:
|
||||
|
||||
My contact dates:\\
|
||||
Pone: xyz\\
|
||||
Fax: +45\\
|
||||
Mobile: abc
|
||||
|
||||
=== Creole 0.5 ===
|
||||
|
||||
|= Header title |= Another header title |
|
||||
| {{{ //not italic text// }}} | {{{ **not bold text** }}} |
|
||||
| //italic text// | ** bold text ** |
|
||||
|
||||
=== Creole 1.0 ===
|
||||
|
||||
If interwiki links are setup in your wiki, this links to the WikiCreole page about Creole 1.0 test cases: [[WikiCreole:Creole1.0TestCases]].
|
@ -1,135 +0,0 @@
|
||||
//This test was taken from http://www.wikicreole.org/wiki/JSPWikiTestCases?skin=raw//
|
||||
|
||||
The following is a test of WikiCreole:
|
||||
|
||||
= Top-level heading (1)
|
||||
== This a test for creole 0.1 (2)
|
||||
=== This is a Subheading (3)
|
||||
==== Subsub (4)
|
||||
===== Subsubsub (5)
|
||||
|
||||
The ending equal signs should not be displayed:
|
||||
|
||||
= Top-level heading (1) =
|
||||
== This a test for creole 0.1 (2) ==
|
||||
=== This is a Subheading (3) ===
|
||||
==== Subsub (4) ====
|
||||
===== Subsubsub (5) =====
|
||||
|
||||
|
||||
You can make things **bold** or //italic// or **//both//** or //**both**//.
|
||||
|
||||
Character formatting extends across line breaks: **bold,
|
||||
this is still bold. This line deliberately does not end in star-star.
|
||||
|
||||
Not bold. Character formatting does not cross paragraph boundaries.
|
||||
|
||||
You can use [[internal links]] or [[http://www.wikicreole.org|external links]],
|
||||
give the link a [[internal links|different]] name.
|
||||
|
||||
Here's another sentence: This wisdom is taken from [[Ward Cunningham's]]
|
||||
[[http://www.c2.com/doc/wikisym/WikiSym2006.pdf|Presentation at the Wikisym 06]].
|
||||
|
||||
Here's a external link without a description: [[http://www.wikicreole.org]]
|
||||
|
||||
Free links without braces should be rendered as well, like http://www.wikicreole.org/ and http://www.wikicreole.org/users/~example.
|
||||
|
||||
Creole1.0 specifies that http://bar and ftp://bar should not render italic,
|
||||
something like foo://bar should render as italic.
|
||||
|
||||
You can use this to draw a line to separate the page:
|
||||
----
|
||||
|
||||
You can use lists, start it at the first column for now, please...
|
||||
|
||||
unnumbered lists are like
|
||||
* item a
|
||||
* item b
|
||||
* **bold item c**
|
||||
|
||||
blank space is also permitted before lists like:
|
||||
* item a
|
||||
* item b
|
||||
* item c
|
||||
** item c.a
|
||||
|
||||
or you can number them
|
||||
# [[item 1]]
|
||||
# item 2
|
||||
# // italic item 3 //
|
||||
## item 3.1
|
||||
## item 3.2
|
||||
|
||||
up to five levels
|
||||
* 1
|
||||
** 2
|
||||
*** 3
|
||||
**** 4
|
||||
***** 5
|
||||
|
||||
* You can have
|
||||
multiline list items
|
||||
* this is a second multiline
|
||||
list item
|
||||
|
||||
You can use nowiki syntax if you would like do stuff like this:
|
||||
|
||||
{{{
|
||||
Guitar Chord C:
|
||||
|
||||
||---|---|---|
|
||||
||-1-|---|---|
|
||||
||---|---|---|
|
||||
||---|-2-|---|
|
||||
||---|---|-3-|
|
||||
||---|---|---|
|
||||
~}}}
|
||||
|
||||
Note: if you look at the source code of the above, you see the escape char (tilde, ~ )
|
||||
being used to escape the closing triple curly braces. This is to do nesting because
|
||||
all this text is enclosed in nowiki markup.
|
||||
}}}
|
||||
|
||||
You can also use it inline nowiki {{{ in a sentence }}} like this.
|
||||
|
||||
= Escapes =
|
||||
Normal Link: http://wikicreole.org/ - now same link, but escaped: ~http://wikicreole.org/
|
||||
|
||||
Normal asterisks: ~**not bold~**
|
||||
|
||||
a tilde alone: ~
|
||||
|
||||
a tilde escapes itself: ~~xxx
|
||||
|
||||
=== Creole 0.2 ===
|
||||
|
||||
This should be a flower with the ALT text "this is a flower" if your wiki supports ALT text on images:
|
||||
|
||||
{{/Red_Flower.jpg|here is a red flower}}
|
||||
|
||||
=== Creole 0.4 ===
|
||||
|
||||
Tables are done like this:
|
||||
|
||||
|=header col1|=header col2|
|
||||
|col1|col2|
|
||||
|you |can |
|
||||
|also |align\\ it. |
|
||||
|links |[[http://www.wikicreole.org/|Should Work]]|
|
||||
|
||||
You can format an address by simply forcing linebreaks:
|
||||
|
||||
My contact dates:\\
|
||||
Pone: xyz\\
|
||||
Fax: +45\\
|
||||
Mobile: abc
|
||||
|
||||
=== Creole 0.5 ===
|
||||
|
||||
|= Header title |= Another header title |
|
||||
| {{{ //not italic text// }}} | {{{ **not bold text** }}} |
|
||||
| //italic text// | ** bold text ** |
|
||||
|
||||
=== Creole 1.0 ===
|
||||
|
||||
If interwiki links are setup in your wiki, this links to the WikiCreole page about Creole 1.0 test cases: [[WikiCreole:Creole1.0TestCases]].
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Testcreole",
|
||||
"comment": "Update :Main/Testcreole",
|
||||
"author": "",
|
||||
"time": 1593881943,
|
||||
"text_mime": "text/creole",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.txt",
|
||||
"binary_name": ""
|
||||
},
|
||||
"2": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "Testcreole",
|
||||
"comment": "Update :Main/Testcreole",
|
||||
"author": "",
|
||||
"time": 1593882073,
|
||||
"text_mime": "text/creole",
|
||||
"binary_mime": "",
|
||||
"text_name": "2.txt",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
# Gemtext cheatsheet
|
||||
Here's the basics of how text works in Gemtext:
|
||||
|
||||
Long lines get wrapped by the client to fit the screen
|
||||
Short lines *don't* get joined together
|
||||
Write paragraphs as single long lines
|
||||
Blank lines are rendered verbatim
|
||||
You get three levels of heading:
|
||||
|
||||
# Heading
|
||||
## Sub-heading
|
||||
### Sub-subheading
|
||||
You get one kind of list and you can't nest them:
|
||||
|
||||
* Mercury
|
||||
* Gemini
|
||||
* Apollo
|
||||
Here's a quote from Maciej Cegłowski:
|
||||
|
||||
> I contend that text-based websites should not exceed in size the major works of Russian literature.
|
||||
Lines which start with ``` will cause clients to toggle in and out of ordinary rendering mode and preformatted mode. In preformatted mode, Gemtext syntax is ignored so links etc. will not be rendered, and text will appear in a monospace font.
|
||||
``` ALT TEXT
|
||||
.
|
||||
('
|
||||
'|
|
||||
|'
|
||||
[::]
|
||||
[::] _......_
|
||||
[::].-' _.-`.
|
||||
[:.' .-. '-._.-`.
|
||||
[/ /\ | \ `-..
|
||||
/ / | `-.' .-. `-.
|
||||
/ `-' ( `. `.
|
||||
| /\ `-._/ \
|
||||
' .'\ / `. _.-'|
|
||||
/ / / \_.-' _.':;:/
|
||||
.' \_/ _.-':;_.-'
|
||||
/ .-. _.-' \;.-'
|
||||
/ ( \ _..-' |
|
||||
\ `._/ _..-' .--. |
|
||||
`-.....-'/ _ _ .' '.|
|
||||
| |_|_| | | \ (o)
|
||||
(o) | |_|_| | | | (\'/)
|
||||
(\'/)/ ''''' | o| \;:;
|
||||
:; | | | |/)
|
||||
;: `-.._ /__..--'\.' ;:
|
||||
:; `--' :; :;
|
||||
```
|
||||
=> https://proxy.vulpes.one/gemini/gemini.circumlunar.space/docs/cheatsheet.gmi Original cheatsheet
|
||||
=> https://proxy.vulpes.one/gemini/gemini.circumlunar.space/docs/cheatsheet.gmi
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": null,
|
||||
"name": "Testgemini",
|
||||
"comment": "Create :Main/Testgemini",
|
||||
"author": "",
|
||||
"time": 1593960824,
|
||||
"text_mime": "text/gemini",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.txt",
|
||||
"binary_name": ""
|
||||
}
|
||||
}
|
||||
}
|
@ -1,232 +0,0 @@
|
||||
|
||||
|
||||
# h1 Heading 8-)
|
||||
## h2 Heading
|
||||
### h3 Heading
|
||||
#### h4 Heading
|
||||
##### h5 Heading
|
||||
###### h6 Heading
|
||||
|
||||
|
||||
## Horizontal Rules
|
||||
|
||||
___
|
||||
|
||||
---
|
||||
|
||||
***
|
||||
|
||||
|
||||
## Typographic replacements
|
||||
|
||||
Enable typographer option to see result.
|
||||
|
||||
(c) (C) (r) (R) (tm) (TM) (p) (P) +-
|
||||
|
||||
test.. test... test..... test?..... test!....
|
||||
|
||||
!!!!!! ???? ,, -- ---
|
||||
|
||||
"Smartypants, double quotes" and 'single quotes'
|
||||
|
||||
|
||||
## Emphasis
|
||||
|
||||
**This is bold text**
|
||||
|
||||
__This is bold text__
|
||||
|
||||
*This is italic text*
|
||||
|
||||
_This is italic text_
|
||||
|
||||
~~Strikethrough~~
|
||||
|
||||
|
||||
## Blockquotes
|
||||
|
||||
|
||||
> Blockquotes can also be nested...
|
||||
>> ...by using additional greater-than signs right next to each other...
|
||||
> > > ...or with spaces between arrows.
|
||||
|
||||
|
||||
## Lists
|
||||
|
||||
Unordered
|
||||
|
||||
+ Create a list by starting a line with `+`, `-`, or `*`
|
||||
+ Sub-lists are made by indenting 2 spaces:
|
||||
- Marker character change forces new list start:
|
||||
* Ac tristique libero volutpat at
|
||||
+ Facilisis in pretium nisl aliquet
|
||||
- Nulla volutpat aliquam velit
|
||||
+ Very easy!
|
||||
|
||||
Ordered
|
||||
|
||||
1. Lorem ipsum dolor sit amet
|
||||
2. Consectetur adipiscing elit
|
||||
3. Integer molestie lorem at massa
|
||||
|
||||
|
||||
1. You can use sequential numbers...
|
||||
1. ...or keep all the numbers as `1.`
|
||||
|
||||
Start numbering with offset:
|
||||
|
||||
57. foo
|
||||
1. bar
|
||||
|
||||
|
||||
## Code
|
||||
|
||||
Inline `code`
|
||||
|
||||
Indented code
|
||||
|
||||
// Some comments
|
||||
line 1 of code
|
||||
line 2 of code
|
||||
line 3 of code
|
||||
|
||||
|
||||
Block code "fences"
|
||||
|
||||
```
|
||||
Sample text here...
|
||||
```
|
||||
|
||||
Syntax highlighting
|
||||
|
||||
``` js
|
||||
var foo = function (bar) {
|
||||
return bar++;
|
||||
};
|
||||
|
||||
console.log(foo(5));
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
| data | path to data files to supply the data that will be passed into templates. |
|
||||
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||
| ext | extension to be used for dest files. |
|
||||
|
||||
Right aligned columns
|
||||
|
||||
| Option | Description |
|
||||
| ------:| -----------:|
|
||||
| data | path to data files to supply the data that will be passed into templates. |
|
||||
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||
| ext | extension to be used for dest files. |
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
[link text](http://dev.nodeca.com)
|
||||
|
||||
[link with title](http://nodeca.github.io/pica/demo/ "title text!")
|
||||
|
||||
Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
|
||||
|
||||
|
||||
## Images
|
||||
|
||||
![Minion](https://octodex.github.com/images/minion.png)
|
||||
![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")
|
||||
|
||||
Like links, Images also have a footnote style syntax
|
||||
|
||||
![Alt text][id]
|
||||
|
||||
With a reference later in the document defining the URL location:
|
||||
|
||||
[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
|
||||
|
||||
|
||||
## Plugins
|
||||
|
||||
The killer feature of `markdown-it` is very effective support of
|
||||
[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin).
|
||||
|
||||
|
||||
### [Emojies](https://github.com/markdown-it/markdown-it-emoji)
|
||||
|
||||
> Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum:
|
||||
>
|
||||
> Shortcuts (emoticons): :-) :-( 8-) ;)
|
||||
|
||||
see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji.
|
||||
|
||||
|
||||
### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)
|
||||
|
||||
- 19^th^
|
||||
- H~2~O
|
||||
|
||||
|
||||
### [\<ins>](https://github.com/markdown-it/markdown-it-ins)
|
||||
|
||||
++Inserted text++
|
||||
|
||||
|
||||
### [\<mark>](https://github.com/markdown-it/markdown-it-mark)
|
||||
|
||||
==Marked text==
|
||||
|
||||
|
||||
### [Footnotes](https://github.com/markdown-it/markdown-it-footnote)
|
||||
|
||||
Footnote 1 link[^first].
|
||||
|
||||
Footnote 2 link[^second].
|
||||
|
||||
Inline footnote^[Text of inline footnote] definition.
|
||||
|
||||
Duplicated footnote reference[^second].
|
||||
|
||||
[^first]: Footnote **can have markup**
|
||||
|
||||
and multiple paragraphs.
|
||||
|
||||
[^second]: Footnote text.
|
||||
|
||||
|
||||
### [Definition lists](https://github.com/markdown-it/markdown-it-deflist)
|
||||
|
||||
Term 1
|
||||
|
||||
: Definition 1
|
||||
with lazy continuation.
|
||||
|
||||
Term 2 with *inline markup*
|
||||
|
||||
: Definition 2
|
||||
|
||||
{ some code, part of Definition 2 }
|
||||
|
||||
Third paragraph of definition 2.
|
||||
|
||||
_Compact style:_
|
||||
|
||||
Term 1
|
||||
~ Definition 1
|
||||
|
||||
Term 2
|
||||
~ Definition 2a
|
||||
~ Definition 2b
|
||||
|
||||
|
||||
### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr)
|
||||
|
||||
This is HTML abbreviation example.
|
||||
|
||||
It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on.
|
||||
|
||||
*[HTML]: Hyper Text Markup Language
|
||||
|
||||
### [Custom containers](https://github.com/markdown-it/markdown-it-container)
|
||||
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"views": 0,
|
||||
"deleted": false,
|
||||
"revisions": {
|
||||
"1": {
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"name": "TestMd",
|
||||
"comment": "Update TestMd",
|
||||
"author": "",
|
||||
"time": 1593340713,
|
||||
"text_mime": "text/markdown",
|
||||
"binary_mime": "",
|
||||
"text_name": "1.markdown",
|
||||
"binary_name": ""
|
||||
}
|
||||
},
|
||||
"Invalid": false,
|
||||
"Err": null
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user