diff --git a/.gitignore b/.gitignore index c450f48..9632b28 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ mycorrhiza +hyphae/*.gog diff --git a/Makefile b/Makefile index 5f2d62e..0b9db7e 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ run: build ./mycorrhiza metarrhiza -run_with_fixed_auth: build +auth_run: build ./mycorrhiza -auth-method fixed metarrhiza +gemini_run: build + ./mycorrhiza -gemini-cert-path "." metarrhiza + build: go generate go build . diff --git a/README.md b/README.md index db4db81..d7269e6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ๐ MycorrhizaWiki 0.12 +# ๐ MycorrhizaWiki 0.13 A wiki engine. [Main wiki](https://mycorrhiza.lesarbr.es) @@ -25,6 +25,8 @@ Options: What auth method to use. Variants: "none", "fixed" (default "none") -fixed-credentials-path string Used when -auth-method=fixed. Path to file with user credentials. (default "mycocredentials.json") + -gemini-cert-path string + Directory where you store Gemini certificates. Leave empty if you don't want to use Gemini. -header-links-hypha string Optional hypha that overrides the header links -home string @@ -57,6 +59,7 @@ Options: * Hyphae can be renamed (recursive renaming of subhyphae is also supported) * Light on resources * Authorization with pre-set credentials +* Basic Gemini protocol support ## Contributing Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where some development is coordinated. You can also sponsor on [boosty](https://boosty.to/bouncepaw). Feel free to open an issue or contact directly. diff --git a/templates/asset.qtpl b/assets/assets.qtpl similarity index 100% rename from templates/asset.qtpl rename to assets/assets.qtpl diff --git a/templates/asset.qtpl.go b/assets/assets.qtpl.go similarity index 58% rename from templates/asset.qtpl.go rename to assets/assets.qtpl.go index 95a984b..160549b 100644 --- a/templates/asset.qtpl.go +++ b/assets/assets.qtpl.go @@ -1,58 +1,124 @@ -// Code generated by qtc from "asset.qtpl". DO NOT EDIT. +// Code generated by qtc from "assets.qtpl". DO NOT EDIT. // See https://github.com/valyala/quicktemplate for details. -//line templates/asset.qtpl:1 -package templates +//line assets/assets.qtpl:1 +package assets -//line templates/asset.qtpl:1 +//line assets/assets.qtpl:1 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) -//line templates/asset.qtpl:1 +//line assets/assets.qtpl:1 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) -//line templates/asset.qtpl:1 +//line assets/assets.qtpl:1 func StreamDefaultCSS(qw422016 *qt422016.Writer) { -//line templates/asset.qtpl:1 +//line assets/assets.qtpl:1 qw422016.N().S(` `) -//line templates/asset.qtpl:2 - qw422016.N().S(`/* Layout stuff */ -@media screen and (min-width: 800px) { - main { padding:1rem 2rem; margin: 0 auto; width: 800px; } - .hypha-tabs { padding: 1rem 2rem; margin: 0 auto; width: 800px; } - header { margin: 0 auto; width: 800px; } +//line assets/assets.qtpl:2 + qw422016.N().S(`.amnt-grid { display: grid; grid-template-columns: 1fr 1fr; } +.upload-binary__input { display: block; margin: .25rem 0; } + +.modal__title { font-size: 2rem; } +.modal__title_small { font-size: 1.5rem; } +.modal__confirmation-msg { margin: 0 0 .5rem 0; } +.modal__action { display: inline-block; font-size: 1rem; padding: .25rem; border-radius: .25rem; } +.modal__submit { border: 1px #999 solid; } +.modal__cancel { border: 1px #999 dashed; text-decoration: none; } + +.hypha-list { padding-left: 0; } +.hypha-list__entry { list-style-type: none; } +.hypha-list__link { text-decoration: none; display: inline-block; padding: .25rem; } +.hypha-list__link:hover { text-decoration: underline; } +.hypha-list__amnt-type { font-size: smaller; color: #999; } + +/* General element positions, from small to big */ +/* Phones and whatnot */ +.layout { display: grid; row-gap: 1rem; } +header { width: 100%; margin-bottom: 1rem; } +.header-links__list, .hypha-tabs__flex { margin: 0; padding: 0; display: flex; flex-wrap: wrap; } +.header-links__entry, .hypha-tabs__tab { list-style-type: none; } + +.header-links__entry { margin-right: .5rem; } +.header-links__entry_user { font-style:italic; } +.header-links__link { display: inline-block; padding: .25rem; text-decoration: none; } + +.hypha-tabs { padding: 0; margin: 0; } +.hypha-tabs__tab { margin-right: .5rem; padding: 0; } +.hypha-tabs__link { display: inline-block; padding: .25rem; text-decoration: none; } +.hypha-tabs__selection { display: inline-block; padding: .25rem; font-weight: bold; } + +.layout-card li { list-style-type: none; } +.backlinks__list { padding: 0; margin: 0; } +.backlinks__link { text-decoration: none; display: block; padding: .25rem; padding-left: 1.25rem; } + +@media screen and (max-width: 800px) { + .amnt-grid { grid-template-columns: 1fr; } + .layout { grid-template-column: auto; grid-template-row: auto auto auto; } + .main-width { width: 100%; } + main { padding: 1rem; margin: 0; } +} + +/* No longer a phone but still small screen: draw normal tabs, center main */ +@media screen and (min-width: 801px) { + .main-width { padding: 1rem 2rem; width: 800px; margin: 0 auto; } + main { border-radius: .25rem; } + .layout-card { width: 800px; margin: 0 auto; } + + .header-links { padding: 0; } .header-links__entry { margin-right: 1.5rem; } .header-links__entry_user { margin: 0 2rem 0 auto; } .header-links__entry:nth-of-type(1), - .hypha-tabs__tab:nth-of-type(1) { margin-left: 2rem; } - .hypha-tabs__tab { margin-right: 1.5rem; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); border-bottom: 2px #ddd solid; padding: 0 .5rem; } + + .hypha-tabs { padding: 0; } + .hypha-tabs__tab { border-radius: .25rem .25rem 0 0; margin-right: 0; } + .hypha-tabs__selection, .hypha-tabs__link { padding: .25rem .5rem; } + + .header-links__entry:nth-of-type(1), .hypha-tabs__tab:nth-of-type(1) { margin-left: 2rem; } } -@media screen and (max-width: 800px) { - main { padding: 1rem; margin: 0; width: 100%; } - .hypha-tabs{ padding: 1rem; margin: 0; width: 100%; } - .hypha-tabs__tab { box-shadow: none; margin-right: .5rem; padding: .25rem .5rem; } - header { width: 100%; } - .header-links__entry { margin-right: .5rem; } + +/* Wide enough to fit two columns ok */ +@media screen and (min-width: 1100px) { + .layout { display: grid; grid-template-columns: auto 1fr; column-gap: 1rem; margin: 0 1rem; row-gap: 1rem; } + .main-width { margin: 0; } + main { grid-column: 1 / span 1; grid-row: 1 / span 2; } + .relative-hyphae { grid-column: 2 / span 1; grid-row: 1 / span 1; } + .layout-card { width: 100%; } } + +@media screen and (min-width: 1250px) { + .layout { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); } + .layout-card { max-width: 16rem; } + .main-width { margin: 0 auto; } + .backlinks { grid-column: 1 / span 1; margin-right: 0; } + main { grid-column: 2 / span 1; } + .relative-hyphae { grid-column: 3 / span 1; margin-left: 0; } + + .backlinks__title { text-align: right; } + .backlinks__link { text-align: right; padding-right: 1.25rem; padding-left: .25rem; } +} + *, *::before, *::after {box-sizing: border-box;} html { height:100%; padding:0; } -body {height:100%; margin:0; font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;} -main {border-radius: 0 0 .25rem .25rem; } +body {height:100%; margin:0; } +body, input { font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;} main > form {margin-bottom:1rem;} textarea {font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;} -.edit_no-preview {height:100%;} -.edit_with-preview .edit-form textarea { min-height: 500px; } + +.edit { min-height: 80vh; } +.edit__title { margin-top: 0; } .edit__preview { border: 2px dashed #ddd; } -.edit-form {height:90%;} -.edit-form textarea {width:100%;height:90%;} +.edit-form {height:70vh;} +.edit-form textarea {width:100%;height:95%;} .edit-form__save { font-weight: bold; } + .icon {margin-right: .25rem; vertical-align: bottom; } main h1:not(.navi-title) {font-size:1.7rem;} @@ -67,7 +133,10 @@ blockquote { margin-left: 0; padding-left: 1rem; } .wikilink_mailto::before { content: url("/static/icon/mailto"); } article { overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; line-height: 150%; } -article h1, article h2, article h3, article h4, article h5, article h6 { margin: 1.5rem 0 0 0; } +main h1, main h2, main h3, main h4, main h5, main h6 { margin: 1.5rem 0 0 0; } +.heading__link { text-decoration: none; display: inline-block; } +.heading__link::after { width: 1rem; content: "ยง"; color: transparent; } +.heading__link:hover::after, .heading__link:active::after { color: #999; } article p { margin: .5rem 0; } article ul, ol { padding-left: 1.5rem; margin: .5rem 0; } article code { padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; } @@ -85,6 +154,7 @@ article pre.codeblock { padding:.5rem; white-space: pre-wrap; border-radius: .25 .binary-container_with-video video, .binary-container_with-audio audio {width: 100%} +.subhyphae__title { padding-bottom: .5rem; clear: both; } .navi-title { padding-bottom: .5rem; margin: .25rem 0; } .navi-title a {text-decoration:none; } .navi-title__separator { margin: 0 .25rem; } @@ -101,19 +171,11 @@ figcaption { padding-bottom: .5rem; } #new-name {width:100%;} -header { margin-bottom: .5rem; } -.header-links__entry_user { font-style:italic; } -.header-links__link { text-decoration: none; display: block; width: 100%; height: 100%; padding: .25rem; } -.hypha-tabs { padding: 0; } -.header-links__list, .hypha-tabs__flex { margin: 0; padding: 0; display: flex; flex-wrap: wrap; } -.header-links__entry, .hypha-tabs__tab { list-style-type: none; } -.hypha-tabs__tab a { text-decoration: none; } -.hypha-tabs__tab_active { font-weight: bold; } -.rc-entry { display: grid; list-style-type: none; padding: .25rem; grid-template-columns: 1fr 1fr; } +.rc-entry { display: grid; list-style-type: none; padding: .25rem; grid-template-columns: 1fr 1fr; border-radius: .25rem; } .rc-entry__time { font-style: italic; } .rc-entry__hash { font-style: italic; text-align: right; } -.rc-entry__links { grid-column: 1 / span 2; } +.rc-entry__links, .rc-entry__msg { grid-column: 1 / span 2; } .rc-entry__author { font-style: italic; } .prevnext__el { display: block-inline; min-width: 40%; padding: .5rem; margin-bottom: .25rem; text-decoration: none; border-radius: .25rem; } @@ -132,6 +194,22 @@ table { border: #ddd 1px solid; border-radius: .25rem; min-width: 4rem; } td { padding: .25rem; } caption { caption-side: top; font-size: small; } +.subhyphae__list, .subhyphae__list ul { display: flex; padding: 0; margin: 0; flex-wrap: wrap; } +.subhyphae__entry { list-style-type: none; border: 1px solid #999; padding: 0; margin: .125rem; border-radius: .25rem; } +.subhyphae__link { display: block; padding: .25rem; text-decoration: none; } +.subhyphae__link:hover { background: #eee; } + +.navitree { padding: 0; margin: 0; } +.navitree__entry { } +.navitree > .navitree__entry > a::before { display: inline-block; width: .5rem; color: #999; margin: 0 .25rem; } +.navitree > .navitree__entry_infertile > a::before { content: " "} /* nbsp, careful */ +.navitree > .navitree__sibling_fertile > a::before { content: "โธ"} +.navitree__trunk { border-left: 1px #999 solid; } +.navitree__link { text-decoration: none; display: block; padding: .25rem; } +.navitree__entry_this > span { display: block; padding: .25rem; font-weight: bold; } +.navitree__entry_this > span::before { content: " "; display: inline-block; width: 1rem; } + + /* Color stuff */ /* Lighter stuff #eee */ article code, @@ -142,11 +220,24 @@ article .codeblock, .prevnext__el, table { background-color: #eee; } +.hypha-tabs__tab { background-color: #eee; } +.hypha-tabs__tab a { color: black; } +.hypha-tabs__tab_active { border-bottom: 2px white solid; background: white; } + @media screen and (max-width: 800px) { - .hypha-tabs { background-color: white; } - .hypha-tabs__tab { box-shadow: none; } + .hypha-tabs, + .hypha-tabs__tab { background-color: white; } } +@media screen and (min-width: 801px) { + .hypha-tabs__tab { border: 1px #ddd solid; } + .hypha-tabs__tab_active { border-bottom: 1px white solid; } +} + +.layout-card { border-radius: .25rem; background-color: white; } +.layout-card__title { font-size: 1rem; margin: 0; padding: .25rem .5rem; border-radius: .25rem .25rem 0 0; } +.layout-card__title { background-color: #eee; } + /* Other stuff */ html { background-color: #ddd; background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23bbbbbb' fill-opacity='0.4'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); @@ -154,11 +245,7 @@ background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox= header { background-color: #bbb; } .header-links__link { color: black; } .header-links__link:hover { background-color: #eee; } - -main, .hypha-tabs__tab { background-color: white; } -.hypha-tabs__tab { clip-path: inset(-20px -20px 0 -20px); } -.hypha-tabs__tab a { color: black; } -.hypha-tabs__tab_active { border-bottom: 2px white solid; } +main { background-color: white; } blockquote { border-left: 4px black solid; } .wikilink_new {color:#a55858;} @@ -169,21 +256,24 @@ blockquote { border-left: 4px black solid; } .upload-amnt { border: #eee 1px solid; } td { border: #ddd 1px solid; } +.navitree__link:hover, .backlinks__link:hover { background-color: #eee; } + /* Dark theme! */ @media (prefers-color-scheme: dark) { html { background: #222; color: #ddd; } -main, article, .hypha-tabs__tab, header { background-color: #343434; color: #ddd; } +main, article, .hypha-tabs__tab, header, .layout-card { background-color: #343434; color: #ddd; } a, .wikilink_external { color: #f1fa8c; } a:visited, .wikilink_external:visited { color: #ffb86c; } .wikilink_new, .wikilink_new:visited { color: #dd4444; } +.navitree__link:hover, .backlinks__link:hover { background-color: #444; } .header-links__link, .header-links__link:visited, .prevnext__el, .prevnext__el:visited { color: #ddd; } .header-links__link:hover { background-color: #444; } .hypha-tabs__tab a, .hypha-tabs__tab { color: #ddd; background-color: #232323; border: 0; } -.hypha-tabs__tab_active { background-color: #343434; } +.layout-card__title, .hypha-tabs__tab_active { background-color: #343434; } blockquote { border-left: 4px #ddd solid; } @@ -206,169 +296,170 @@ mark { background: rgba(130, 80, 30, 5); color: inherit; } } } +.backlinks { display: none; } `) -//line templates/asset.qtpl:2 +//line assets/assets.qtpl:2 qw422016.N().S(` `) -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 } -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 func WriteDefaultCSS(qq422016 qtio422016.Writer) { -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 StreamDefaultCSS(qw422016) -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 qt422016.ReleaseWriter(qw422016) -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 } -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 func DefaultCSS() string { -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 qb422016 := qt422016.AcquireByteBuffer() -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 WriteDefaultCSS(qb422016) -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 qs422016 := string(qb422016.B) -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 qt422016.ReleaseByteBuffer(qb422016) -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 return qs422016 -//line templates/asset.qtpl:3 +//line assets/assets.qtpl:3 } // Next three are from https://remixicon.com/ -//line templates/asset.qtpl:6 +//line assets/assets.qtpl:6 func StreamIconHTTP(qw422016 *qt422016.Writer) { -//line templates/asset.qtpl:6 +//line assets/assets.qtpl:6 qw422016.N().S(` `) -//line templates/asset.qtpl:7 +//line assets/assets.qtpl:7 qw422016.N().S(` `) -//line templates/asset.qtpl:7 +//line assets/assets.qtpl:7 qw422016.N().S(` `) -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 } -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 func WriteIconHTTP(qq422016 qtio422016.Writer) { -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 StreamIconHTTP(qw422016) -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 qt422016.ReleaseWriter(qw422016) -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 } -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 func IconHTTP() string { -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 qb422016 := qt422016.AcquireByteBuffer() -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 WriteIconHTTP(qb422016) -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 qs422016 := string(qb422016.B) -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 qt422016.ReleaseByteBuffer(qb422016) -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 return qs422016 -//line templates/asset.qtpl:8 +//line assets/assets.qtpl:8 } -//line templates/asset.qtpl:10 +//line assets/assets.qtpl:10 func StreamIconGemini(qw422016 *qt422016.Writer) { -//line templates/asset.qtpl:10 +//line assets/assets.qtpl:10 qw422016.N().S(` `) -//line templates/asset.qtpl:11 +//line assets/assets.qtpl:11 qw422016.N().S(` `) -//line templates/asset.qtpl:11 +//line assets/assets.qtpl:11 qw422016.N().S(` `) -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 } -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 func WriteIconGemini(qq422016 qtio422016.Writer) { -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 StreamIconGemini(qw422016) -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 qt422016.ReleaseWriter(qw422016) -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 } -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 func IconGemini() string { -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 qb422016 := qt422016.AcquireByteBuffer() -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 WriteIconGemini(qb422016) -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 qs422016 := string(qb422016.B) -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 qt422016.ReleaseByteBuffer(qb422016) -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 return qs422016 -//line templates/asset.qtpl:12 +//line assets/assets.qtpl:12 } -//line templates/asset.qtpl:14 +//line assets/assets.qtpl:14 func StreamIconMailto(qw422016 *qt422016.Writer) { -//line templates/asset.qtpl:14 +//line assets/assets.qtpl:14 qw422016.N().S(` `) -//line templates/asset.qtpl:15 +//line assets/assets.qtpl:15 qw422016.N().S(` `) -//line templates/asset.qtpl:15 +//line assets/assets.qtpl:15 qw422016.N().S(` `) -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 } -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 func WriteIconMailto(qq422016 qtio422016.Writer) { -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 StreamIconMailto(qw422016) -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 qt422016.ReleaseWriter(qw422016) -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 } -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 func IconMailto() string { -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 qb422016 := qt422016.AcquireByteBuffer() -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 WriteIconMailto(qb422016) -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 qs422016 := string(qb422016.B) -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 qt422016.ReleaseByteBuffer(qb422016) -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 return qs422016 -//line templates/asset.qtpl:16 +//line assets/assets.qtpl:16 } // This is a modified version of https://www.svgrepo.com/svg/232085/rat -//line templates/asset.qtpl:19 +//line assets/assets.qtpl:19 func StreamIconGopher(qw422016 *qt422016.Writer) { -//line templates/asset.qtpl:19 +//line assets/assets.qtpl:19 qw422016.N().S(` `) -//line templates/asset.qtpl:20 +//line assets/assets.qtpl:20 qw422016.N().S(` `) -//line templates/asset.qtpl:20 +//line assets/assets.qtpl:20 qw422016.N().S(` `) -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 } -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 func WriteIconGopher(qq422016 qtio422016.Writer) { -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 StreamIconGopher(qw422016) -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 qt422016.ReleaseWriter(qw422016) -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 } -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 func IconGopher() string { -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 qb422016 := qt422016.AcquireByteBuffer() -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 WriteIconGopher(qb422016) -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 qs422016 := string(qb422016.B) -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 qt422016.ReleaseByteBuffer(qb422016) -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 return qs422016 -//line templates/asset.qtpl:21 +//line assets/assets.qtpl:21 } diff --git a/templates/default.css b/assets/default.css similarity index 53% rename from templates/default.css rename to assets/default.css index 0d1ccda..9c93b48 100644 --- a/templates/default.css +++ b/assets/default.css @@ -1,33 +1,99 @@ -/* Layout stuff */ -@media screen and (min-width: 800px) { - main { padding:1rem 2rem; margin: 0 auto; width: 800px; } - .hypha-tabs { padding: 1rem 2rem; margin: 0 auto; width: 800px; } - header { margin: 0 auto; width: 800px; } +.amnt-grid { display: grid; grid-template-columns: 1fr 1fr; } +.upload-binary__input { display: block; margin: .25rem 0; } + +.modal__title { font-size: 2rem; } +.modal__title_small { font-size: 1.5rem; } +.modal__confirmation-msg { margin: 0 0 .5rem 0; } +.modal__action { display: inline-block; font-size: 1rem; padding: .25rem; border-radius: .25rem; } +.modal__submit { border: 1px #999 solid; } +.modal__cancel { border: 1px #999 dashed; text-decoration: none; } + +.hypha-list { padding-left: 0; } +.hypha-list__entry { list-style-type: none; } +.hypha-list__link { text-decoration: none; display: inline-block; padding: .25rem; } +.hypha-list__link:hover { text-decoration: underline; } +.hypha-list__amnt-type { font-size: smaller; color: #999; } + +/* General element positions, from small to big */ +/* Phones and whatnot */ +.layout { display: grid; row-gap: 1rem; } +header { width: 100%; margin-bottom: 1rem; } +.header-links__list, .hypha-tabs__flex { margin: 0; padding: 0; display: flex; flex-wrap: wrap; } +.header-links__entry, .hypha-tabs__tab { list-style-type: none; } + +.header-links__entry { margin-right: .5rem; } +.header-links__entry_user { font-style:italic; } +.header-links__link { display: inline-block; padding: .25rem; text-decoration: none; } + +.hypha-tabs { padding: 0; margin: 0; } +.hypha-tabs__tab { margin-right: .5rem; padding: 0; } +.hypha-tabs__link { display: inline-block; padding: .25rem; text-decoration: none; } +.hypha-tabs__selection { display: inline-block; padding: .25rem; font-weight: bold; } + +.layout-card li { list-style-type: none; } +.backlinks__list { padding: 0; margin: 0; } +.backlinks__link { text-decoration: none; display: block; padding: .25rem; padding-left: 1.25rem; } + +@media screen and (max-width: 800px) { + .amnt-grid { grid-template-columns: 1fr; } + .layout { grid-template-column: auto; grid-template-row: auto auto auto; } + .main-width { width: 100%; } + main { padding: 1rem; margin: 0; } +} + +/* No longer a phone but still small screen: draw normal tabs, center main */ +@media screen and (min-width: 801px) { + .main-width { padding: 1rem 2rem; width: 800px; margin: 0 auto; } + main { border-radius: .25rem; } + .layout-card { width: 800px; margin: 0 auto; } + + .header-links { padding: 0; } .header-links__entry { margin-right: 1.5rem; } .header-links__entry_user { margin: 0 2rem 0 auto; } .header-links__entry:nth-of-type(1), - .hypha-tabs__tab:nth-of-type(1) { margin-left: 2rem; } - .hypha-tabs__tab { margin-right: 1.5rem; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); border-bottom: 2px #ddd solid; padding: 0 .5rem; } + + .hypha-tabs { padding: 0; } + .hypha-tabs__tab { border-radius: .25rem .25rem 0 0; margin-right: 0; } + .hypha-tabs__selection, .hypha-tabs__link { padding: .25rem .5rem; } + + .header-links__entry:nth-of-type(1), .hypha-tabs__tab:nth-of-type(1) { margin-left: 2rem; } } -@media screen and (max-width: 800px) { - main { padding: 1rem; margin: 0; width: 100%; } - .hypha-tabs{ padding: 1rem; margin: 0; width: 100%; } - .hypha-tabs__tab { box-shadow: none; margin-right: .5rem; padding: .25rem .5rem; } - header { width: 100%; } - .header-links__entry { margin-right: .5rem; } + +/* Wide enough to fit two columns ok */ +@media screen and (min-width: 1100px) { + .layout { display: grid; grid-template-columns: auto 1fr; column-gap: 1rem; margin: 0 1rem; row-gap: 1rem; } + .main-width { margin: 0; } + main { grid-column: 1 / span 1; grid-row: 1 / span 2; } + .relative-hyphae { grid-column: 2 / span 1; grid-row: 1 / span 1; } + .layout-card { width: 100%; } } + +@media screen and (min-width: 1250px) { + .layout { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); } + .layout-card { max-width: 16rem; } + .main-width { margin: 0 auto; } + .backlinks { grid-column: 1 / span 1; margin-right: 0; } + main { grid-column: 2 / span 1; } + .relative-hyphae { grid-column: 3 / span 1; margin-left: 0; } + + .backlinks__title { text-align: right; } + .backlinks__link { text-align: right; padding-right: 1.25rem; padding-left: .25rem; } +} + *, *::before, *::after {box-sizing: border-box;} html { height:100%; padding:0; } -body {height:100%; margin:0; font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;} -main {border-radius: 0 0 .25rem .25rem; } +body {height:100%; margin:0; } +body, input { font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;} main > form {margin-bottom:1rem;} textarea {font-size:16px; font-family: 'PT Sans', 'Liberation Sans', sans-serif;} -.edit_no-preview {height:100%;} -.edit_with-preview .edit-form textarea { min-height: 500px; } + +.edit { min-height: 80vh; } +.edit__title { margin-top: 0; } .edit__preview { border: 2px dashed #ddd; } -.edit-form {height:90%;} -.edit-form textarea {width:100%;height:90%;} +.edit-form {height:70vh;} +.edit-form textarea {width:100%;height:95%;} .edit-form__save { font-weight: bold; } + .icon {margin-right: .25rem; vertical-align: bottom; } main h1:not(.navi-title) {font-size:1.7rem;} @@ -42,7 +108,10 @@ blockquote { margin-left: 0; padding-left: 1rem; } .wikilink_mailto::before { content: url("/static/icon/mailto"); } article { overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; line-height: 150%; } -article h1, article h2, article h3, article h4, article h5, article h6 { margin: 1.5rem 0 0 0; } +main h1, main h2, main h3, main h4, main h5, main h6 { margin: 1.5rem 0 0 0; } +.heading__link { text-decoration: none; display: inline-block; } +.heading__link::after { width: 1rem; content: "ยง"; color: transparent; } +.heading__link:hover::after, .heading__link:active::after { color: #999; } article p { margin: .5rem 0; } article ul, ol { padding-left: 1.5rem; margin: .5rem 0; } article code { padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; } @@ -60,6 +129,7 @@ article pre.codeblock { padding:.5rem; white-space: pre-wrap; border-radius: .25 .binary-container_with-video video, .binary-container_with-audio audio {width: 100%} +.subhyphae__title { padding-bottom: .5rem; clear: both; } .navi-title { padding-bottom: .5rem; margin: .25rem 0; } .navi-title a {text-decoration:none; } .navi-title__separator { margin: 0 .25rem; } @@ -76,19 +146,11 @@ figcaption { padding-bottom: .5rem; } #new-name {width:100%;} -header { margin-bottom: .5rem; } -.header-links__entry_user { font-style:italic; } -.header-links__link { text-decoration: none; display: block; width: 100%; height: 100%; padding: .25rem; } -.hypha-tabs { padding: 0; } -.header-links__list, .hypha-tabs__flex { margin: 0; padding: 0; display: flex; flex-wrap: wrap; } -.header-links__entry, .hypha-tabs__tab { list-style-type: none; } -.hypha-tabs__tab a { text-decoration: none; } -.hypha-tabs__tab_active { font-weight: bold; } -.rc-entry { display: grid; list-style-type: none; padding: .25rem; grid-template-columns: 1fr 1fr; } +.rc-entry { display: grid; list-style-type: none; padding: .25rem; grid-template-columns: 1fr 1fr; border-radius: .25rem; } .rc-entry__time { font-style: italic; } .rc-entry__hash { font-style: italic; text-align: right; } -.rc-entry__links { grid-column: 1 / span 2; } +.rc-entry__links, .rc-entry__msg { grid-column: 1 / span 2; } .rc-entry__author { font-style: italic; } .prevnext__el { display: block-inline; min-width: 40%; padding: .5rem; margin-bottom: .25rem; text-decoration: none; border-radius: .25rem; } @@ -107,6 +169,22 @@ table { border: #ddd 1px solid; border-radius: .25rem; min-width: 4rem; } td { padding: .25rem; } caption { caption-side: top; font-size: small; } +.subhyphae__list, .subhyphae__list ul { display: flex; padding: 0; margin: 0; flex-wrap: wrap; } +.subhyphae__entry { list-style-type: none; border: 1px solid #999; padding: 0; margin: .125rem; border-radius: .25rem; } +.subhyphae__link { display: block; padding: .25rem; text-decoration: none; } +.subhyphae__link:hover { background: #eee; } + +.navitree { padding: 0; margin: 0; } +.navitree__entry { } +.navitree > .navitree__entry > a::before { display: inline-block; width: .5rem; color: #999; margin: 0 .25rem; } +.navitree > .navitree__entry_infertile > a::before { content: " "} /* nbsp, careful */ +.navitree > .navitree__sibling_fertile > a::before { content: "โธ"} +.navitree__trunk { border-left: 1px #999 solid; } +.navitree__link { text-decoration: none; display: block; padding: .25rem; } +.navitree__entry_this > span { display: block; padding: .25rem; font-weight: bold; } +.navitree__entry_this > span::before { content: " "; display: inline-block; width: 1rem; } + + /* Color stuff */ /* Lighter stuff #eee */ article code, @@ -117,11 +195,24 @@ article .codeblock, .prevnext__el, table { background-color: #eee; } +.hypha-tabs__tab { background-color: #eee; } +.hypha-tabs__tab a { color: black; } +.hypha-tabs__tab_active { border-bottom: 2px white solid; background: white; } + @media screen and (max-width: 800px) { - .hypha-tabs { background-color: white; } - .hypha-tabs__tab { box-shadow: none; } + .hypha-tabs, + .hypha-tabs__tab { background-color: white; } } +@media screen and (min-width: 801px) { + .hypha-tabs__tab { border: 1px #ddd solid; } + .hypha-tabs__tab_active { border-bottom: 1px white solid; } +} + +.layout-card { border-radius: .25rem; background-color: white; } +.layout-card__title { font-size: 1rem; margin: 0; padding: .25rem .5rem; border-radius: .25rem .25rem 0 0; } +.layout-card__title { background-color: #eee; } + /* Other stuff */ html { background-color: #ddd; background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23bbbbbb' fill-opacity='0.4'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); @@ -129,11 +220,7 @@ background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox= header { background-color: #bbb; } .header-links__link { color: black; } .header-links__link:hover { background-color: #eee; } - -main, .hypha-tabs__tab { background-color: white; } -.hypha-tabs__tab { clip-path: inset(-20px -20px 0 -20px); } -.hypha-tabs__tab a { color: black; } -.hypha-tabs__tab_active { border-bottom: 2px white solid; } +main { background-color: white; } blockquote { border-left: 4px black solid; } .wikilink_new {color:#a55858;} @@ -144,21 +231,24 @@ blockquote { border-left: 4px black solid; } .upload-amnt { border: #eee 1px solid; } td { border: #ddd 1px solid; } +.navitree__link:hover, .backlinks__link:hover { background-color: #eee; } + /* Dark theme! */ @media (prefers-color-scheme: dark) { html { background: #222; color: #ddd; } -main, article, .hypha-tabs__tab, header { background-color: #343434; color: #ddd; } +main, article, .hypha-tabs__tab, header, .layout-card { background-color: #343434; color: #ddd; } a, .wikilink_external { color: #f1fa8c; } a:visited, .wikilink_external:visited { color: #ffb86c; } .wikilink_new, .wikilink_new:visited { color: #dd4444; } +.navitree__link:hover, .backlinks__link:hover { background-color: #444; } .header-links__link, .header-links__link:visited, .prevnext__el, .prevnext__el:visited { color: #ddd; } .header-links__link:hover { background-color: #444; } .hypha-tabs__tab a, .hypha-tabs__tab { color: #ddd; background-color: #232323; border: 0; } -.hypha-tabs__tab_active { background-color: #343434; } +.layout-card__title, .hypha-tabs__tab_active { background-color: #343434; } blockquote { border-left: 4px #ddd solid; } @@ -181,3 +271,4 @@ mark { background: rgba(130, 80, 30, 5); color: inherit; } } } +.backlinks { display: none; } diff --git a/templates/icon/gemini-protocol-icon.svg b/assets/icon/gemini-protocol-icon.svg similarity index 100% rename from templates/icon/gemini-protocol-icon.svg rename to assets/icon/gemini-protocol-icon.svg diff --git a/templates/icon/gopher-protocol-icon.svg b/assets/icon/gopher-protocol-icon.svg similarity index 100% rename from templates/icon/gopher-protocol-icon.svg rename to assets/icon/gopher-protocol-icon.svg diff --git a/templates/icon/http-protocol-icon.svg b/assets/icon/http-protocol-icon.svg similarity index 100% rename from templates/icon/http-protocol-icon.svg rename to assets/icon/http-protocol-icon.svg diff --git a/templates/icon/mailto-protocol-icon.svg b/assets/icon/mailto-protocol-icon.svg similarity index 100% rename from templates/icon/mailto-protocol-icon.svg rename to assets/icon/mailto-protocol-icon.svg diff --git a/flag.go b/flag.go index 47a2134..77a7d05 100644 --- a/flag.go +++ b/flag.go @@ -19,6 +19,7 @@ func init() { flag.StringVar(&util.AuthMethod, "auth-method", "none", "What auth method to use. Variants: \"none\", \"fixed\"") flag.StringVar(&util.FixedCredentialsPath, "fixed-credentials-path", "mycocredentials.json", "Used when -auth-method=fixed. Path to file with user credentials.") flag.StringVar(&util.HeaderLinksHypha, "header-links-hypha", "", "Optional hypha that overrides the header links") + flag.StringVar(&util.GeminiCertPath, "gemini-cert-path", "", "Directory where you store Gemini certificates. Leave empty if you don't want to use Gemini.") } // Do the things related to cli args and die maybe @@ -41,9 +42,9 @@ func parseCliArgs() { util.URL = "http://0.0.0.0:" + util.ServerPort } - util.HomePage = CanonicalName(util.HomePage) - util.UserHypha = CanonicalName(util.UserHypha) - util.HeaderLinksHypha = CanonicalName(util.HeaderLinksHypha) + util.HomePage = util.CanonicalName(util.HomePage) + util.UserHypha = util.CanonicalName(util.UserHypha) + util.HeaderLinksHypha = util.CanonicalName(util.HeaderLinksHypha) switch util.AuthMethod { case "none": diff --git a/gemini.go b/gemini.go new file mode 100644 index 0000000..b86c3a4 --- /dev/null +++ b/gemini.go @@ -0,0 +1,84 @@ +package main + +import ( + "crypto/tls" + "crypto/x509/pkix" + "io/ioutil" + "log" + "path/filepath" + "time" + + "git.sr.ht/~adnano/go-gemini" + "git.sr.ht/~adnano/go-gemini/certificate" + + "github.com/bouncepaw/mycorrhiza/hyphae" + "github.com/bouncepaw/mycorrhiza/markup" + "github.com/bouncepaw/mycorrhiza/util" +) + +func geminiHomeHypha(w *gemini.ResponseWriter, rq *gemini.Request) { + log.Println(rq.URL) + w.Write([]byte(`# MycorrhizaWiki + +You have successfully served the wiki through Gemini. Currently, support is really work-in-progress; you should resort to using Mycorrhiza through the web protocols. + +Visit home hypha: +=> /hypha/` + util.HomePage)) +} + +func geminiHypha(w *gemini.ResponseWriter, rq *gemini.Request) { + log.Println(rq.URL) + var ( + hyphaName = geminiHyphaNameFromRq(rq, "page", "hypha") + h = hyphae.ByName(hyphaName) + hasAmnt = h.Exists && h.BinaryPath != "" + contents string + ) + if h.Exists { + fileContentsT, errT := ioutil.ReadFile(h.TextPath) + if errT == nil { + md := markup.Doc(hyphaName, string(fileContentsT)) + contents = md.AsGemtext() + } + } + if hasAmnt { + w.Write([]byte("This hypha has an attachment\n")) + } + w.Write([]byte(contents)) +} + +func handleGemini() { + if util.GeminiCertPath == "" { + return + } + certPath, err := filepath.Abs(util.GeminiCertPath) + if err != nil { + log.Fatal(err) + } + + var server gemini.Server + server.ReadTimeout = 30 * time.Second + server.WriteTimeout = 1 * time.Minute + if err := server.Certificates.Load(certPath); err != nil { + log.Fatal(err) + } + server.CreateCertificate = func(hostname string) (tls.Certificate, error) { + return certificate.Create(certificate.CreateOptions{ + Subject: pkix.Name{ + CommonName: hostname, + }, + DNSNames: []string{hostname}, + Duration: 365 * 24 * time.Hour, + }) + } + + var mux gemini.ServeMux + mux.HandleFunc("/", geminiHomeHypha) + mux.HandleFunc("/hypha/", geminiHypha) + mux.HandleFunc("/page/", geminiHypha) + + server.Handle("localhost", &mux) + if err := server.ListenAndServe(); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 137cbae..26dbe9f 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module github.com/bouncepaw/mycorrhiza go 1.14 require ( + git.sr.ht/~adnano/go-gemini v0.1.13 github.com/adrg/xdg v0.2.2 github.com/gorilla/feeds v1.1.1 github.com/kr/pretty v0.2.1 // indirect github.com/valyala/quicktemplate v1.6.3 + tildegit.org/solderpunk/gemcert v0.0.0-20200801165357-fc14deb27512 // indirect ) diff --git a/go.sum b/go.sum index f950cb8..0aa3df2 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +git.sr.ht/~adnano/go-gemini v0.1.13 h1:vzKkkVrOzMpfJ1AAeE/PChg0Rw5Zf+9HrnwsgVxXUT4= +git.sr.ht/~adnano/go-gemini v0.1.13/go.mod h1:If1VxEWcZDrRt5FeAFnGTcM2Ud1E3BXs3VJ5rnZWKq0= github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo= github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= @@ -33,3 +35,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +tildegit.org/solderpunk/gemcert v0.0.0-20200801165357-fc14deb27512 h1:reGEt1vmGompn/6FitHdBatILTsK9CYnQOCw3weoW/s= +tildegit.org/solderpunk/gemcert v0.0.0-20200801165357-fc14deb27512/go.mod h1:gqBK7AJ5wPR1bpFOuPmlQObYxwXrFdZmNb2vdzquqoA= diff --git a/history/history.go b/history/history.go index f3d117d..d02e156 100644 --- a/history/history.go +++ b/history/history.go @@ -10,7 +10,6 @@ import ( "strings" "time" - "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" ) @@ -77,8 +76,8 @@ func (rev Revision) TimeString() string { return rev.Time.Format(time.RFC822) } -// HyphaeLinks returns a comma-separated list of hyphae that were affected by this revision as HTML string. -func (rev Revision) HyphaeLinks() (html string) { +// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string. +func (rev Revision) HyphaeLinksHTML() (html string) { hyphae := rev.hyphaeAffected() for i, hyphaName := range hyphae { if i > 0 { @@ -92,7 +91,7 @@ func (rev Revision) HyphaeLinks() (html string) { func (rev *Revision) descriptionForFeed() (html string) { return fmt.Sprintf( `
%s
-Hyphae affected: %s
`, rev.Message, rev.HyphaeLinks()) +Hyphae affected: %s
`, rev.Message, rev.HyphaeLinksHTML()) } // Try and guess what link is the most important by looking at the message. @@ -111,23 +110,6 @@ func (rev *Revision) bestLink() string { } } -func (rev Revision) RecentChangesEntry() (html string) { - if user.AuthUsed && rev.Username != "anon" { - return fmt.Sprintf(` - -%v
", hop.Errs))
- return
- }
- http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
-}
-
-func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "rename-ask")
- _, isOld = HyphaStorage[hyphaName]
- u = user.FromRequest(rq)
- )
- if !u.CanProceed("rename-confirm") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.")
- log.Println("Rejected", rq.URL)
- return
- }
- util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(rq, hyphaName, isOld), u))
-}
-
-func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "rename-confirm")
- _, isOld = HyphaStorage[hyphaName]
- newName = CanonicalName(rq.PostFormValue("new-name"))
- _, newNameIsUsed = HyphaStorage[newName]
- recursive = rq.PostFormValue("recursive") == "true"
- u = user.FromRequest(rq)
- )
- switch {
- case !u.CanProceed("rename-confirm"):
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.")
- log.Println("Rejected", rq.URL)
- case newNameIsUsed:
- HttpErr(w, http.StatusBadRequest, hyphaName, "Error: hypha exists",
- fmt.Sprintf("Hypha named %s already exists.", hyphaName, hyphaName))
- case newName == "":
- HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no name",
- "No new name is given.")
- case !isOld:
- HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no such hypha",
- "Cannot rename a hypha that does not exist yet.")
- case !HyphaPattern.MatchString(newName):
- HttpErr(w, http.StatusBadRequest, hyphaName, "Error: invalid name",
- "Invalid new name. Names cannot contain characters ^?!:#@><*|\"\\'&%
")
- default:
- if hop := RenameHypha(hyphaName, newName, recursive, u); len(hop.Errs) != 0 {
- HttpErr(w, http.StatusInternalServerError, hyphaName,
- "Error: could not rename hypha",
- fmt.Sprintf("Could not rename this hypha due to an internal error. Server errors: %v
", hop.Errs))
- } else {
- http.Redirect(w, rq, "/page/"+newName, http.StatusSeeOther)
+func factoryHandlerAsker(
+ actionPath string,
+ asker func(*user.User, *hyphae.Hypha) (error, string),
+ succTitleTemplate string,
+ succPageTemplate func(*http.Request, string, bool) string,
+) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ var (
+ hyphaName = HyphaNameFromRq(rq, actionPath)
+ h = hyphae.ByName(hyphaName)
+ u = user.FromRequest(rq)
+ )
+ if err, errtitle := asker(u, h); err != nil {
+ HttpErr(
+ w,
+ http.StatusInternalServerError,
+ hyphaName,
+ errtitle,
+ err.Error())
+ return
}
+ util.HTTP200Page(
+ w,
+ base(
+ fmt.Sprintf(succTitleTemplate, hyphaName),
+ succPageTemplate(rq, hyphaName, h.Exists),
+ u))
}
}
-// handlerDeleteAsk shows a delete dialog.
-func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "delete-ask")
- _, isOld = HyphaStorage[hyphaName]
- u = user.FromRequest(rq)
- )
- if !u.CanProceed("delete-ask") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
- log.Println("Rejected", rq.URL)
- return
+var handlerUnattachAsk = factoryHandlerAsker(
+ "unattach-ask",
+ shroom.CanUnattach,
+ "Unattach %s?",
+ views.UnattachAskHTML,
+)
+
+var handlerDeleteAsk = factoryHandlerAsker(
+ "delete-ask",
+ shroom.CanDelete,
+ "Delete %s?",
+ views.DeleteAskHTML,
+)
+
+var handlerRenameAsk = factoryHandlerAsker(
+ "rename-ask",
+ shroom.CanRename,
+ "Rename %s?",
+ views.RenameAskHTML,
+)
+
+func factoryHandlerConfirmer(
+ actionPath string,
+ confirmer func(*hyphae.Hypha, *user.User, *http.Request) (*history.HistoryOp, string),
+) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, rq *http.Request) {
+ log.Println(rq.URL)
+ var (
+ hyphaName = HyphaNameFromRq(rq, actionPath)
+ h = hyphae.ByName(hyphaName)
+ u = user.FromRequest(rq)
+ )
+ if hop, errtitle := confirmer(h, u, rq); hop.HasErrors() {
+ HttpErr(w, http.StatusInternalServerError, hyphaName,
+ errtitle,
+ hop.FirstErrorText())
+ return
+ }
+ http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther)
}
- util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(rq, hyphaName, isOld), u))
}
-// handlerDeleteConfirm deletes a hypha for sure
-func handlerDeleteConfirm(w http.ResponseWriter, rq *http.Request) {
- log.Println(rq.URL)
- var (
- hyphaName = HyphaNameFromRq(rq, "delete-confirm")
- hyphaData, isOld = HyphaStorage[hyphaName]
- u = user.FromRequest(rq)
- )
- if !u.CanProceed("delete-confirm") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
- log.Println("Rejected", rq.URL)
- return
- }
- if !isOld {
- // The precondition is to have the hypha in the first place.
- HttpErr(w, http.StatusPreconditionFailed, hyphaName,
- "Error: no such hypha",
- "Could not delete this hypha because it does not exist.")
- return
- }
- if hop := hyphaData.DeleteHypha(hyphaName, u); len(hop.Errs) != 0 {
- HttpErr(w, http.StatusInternalServerError, hyphaName,
- "Error: could not delete hypha",
- fmt.Sprintf("Could not delete this hypha due to internal errors. Server errors: %v
", hop.Errs))
- return
- }
- http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
-}
+var handlerUnattachConfirm = factoryHandlerConfirmer(
+ "unattach-confirm",
+ func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.HistoryOp, string) {
+ return shroom.UnattachHypha(u, h)
+ },
+)
+
+var handlerDeleteConfirm = factoryHandlerConfirmer(
+ "delete-confirm",
+ func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.HistoryOp, string) {
+ return shroom.DeleteHypha(u, h)
+ },
+)
+
+var handlerRenameConfirm = factoryHandlerConfirmer(
+ "rename-confirm",
+ func(oldHypha *hyphae.Hypha, u *user.User, rq *http.Request) (*history.HistoryOp, string) {
+ var (
+ newName = util.CanonicalName(rq.PostFormValue("new-name"))
+ recursive = rq.PostFormValue("recursive") == "true"
+ newHypha = hyphae.ByName(newName)
+ )
+ return shroom.RenameHypha(oldHypha, newHypha, recursive, u)
+ },
+)
// 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
- u = user.FromRequest(rq)
+ hyphaName = HyphaNameFromRq(rq, "edit")
+ h = hyphae.ByName(hyphaName)
+ warning string
+ textAreaFill string
+ err error
+ u = user.FromRequest(rq)
)
- if !u.CanProceed("edit") {
- HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
- log.Println("Rejected", rq.URL)
+ if err, errtitle := shroom.CanEdit(u, h); err != nil {
+ HttpErr(w, http.StatusInternalServerError, hyphaName,
+ errtitle,
+ err.Error())
return
}
- if isOld {
- textAreaFill, err = FetchTextPart(hyphaData)
+ if h.Exists {
+ textAreaFill, err = shroom.FetchTextPart(h)
if err != nil {
log.Println(err)
- HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", "Could not fetch text data")
+ HttpErr(w, http.StatusInternalServerError, hyphaName,
+ "Error",
+ "Could not fetch text data")
return
}
} else {
- warning = `You are creating a new hypha.
` + warning = `You are creating a new hypha.
` } - util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(rq, hyphaName, textAreaFill, warning), u)) + util.HTTP200Page( + w, + base( + "Edit "+hyphaName, + views.EditHTML(rq, hyphaName, textAreaFill, warning), + u)) } // handlerUploadText uploads a new text part for the hypha. @@ -208,62 +169,79 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) var ( hyphaName = HyphaNameFromRq(rq, "upload-text") + h = hyphae.ByName(hyphaName) textData = rq.PostFormValue("text") action = rq.PostFormValue("action") u = user.FromRequest(rq) + hop *history.HistoryOp + errtitle string ) - if !u.CanProceed("upload-text") { - HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.") - log.Println("Rejected", rq.URL) - return - } - if textData == "" { - HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed") - return + + if action != "Preview" { + hop, errtitle = shroom.UploadText(h, []byte(textData), u) + if hop.HasErrors() { + HttpErr(w, http.StatusForbidden, hyphaName, + errtitle, + hop.FirstErrorText()) + return + } } + if action == "Preview" { - util.HTTP200Page(w, base("Preview "+hyphaName, templates.PreviewHTML(rq, hyphaName, textData, "", markup.Doc(hyphaName, textData).AsHTML()), u)) - } else if hop := UploadText(hyphaName, textData, u); len(hop.Errs) != 0 { - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error()) + util.HTTP200Page( + w, + base( + "Preview "+hyphaName, + views.PreviewHTML( + rq, + hyphaName, + textData, + "", + markup.Doc(hyphaName, textData).AsHTML()), + u)) } else { - http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) + http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther) } } // handlerUploadBinary uploads a new binary part for the hypha. func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) - var ( - hyphaName = HyphaNameFromRq(rq, "upload-binary") - u = user.FromRequest(rq) - ) - if !u.CanProceed("upload-binary") { - HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to upload attachments.") - log.Println("Rejected", rq.URL) - return - } - rq.ParseMultipartForm(10 << 20) // Set upload limit - file, handler, err := rq.FormFile("binary") - if file != nil { - defer file.Close() + var ( + hyphaName = HyphaNameFromRq(rq, "upload-binary") + h = hyphae.ByName(hyphaName) + u = user.FromRequest(rq) + file, handler, err = rq.FormFile("binary") + ) + if err != nil { + HttpErr(w, http.StatusInternalServerError, hyphaName, + "Error", + err.Error()) + } + if err, errtitle := shroom.CanAttach(u, h); err != nil { + HttpErr(w, http.StatusInternalServerError, hyphaName, + errtitle, + err.Error()) } // If file is not passed: if err != nil { - HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No binary data passed") return } // If file is passed: + if file != nil { + defer file.Close() + } var ( - mime = handler.Header.Get("Content-Type") - hop = UploadBinary(hyphaName, mime, file, u) + mime = handler.Header.Get("Content-Type") + hop, errtitle = shroom.UploadBinary(h, mime, file, u) ) - if len(hop.Errs) != 0 { - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error()) + if hop.HasErrors() { + HttpErr(w, http.StatusInternalServerError, hyphaName, errtitle, hop.FirstErrorText()) return } - http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) + http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther) } diff --git a/http_readers.go b/http_readers.go index 970200d..a75b682 100644 --- a/http_readers.go +++ b/http_readers.go @@ -10,18 +10,35 @@ import ( "strings" "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/hyphae" "github.com/bouncepaw/mycorrhiza/markup" - "github.com/bouncepaw/mycorrhiza/templates" - "github.com/bouncepaw/mycorrhiza/tree" + "github.com/bouncepaw/mycorrhiza/mimetype" "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" + "github.com/bouncepaw/mycorrhiza/views" ) func init() { - http.HandleFunc("/page/", handlerPage) + http.HandleFunc("/page/", handlerHypha) + http.HandleFunc("/hypha/", handlerHypha) http.HandleFunc("/text/", handlerText) http.HandleFunc("/binary/", handlerBinary) http.HandleFunc("/rev/", handlerRevision) + http.HandleFunc("/attachment/", handlerAttachment) +} + +func handlerAttachment(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + hyphaName = HyphaNameFromRq(rq, "attachment") + h = hyphae.ByName(hyphaName) + u = user.FromRequest(rq) + ) + util.HTTP200Page(w, + views.BaseHTML( + fmt.Sprintf("Attachment of %s", util.BeautifulName(hyphaName)), + views.AttachmentMenuHTML(rq, h, u), + u)) } // handlerRevision displays a specific revision of text part a page @@ -31,37 +48,34 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { shorterUrl = strings.TrimPrefix(rq.URL.Path, "/rev/") firstSlashIndex = strings.IndexRune(shorterUrl, '/') revHash = shorterUrl[:firstSlashIndex] - hyphaName = CanonicalName(shorterUrl[firstSlashIndex+1:]) + hyphaName = util.CanonicalName(shorterUrl[firstSlashIndex+1:]) + h = hyphae.ByName(hyphaName) contents = fmt.Sprintf(`This hypha had no text at this revision.
`) - textPath = hyphaName + ".myco" - textContents, err = history.FileAtRevision(textPath, revHash) + textContents, err = history.FileAtRevision(h.TextPath, revHash) u = user.FromRequest(rq) ) if err == nil { contents = markup.Doc(hyphaName, textContents).AsHTML() } - treeHTML, _, _ := tree.Tree(hyphaName, IterateHyphaNamesWith) - page := templates.RevisionHTML( + page := views.RevisionHTML( rq, - hyphaName, - naviTitle(hyphaName), + h, contents, - treeHTML, revHash, ) w.Header().Set("Content-Type", "text/html;charset=utf-8") w.WriteHeader(http.StatusOK) - w.Write([]byte(base(hyphaName, page, u))) + w.Write([]byte(base(util.BeautifulName(hyphaName), page, u))) } // 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) + if h := hyphae.ByName(hyphaName); h.Exists { + log.Println("Serving", h.TextPath) w.Header().Set("Content-Type", "text/plain; charset=utf-8") - http.ServeFile(w, rq, data.textPath) + http.ServeFile(w, rq, h.TextPath) } } @@ -69,45 +83,39 @@ func handlerText(w http.ResponseWriter, rq *http.Request) { 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", ExtensionToMime(filepath.Ext(data.binaryPath))) - http.ServeFile(w, rq, data.binaryPath) + if h := hyphae.ByName(hyphaName); h.Exists { + log.Println("Serving", h.BinaryPath) + w.Header().Set("Content-Type", mimetype.FromExtension(filepath.Ext(h.BinaryPath))) + http.ServeFile(w, rq, h.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) { +// handlerHypha is the main hypha action that displays the hypha and the binary upload form along with some navigation. +func handlerHypha(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) var ( - hyphaName = HyphaNameFromRq(rq, "page") - data, hyphaExists = HyphaStorage[hyphaName] - hasAmnt = hyphaExists && data.binaryPath != "" - contents string - openGraph string - u = user.FromRequest(rq) + hyphaName = HyphaNameFromRq(rq, "page", "hypha") + h = hyphae.ByName(hyphaName) + contents string + openGraph string + u = user.FromRequest(rq) ) - if hyphaExists { - fileContentsT, errT := ioutil.ReadFile(data.textPath) - _, errB := os.Stat(data.binaryPath) + if h.Exists { + fileContentsT, errT := ioutil.ReadFile(h.TextPath) + _, errB := os.Stat(h.BinaryPath) if errT == nil { md := markup.Doc(hyphaName, string(fileContentsT)) contents = md.AsHTML() openGraph = md.OpenGraphHTML() } if !os.IsNotExist(errB) { - contents = binaryHtmlBlock(hyphaName, data) + contents + contents = views.AttachmentHTML(h) + contents } } - treeHTML, prevHypha, nextHypha := tree.Tree(hyphaName, IterateHyphaNamesWith) util.HTTP200Page(w, - templates.BaseHTML( - hyphaName, - templates.PageHTML(rq, hyphaName, - naviTitle(hyphaName), - contents, - treeHTML, prevHypha, nextHypha, - hasAmnt), + views.BaseHTML( + util.BeautifulName(hyphaName), + views.HyphaHTML(rq, h, contents), u, openGraph)) } diff --git a/hypha.go b/hypha.go deleted file mode 100644 index be7fef4..0000000 --- a/hypha.go +++ /dev/null @@ -1,329 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io/ioutil" - "log" - "mime/multipart" - "os" - "path/filepath" - "regexp" - - "github.com/bouncepaw/mycorrhiza/history" - "github.com/bouncepaw/mycorrhiza/hyphae" - "github.com/bouncepaw/mycorrhiza/markup" - "github.com/bouncepaw/mycorrhiza/user" - "github.com/bouncepaw/mycorrhiza/util" -) - -func init() { - markup.HyphaExists = func(hyphaName string) bool { - _, hyphaExists := HyphaStorage[hyphaName] - return hyphaExists - } - markup.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 - } - markup.HyphaIterate = IterateHyphaNamesWith - markup.HyphaImageForOG = func(hyphaName string) string { - if hd, isOld := GetHyphaData(hyphaName); isOld && hd.binaryPath != "" { - return util.URL + "/binary/" + hyphaName - } - return util.URL + "/favicon.ico" - } -} - -// GetHyphaData finds a hypha addressed by `hyphaName` and returns its `hyphaData`. `hyphaData` is set to a zero value if this hypha does not exist. `isOld` is false if this hypha does not exist. -func GetHyphaData(hyphaName string) (hyphaData *HyphaData, isOld bool) { - hyphaData, isOld = HyphaStorage[hyphaName] - if hyphaData == nil { - hyphaData = &HyphaData{} - } - return -} - -// HyphaData represents a hypha's meta information: binary and text parts rooted paths and content types. -type HyphaData struct { - textPath string - binaryPath string -} - -// uploadHelp is a helper function for UploadText and UploadBinary -func uploadHelp(hop *history.HistoryOp, hyphaName, ext string, data []byte, u *user.User) *history.HistoryOp { - var ( - hyphaData, isOld = GetHyphaData(hyphaName) - fullPath = filepath.Join(WikiDir, hyphaName+ext) - originalFullPath = &hyphaData.textPath - ) - if hop.Type == history.TypeEditBinary { - originalFullPath = &hyphaData.binaryPath - } - - if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil { - return hop.WithError(err) - } - - if err := ioutil.WriteFile(fullPath, data, 0644); err != nil { - return hop.WithError(err) - } - - if isOld && *originalFullPath != fullPath && *originalFullPath != "" { - if err := history.Rename(*originalFullPath, fullPath); err != nil { - return hop.WithError(err) - } - log.Println("Move", *originalFullPath, "to", fullPath) - } - // New hyphae must be added to the hypha storage - if !isOld { - HyphaStorage[hyphaName] = hyphaData - hyphae.IncrementCount() - } - *originalFullPath = fullPath - if isOld && hop.Type == history.TypeEditText && !history.FileChanged(fullPath) { - return hop.Abort() - } - return hop.WithFiles(fullPath). - WithUser(u). - Apply() -} - -// UploadText loads a new text part from `textData` for hypha `hyphaName`. -func UploadText(hyphaName, textData string, u *user.User) *history.HistoryOp { - return uploadHelp( - history. - Operation(history.TypeEditText). - WithMsg(fmt.Sprintf("Edit โ%sโ", hyphaName)), - hyphaName, ".myco", []byte(textData), u) -} - -// UploadBinary loads a new binary part from `file` for hypha `hyphaName` with `hd`. The contents have the specified `mime` type. It must be marked if the hypha `isOld`. -func UploadBinary(hyphaName, mime string, file multipart.File, u *user.User) *history.HistoryOp { - var ( - hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload binary part for โ%sโ with type โ%sโ", hyphaName, mime)) - data, err = ioutil.ReadAll(file) - ) - if err != nil { - return hop.WithError(err).Apply() - } - return uploadHelp(hop, hyphaName, MimeToExtension(mime), data, u) -} - -// DeleteHypha deletes hypha and makes a history record about that. -func (hd *HyphaData) DeleteHypha(hyphaName string, u *user.User) *history.HistoryOp { - hop := history.Operation(history.TypeDeleteHypha). - WithFilesRemoved(hd.textPath, hd.binaryPath). - WithMsg(fmt.Sprintf("Delete โ%sโ", hyphaName)). - WithUser(u). - Apply() - if len(hop.Errs) == 0 { - delete(HyphaStorage, hyphaName) - hyphae.DecrementCount() - } - return hop -} - -// UnattachHypha unattaches hypha and makes a history record about that. -func (hd *HyphaData) UnattachHypha(hyphaName string, u *user.User) *history.HistoryOp { - hop := history.Operation(history.TypeUnattachHypha). - WithFilesRemoved(hd.binaryPath). - WithMsg(fmt.Sprintf("Unattach โ%sโ", hyphaName)). - WithUser(u). - Apply() - if len(hop.Errs) == 0 { - hd, ok := HyphaStorage[hyphaName] - if ok { - if hd.binaryPath != "" { - hd.binaryPath = "" - } - // If nothing is left of the hypha - if hd.textPath == "" { - delete(HyphaStorage, hyphaName) - hyphae.DecrementCount() - } - } - } - return hop -} - -func findHyphaeToRename(hyphaName string, recursive bool) []string { - hyphae := []string{hyphaName} - if recursive { - hyphae = append(hyphae, util.FindSubhyphae(hyphaName, IterateHyphaNamesWith)...) - } - return hyphae -} - -func renamingPairs(hyphaNames []string, replaceName func(string) string) (map[string]string, error) { - renameMap := make(map[string]string) - for _, hn := range hyphaNames { - if hd, ok := HyphaStorage[hn]; ok { - if _, nameUsed := HyphaStorage[replaceName(hn)]; nameUsed { - return nil, errors.New("Hypha " + replaceName(hn) + " already exists") - } - if hd.textPath != "" { - renameMap[hd.textPath] = replaceName(hd.textPath) - } - if hd.binaryPath != "" { - renameMap[hd.binaryPath] = replaceName(hd.binaryPath) - } - } - } - return renameMap, nil -} - -// word Data is plural here -func relocateHyphaData(hyphaNames []string, replaceName func(string) string) { - for _, hyphaName := range hyphaNames { - if hd, ok := HyphaStorage[hyphaName]; ok { - hd.textPath = replaceName(hd.textPath) - hd.binaryPath = replaceName(hd.binaryPath) - HyphaStorage[replaceName(hyphaName)] = hd - delete(HyphaStorage, hyphaName) - } - } -} - -// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way. -func RenameHypha(hyphaName, newName string, recursive bool, u *user.User) *history.HistoryOp { - var ( - re = regexp.MustCompile(`(?i)` + hyphaName) - replaceName = func(str string) string { - return re.ReplaceAllString(CanonicalName(str), newName) - } - hyphaNames = findHyphaeToRename(hyphaName, recursive) - renameMap, err = renamingPairs(hyphaNames, replaceName) - renameMsg = "Rename โ%sโ to โ%sโ" - hop = history.Operation(history.TypeRenameHypha) - ) - if err != nil { - hop.Errs = append(hop.Errs, err) - return hop - } - if recursive { - renameMsg += " recursively" - } - hop.WithFilesRenamed(renameMap). - WithMsg(fmt.Sprintf(renameMsg, hyphaName, newName)). - WithUser(u). - Apply() - if len(hop.Errs) == 0 { - relocateHyphaData(hyphaNames, replaceName) - } - return hop -} - -// binaryHtmlBlock creates an html block for binary part of the hypha. -func binaryHtmlBlock(hyphaName string, hd *HyphaData) string { - switch filepath.Ext(hd.binaryPath) { - case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico": - return fmt.Sprintf(` - `, hyphaName) - case ".ogg", ".webm", ".mp4": - return fmt.Sprintf(` -This hypha's media cannot be rendered. Download it
-).
+func MatchesHorizontalLine(line string) bool {
+ counter := 0
+
+ // Check initially that the symbol is "-". If it is not a "-", it is most likely a space or another character.
+ // With unicode.IsLetter() we can separate spaces and characters.
+ for _, ch := range line {
+ if ch == '-' {
+ counter++
+ continue
+ }
+ // If we bump into any other character (letter) in the line, it is immediately an incorrect horizontal line.
+ // There is no point in counting further, we end the loop.
+ if unicode.IsLetter(ch) {
+ counter = 0
+ break
+ }
+ }
+
+ if counter >= 4 {
+ return true
+ }
+
+ return false
+}
diff --git a/markup/img.go b/markup/img.go
index 99c919b..b1a59c1 100644
--- a/markup/img.go
+++ b/markup/img.go
@@ -5,7 +5,7 @@ import (
"regexp"
"strings"
- "github.com/bouncepaw/mycorrhiza/util"
+ "github.com/bouncepaw/mycorrhiza/link"
)
var imgRe = regexp.MustCompile(`^img\s+{`)
@@ -14,44 +14,6 @@ func MatchesImg(line string) bool {
return imgRe.MatchString(line)
}
-type imgEntry struct {
- trimmedPath string
- path strings.Builder
- sizeW strings.Builder
- sizeH strings.Builder
- desc strings.Builder
-}
-
-func (entry *imgEntry) descriptionAsHtml(hyphaName string) (html string) {
- if entry.desc.Len() == 0 {
- return ""
- }
- lines := strings.Split(entry.desc.String(), "\n")
- for _, line := range lines {
- if line = strings.TrimSpace(line); line != "" {
- if html != "" {
- html += ` `)
+
+func extractLinks(html string, ch chan string) {
+ if results := reLinks.FindAllStringSubmatch(html, -1); results != nil {
+ for _, result := range results {
+ // result[0] is always present at this point and is not needed, because it is the whole matched substring (which we don't need)
+ ch <- result[1]
+ }
+ }
+}
+
+func extractImageLinks(img Img, ch chan string) {
+ for _, entry := range img.entries {
+ if entry.srclink.Kind == link.LinkLocalHypha {
+ ch <- entry.srclink.Address
+ }
+ }
+}
diff --git a/markup/paragraph.go b/markup/paragraph.go
index f506bb6..a576fee 100644
--- a/markup/paragraph.go
+++ b/markup/paragraph.go
@@ -55,7 +55,8 @@ func getLinkNode(input *bytes.Buffer, hyphaName string, isBracketedLink bool) st
} else if isBracketedLink && b == ']' && bytes.HasPrefix(input.Bytes(), []byte{']'}) {
input.Next(1)
break
- } else if !isBracketedLink && unicode.IsSpace(rune(b)) {
+ } else if !isBracketedLink && (unicode.IsSpace(rune(b)) || strings.ContainsRune("<>{}|\\^[]`,()", rune(b))) {
+ input.UnreadByte()
break
} else {
currBuf.WriteByte(b)
diff --git a/metarrhiza b/metarrhiza
index be5b922..e7040f3 160000
--- a/metarrhiza
+++ b/metarrhiza
@@ -1 +1 @@
-Subproject commit be5b922e9b564551601d21ed45bf7d9ced65c6bb
+Subproject commit e7040f3e0dc41809063b77fcbc12fe33b234ea87
diff --git a/mime.go b/mime.go
deleted file mode 100644
index c16eebe..0000000
--- a/mime.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package main
-
-import (
- "path/filepath"
- "strings"
-)
-
-func MimeToExtension(mime string) string {
- mm := map[string]string{
- "application/octet-stream": "bin",
- "image/jpeg": "jpg",
- "image/gif": "gif",
- "image/png": "png",
- "image/webp": "webp",
- "image/svg+xml": "svg",
- "image/x-icon": "ico",
- "application/ogg": "ogg",
- "video/webm": "webm",
- "audio/mp3": "mp3",
- "video/mp4": "mp4",
- }
- if ext, ok := mm[mime]; ok {
- return "." + ext
- }
- return ".bin"
-}
-
-func ExtensionToMime(ext string) string {
- mm := map[string]string{
- ".bin": "application/octet-stream",
- ".jpg": "image/jpeg",
- ".jpeg": "image/jpeg",
- ".gif": "image/gif",
- ".png": "image/png",
- ".webp": "image/webp",
- ".svg": "image/svg+xml",
- ".ico": "image/x-icon",
- ".ogg": "application/ogg",
- ".webm": "video/webm",
- ".mp3": "audio/mp3",
- ".mp4": "video/mp4",
- }
- if mime, ok := mm[ext]; ok {
- return mime
- }
- return "application/octet-stream"
-}
-
-// 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) (name string, isText bool, skip bool) {
- shortPath := strings.TrimPrefix(fullPath, WikiDir)[1:]
- ext := filepath.Ext(shortPath)
- name = CanonicalName(strings.TrimSuffix(shortPath, ext))
- switch ext {
- case ".myco":
- isText = true
- case "", shortPath:
- skip = true
- }
-
- return
-}
diff --git a/mimetype/mime.go b/mimetype/mime.go
new file mode 100644
index 0000000..7e18ca7
--- /dev/null
+++ b/mimetype/mime.go
@@ -0,0 +1,68 @@
+package mimetype
+
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+// ToExtension returns dotted extension for given mime-type.
+func ToExtension(mime string) string {
+ if ext, ok := mapMime2Ext[mime]; ok {
+ return "." + ext
+ }
+ return ".bin"
+}
+
+// FromExtension returns mime-type for given extension. The extension must start with a dot.
+func FromExtension(ext string) string {
+ if mime, ok := mapExt2Mime[ext]; ok {
+ return mime
+ }
+ return "application/octet-stream"
+}
+
+// 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.
+func DataFromFilename(fullPath string) (name string, isText bool, skip bool) {
+ shortPath := util.ShorterPath(fullPath)
+ ext := filepath.Ext(shortPath)
+ name = util.CanonicalName(strings.TrimSuffix(shortPath, ext))
+ switch ext {
+ case ".myco":
+ isText = true
+ case "", shortPath:
+ skip = true
+ }
+
+ return
+}
+
+var mapMime2Ext = map[string]string{
+ "application/octet-stream": "bin",
+ "image/jpeg": "jpg",
+ "image/gif": "gif",
+ "image/png": "png",
+ "image/webp": "webp",
+ "image/svg+xml": "svg",
+ "image/x-icon": "ico",
+ "application/ogg": "ogg",
+ "video/webm": "webm",
+ "audio/mp3": "mp3",
+ "video/mp4": "mp4",
+}
+
+var mapExt2Mime = map[string]string{
+ ".bin": "application/octet-stream",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".gif": "image/gif",
+ ".png": "image/png",
+ ".webp": "image/webp",
+ ".svg": "image/svg+xml",
+ ".ico": "image/x-icon",
+ ".ogg": "application/ogg",
+ ".webm": "video/webm",
+ ".mp3": "audio/mp3",
+ ".mp4": "video/mp4",
+}
diff --git a/name.go b/name.go
index 2a859d8..7da76ba 100644
--- a/name.go
+++ b/name.go
@@ -1,52 +1,34 @@
package main
import (
- "fmt"
+ "log"
"net/http"
"strings"
+ "git.sr.ht/~adnano/go-gemini"
+
"github.com/bouncepaw/mycorrhiza/util"
)
-// 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.
-// TODO: rework as a template
-func naviTitle(canonicalName string) string {
- var (
- html = fmt.Sprintf(` Administrator of this wiki have not configured any authorization method. You can make edits anonymously. Unknown username. Wrong password. {%s err %} You cannot log out because you are not logged in. Administrator of this wiki have not configured any authorization method. You can make edits anonymously. Unknown username. Wrong password. `)
-//line templates/auth.qtpl:39
- qw422016.E().S(err)
-//line templates/auth.qtpl:39
- qw422016.N().S(` You cannot log out because you are not logged in.
`
- }
- html += ParagraphToHtml(hyphaName, line)
- }
- }
- return `
`
+ }
+ html += ParagraphToHtml(hyphaName, line)
+ }
+ }
+ return `
"})
case MatchesImg(line):
diff --git a/markup/link.go b/markup/link.go
index af3e51c..9243fca 100644
--- a/markup/link.go
+++ b/markup/link.go
@@ -1,47 +1,22 @@
package markup
import (
- "fmt"
- "path"
"strings"
+
+ "github.com/bouncepaw/mycorrhiza/link"
)
// LinkParts determines what href, text and class should resulting have based on mycomarkup's addr, display and hypha name.
//
// => addr display
// [[addr|display]]
+// TODO: deprecate
func LinkParts(addr, display, hyphaName string) (href, text, class string) {
- if display == "" {
- text = addr
- } else {
- text = strings.TrimSpace(display)
+ l := link.From(addr, display, hyphaName)
+ if l.Kind == link.LinkLocalHypha && !HyphaExists(l.Address) {
+ l.DestinationUnknown = true
}
- class = "wikilink wikilink_internal"
-
- switch {
- case strings.ContainsRune(addr, ':'):
- pos := strings.IndexRune(addr, ':')
- destination := addr[:pos]
- if display == "" {
- text = addr[pos+1:]
- if strings.HasPrefix(text, "//") && len(text) > 2 {
- text = text[2:]
- }
- }
- return addr, text, fmt.Sprintf("wikilink wikilink_external wikilink_%s", destination)
- case strings.HasPrefix(addr, "/"):
- return addr, text, class
- case strings.HasPrefix(addr, "./"):
- hyphaName = canonicalName(path.Join(hyphaName, addr[2:]))
- case strings.HasPrefix(addr, "../"):
- hyphaName = canonicalName(path.Join(path.Dir(hyphaName), addr[3:]))
- default:
- hyphaName = canonicalName(addr)
- }
- if !HyphaExists(hyphaName) {
- class += " wikilink_new"
- }
- return "/page/" + hyphaName, text, class
+ return l.Href(), l.Display, l.Classes()
}
// Parse markup line starting with "=>" according to wikilink rules.
diff --git a/markup/mycomarkup.go b/markup/mycomarkup.go
index 656ca28..2fadd4e 100644
--- a/markup/mycomarkup.go
+++ b/markup/mycomarkup.go
@@ -7,6 +7,7 @@ import (
"regexp"
"strings"
+ "github.com/bouncepaw/mycorrhiza/link"
"github.com/bouncepaw/mycorrhiza/util"
)
@@ -47,6 +48,11 @@ func (md *MycoDoc) AsHTML() string {
return md.html
}
+// AsGemtext returns a gemtext representation of the document. Currently really limited, just returns source text
+func (md *MycoDoc) AsGemtext() string {
+ return md.contents
+}
+
// Used to clear opengraph description from html tags. This method is usually bad because of dangers of malformed HTML, but I'm going to use it only for Mycorrhiza-generated HTML, so it's okay. The question mark is required; without it the whole string is eaten away.
var htmlTagRe = regexp.MustCompile(`<.*?>`)
@@ -57,15 +63,16 @@ func (md *MycoDoc) OpenGraphHTML() string {
ogTag("title", md.hyphaName),
ogTag("type", "article"),
ogTag("image", md.firstImageURL),
- ogTag("url", util.URL+"/page/"+md.hyphaName),
+ ogTag("url", util.URL+"/hypha/"+md.hyphaName),
ogTag("determiner", ""),
ogTag("description", htmlTagRe.ReplaceAllString(md.description, "")),
}, "\n")
}
func (md *MycoDoc) ogFillVars() *MycoDoc {
+ md.firstImageURL = util.URL + "/favicon.ico"
foundDesc := false
- md.firstImageURL = HyphaImageForOG(md.hyphaName)
+ foundImg := false
for _, line := range md.ast {
switch v := line.contents.(type) {
case string:
@@ -74,8 +81,12 @@ func (md *MycoDoc) ogFillVars() *MycoDoc {
foundDesc = true
}
case Img:
- if len(v.entries) > 0 {
- md.firstImageURL = v.entries[0].path.String()
+ if !foundImg && len(v.entries) > 0 {
+ md.firstImageURL = v.entries[0].srclink.ImgSrc()
+ if v.entries[0].srclink.Kind != link.LinkExternal {
+ md.firstImageURL = util.URL + md.firstImageURL
+ }
+ foundImg = true
}
}
}
@@ -153,6 +164,7 @@ func crawl(name, content string) []string {
preAcc += html.EscapeString(line)
}
}
+ break
}
return []string{}
diff --git a/markup/outlink.go b/markup/outlink.go
new file mode 100644
index 0000000..0580371
--- /dev/null
+++ b/markup/outlink.go
@@ -0,0 +1,56 @@
+package markup
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/bouncepaw/mycorrhiza/link"
+)
+
+// OutLinks returns a channel of names of hyphae this mycodocument links.
+// Links include:
+// * Regular links
+// * Rocketlinks
+// * Transclusion
+// * Image galleries
+func (md *MycoDoc) OutLinks() chan string {
+ ch := make(chan string)
+ if !md.parsedAlready {
+ md.Lex(0)
+ }
+ go func() {
+ for _, line := range md.ast {
+ switch v := line.contents.(type) {
+ case string:
+ if strings.HasPrefix(v, "
- %s `, util.HomePage, util.SiteNavIcon)
- prevAcc = `/page/`
- parts = strings.Split(canonicalName, "/")
- rel = "up"
- )
- for i, part := range parts {
- if i > 0 {
- html += ` `
+// HyphaNameFromRq extracts hypha name from http request. You have to also pass the action which is embedded in the url or several actions. For url /hypha/hypha, the action would be "hypha".
+func HyphaNameFromRq(rq *http.Request, actions ...string) string {
+ p := rq.URL.Path
+ for _, action := range actions {
+ if strings.HasPrefix(p, "/"+action+"/") {
+ return util.CanonicalName(strings.TrimPrefix(p, "/"+action+"/"))
}
- if i == len(parts)-1 {
- rel = "bookmark"
- }
- html += fmt.Sprintf(
- `%s`,
- prevAcc+part,
- rel,
- util.BeautifulName(part),
- )
- prevAcc += part + "/"
}
- return html + "
"
+ panic("HyphaNameFromRq: no matching action passed")
}
-// 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+"/"))
+// geminiHyphaNameFromRq extracts hypha name from gemini request. You have to also pass the action which is embedded in the url or several actions. For url /hypha/hypha, the action would be "hypha".
+func geminiHyphaNameFromRq(rq *gemini.Request, actions ...string) string {
+ p := rq.URL.Path
+ for _, action := range actions {
+ if strings.HasPrefix(p, "/"+action+"/") {
+ return util.CanonicalName(strings.TrimPrefix(p, "/"+action+"/"))
+ }
+ }
+ log.Fatal("HyphaNameFromRq: no matching action passed")
+ return ""
}
diff --git a/shroom/backlink.go b/shroom/backlink.go
new file mode 100644
index 0000000..860ba56
--- /dev/null
+++ b/shroom/backlink.go
@@ -0,0 +1,36 @@
+package shroom
+
+import (
+ "io/ioutil"
+ "log"
+
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/markup"
+)
+
+// FindAllBacklinks iterates over all hyphae that have text parts, sets their outlinks and then sets backlinks.
+func FindAllBacklinks() {
+ for h := range hyphae.FilterTextHyphae(hyphae.YieldExistingHyphae()) {
+ findBacklinkWorker(h)
+ }
+}
+
+func findBacklinkWorker(h *hyphae.Hypha) {
+ var (
+ textContents, err = ioutil.ReadFile(h.TextPath)
+ )
+ if err == nil {
+ for outlink := range markup.Doc(h.Name, string(textContents)).OutLinks() {
+ outlinkHypha := hyphae.ByName(outlink)
+ if outlinkHypha == h {
+ break
+ }
+
+ outlinkHypha.AddBackLink(h)
+ outlinkHypha.InsertIfNewKeepExistence()
+ h.AddOutLink(outlinkHypha)
+ }
+ } else {
+ log.Println("Error when reading text contents of โ%sโ: %s", h.Name, err.Error())
+ }
+}
diff --git a/shroom/can.go b/shroom/can.go
new file mode 100644
index 0000000..152d896
--- /dev/null
+++ b/shroom/can.go
@@ -0,0 +1,92 @@
+package shroom
+
+import (
+ "errors"
+
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/user"
+)
+
+func canFactory(
+ rejectLogger func(*hyphae.Hypha, *user.User, string),
+ action string,
+ dispatcher func(*hyphae.Hypha, *user.User) (string, string),
+ noRightsMsg string,
+ notExistsMsg string,
+ careAboutExistince bool,
+) func(*user.User, *hyphae.Hypha) (error, string) {
+ return func(u *user.User, h *hyphae.Hypha) (error, string) {
+ if !u.CanProceed(action) {
+ rejectLogger(h, u, "no rights")
+ return errors.New(noRightsMsg), "Not enough rights"
+ }
+
+ if careAboutExistince && !h.Exists {
+ rejectLogger(h, u, "does not exist")
+ return errors.New(notExistsMsg), "Does not exist"
+ }
+
+ if dispatcher == nil {
+ return nil, ""
+ }
+ errmsg, errtitle := dispatcher(h, u)
+ if errtitle == "" {
+ return nil, ""
+ }
+ return errors.New(errmsg), errtitle
+ }
+}
+
+var (
+ CanDelete = canFactory(
+ rejectDeleteLog,
+ "delete-confirm",
+ nil,
+ "Not enough rights to delete, you must be a moderator",
+ "Cannot delete this hypha because it does not exist",
+ true,
+ )
+
+ CanRename = canFactory(
+ rejectRenameLog,
+ "rename-confirm",
+ nil,
+ "Not enough rights to rename, you must be a trusted editor",
+ "Cannot rename this hypha because it does not exist",
+ true,
+ )
+
+ CanUnattach = canFactory(
+ rejectUnattachLog,
+ "unattach-confirm",
+ func(h *hyphae.Hypha, u *user.User) (errmsg, errtitle string) {
+ if h.BinaryPath == "" {
+ rejectUnattachLog(h, u, "no amnt")
+ return "Cannot unattach this hypha because it has no attachment", "No attachment"
+ }
+
+ return "", ""
+ },
+ "Not enough rights to unattach, you must be a trusted editor",
+ "Cannot unattach this hypha because it does not exist",
+ true,
+ )
+
+ CanEdit = canFactory(
+ rejectEditLog,
+ "upload-text",
+ nil,
+ "You must be an editor to edit a hypha",
+ "You cannot edit a hypha that does not exist",
+ false,
+ )
+
+ CanAttach = canFactory(
+ rejectAttachLog,
+ "upload-binary",
+ nil,
+ "You must be an editor to attach a hypha",
+ "You cannot attach a hypha that does not exist",
+ false,
+ )
+)
diff --git a/shroom/delete.go b/shroom/delete.go
new file mode 100644
index 0000000..2341351
--- /dev/null
+++ b/shroom/delete.go
@@ -0,0 +1,29 @@
+package shroom
+
+import (
+ "fmt"
+
+ "github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/user"
+)
+
+// DeleteHypha deletes hypha and makes a history record about that.
+func DeleteHypha(u *user.User, h *hyphae.Hypha) (hop *history.HistoryOp, errtitle string) {
+ hop = history.Operation(history.TypeDeleteHypha)
+
+ if err, errtitle := CanDelete(u, h); errtitle != "" {
+ hop.WithErrAbort(err)
+ return hop, errtitle
+ }
+
+ hop.
+ WithFilesRemoved(h.TextPath, h.BinaryPath).
+ WithMsg(fmt.Sprintf("Delete โ%sโ", h.Name)).
+ WithUser(u).
+ Apply()
+ if !hop.HasErrors() {
+ h.Delete()
+ }
+ return hop, ""
+}
diff --git a/shroom/init.go b/shroom/init.go
new file mode 100644
index 0000000..96a6195
--- /dev/null
+++ b/shroom/init.go
@@ -0,0 +1,38 @@
+package shroom
+
+import (
+ "errors"
+
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/markup"
+ "github.com/bouncepaw/mycorrhiza/util"
+ "github.com/bouncepaw/mycorrhiza/views"
+)
+
+func init() {
+ markup.HyphaExists = func(hyphaName string) bool {
+ return hyphae.ByName(hyphaName).Exists
+ }
+ markup.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) {
+ if h := hyphae.ByName(hyphaName); h.Exists {
+ rawText, err = FetchTextPart(h)
+ if h.BinaryPath != "" {
+ binaryBlock = views.AttachmentHTML(h)
+ }
+ } else {
+ err = errors.New("Hypha " + hyphaName + " does not exist")
+ }
+ return
+ }
+ markup.HyphaIterate = func(ฮป func(string)) {
+ for h := range hyphae.YieldExistingHyphae() {
+ ฮป(h.Name)
+ }
+ }
+ markup.HyphaImageForOG = func(hyphaName string) string {
+ if h := hyphae.ByName(hyphaName); h.Exists && h.BinaryPath != "" {
+ return util.URL + "/binary/" + hyphaName
+ }
+ return util.URL + "/favicon.ico"
+ }
+}
diff --git a/shroom/log.go b/shroom/log.go
new file mode 100644
index 0000000..0bfee28
--- /dev/null
+++ b/shroom/log.go
@@ -0,0 +1,24 @@
+package shroom
+
+import (
+ "log"
+
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/user"
+)
+
+func rejectDeleteLog(h *hyphae.Hypha, u *user.User, errmsg string) {
+ log.Printf("Reject delete โ%sโ by @%s: %s\n", h.Name, u.Name, errmsg)
+}
+func rejectRenameLog(h *hyphae.Hypha, u *user.User, errmsg string) {
+ log.Printf("Reject rename โ%sโ by @%s: %s\n", h.Name, u.Name, errmsg)
+}
+func rejectUnattachLog(h *hyphae.Hypha, u *user.User, errmsg string) {
+ log.Printf("Reject unattach โ%sโ by @%s: %s\n", h.Name, u.Name, errmsg)
+}
+func rejectEditLog(h *hyphae.Hypha, u *user.User, errmsg string) {
+ log.Printf("Reject edit โ%sโ by @%s: %s\n", h.Name, u.Name, errmsg)
+}
+func rejectAttachLog(h *hyphae.Hypha, u *user.User, errmsg string) {
+ log.Printf("Reject attach โ%sโ by @%s: %s\n", h.Name, u.Name, errmsg)
+}
diff --git a/shroom/rename.go b/shroom/rename.go
new file mode 100644
index 0000000..a2d44da
--- /dev/null
+++ b/shroom/rename.go
@@ -0,0 +1,106 @@
+package shroom
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+
+ "github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/user"
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+func canRenameThisToThat(oh *hyphae.Hypha, nh *hyphae.Hypha, u *user.User) (err error, errtitle string) {
+ if nh.Exists {
+ rejectRenameLog(oh, u, fmt.Sprintf("name โ%sโ taken already", nh.Name))
+ return errors.New(fmt.Sprintf("Hypha named %[1]s already exists, cannot rename", nh.Name)), "Name taken"
+ }
+
+ if nh.Name == "" {
+ rejectRenameLog(oh, u, "no new name given")
+ return errors.New("No new name is given"), "No name given"
+ }
+
+ if !hyphae.HyphaPattern.MatchString(nh.Name) {
+ rejectRenameLog(oh, u, fmt.Sprintf("new name โ%sโ invalid", nh.Name))
+ return errors.New("Invalid new name. Names cannot contain characters ^?!:#@><*|\"\\'&%
"), "Invalid name"
+ }
+
+ return nil, ""
+}
+
+// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way.
+func RenameHypha(h *hyphae.Hypha, newHypha *hyphae.Hypha, recursive bool, u *user.User) (hop *history.HistoryOp, errtitle string) {
+ newHypha.Lock()
+ defer newHypha.Unlock()
+ hop = history.Operation(history.TypeRenameHypha)
+
+ if err, errtitle := CanRename(u, h); errtitle != "" {
+ hop.WithErrAbort(err)
+ return hop, errtitle
+ }
+ if err, errtitle := canRenameThisToThat(h, newHypha, u); errtitle != "" {
+ hop.WithErrAbort(err)
+ return hop, errtitle
+ }
+
+ var (
+ re = regexp.MustCompile(`(?i)` + h.Name)
+ replaceName = func(str string) string {
+ return re.ReplaceAllString(util.CanonicalName(str), newHypha.Name)
+ }
+ hyphaeToRename = findHyphaeToRename(h, recursive)
+ renameMap, err = renamingPairs(hyphaeToRename, replaceName)
+ renameMsg = "Rename โ%sโ to โ%sโ"
+ )
+ if err != nil {
+ hop.Errs = append(hop.Errs, err)
+ return hop, hop.FirstErrorText()
+ }
+ if recursive && len(hyphaeToRename) > 0 {
+ renameMsg += " recursively"
+ }
+ hop.WithFilesRenamed(renameMap).
+ WithMsg(fmt.Sprintf(renameMsg, h.Name, newHypha.Name)).
+ WithUser(u).
+ Apply()
+ if len(hop.Errs) == 0 {
+ for _, h := range hyphaeToRename {
+ h.RenameTo(replaceName(h.Name))
+ h.Lock()
+ h.TextPath = replaceName(h.TextPath)
+ h.BinaryPath = replaceName(h.BinaryPath)
+ h.Unlock()
+ }
+ }
+ return hop, ""
+}
+
+func findHyphaeToRename(superhypha *hyphae.Hypha, recursive bool) []*hyphae.Hypha {
+ hyphae := []*hyphae.Hypha{superhypha}
+ if recursive {
+ hyphae = append(hyphae, superhypha.Subhyphae()...)
+ }
+ return hyphae
+}
+
+func renamingPairs(hyphaeToRename []*hyphae.Hypha, replaceName func(string) string) (map[string]string, error) {
+ renameMap := make(map[string]string)
+ newNames := make([]string, len(hyphaeToRename))
+ for _, h := range hyphaeToRename {
+ h.Lock()
+ newNames = append(newNames, replaceName(h.Name))
+ if h.TextPath != "" {
+ renameMap[h.TextPath] = replaceName(h.TextPath)
+ }
+ if h.BinaryPath != "" {
+ renameMap[h.BinaryPath] = replaceName(h.BinaryPath)
+ }
+ h.Unlock()
+ }
+ if firstFailure, ok := hyphae.AreFreeNames(newNames...); !ok {
+ return nil, errors.New("Hypha " + firstFailure + " already exists")
+ }
+ return renameMap, nil
+}
diff --git a/shroom/unattach.go b/shroom/unattach.go
new file mode 100644
index 0000000..60f31e6
--- /dev/null
+++ b/shroom/unattach.go
@@ -0,0 +1,41 @@
+package shroom
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/user"
+)
+
+// UnattachHypha unattaches hypha and makes a history record about that.
+func UnattachHypha(u *user.User, h *hyphae.Hypha) (hop *history.HistoryOp, errtitle string) {
+ hop = history.Operation(history.TypeUnattachHypha)
+
+ if err, errtitle := CanUnattach(u, h); errtitle != "" {
+ hop.WithErrAbort(err)
+ return hop, errtitle
+ }
+
+ hop.
+ WithFilesRemoved(h.BinaryPath).
+ WithMsg(fmt.Sprintf("Unattach โ%sโ", h.Name)).
+ WithUser(u).
+ Apply()
+
+ if len(hop.Errs) > 0 {
+ rejectUnattachLog(h, u, "fail")
+ // FIXME: something may be wrong here
+ return hop.WithErrAbort(errors.New(fmt.Sprintf("Could not unattach this hypha due to internal server errors: %v
", hop.Errs))), "Error"
+ }
+
+ if h.BinaryPath != "" {
+ h.BinaryPath = ""
+ }
+ // If nothing is left of the hypha
+ if h.TextPath == "" {
+ h.Delete()
+ }
+ return hop, ""
+}
diff --git a/shroom/upload.go b/shroom/upload.go
new file mode 100644
index 0000000..c72ec9a
--- /dev/null
+++ b/shroom/upload.go
@@ -0,0 +1,87 @@
+package shroom
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "mime/multipart"
+ "os"
+ "path/filepath"
+
+ "github.com/bouncepaw/mycorrhiza/history"
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/mimetype"
+ "github.com/bouncepaw/mycorrhiza/user"
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+func UploadText(h *hyphae.Hypha, data []byte, u *user.User) (hop *history.HistoryOp, errtitle string) {
+ hop = history.Operation(history.TypeEditText)
+ if h.Exists {
+ hop.WithMsg(fmt.Sprintf("Edit โ%sโ", h.Name))
+ } else {
+ hop.WithMsg(fmt.Sprintf("Create โ%sโ", h.Name))
+ }
+
+ if err, errtitle := CanEdit(u, h); err != nil {
+ return hop.WithErrAbort(err), errtitle
+ }
+ if len(data) == 0 {
+ return hop.WithErrAbort(errors.New("No data passed")), "Empty"
+ }
+
+ return uploadHelp(h, hop, ".myco", data, u)
+}
+
+func UploadBinary(h *hyphae.Hypha, mime string, file multipart.File, u *user.User) (*history.HistoryOp, string) {
+ var (
+ hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload binary part for โ%sโ with type โ%sโ", h.Name, mime))
+ data, err = ioutil.ReadAll(file)
+ )
+
+ if err != nil {
+ return hop.WithErrAbort(err), err.Error()
+ }
+ if err, errtitle := CanAttach(u, h); err != nil {
+ return hop.WithErrAbort(err), errtitle
+ }
+ if len(data) == 0 {
+ return hop.WithErrAbort(errors.New("No data passed")), "Empty"
+ }
+
+ return uploadHelp(h, hop, mimetype.ToExtension(mime), data, u)
+}
+
+// uploadHelp is a helper function for UploadText and UploadBinary
+func uploadHelp(h *hyphae.Hypha, hop *history.HistoryOp, ext string, data []byte, u *user.User) (*history.HistoryOp, string) {
+ var (
+ fullPath = filepath.Join(util.WikiDir, h.Name+ext)
+ originalFullPath = &h.TextPath
+ )
+ if hop.Type == history.TypeEditBinary {
+ originalFullPath = &h.BinaryPath
+ }
+
+ if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
+ return hop.WithErrAbort(err), err.Error()
+ }
+
+ if err := ioutil.WriteFile(fullPath, data, 0644); err != nil {
+ return hop.WithErrAbort(err), err.Error()
+ }
+
+ if h.Exists && *originalFullPath != fullPath && *originalFullPath != "" {
+ if err := history.Rename(*originalFullPath, fullPath); err != nil {
+ return hop.WithErrAbort(err), err.Error()
+ }
+ log.Println("Move", *originalFullPath, "to", fullPath)
+ }
+
+ h.InsertIfNew()
+ if h.Exists && h.TextPath != "" && hop.Type == history.TypeEditText && !history.FileChanged(fullPath) {
+ return hop.Abort(), "No changes"
+ }
+ *originalFullPath = fullPath
+ return hop.WithFiles(fullPath).WithUser(u).Apply(), ""
+}
diff --git a/shroom/view.go b/shroom/view.go
new file mode 100644
index 0000000..05becb9
--- /dev/null
+++ b/shroom/view.go
@@ -0,0 +1,38 @@
+package shroom
+
+import (
+ "io/ioutil"
+ "os"
+
+ "github.com/bouncepaw/mycorrhiza/hyphae"
+ "github.com/bouncepaw/mycorrhiza/markup"
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+// FetchTextPart tries to read text file of the given hypha. If there is no file, empty string is returned.
+func FetchTextPart(h *hyphae.Hypha) (string, error) {
+ if h.TextPath == "" {
+ return "", nil
+ }
+ text, err := ioutil.ReadFile(h.TextPath)
+ if os.IsNotExist(err) {
+ return "", nil
+ } else if err != nil {
+ return "", err
+ }
+ return string(text), nil
+}
+
+func SetHeaderLinks() {
+ if userLinksHypha := hyphae.ByName(util.HeaderLinksHypha); !userLinksHypha.Exists {
+ util.SetDefaultHeaderLinks()
+ } else {
+ contents, err := ioutil.ReadFile(userLinksHypha.TextPath)
+ if err != nil || len(contents) == 0 {
+ util.SetDefaultHeaderLinks()
+ } else {
+ text := string(contents)
+ util.ParseHeaderLinks(text, markup.Rocketlink)
+ }
+ }
+}
diff --git a/templates/auth.qtpl b/templates/auth.qtpl
deleted file mode 100644
index 32ab3d8..0000000
--- a/templates/auth.qtpl
+++ /dev/null
@@ -1,60 +0,0 @@
-{% import "github.com/bouncepaw/mycorrhiza/user" %}
-
-{% func LoginHTML() %}
-Login
-
- {% else %}
- Log out?
-
-
- {% else %}
- Login
-
- `)
-//line templates/auth.qtpl:22
- } else {
-//line templates/auth.qtpl:22
- qw422016.N().S(`
- Log out?
-
-
- `)
-//line templates/auth.qtpl:53
- } else {
-//line templates/auth.qtpl:53
- qw422016.N().S(`
-