diff --git a/config.go b/config.go index c648696..34fc6d6 100644 --- a/config.go +++ b/config.go @@ -3,9 +3,10 @@ package main const ( TitleTemplate = `%s at MycorrhizaWiki` DefaultTitle = "MycorrhizaWiki" - DefaultHeaderText = `MycorrhizaWiki` - DefaultFooterText = "MycorrhizaWiki" + DefaultHeaderText = `MycorrhizaWiki 🍄` + DefaultFooterText = `This website runs MycorrhizaWiki.` DefaultSidebar = "" + DefaultBodyBottom = "" DefaultContent = "It is empty here" DefaultStyles = ` diff --git a/handlers.go b/handlers.go index 4a60771..3951971 100644 --- a/handlers.go +++ b/handlers.go @@ -1,8 +1,13 @@ package main import ( + "io/ioutil" "log" "net/http" + "path/filepath" + "strconv" + "strings" + "time" "github.com/gorilla/mux" ) @@ -63,7 +68,118 @@ func HandlerRename(w http.ResponseWriter, r *http.Request) { log.Println("Attempt to access an unimplemented thing") } -func HandlerUpdate(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) - log.Println("Attempt to access an unimplemented thing") +func makeTagsSlice(responseTagsString string) (ret []string) { + // `responseTagsString` is string like "foo,, bar,kek". Whitespace around commas is insignificant. Expected output: []string{"foo", "bar", "kek"} + 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")) } diff --git a/hypha.go b/hypha.go index b29b602..1189a57 100644 --- a/hypha.go +++ b/hypha.go @@ -1,20 +1,24 @@ package main import ( + "encoding/json" "fmt" "io/ioutil" + "log" "net/http" + "os" + "path/filepath" "strconv" "strings" ) type Hypha struct { - FullName string - Path string + FullName string `json:"-"` + Path string `json:"-"` ViewCount int `json:"views"` Deleted bool `json:"deleted"` Revisions map[string]*Revision `json:"revisions"` - ChildrenNames []string + ChildrenNames []string `json:"-"` parentName string } @@ -39,7 +43,15 @@ func (h *Hypha) Name() string { return h.FullName } +func (h *Hypha) GetNewestRevision() Revision { + return *h.Revisions[h.NewestRevision()] +} + func (h *Hypha) NewestRevision() string { + return strconv.Itoa(h.NewestRevisionInt()) +} + +func (h *Hypha) NewestRevisionInt() int { var largest int for k, _ := range h.Revisions { rev, _ := strconv.Atoi(k) @@ -47,16 +59,38 @@ func (h *Hypha) NewestRevision() string { 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 { 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) { 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] if !ok { initContents = "Describe " + hyphaName + "here." @@ -71,10 +105,9 @@ func ActionEdit(hyphaName string, w http.ResponseWriter) { } initContents = string(contents) initTextMime = newestRev.TextMime - initBinaryMime = newestRev.BinaryMime initTags = strings.Join(newestRev.Tags, ",") } w.WriteHeader(http.StatusOK) - w.Write([]byte(EditHyphaPage(hyphaName, initTextMime, initBinaryMime, initContents, initTags))) + w.Write([]byte(EditHyphaPage(hyphaName, initTextMime, initContents, initTags))) } diff --git a/main.go b/main.go index be1daa0..d957eeb 100644 --- a/main.go +++ b/main.go @@ -101,7 +101,9 @@ func main() { r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). HandlerFunc(HandlerRename) - r.Queries("action", "update").Path(hyphaUrl). + r.Queries( + "action", "update", + ).Path(hyphaUrl).Methods("POST"). HandlerFunc(HandlerUpdate) r.HandleFunc(hyphaUrl, HandlerView) diff --git a/render.go b/render.go index ff74fdf..21a8f0b 100644 --- a/render.go +++ b/render.go @@ -3,29 +3,36 @@ package main import ( "bytes" "fmt" + "io/ioutil" "path" "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{ - "Title": fmt.Sprintf(TitleTemplate, name), + "Title": fmt.Sprintf(TitleTemplate, "Edit "+name), + "Header": renderFromString(name, "Hypha/edit/header.html"), } page := map[string]string{ "Text": content, - "TextMime": text_mime, - "BinMime": binary_mime, + "TextMime": textMime, "Name": name, "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 { - keys := map[string]string{ - "Title": fmt.Sprintf(TitleTemplate, rev.FullName), + sidebar := DefaultSidebar + 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 { page := map[string]string{ - "Title": DefaultTitle, - "Header": renderFromString(DefaultHeaderText, "header.html"), - "Footer": renderFromString(DefaultFooterText, "footer.html"), - "Sidebar": DefaultSidebar, - "Main": DefaultContent, + "Title": DefaultTitle, + "Head": DefaultStyles, + "Sidebar": DefaultSidebar, + "Main": DefaultContent, + "BodyBottom": DefaultBodyBottom, + "Header": renderFromString(DefaultHeaderText, "header.html"), + "Footer": renderFromString(DefaultFooterText, "footer.html"), } for key, val := range keys { page[key] = val diff --git a/revision.go b/revision.go index 9e7cf51..3e0c519 100644 --- a/revision.go +++ b/revision.go @@ -6,12 +6,15 @@ import ( "log" "net/http" + "strconv" + "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 { - Id int - FullName string + Id int `json:"-"` + FullName string `json:"-"` Tags []string `json:"tags"` ShortName string `json:"name"` Comment string `json:"comment"` @@ -19,8 +22,12 @@ type Revision struct { Time int `json:"time"` TextMime string `json:"text_mime"` BinaryMime string `json:"binary_mime"` - TextPath string - BinaryPath string + TextPath string `json:"-"` + 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. diff --git a/templates/Hypha/edit/header.html b/templates/Hypha/edit/header.html new file mode 100644 index 0000000..31dfc0f --- /dev/null +++ b/templates/Hypha/edit/header.html @@ -0,0 +1 @@ +

Edit {{ . }}

diff --git a/templates/Hypha/edit.html b/templates/Hypha/edit/index.html similarity index 66% rename from templates/Hypha/edit.html rename to templates/Hypha/edit/index.html index 45536c5..4bea3b7 100644 --- a/templates/Hypha/edit.html +++ b/templates/Hypha/edit/index.html @@ -1,13 +1,16 @@ \ No newline at end of file + diff --git a/templates/Hypha/view/bodybottom.html b/templates/Hypha/view/bodybottom.html new file mode 100644 index 0000000..0759741 --- /dev/null +++ b/templates/Hypha/view/bodybottom.html @@ -0,0 +1,9 @@ + diff --git a/templates/Hypha/index.html b/templates/Hypha/view/index.html similarity index 100% rename from templates/Hypha/index.html rename to templates/Hypha/view/index.html diff --git a/templates/Hypha/view/sidebar.html b/templates/Hypha/view/sidebar.html new file mode 100644 index 0000000..7d3a6b8 --- /dev/null +++ b/templates/Hypha/view/sidebar.html @@ -0,0 +1,10 @@ + diff --git a/templates/base.html b/templates/base.html index f37d0b9..03cf231 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,11 +1,25 @@ {{ .Title }} + {{ .Head }} -
{{ .Header }}
+
+ +
{{ .Main }}
- - +
+
+
+ +
+
+
{{ .Header }}
+ + +
+
+
+ {{ .BodyBottom }} - \ No newline at end of file + diff --git a/w/m/Fruit/Apple/2.bin b/w/m/Fruit/Apple/2.bin new file mode 100644 index 0000000..7127c67 Binary files /dev/null and b/w/m/Fruit/Apple/2.bin differ diff --git a/w/m/Fruit/Apple/2.txt b/w/m/Fruit/Apple/2.txt new file mode 100644 index 0000000..77b6494 --- /dev/null +++ b/w/m/Fruit/Apple/2.txt @@ -0,0 +1,3 @@ +Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности. + +Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg \ No newline at end of file diff --git a/w/m/Fruit/Apple/3.txt b/w/m/Fruit/Apple/3.txt new file mode 100644 index 0000000..84903ab --- /dev/null +++ b/w/m/Fruit/Apple/3.txt @@ -0,0 +1,6 @@ +Mycorrhiza is pure happiness + +Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности. + +Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg + \ No newline at end of file diff --git a/w/m/Fruit/Apple/4.bin b/w/m/Fruit/Apple/4.bin new file mode 100644 index 0000000..8ccc20e Binary files /dev/null and b/w/m/Fruit/Apple/4.bin differ diff --git a/w/m/Fruit/Apple/4.txt b/w/m/Fruit/Apple/4.txt new file mode 100644 index 0000000..cd819fb --- /dev/null +++ b/w/m/Fruit/Apple/4.txt @@ -0,0 +1,5 @@ + Mycorrhiza is pure happiness + +Красное яблоко. Для съёмки использовалась белая бумага позади и над яблоком, и фотовспышка SB-600 в 1/4 мощности. + +Source: https://commons.wikimedia.org/wiki/File:Red_Apple.jpg diff --git a/w/m/Fruit/Apple/meta.json b/w/m/Fruit/Apple/meta.json index 732f242..84375ef 100644 --- a/w/m/Fruit/Apple/meta.json +++ b/w/m/Fruit/Apple/meta.json @@ -1,13 +1 @@ -{ -"revisions":{ - "1":{ - "name": "Apple", - "time": 1591639464, - "author": "bouncepaw", - "comment": "add apple pic hehehe", - "tags": ["img"], - "text_mime": "text/plain", - "binary_mime": "image/jpeg" - } -} -} +{"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"}}} \ No newline at end of file diff --git a/w/m/sys/main.css/1.txt b/w/m/sys/main.css/1.txt index 2ab4057..e08d902 100644 --- a/w/m/sys/main.css/1.txt +++ b/w/m/sys/main.css/1.txt @@ -1,7 +1,236 @@ -b { color: red; } -article { border: 1px black solid; } +*, *::before, *::after { + 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 .naviwrapper__buttons { 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 } +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; +} +