1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-12-12 13:30:26 +00:00

Merge remote-tracking branch 'origin/master' into features/templates-from-html

This commit is contained in:
Dan Konshin 2020-06-19 20:17:00 +05:00
commit 5ccaacb6d9
19 changed files with 490 additions and 58 deletions

View File

@ -3,9 +3,10 @@ package main
const ( const (
TitleTemplate = `%s at MycorrhizaWiki` TitleTemplate = `%s at MycorrhizaWiki`
DefaultTitle = "MycorrhizaWiki" DefaultTitle = "MycorrhizaWiki"
DefaultHeaderText = `MycorrhizaWiki` DefaultHeaderText = `MycorrhizaWiki 🍄`
DefaultFooterText = "MycorrhizaWiki" DefaultFooterText = `This website runs <a href="https://github.com/bouncepaw/mycorrhiza">MycorrhizaWiki</a>.`
DefaultSidebar = "" DefaultSidebar = ""
DefaultBodyBottom = ""
DefaultContent = "It is empty here" DefaultContent = "It is empty here"
DefaultStyles = ` DefaultStyles = `
<link rel="stylesheet" href="/sys/main.css?action=raw"> <link rel="stylesheet" href="/sys/main.css?action=raw">

View File

@ -1,8 +1,13 @@
package main package main
import ( import (
"io/ioutil"
"log" "log"
"net/http" "net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -63,7 +68,118 @@ func HandlerRename(w http.ResponseWriter, r *http.Request) {
log.Println("Attempt to access an unimplemented thing") log.Println("Attempt to access an unimplemented thing")
} }
func HandlerUpdate(w http.ResponseWriter, r *http.Request) { func makeTagsSlice(responseTagsString string) (ret []string) {
w.WriteHeader(http.StatusNotImplemented) // `responseTagsString` is string like "foo,, bar,kek". Whitespace around commas is insignificant. Expected output: []string{"foo", "bar", "kek"}
log.Println("Attempt to access an unimplemented thing") for _, tag := range strings.Split(responseTagsString, ",") {
if trimmed := strings.TrimSpace(tag); "" == trimmed {
ret = append(ret, trimmed)
}
}
return ret
}
// Return an existing hypha it exists in `hyphae` or create a new one. If it `isNew`, you'll have to insert it to `hyphae` yourself.
func getHypha(name string) (*Hypha, bool) {
log.Println("Accessing hypha", name)
if h, ok := hyphae[name]; ok {
log.Println("Got hypha", name)
return h, false
}
log.Println("Create hypha", name)
h := &Hypha{
FullName: name,
Path: filepath.Join(rootWikiDir, name),
Revisions: make(map[string]*Revision),
parentName: filepath.Dir(name),
}
return h, true
}
// Create a new revison for hypha `h`. All data is fetched from `r`, except for BinaryMime and BinaryPath which require additional processing. You'll have te insert the revision to `h` yourself.
func revisionFromHttpData(h *Hypha, r *http.Request) *Revision {
idStr := strconv.Itoa(h.NewestRevisionInt() + 1)
log.Println(idStr)
rev := &Revision{
Id: h.NewestRevisionInt() + 1,
FullName: h.FullName,
Tags: makeTagsSlice(r.PostFormValue("tags")),
Comment: r.PostFormValue("comment"),
Author: r.PostFormValue("author"),
Time: int(time.Now().Unix()),
TextMime: r.PostFormValue("text_mime"),
TextPath: filepath.Join(h.Path, idStr+".txt"),
// Left: BinaryMime, BinaryPath
}
return rev
}
func writeTextFileFromHttpData(rev *Revision, r *http.Request) error {
data := []byte(r.PostFormValue("text"))
err := ioutil.WriteFile(rev.TextPath, data, 0644)
if err != nil {
log.Println("Failed to write", len(data), "bytes to", rev.TextPath)
}
return err
}
func writeBinaryFileFromHttpData(h *Hypha, oldRev Revision, newRev *Revision, r *http.Request) error {
// 10 MB file size limit
r.ParseMultipartForm(10 << 20)
// Read file
file, handler, err := r.FormFile("binary")
if file != nil {
defer file.Close()
}
if err != nil {
log.Println("No binary data passed for", newRev.FullName)
newRev.BinaryMime = oldRev.BinaryMime
newRev.BinaryPath = oldRev.BinaryPath
log.Println("Set previous revision's binary data")
return nil
}
newRev.BinaryMime = handler.Header.Get("Content-Type")
newRev.BinaryPath = filepath.Join(h.Path, newRev.IdAsStr()+".bin")
data, err := ioutil.ReadAll(file)
if err != nil {
log.Println(err)
return err
}
log.Println("Got", len(data), "of binary data for", newRev.FullName)
err = ioutil.WriteFile(newRev.BinaryPath, data, 0644)
if err != nil {
log.Println("Failed to write", len(data), "bytes to", newRev.TextPath)
return err
}
log.Println("Written", len(data), "of binary data for", newRev.FullName)
return nil
}
func HandlerUpdate(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
log.Println("Attempt to update hypha", mux.Vars(r)["hypha"])
h, isNew := getHypha(vars["hypha"])
oldRev := h.GetNewestRevision()
newRev := revisionFromHttpData(h, r)
if isNew {
h.CreateDir()
}
err := writeTextFileFromHttpData(newRev, r)
if err != nil {
log.Println(err)
return
}
err = writeBinaryFileFromHttpData(h, oldRev, newRev, r)
if err != nil {
log.Println(err)
return
}
h.Revisions[newRev.IdAsStr()] = newRev
h.SaveJson()
log.Println("Current hyphae storage is", hyphae)
w.WriteHeader(http.StatusOK)
w.Write([]byte("Saved successfully"))
} }

View File

@ -1,20 +1,24 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
) )
type Hypha struct { type Hypha struct {
FullName string FullName string `json:"-"`
Path string Path string `json:"-"`
ViewCount int `json:"views"` ViewCount int `json:"views"`
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
Revisions map[string]*Revision `json:"revisions"` Revisions map[string]*Revision `json:"revisions"`
ChildrenNames []string ChildrenNames []string `json:"-"`
parentName string parentName string
} }
@ -39,7 +43,15 @@ func (h *Hypha) Name() string {
return h.FullName return h.FullName
} }
func (h *Hypha) GetNewestRevision() Revision {
return *h.Revisions[h.NewestRevision()]
}
func (h *Hypha) NewestRevision() string { func (h *Hypha) NewestRevision() string {
return strconv.Itoa(h.NewestRevisionInt())
}
func (h *Hypha) NewestRevisionInt() int {
var largest int var largest int
for k, _ := range h.Revisions { for k, _ := range h.Revisions {
rev, _ := strconv.Atoi(k) rev, _ := strconv.Atoi(k)
@ -47,16 +59,38 @@ func (h *Hypha) NewestRevision() string {
largest = rev largest = rev
} }
} }
return strconv.Itoa(largest) return largest
}
func (h *Hypha) MetaJsonPath() string {
return filepath.Join(h.Path, "meta.json")
}
func (h *Hypha) CreateDir() error {
return os.MkdirAll(h.Path, 0644)
} }
func (h *Hypha) ParentName() string { func (h *Hypha) ParentName() string {
return h.parentName return h.parentName
} }
func (h *Hypha) SaveJson() {
data, err := json.Marshal(h)
if err != nil {
log.Println("Failed to create JSON of hypha.", err)
return
}
err = ioutil.WriteFile(h.MetaJsonPath(), data, 0644)
if err != nil {
log.Println("Failed to save JSON of hypha.", err)
return
}
log.Println("Saved JSON data of", h.FullName)
}
func ActionEdit(hyphaName string, w http.ResponseWriter) { func ActionEdit(hyphaName string, w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
var initContents, initTextMime, initBinaryMime, initTags string var initContents, initTextMime, initTags string
hypha, ok := hyphae[hyphaName] hypha, ok := hyphae[hyphaName]
if !ok { if !ok {
initContents = "Describe " + hyphaName + "here." initContents = "Describe " + hyphaName + "here."
@ -71,10 +105,9 @@ func ActionEdit(hyphaName string, w http.ResponseWriter) {
} }
initContents = string(contents) initContents = string(contents)
initTextMime = newestRev.TextMime initTextMime = newestRev.TextMime
initBinaryMime = newestRev.BinaryMime
initTags = strings.Join(newestRev.Tags, ",") initTags = strings.Join(newestRev.Tags, ",")
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte(EditHyphaPage(hyphaName, initTextMime, initBinaryMime, initContents, initTags))) w.Write([]byte(EditHyphaPage(hyphaName, initTextMime, initContents, initTags)))
} }

