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(` -
  • -
  • %[2]s
  • -
  • %[5]s
  • -
  • %[6]s by
  • -`, rev.TimeString(), rev.Hash, util.UserHypha, rev.Username, rev.HyphaeLinks(), rev.Message) - } - return fmt.Sprintf(` -
  • -
  • %[2]s
  • -
  • %[3]s
  • -
  • %[4]s
  • -`, rev.TimeString(), rev.Hash, rev.HyphaeLinks(), rev.Message) -} - // Path to git executable. Set at init() var gitpath string diff --git a/history/information.go b/history/information.go index f2c0aad..f3cf8a7 100644 --- a/history/information.go +++ b/history/information.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/util" "github.com/gorilla/feeds" ) @@ -61,7 +60,7 @@ func RecentChangesJSON() (string, error) { return recentChangesFeed().ToJSON() } -func RecentChanges(n int) string { +func RecentChanges(n int) []Revision { var ( out, err = gitsh( "log", "--oneline", "--no-merges", @@ -75,11 +74,7 @@ func RecentChanges(n int) string { revs = append(revs, parseRevisionLine(line)) } } - entries := make([]string, len(revs)) - for i, rev := range revs { - entries[i] = rev.RecentChangesEntry() - } - return templates.RecentChangesHTML(entries, n) + return revs } // FileChanged tells you if the file has been changed. @@ -177,6 +172,6 @@ func parseRevisionLine(line string) Revision { // See how the file with `filepath` looked at commit with `hash`. func FileAtRevision(filepath, hash string) (string, error) { - out, err := gitsh("show", hash+":"+filepath) + out, err := gitsh("show", hash+":"+strings.TrimPrefix(filepath, util.WikiDir+"/")) return out.String(), err } diff --git a/history/operations.go b/history/operations.go index a8177ba..97e7aa7 100644 --- a/history/operations.go +++ b/history/operations.go @@ -59,12 +59,16 @@ func (hop *HistoryOp) gitop(args ...string) *HistoryOp { return hop } -// WithError appends the `err` to the list of errors. -func (hop *HistoryOp) WithError(err error) *HistoryOp { +// WithErr appends the `err` to the list of errors. +func (hop *HistoryOp) WithErr(err error) *HistoryOp { hop.Errs = append(hop.Errs, err) return hop } +func (hop *HistoryOp) WithErrAbort(err error) *HistoryOp { + return hop.WithErr(err).Abort() +} + // WithFilesRemoved git-rm-s all passed `paths`. Paths can be rooted or not. Paths that are empty strings are ignored. func (hop *HistoryOp) WithFilesRemoved(paths ...string) *HistoryOp { args := []string{"rm", "--quiet", "--"} @@ -134,3 +138,11 @@ func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp { } return hop } + +func (hop *HistoryOp) HasErrors() bool { + return len(hop.Errs) > 0 +} + +func (hop *HistoryOp) FirstErrorText() string { + return hop.Errs[0].Error() +} diff --git a/http_admin.go b/http_admin.go new file mode 100644 index 0000000..2b20fb5 --- /dev/null +++ b/http_admin.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + "net/http" + + "github.com/bouncepaw/mycorrhiza/user" + "github.com/bouncepaw/mycorrhiza/views" +) + +// This is not init(), because user.AuthUsed is not set at init-stage. +func initAdmin() { + if user.AuthUsed { + http.HandleFunc("/admin", handlerAdmin) + http.HandleFunc("/admin/shutdown", handlerAdminShutdown) + } +} + +func handlerAdmin(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + if user.CanProceed(rq, "admin") { + w.Header().Set("Content-Type", "text/html;charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte(base("Admin panel", views.AdminPanelHTML(), user.FromRequest(rq)))) + } +} + +func handlerAdminShutdown(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + if user.CanProceed(rq, "admin/shutdown") && rq.Method == "POST" { + log.Fatal("An admin commanded the wiki to shutdown") + } +} diff --git a/http_auth.go b/http_auth.go index cfe0799..9981520 100644 --- a/http_auth.go +++ b/http_auth.go @@ -4,8 +4,9 @@ import ( "log" "net/http" - "github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/user" + "github.com/bouncepaw/mycorrhiza/util" + "github.com/bouncepaw/mycorrhiza/views" ) func init() { @@ -28,7 +29,7 @@ func handlerLogout(w http.ResponseWriter, rq *http.Request) { log.Println("Unknown user tries to log out") w.WriteHeader(http.StatusForbidden) } - w.Write([]byte(base("Logout?", templates.LogoutHTML(can), u))) + w.Write([]byte(base("Logout?", views.LogoutHTML(can), u))) } func handlerLogoutConfirm(w http.ResponseWriter, rq *http.Request) { @@ -39,12 +40,12 @@ func handlerLogoutConfirm(w http.ResponseWriter, rq *http.Request) { func handlerLoginData(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) var ( - username = CanonicalName(rq.PostFormValue("username")) + username = util.CanonicalName(rq.PostFormValue("username")) password = rq.PostFormValue("password") err = user.LoginDataHTTP(w, rq, username, password) ) if err != "" { - w.Write([]byte(base(err, templates.LoginErrorHTML(err), user.EmptyUser()))) + w.Write([]byte(base(err, views.LoginErrorHTML(err), user.EmptyUser()))) } else { http.Redirect(w, rq, "/", http.StatusSeeOther) } @@ -58,5 +59,5 @@ func handlerLogin(w http.ResponseWriter, rq *http.Request) { } else { w.WriteHeader(http.StatusForbidden) } - w.Write([]byte(base("Login", templates.LoginHTML(), user.EmptyUser()))) + w.Write([]byte(base("Login", views.LoginHTML(), user.EmptyUser()))) } diff --git a/http_history.go b/http_history.go index 3adda8a..110ca3a 100644 --- a/http_history.go +++ b/http_history.go @@ -8,9 +8,9 @@ import ( "strings" "github.com/bouncepaw/mycorrhiza/history" - "github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" + "github.com/bouncepaw/mycorrhiza/views" ) func init() { @@ -35,7 +35,7 @@ func handlerHistory(w http.ResponseWriter, rq *http.Request) { log.Println("Found", len(revs), "revisions for", hyphaName) util.HTTP200Page(w, - base(hyphaName, templates.HistoryHTML(rq, hyphaName, list), user.FromRequest(rq))) + base(hyphaName, views.HistoryHTML(rq, hyphaName, list), user.FromRequest(rq))) } // Recent changes @@ -46,7 +46,7 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) { n, err = strconv.Atoi(noPrefix) ) if err == nil && n < 101 { - util.HTTP200Page(w, base(strconv.Itoa(n)+" recent changes", history.RecentChanges(n), user.FromRequest(rq))) + util.HTTP200Page(w, base(strconv.Itoa(n)+" recent changes", views.RecentChangesHTML(n), user.FromRequest(rq))) } else { http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther) } diff --git a/http_mutators.go b/http_mutators.go index ca48d07..5835420 100644 --- a/http_mutators.go +++ b/http_mutators.go @@ -5,10 +5,13 @@ import ( "log" "net/http" + "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/hyphae" "github.com/bouncepaw/mycorrhiza/markup" - "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/shroom" "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" + "github.com/bouncepaw/mycorrhiza/views" ) func init() { @@ -25,182 +28,140 @@ func init() { http.HandleFunc("/unattach-confirm/", handlerUnattachConfirm) } -func handlerUnattachAsk(w http.ResponseWriter, rq *http.Request) { - log.Println(rq.URL) - var ( - hyphaName = HyphaNameFromRq(rq, "unattach-ask") - hd, isOld = HyphaStorage[hyphaName] - hasAmnt = hd != nil && hd.binaryPath != "" - ) - if !hasAmnt { - HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach") - log.Println("Rejected (no amnt):", rq.URL) - return - } else if ok := user.CanProceed(rq, "unattach-confirm"); !ok { - HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments") - log.Println("Rejected (no rights):", rq.URL) - return - } - util.HTTP200Page(w, base("Unattach "+hyphaName+"?", templates.UnattachAskHTML(rq, hyphaName, isOld), user.FromRequest(rq))) -} - -func handlerUnattachConfirm(w http.ResponseWriter, rq *http.Request) { - log.Println(rq.URL) - var ( - hyphaName = HyphaNameFromRq(rq, "unattach-confirm") - hyphaData, isOld = HyphaStorage[hyphaName] - hasAmnt = hyphaData != nil && hyphaData.binaryPath != "" - u = user.FromRequest(rq) - ) - if !u.CanProceed("unattach-confirm") { - HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments") - log.Println("Rejected (no rights):", rq.URL) - return - } - if !hasAmnt { - HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach") - log.Println("Rejected (no amnt):", rq.URL) - return - } else if !isOld { - // The precondition is to have the hypha in the first place. - HttpErr(w, http.StatusPreconditionFailed, hyphaName, - "Error: no such hypha", - "Could not unattach this hypha because it does not exist") - return - } - if hop := hyphaData.UnattachHypha(hyphaName, u); len(hop.Errs) != 0 { - HttpErr(w, http.StatusInternalServerError, hyphaName, - "Error: could not unattach hypha", - fmt.Sprintf("Could not unattach this hypha due to internal errors. Server errors: %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(` -
    - - `, hyphaName) - case ".mp3": - return fmt.Sprintf(` -
    - - `, hyphaName) - default: - return fmt.Sprintf(` -
    -

    This hypha's media cannot be rendered. Download it

    -
    - `, hyphaName) - } -} - -// Index finds all hypha files in the full `path` and saves them to HyphaStorage. This function is recursive. -func Index(path string) { - nodes, err := ioutil.ReadDir(path) - if err != nil { - log.Fatal(err) - } - - for _, node := range nodes { - // If this hypha looks like it can be a hypha path, go deeper. Do not touch the .git and static folders for they have an admnistrative importance! - if node.IsDir() && isCanonicalName(node.Name()) && node.Name() != ".git" && node.Name() != "static" { - Index(filepath.Join(path, node.Name())) - continue - } - - var ( - hyphaPartPath = filepath.Join(path, node.Name()) - hyphaName, isText, skip = DataFromFilename(hyphaPartPath) - hyphaData *HyphaData - ) - if !skip { - // Reuse the entry for existing hyphae, create a new one for those that do not exist yet. - if hd, ok := HyphaStorage[hyphaName]; ok { - hyphaData = hd - } else { - hyphaData = &HyphaData{} - HyphaStorage[hyphaName] = hyphaData - hyphae.IncrementCount() - } - if isText { - hyphaData.textPath = hyphaPartPath - } else { - // Notify the user about binary part collisions. It's a design decision to just use any of them, it's the user's fault that they have screwed up the folder structure, but the engine should at least let them know, right? - if hyphaData.binaryPath != "" { - log.Println("There is a file collision for binary part of a hypha:", hyphaData.binaryPath, "and", hyphaPartPath, "-- going on with the latter") - } - hyphaData.binaryPath = hyphaPartPath - } - } - - } -} - -// FetchTextPart tries to read text file in the `d`. If there is no file, empty string is returned. -func FetchTextPart(d *HyphaData) (string, error) { - if d.textPath == "" { - return "", nil - } - _, err := os.Stat(d.textPath) - if os.IsNotExist(err) { - return "", nil - } else if err != nil { - return "", err - } - text, err := ioutil.ReadFile(d.textPath) - if err != nil { - return "", err - } - return string(text), nil -} - -func setHeaderLinks() { - if userLinksHypha, ok := GetHyphaData(util.HeaderLinksHypha); !ok { - 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/hyphae/count.go b/hyphae/count.go index d090606..7d20f93 100644 --- a/hyphae/count.go +++ b/hyphae/count.go @@ -10,21 +10,21 @@ var count = struct { sync.Mutex }{} -// Set the value of hyphae count to zero. +// Set the value of hyphae count to zero. Use when reloading hyphae. func ResetCount() { - count.Lock() - count.value = 0 - count.Unlock() + count.Lock() + count.value = 0 + count.Unlock() } -// Increment the value of hyphae count. +// Increment the value of the hyphae counter. Use when creating new hyphae or loading hyphae from disk. func IncrementCount() { count.Lock() count.value++ count.Unlock() } -// Decrement the value of hyphae count. +// Decrement the value of the hyphae counter. Use when deleting existing hyphae. func DecrementCount() { count.Lock() count.value-- @@ -33,6 +33,5 @@ func DecrementCount() { // Count how many hyphae there are. func Count() int { - // it is concurrent-safe to not lock here, right? return count.value } diff --git a/hyphae/files.go b/hyphae/files.go new file mode 100644 index 0000000..56ccda5 --- /dev/null +++ b/hyphae/files.go @@ -0,0 +1,66 @@ +package hyphae + +import ( + "io/ioutil" + "log" + "path/filepath" + + "github.com/bouncepaw/mycorrhiza/mimetype" + "github.com/bouncepaw/mycorrhiza/util" +) + +// Index finds all hypha files in the full `path` and saves them to the hypha storage. +func Index(path string) { + byNamesMutex.Lock() + defer byNamesMutex.Unlock() + byNames = make(map[string]*Hypha) + ch := make(chan *Hypha, 5) + + go func(ch chan *Hypha) { + indexHelper(path, 0, ch) + close(ch) + }(ch) + + for h := range ch { + // At this time it is safe to ignore the mutex, because there is only one worker. + if oldHypha, ok := byNames[h.Name]; ok { + oldHypha.MergeIn(h) + } else { + byNames[h.Name] = h + IncrementCount() + } + } +} + +// indexHelper finds all hypha files in the full `path` and sends them to the channel. Handling of duplicate entries and attachment and counting them is up to the caller. +func indexHelper(path string, nestLevel uint, ch chan *Hypha) { + nodes, err := ioutil.ReadDir(path) + if err != nil { + log.Fatal(err) + } + + for _, node := range nodes { + // If this hypha looks like it can be a hypha path, go deeper. Do not touch the .git and static folders for they have an admnistrative importance! + if node.IsDir() && + util.IsCanonicalName(node.Name()) && + node.Name() != ".git" && + !(nestLevel == 0 && node.Name() == "static") { + indexHelper(filepath.Join(path, node.Name()), nestLevel+1, ch) + continue + } + + var ( + hyphaPartPath = filepath.Join(path, node.Name()) + hyphaName, isText, skip = mimetype.DataFromFilename(hyphaPartPath) + hypha = &Hypha{Name: hyphaName, Exists: true} + ) + if !skip { + if isText { + hypha.TextPath = hyphaPartPath + } else { + hypha.BinaryPath = hyphaPartPath + } + ch <- hypha + } + } +} diff --git a/hyphae/hypha.go b/hyphae/hypha.go deleted file mode 100644 index d70cd1e..0000000 --- a/hyphae/hypha.go +++ /dev/null @@ -1,21 +0,0 @@ -package hyphae - -// TODO: do -import () - -type Hypha struct { - Name string - Exists bool - TextPath string - BinaryPath string - OutLinks []string - BackLinks []string -} - -// AddHypha adds a hypha named `name` with such `textPath` and `binaryPath`. Both paths can be empty. Does //not// check for hypha's existence beforehand. Count is handled. -func AddHypha(name, textPath, binaryPath string) { -} - -// DeleteHypha clears both paths and all out-links from the named hypha and marks it as non-existent. It does not actually delete it from the memdb. Count is handled. -func DeleteHypha(name string) { -} diff --git a/hyphae/hyphae.go b/hyphae/hyphae.go new file mode 100644 index 0000000..59e2763 --- /dev/null +++ b/hyphae/hyphae.go @@ -0,0 +1,175 @@ +// The `hyphae` package is for the Hypha type, hypha storage and stuff like that. It shall not depend on mycorrhiza modules other than util. +package hyphae + +import ( + "log" + "regexp" + "sync" +) + +// HyphaPattern is a pattern which all hyphae must match. +var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%{}]+`) + +type Hypha struct { + sync.RWMutex + + Name string + Exists bool + TextPath string + BinaryPath string + OutLinks []*Hypha + BackLinks []*Hypha +} + +var byNames = make(map[string]*Hypha) +var byNamesMutex = sync.Mutex{} + +// EmptyHypha returns an empty hypha struct with given name. +func EmptyHypha(hyphaName string) *Hypha { + return &Hypha{ + Name: hyphaName, + Exists: false, + TextPath: "", + BinaryPath: "", + OutLinks: make([]*Hypha, 0), + BackLinks: make([]*Hypha, 0), + } +} + +// ByName returns a hypha by name. If h.Exists, the returned hypha pointer is known to be part of the hypha index (byNames map). +func ByName(hyphaName string) (h *Hypha) { + h, exists := byNames[hyphaName] + if exists { + return h + } + return EmptyHypha(hyphaName) +} + +// Insert inserts the hypha into the storage. It overwrites the previous record, if there was any, and returns false. If the was no previous record, return true. +func (h *Hypha) Insert() (justCreated bool) { + hp := ByName(h.Name) + + byNamesMutex.Lock() + defer byNamesMutex.Unlock() + if hp.Exists { + hp = h + } else { + h.Exists = true + byNames[h.Name] = h + IncrementCount() + } + + return !hp.Exists +} + +func (h *Hypha) InsertIfNew() (justCreated bool) { + if !h.Exists { + return h.Insert() + } + return false +} + +func (h *Hypha) InsertIfNewKeepExistence() { + hp := ByName(h.Name) + + byNamesMutex.Lock() + defer byNamesMutex.Unlock() + if hp.Exists { + hp = h + } else { + byNames[h.Name] = h + } +} + +func (h *Hypha) Delete() { + byNamesMutex.Lock() + h.Lock() + delete(byNames, h.Name) + DecrementCount() + byNamesMutex.Unlock() + h.Unlock() + + for _, outlinkHypha := range h.OutLinks { + outlinkHypha.DropBackLink(h) + } +} + +func (h *Hypha) RenameTo(newName string) { + byNamesMutex.Lock() + h.Lock() + delete(byNames, h.Name) + h.Name = newName + byNames[h.Name] = h + byNamesMutex.Unlock() + h.Unlock() +} + +// MergeIn merges in content file paths from a different hypha object. Prints warnings sometimes. +func (h *Hypha) MergeIn(oh *Hypha) { + if h.TextPath == "" && oh.TextPath != "" { + h.TextPath = oh.TextPath + } + if oh.BinaryPath != "" { + if h.BinaryPath != "" { + log.Println("There is a file collision for binary part of a hypha:", h.BinaryPath, "and", oh.BinaryPath, "-- going on with the latter") + } + h.BinaryPath = oh.BinaryPath + } +} + +// ## Link related stuff +// Notes in pseudocode and whatnot: +// * (Reader h) does not mutate h => safe +// * (Rename h) reuses the same hypha object => safe +// * (Unattach h) and (Attach h) do not change (Backlinks h) => safe + +// * (Delete h) does not change (Backlinks h), but changes (Outlinks h), removing h from them => make it safe +// * (Unattach h) and (Attach h) => h may start or stop existing => may change (Outlinks h) => make it safe +// * (Edit h) => h may start existing => may change (Backlinks h) => make it safe +// * (Edit h) may add or remove h to or from (Outlinks h) => make it safe + +func (h *Hypha) AddOutLink(oh *Hypha) (added bool) { + h.Lock() + defer h.Unlock() + + for _, outlink := range h.OutLinks { + if outlink == oh { + return false + } + } + h.OutLinks = append(h.OutLinks, oh) + return true +} + +func (h *Hypha) AddBackLink(bh *Hypha) (added bool) { + h.Lock() + defer h.Unlock() + + for _, backlink := range h.BackLinks { + if backlink == h { + return false + } + } + h.BackLinks = append(h.BackLinks, bh) + return true +} + +func (h *Hypha) DropBackLink(bh *Hypha) { + h.Lock() + defer h.Unlock() + + if len(h.BackLinks) <= 1 { + h.BackLinks = make([]*Hypha, 0) + return + } + lastBackLinkIndex := len(h.BackLinks) + for i, backlink := range h.BackLinks { + if backlink == bh { + if i != lastBackLinkIndex { + h.BackLinks[i] = h.BackLinks[lastBackLinkIndex] + } + h.BackLinks = h.BackLinks[:lastBackLinkIndex] + return + } + } +} diff --git a/hyphae/iterators.go b/hyphae/iterators.go new file mode 100644 index 0000000..8e5fcd5 --- /dev/null +++ b/hyphae/iterators.go @@ -0,0 +1,57 @@ +// File `iterators.go` contains stuff that iterates over hyphae. +package hyphae + +import ( + "strings" +) + +// YieldExistingHyphae iterates over all hyphae and yields all existing ones. +func YieldExistingHyphae() chan *Hypha { + ch := make(chan *Hypha) + go func() { + for _, h := range byNames { + if h.Exists { + ch <- h + } + } + close(ch) + }() + return ch +} + +// FilterTextHyphae filters the source channel and yields only those hyphae than have text parts. +func FilterTextHyphae(src chan *Hypha) chan *Hypha { + sink := make(chan *Hypha) + go func() { + for h := range src { + if h.TextPath != "" { + sink <- h + } + } + close(sink) + }() + return sink +} + +// Subhyphae returns slice of subhyphae. +func (h *Hypha) Subhyphae() []*Hypha { + hyphae := []*Hypha{} + for subh := range YieldExistingHyphae() { + if strings.HasPrefix(subh.Name, h.Name+"/") { + hyphae = append(hyphae, subh) + } + } + return hyphae +} + +// AreFreeNames checks if all given `hyphaNames` are not taken. If they are not taken, `ok` is true. If not, `firstFailure` is the name of the first met hypha that is not free. +func AreFreeNames(hyphaNames ...string) (firstFailure string, ok bool) { + for h := range YieldExistingHyphae() { + for _, hn := range hyphaNames { + if hn == h.Name { + return hn, false + } + } + } + return "", true +} diff --git a/link/link.go b/link/link.go new file mode 100644 index 0000000..6fe3323 --- /dev/null +++ b/link/link.go @@ -0,0 +1,132 @@ +package link + +import ( + "fmt" + "path" + "strings" + + "github.com/bouncepaw/mycorrhiza/util" +) + +// LinkType tells what type the given link is. +type LinkType int + +const ( + LinkInavild LinkType = iota + // LinkLocalRoot is a link like "/list", "/user-list", etc. + LinkLocalRoot + // LinkLocalHypha is a link like "test", "../test", etc. + LinkLocalHypha + // LinkExternal is an external link with specified protocol. + LinkExternal + // LinkInterwiki is currently unused. + LinkInterwiki +) + +// Link is an abstraction for universal representation of links, be they links in mycomarkup links or whatever. +type Link struct { + // Address is what the link points to. + Address string + // Display is what gets nested into the tag. + Display string + Kind LinkType + DestinationUnknown bool + + // #... + Anchor string + Protocol string + // How the link address looked originally in source text. + SrcAddress string + // How the link display text looked originally in source text. May be empty. + SrcDisplay string + // RelativeTo is hypha name to which the link is relative to. + RelativeTo string +} + +// DoubtExistence sets DestinationUnknown to true if the link is local hypha link. +func (l *Link) DoubtExistence() { + if l.Kind == LinkLocalHypha { + l.DestinationUnknown = true + } +} + +// Classes returns CSS class string for given link. +func (l *Link) Classes() string { + if l.Kind == LinkExternal { + return fmt.Sprintf("wikilink wikilink_external wikilink_%s", l.Protocol) + } + classes := "wikilink wikilink_internal" + if l.DestinationUnknown { + classes += " wikilink_new" + } + return classes +} + +// Href returns content for the href attrubite for hyperlink. You should always use it. +func (l *Link) Href() string { + switch l.Kind { + case LinkExternal, LinkLocalRoot: + return l.Address + default: + return "/hypha/" + l.Address + l.Anchor + } +} + +// ImgSrc returns content for src attribute of img tag. Used with `img{}`. +func (l *Link) ImgSrc() string { + switch l.Kind { + case LinkExternal, LinkLocalRoot: + return l.Address + default: + return "/binary/" + l.Address + } +} + +// From returns a Link object given these `address` and `display` on relative to given `hyphaName`. +func From(address, display, hyphaName string) *Link { + address = strings.TrimSpace(address) + link := Link{ + SrcAddress: address, + SrcDisplay: display, + RelativeTo: hyphaName, + } + + if display == "" { + link.Display = address + } else { + link.Display = strings.TrimSpace(display) + } + + switch { + case strings.ContainsRune(address, ':'): + pos := strings.IndexRune(address, ':') + link.Protocol = address[:pos] + link.Kind = LinkExternal + + if display == "" { + link.Display = address[pos+1:] + if strings.HasPrefix(link.Display, "//") && len(link.Display) > 2 { + link.Display = link.Display[2:] + } + } + link.Address = address + case strings.HasPrefix(address, "/"): + link.Address = address + link.Kind = LinkLocalRoot + case strings.HasPrefix(address, "./"): + link.Kind = LinkLocalHypha + link.Address = util.CanonicalName(path.Join(hyphaName, address[2:])) + case strings.HasPrefix(address, "../"): + link.Kind = LinkLocalHypha + link.Address = util.CanonicalName(path.Join(path.Dir(hyphaName), address[3:])) + case strings.HasPrefix(address, "#"): + link.Kind = LinkLocalHypha + link.Address = util.CanonicalName(hyphaName) + link.Anchor = address + default: + link.Kind = LinkLocalHypha + link.Address = util.CanonicalName(address) + } + + return &link +} diff --git a/main.go b/main.go index c87c96e..f8780b1 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,6 @@ //go:generate go get -u github.com/valyala/quicktemplate/qtc -//go:generate qtc -dir=templates +//go:generate qtc -dir=assets +//go:generate qtc -dir=views package main import ( @@ -9,33 +10,20 @@ import ( "math/rand" "net/http" "os" - "path/filepath" - "regexp" "strings" + "github.com/bouncepaw/mycorrhiza/assets" "github.com/bouncepaw/mycorrhiza/history" "github.com/bouncepaw/mycorrhiza/hyphae" - "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/shroom" "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" + "github.com/bouncepaw/mycorrhiza/views" ) // WikiDir is a rooted path to the wiki storage directory. var WikiDir string -// HyphaPattern is a pattern which all hyphae must match. -var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%{}]+`) - -// HyphaStorage is a mapping between canonical hypha names and their meta information. -var HyphaStorage = make(map[string]*HyphaData) - -// IterateHyphaNamesWith is a closure to be passed to subpackages to let them iterate all hypha names read-only. -func IterateHyphaNamesWith(f func(string)) { - for hyphaName := range HyphaStorage { - f(hyphaName) - } -} - // HttpErr is used by many handlers to signal errors in a compact way. func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) { log.Println(errMsg, "for", name) @@ -46,7 +34,7 @@ func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) { base( title, fmt.Sprintf( - `

    %s. Go back to the hypha.

    `, + `

    %s. Go back to the hypha.

    `, errMsg, name, ), @@ -58,19 +46,11 @@ func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) { // Show all hyphae func handlerList(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) - var ( - tbody string - pageCount = hyphae.Count() - u = user.FromRequest(rq) - ) - for hyphaName, data := range HyphaStorage { - tbody += templates.HyphaListRowHTML(hyphaName, ExtensionToMime(filepath.Ext(data.binaryPath)), data.binaryPath != "") - } - util.HTTP200Page(w, base("List of pages", templates.HyphaListHTML(tbody, pageCount), u)) + util.HTTP200Page(w, base("List of pages", views.HyphaListHTML(), user.FromRequest(rq))) } // This part is present in all html documents. -var base = templates.BaseHTML +var base = views.BaseHTML // Reindex all hyphae by checking the wiki storage directory anew. func handlerReindex(w http.ResponseWriter, rq *http.Request) { @@ -81,14 +61,15 @@ func handlerReindex(w http.ResponseWriter, rq *http.Request) { return } hyphae.ResetCount() - HyphaStorage = make(map[string]*HyphaData) log.Println("Wiki storage directory is", WikiDir) log.Println("Start indexing hyphae...") - Index(WikiDir) + hyphae.Index(WikiDir) log.Println("Indexed", hyphae.Count(), "hyphae") http.Redirect(w, rq, "/", http.StatusSeeOther) } +// Stop the wiki + // Update header links by reading the configured hypha, if there is any, or resorting to default values. func handlerUpdateHeaderLinks(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) @@ -97,7 +78,7 @@ func handlerUpdateHeaderLinks(w http.ResponseWriter, rq *http.Request) { log.Println("Rejected", rq.URL) return } - setHeaderLinks() + shroom.SetHeaderLinks() http.Redirect(w, rq, "/", http.StatusSeeOther) } @@ -106,23 +87,25 @@ func handlerRandom(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) var randomHyphaName string i := rand.Intn(hyphae.Count()) - for hyphaName := range HyphaStorage { + for h := range hyphae.YieldExistingHyphae() { if i == 0 { - randomHyphaName = hyphaName - break + randomHyphaName = h.Name } i-- } - http.Redirect(w, rq, "/page/"+randomHyphaName, http.StatusSeeOther) + http.Redirect(w, rq, "/hypha/"+randomHyphaName, http.StatusSeeOther) } func handlerStyle(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) - if _, err := os.Stat(WikiDir + "/static/common.css"); err == nil { - http.ServeFile(w, rq, WikiDir+"/static/common.css") + if _, err := os.Stat(util.WikiDir + "/static/common.css"); err == nil { + http.ServeFile(w, rq, util.WikiDir+"/static/common.css") } else { w.Header().Set("Content-Type", "text/css;charset=utf-8") - w.Write([]byte(templates.DefaultCSS())) + w.Write([]byte(assets.DefaultCSS())) + } + if bytes, err := ioutil.ReadFile(util.WikiDir + "/static/custom.css"); err == nil { + w.Write(bytes) } } @@ -143,20 +126,26 @@ func handlerIcon(w http.ResponseWriter, rq *http.Request) { w.Header().Set("Content-Type", "image/svg+xml") switch iconName { case "gemini": - w.Write([]byte(templates.IconGemini())) + w.Write([]byte(assets.IconGemini())) case "mailto": - w.Write([]byte(templates.IconMailto())) + w.Write([]byte(assets.IconMailto())) case "gopher": - w.Write([]byte(templates.IconGopher())) + w.Write([]byte(assets.IconGopher())) default: - w.Write([]byte(templates.IconHTTP())) + w.Write([]byte(assets.IconHTTP())) } } func handlerAbout(w http.ResponseWriter, rq *http.Request) { w.Header().Set("Content-Type", "text/html;charset=utf-8") w.WriteHeader(http.StatusOK) - w.Write([]byte(base("About "+util.SiteName, templates.AboutHTML(), user.FromRequest(rq)))) + w.Write([]byte(base("About "+util.SiteName, views.AboutHTML(), user.FromRequest(rq)))) +} + +func handlerUserList(w http.ResponseWriter, rq *http.Request) { + w.Header().Set("Content-Type", "text/html;charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte(base("User list", views.UserListHTML(), user.FromRequest(rq)))) } func handlerRobotsTxt(w http.ResponseWriter, rq *http.Request) { @@ -177,13 +166,19 @@ func main() { log.Fatal(err) } log.Println("Wiki storage directory is", WikiDir) - Index(WikiDir) + hyphae.Index(WikiDir) log.Println("Indexed", hyphae.Count(), "hyphae") + shroom.FindAllBacklinks() + log.Println("Found all backlinks") history.Start(WikiDir) - setHeaderLinks() + shroom.SetHeaderLinks() - // See http_readers.go for /page/, /text/, /binary/ + go handleGemini() + + // See http_admin.go for /admin, /admin/* + initAdmin() + // See http_readers.go for /page/, /hypha/, /text/, /binary/, /attachment/ // See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/, /rename-ask/, /rename-confirm/, /unattach-ask/, /unattach-confirm/ // See http_auth.go for /login, /login-data, /logout, /logout-confirm // See http_history.go for /history/, /recent-changes @@ -192,15 +187,16 @@ func main() { http.HandleFunc("/update-header-links", handlerUpdateHeaderLinks) http.HandleFunc("/random", handlerRandom) http.HandleFunc("/about", handlerAbout) + http.HandleFunc("/user-list", handlerUserList) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static")))) http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) { http.ServeFile(w, rq, WikiDir+"/static/favicon.ico") }) http.HandleFunc("/static/common.css", handlerStyle) http.HandleFunc("/static/icon/", handlerIcon) - http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { - http.Redirect(w, rq, "/page/"+util.HomePage, http.StatusSeeOther) - }) http.HandleFunc("/robots.txt", handlerRobotsTxt) + http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { + http.Redirect(w, rq, "/hypha/"+util.HomePage, http.StatusSeeOther) + }) log.Fatal(http.ListenAndServe("0.0.0.0:"+util.ServerPort, nil)) } diff --git a/markup/hr.go b/markup/hr.go new file mode 100644 index 0000000..278138a --- /dev/null +++ b/markup/hr.go @@ -0,0 +1,34 @@ +package markup + +import ( + "unicode" +) + +// MatchesHorizontalLine checks if the string can be interpreted as suitable for rendering as
    . +// +// The rule is: if there are more than 4 characters "-" in the string, then make it a horizontal line. +// Otherwise it is a paragraph (

    ). +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 += `
    ` - } - html += ParagraphToHtml(hyphaName, line) - } - } - return `

    ` + html + `
    ` -} - -func (entry *imgEntry) sizeWAsAttr() string { - if entry.sizeW.Len() == 0 { - return "" - } - return ` width="` + entry.sizeW.String() + `"` -} - -func (entry *imgEntry) sizeHAsAttr() string { - if entry.sizeH.Len() == 0 { - return "" - } - return ` height="` + entry.sizeH.String() + `"` -} - type imgState int const ( @@ -71,6 +33,8 @@ type Img struct { func (img *Img) pushEntry() { if strings.TrimSpace(img.currEntry.path.String()) != "" { + img.currEntry.srclink = link.From(img.currEntry.path.String(), "", img.hyphaName) + img.currEntry.srclink.DoubtExistence() img.entries = append(img.entries, img.currEntry) img.currEntry = imgEntry{} img.currEntry.path.Reset() @@ -177,23 +141,6 @@ func ImgFromFirstLine(line, hyphaName string) (img *Img, shouldGoBackToNormal bo return img, img.Process(line) } -func (img *Img) binaryPathFor(path string) string { - path = strings.TrimSpace(path) - if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 { - return path - } else { - return "/binary/" + xclCanonicalName(img.hyphaName, path) - } -} - -func (img *Img) ogBinaryPathFor(path string) string { - path = img.binaryPathFor(path) - if strings.HasPrefix(path, "/binary/") { - return util.URL + path - } - return path -} - func (img *Img) pagePathFor(path string) string { path = strings.TrimSpace(path) if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 { @@ -214,30 +161,18 @@ func parseDimensions(dimensions string) (sizeW, sizeH string) { return } -func (img *Img) checkLinks() map[string]bool { - m := make(map[string]bool) - for i, entry := range img.entries { - // Also trim them for later use - entry.trimmedPath = strings.TrimSpace(entry.path.String()) - isAbsoluteUrl := strings.ContainsRune(entry.trimmedPath, ':') - if !isAbsoluteUrl { - entry.trimmedPath = canonicalName(entry.trimmedPath) - } - img.entries[i] = entry - m[entry.trimmedPath] = isAbsoluteUrl - } - HyphaIterate(func(hyphaName string) { +func (img *Img) markExistenceOfSrcLinks() { + HyphaIterate(func(hn string) { for _, entry := range img.entries { - if hyphaName == xclCanonicalName(img.hyphaName, entry.trimmedPath) { - m[entry.trimmedPath] = true + if hn == entry.srclink.Address { + entry.srclink.DestinationUnknown = false } } }) - return m } func (img *Img) ToHtml() (html string) { - linkAvailabilityMap := img.checkLinks() + img.markExistenceOfSrcLinks() isOneImageOnly := len(img.entries) == 1 && img.entries[0].desc.Len() == 0 if isOneImageOnly { html += `