1
0
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:
Timur Ismagilov 2020-08-05 20:08:59 +05:00
parent 24d11c04c2
commit 673c2b1836
120 changed files with 1102 additions and 3600 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "metarrhiza"]
path = metarrhiza
url = https://github.com/bouncepaw/metarrhiza.git

View File

@ -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."

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
View 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
View 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>=&gt; 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
View 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
View 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
View 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
View 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
View 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
View File

@ -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
View File

@ -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=

View File

@ -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
View 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
View 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
View 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
View File

@ -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

@ -0,0 +1 @@
Subproject commit a22fcac89f10ad1e1db77d765788dfd8966cbb36

108
mime.go Normal file
View 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
View 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))
}

View File

@ -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
View 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+"/"))
}

View File

@ -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>",
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)))
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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]
}

View File

@ -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)
}
}
}

View File

@ -1 +0,0 @@
This is root wiki directory.

View File

@ -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"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

View File

@ -1 +0,0 @@
This is directory with hyphae

View File

@ -1 +0,0 @@
# Help

View File

@ -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
}

View File

@ -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.

View File

@ -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
}

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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": ""
}
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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": ""
}
}
}

View File

@ -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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -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": ""
}
}
}

View File

@ -1 +0,0 @@
Fruit is a type of fetus. Is tasty so cool coo l ha i love fwriotwsn

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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"
}
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)
По-русски: груша

View File

@ -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": ""
}
}
}

View File

@ -1 +0,0 @@
<b>Pineapple is apple from a pine</b>

View File

@ -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": ""
}
}
}

View File

@ -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.

View File

@ -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.
По-русски: грибы.

View File

@ -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)!

View File

@ -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)!

View File

@ -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)!

View File

@ -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)!

View File

@ -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

View File

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

View File

@ -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"
}
}
}

View File

@ -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": ""
}
}
}

View File

@ -1 +0,0 @@
This is a testing page to test that editing works, etc

View File

@ -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": ""
}
}
}

View File

@ -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]].

View File

@ -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]].

View File

@ -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": ""
}
}
}

View File

@ -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

View File

@ -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": ""
}
}
}

View File

@ -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)

View File

@ -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
}

View File

Some files were not shown because too many files have changed in this diff Show More