View File

@ -101,7 +101,9 @@ func main() {
r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl).
HandlerFunc(HandlerRename) HandlerFunc(HandlerRename)
r.Queries("action", "update").Path(hyphaUrl). r.Queries(
"action", "update",
).Path(hyphaUrl).Methods("POST").
HandlerFunc(HandlerUpdate) HandlerFunc(HandlerUpdate)
r.HandleFunc(hyphaUrl, HandlerView) r.HandleFunc(hyphaUrl, HandlerView)

View File

@ -3,29 +3,36 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"path" "path"
"text/template" "text/template"
) )
func EditHyphaPage(name, text_mime, binary_mime, content, tags string) string { func EditHyphaPage(name, textMime, content, tags string) string {
keys := map[string]string{ keys := map[string]string{
"Title": fmt.Sprintf(TitleTemplate, name), "Title": fmt.Sprintf(TitleTemplate, "Edit "+name),
"Header": renderFromString(name, "Hypha/edit/header.html"),
} }
page := map[string]string{ page := map[string]string{
"Text": content, "Text": content,
"TextMime": text_mime, "TextMime": textMime,
"BinMime": binary_mime,
"Name": name, "Name": name,
"Tags": tags, "Tags": tags,
} }
return renderBase(renderFromMap(page, "Hypha/edit.html"), keys) return renderBase(renderFromMap(page, "Hypha/edit/index.html"), keys)
} }
func HyphaPage(hyphae map[string]*Hypha, rev Revision, content string) string { func HyphaPage(hyphae map[string]*Hypha, rev Revision, content string) string {
keys := map[string]string{ sidebar := DefaultSidebar
"Title": fmt.Sprintf(TitleTemplate, rev.FullName), bside, err := ioutil.ReadFile("Hypha/view/sidebar.html")
if err == nil {
sidebar = string(bside)
} }
return renderBase(renderFromString(content, "Hypha/index.html"), keys) keys := map[string]string{
"Title": fmt.Sprintf(TitleTemplate, rev.FullName),
"Sidebar": sidebar,
}
return renderBase(renderFromString(content, "Hypha/view/index.html"), keys)
} }
/* /*
@ -36,11 +43,13 @@ Args:
*/ */
func renderBase(content string, keys map[string]string) string { func renderBase(content string, keys map[string]string) string {
page := map[string]string{ page := map[string]string{
"Title": DefaultTitle, "Title": DefaultTitle,
"Header": renderFromString(DefaultHeaderText, "header.html"), "Head": DefaultStyles,
"Footer": renderFromString(DefaultFooterText, "footer.html"), "Sidebar": DefaultSidebar,
"Sidebar": DefaultSidebar, "Main": DefaultContent,
"Main": DefaultContent, "BodyBottom": DefaultBodyBottom,
"Header": renderFromString(DefaultHeaderText, "header.html"),
"Footer": renderFromString(DefaultFooterText, "footer.html"),
} }
for key, val := range keys { for key, val := range keys {
page[key] = val page[key] = val

View File

@ -6,12 +6,15 @@ import (
"log" "log"
"net/http" "net/http"
"strconv"
"github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown"
) )
// In different places, revision variable is called `r`. But when there is an http.Request as well, the revision becomes `rev`. TODO: name them consistently.
type Revision struct { type Revision struct {
Id int Id int `json:"-"`
FullName string FullName string `json:"-"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
ShortName string `json:"name"` ShortName string `json:"name"`
Comment string `json:"comment"` Comment string `json:"comment"`
@ -19,8 +22,12 @@ type Revision struct {
Time int `json:"time"` Time int `json:"time"`
TextMime string `json:"text_mime"` TextMime string `json:"text_mime"`
BinaryMime string `json:"binary_mime"` BinaryMime string `json:"binary_mime"`
TextPath string TextPath string `json:"-"`
BinaryPath string BinaryPath string `json:"-"`
}
func (r *Revision) IdAsStr() string {
return strconv.Itoa(r.Id)
} }
// During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data. // During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data.

View File

@ -0,0 +1 @@
<h1 class="header__edit-title">Edit {{ . }}</h1>

View File

@ -1,13 +1,16 @@
<div class="naviwrapper"> <div class="naviwrapper">
<form class="naviwrapper__edit edit-box"> <form class="naviwrapper__edit edit-box"
method="POST"
enctype="multipart/form-data"
action="?action=update">
<div class="naviwrapper__buttons"> <div class="naviwrapper__buttons">
<input type="submit" name="action" value="update"/> <input type="submit" value="update"/>
</div> </div>
<div class="edit-box__left"> <div class="edit-box__left">
<h4>Edit box</h4> <h4>Edit box</h4>
<textarea class="edit-box__text" name="text" cols="80" rows="25"> <textarea class="edit-box__text" name="text" cols="80" rows="25">
{{ .Text }} {{ .Text }}
</textarea> </textarea>
<h4>Upload file</h4> <h4>Upload file</h4>
@ -20,17 +23,13 @@
<p>Good types are <code>text/markdown</code> and <code>text/plain</code></p> <p>Good types are <code>text/markdown</code> and <code>text/plain</code></p>
<input type="text" name="text_mime" value="{{ .TextMime }}"/> <input type="text" name="text_mime" value="{{ .TextMime }}"/>
<h4>Media MIME-type</h4>
<p>For now, only image formats are supported. Choose any, but <code>image/jpeg</code> and <code>image/png</code> are recommended</p>
<input type="text" name="binary_mime" value="{{ .BinMime }}"/>
<h4>Revision comment</h4> <h4>Revision comment</h4>
<p>Please make your comment helpful</p> <p>Please make your comment helpful</p>
<input type="text" name="comment" value="Update {{ .Name }}"/> <input type="text" name="comment" value="Update {{ .Name }}"/>
<h4>Edit tags</h4> <h4>Edit tags</h4>
<p>Tags are separated by commas, whitespace is ignored</p> <p>Tags are separated by commas, whitespace is ignored</p>
<input type="text" name="comment" value="{{ .Tags }}"/> <input type="text" name="tags" value="{{ .Tags }}"/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -0,0 +1,9 @@
<script type="text/javascript">
var menu = document.getElementById('shroomburgerMenu');
document.getElementById('shroomBtn').addEventListener('click', function() {
menu.classList.add('active');
});
document.getElementById('mushroomBtn').addEventListener('click', function() {
menu.classList.remove('active');
});
</script>

View File

@ -0,0 +1,10 @@
<div class="naviwrapper">
<div class="hypha-actions">
<ul>
<li><a href="?action=edit">Edit</a>
<li><a href="?action=getBinary">Download</a>
<li><a href="?action=zen">Zen mode</a>
<li><a href="?action=raw">View raw</a>
</ul>
</div>
</div>

View File

@ -1,11 +1,25 @@
<html> <html>
<head> <head>
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
{{ .Head }}
</head> </head>
<body> <body>
<header class="header">{{ .Header }}</header> <div class="shroom">
<button class="shroom__button" id="shroomBtn"><span>🍄</span> Open mycelium</button>
</div>
<main class="main">{{ .Main }}</main> <main class="main">{{ .Main }}</main>
<aside class="sidebar">{{ .Sidebar }}</aside> <div class="left-panel" id="shroomburgerMenu">
<footer class="footer">{{ .Footer }}</footer> <div class="left-panel__in">
<div class="shroom mushroom">
<button class="shroom__button" id="mushroomBtn"><span>🍄</span> Close mycelium</button>
</div>
<div class="left-panel__contents">
<header class="header">{{ .Header }}</header>
<aside class="sidebar">{{ .Sidebar }}</aside>
<footer class="footer">{{ .Footer }}</footer>
</div>
</div>
</div>
{{ .BodyBottom }}
</body> </body>
</html> </html>

BIN
w/m/Fruit/Apple/2.bin Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

3
w/m/Fruit/Apple/2.txt Normal file
View File

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

6
w/m/Fruit/Apple/3.txt Normal file
View File

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

BIN
w/m/Fruit/Apple/4.bin Normal file

Binary file not shown.

5
w/m/Fruit/Apple/4.txt Normal file
View File

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

View File

@ -1,13 +1 @@
{ {"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"},"2":{"tags":null,"name":"","comment":"Update Fruit/Apple","author":"","time":1592570366,"text_mime":"text/plain","binary_mime":"image/jpeg"},"3":{"tags":null,"name":"","comment":"Test fs dumping","author":"","time":1592570926,"text_mime":"text/plain","binary_mime":"image/jpeg"},"4":{"tags":[""],"name":"","comment":"Update Fruit/Apple","author":"","time":1592579243,"text_mime":"text/plain","binary_mime":"application/pdf"}}}
"revisions":{
"1":{
"name": "Apple",
"time": 1591639464,
"author": "bouncepaw",
"comment": "add apple pic hehehe",
"tags": ["img"],
"text_mime": "text/plain",
"binary_mime": "image/jpeg"
}
}
}

View File

@ -1,7 +1,236 @@
b { color: red; } *, *::before, *::after {
article { border: 1px black solid; } box-sizing: border-box;
}
html {
height: 100%;
}
body {
font: 15px/1.5 system-ui, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Helvetica', 'Roboto', 'Arial', sans-serif;
max-width: 500px;
min-height: 100%;
margin: 0 auto;
padding: 12px 24px;
}
@media (min-width: 700px) {
body {
/*
display: grid;
grid-template-columns: 1fr 250px;
grid-template-rows: auto 1fr auto;
grid-column-gap: 24px;
grid-template-areas:
"main header"
"main sidebar"
"main footer";
*/
}
}
.shroom {
/*margin: 8px 0 24px;*/
/*margin: -12px;*/
margin: 0;
}
.shroom__button {
/*
padding: 2px 12px;
border: 1px solid #ddd;
*/border-radius: 8px;/*
background: none;
*/
padding: 8px 16px 8px 0;
border: none;
background: #f0f2f4;
color: #444;
font: inherit;
font-size: 15px;
font-weight: 500;
text-align: left;
}
.shroom span {
margin-left: 16px;
margin-right: 8px;
font-size: 20px;
vertical-align: -0.04em;
}
.mushroom .shroom__button {
background: #44484a;
color: #dddfe4;
}
/*
header {
grid-area: header;
}
main {
grid-area: main;
}
aside {
grid-area: sidebar;
}
footer {
grid-area: footer;
}
*/
.header {
padding: 8px 0;
}
.header h1 {
margin: 0;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.02em;
color: #222428;
}
a {
color: #44e;
}
a:visited {
color: #44a;
}
h1, h2, h3, h4, h5, h6 {
margin: 1em 0 0.25em;
}
.page {
font-family: 'Georgia', serif;
font-size: 16px;
line-height: 1.666;
max-width: 40em;
hyphens: auto;
}
.page__title {
font-size: 36px;
font-weight: normal;
}
.edit-box { display: grid; grid-template-columns: 7fr 5fr; } .edit-box { display: grid; grid-template-columns: 7fr 5fr; }
.edit-box .naviwrapper__buttons { grid-column: 1; grid-row: 2 } .edit-box .naviwrapper__buttons { grid-column: 1; grid-row: 2 }
.edit-box__left { grid-column: 1; grid-row: 2 } .edit-box__left { grid-column: 1; grid-row: 2 }
.edit-box__right { grid-column: 2; grid-row: 1 / span 2 } .edit-box__right { grid-column: 2; grid-row: 1 / span 2 }
footer {
/*margin-top: 40px;
border-top: 1px solid #ddd;*/
padding: 1em 0;
font-size: 12px;
color: #888;
}
footer a, footer a:visited {
color: #666;
}
.left-panel {
display: none;
}
.left-panel.active {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fafafa;
}
.left-panel.active .sidebar {
background: #fff;
}
.left-panel__in {
width: 100%;
height: 100%;
max-width: 500px;
margin: 0 auto;
padding: 12px 24px;
}
.left-panel__contents {
width: 100%;
display: grid;
grid-template-rows: auto 1fr auto;
}
.left-panel .shroom {
margin-bottom: 16px;
}
@media (min-width: 700px) {
body {
max-width: 1200px;
padding: 8px 16px;
padding-right: 274px; /* 250px + 16px + 16px */
}
.shroom {
display: none;
}
.page {
font-size: 18px;
}
.left-panel {
display: block;
position: fixed;
top: 0;
bottom: 0;
width: 274px; /* 250px + 24px * 2 */
/*right: calc(50% + 900px / 2 + 24px);*/
right: 0;
}
.left-panel__contents {
height: 100%;
}
}
.sidebar {
padding: 16px 0;
border-radius: 8px;
background: #f4f4f4;
}
.hypha-actions ul {
margin: 0;
padding: 0;
}
.hypha-actions li {
list-style: none;
}
.hypha-actions a {
display: block;
padding: 6px 16px;
font: inherit;
text-decoration: none;
color: #666;
transition: 0.1s background;
}
aside .hypha-actions a:hover {
background: #eaeaea;
}