Compare commits

...

85 Commits

Author SHA1 Message Date
Alyx Batte
3313e17efa Changed default config for docker build
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-10-07 16:23:26 +00:00
Sunny
f8bba997bf
Fix URL of dialog close icon (#267) 2025-09-03 18:16:49 +03:00
Timur Ismagilov
4cf4d1695f Rip qtpl from dependencies AT LONG LAST 2025-07-03 11:59:11 +03:00
Timur Ismagilov
1b5abe4de1 Liberate mycoopts from qtpl shackles 2025-07-03 11:51:15 +03:00
Timur Ismagilov
5dfbbdb775 Liberate history package from the qtpl shackles 2025-07-03 11:35:57 +03:00
Timur Ismagilov
7e1948c93f Ignore .idea dir 2025-07-03 10:57:02 +03:00
Timur Ismagilov
3718f6ec7c Fix mp3, add flac and wav support 2025-07-03 10:51:33 +03:00
Timur Ismagilov
ce108bc07d Replace Alt+arrows with Alt+Shift+arrows and increase shortcut help dialog max-width
Fixes: https://github.com/bouncepaw/mycorrhiza/issues/262
2025-07-03 10:24:44 +03:00
Chris Sexton
da84a76e79
Reorder OpenGraph search. (#265)
The previous ordering prevented the visitors from finding a description or
image.
2025-02-20 02:02:07 +03:00
dependabot[bot]
d679eb4661
Bump golang.org/x/crypto from 0.27.0 to 0.31.0 (#261)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 06:41:34 +03:00
novenary
0f7525a23c
Don't exit when removing socket fails (#260)
Prior to migrating to slog, errors when removing the socket were
ignored.
This is fine and desirable: the most likely error condition is that the
socket did not exist in the first place. Exiting on error in that case
effectively prevents Mycorrhiza to be used with unix sockets.
If the removal fails for another reason, then starting the server will
fail, so logging a warning is sufficient for troubleshooting.
2024-12-12 00:20:24 +03:00
Timur Ismagilov
727280147a Update README.md 2024-10-05 22:30:17 +03:00
Timur Ismagilov
a3e8654c5b Update README.md 2024-10-05 22:03:54 +03:00
Timur Ismagilov
a4cc67cd74
Migrate from log to slog #109 (#255)
* Migrate httpd.go

* Migrate history and main

* Migrate hypview

* Migrate interwiki

* Migrate misc

* Migrate utils

* Migrate backlinks

* Migrate categories

* Reformat some imports

* Migrate hyphae

* Migrate migration

* Reformat more imports

* Migrate user

* Migrate shroom

* Migrate viewutil

* Migrate web

* Migrate others

* Migrate main

* Wording concerns
2024-09-07 23:55:39 +03:00
Timur Ismagilov
4c5f385afd Drop exp/slices dependency and use the built-in slices
This is the future
2024-09-07 21:39:59 +03:00
Timur Ismagilov
d70d8aa990 Revert "Make shortcuts work outside of English layout (#227)"
This reverts commit b43f2836c1.
2024-09-07 21:35:22 +03:00
Timur Ismagilov
380a8c321a Drop Go version to 1.21 and update dependencies 2024-09-07 21:32:45 +03:00
Timur Ismagilov
41733c50bd
New templates #117 (#236)
Didn't have the chance to migrate //all// templates just yet. We'll get there.

* Implement yet another template system

* Move orphans to the new system and fix a bug in it

* Link orphans in the admin panel

* Move the backlink handlers to the web package

* Move auth routing to web

* Move /user-list to the new system

* Move change password and translate it

* Move stuff

* Move admin-related stuff to the web

* Move a lot of files into internal dir

Outside of it are web and stuff that needs further refactoring

* Fix static not loading and de-qtpl tree

* Move tree to internal

* Keep the globe on the same line #230

* Revert "Keep the globe on the same line #230"

This reverts commit ae78e5e459.

* Migrate templates from hypview: delete, edit, start empty and existing WIP

The delete media view was removed, I didn't even know it still existed as a GET. A rudiment.

* Make views multi-file and break compilation

* Megarefactoring of hypha views

* Auth-related stuffs

* Fix some of those weird imports

* Migrate cat views

* Fix cat js

* Lower standards

* Internalize trauma
2024-09-07 21:22:41 +03:00
Douglas Pi
a9ee700aad Fix Windows slashes 2024-08-25 20:16:24 +03:00
Danila Gorelko
ea7d60dd72
Trick safari to not recognise any "name" in the input field (#254)
Safari looks at id and placeholder in order to enable contacts autocomplete.

Fixes: #253
2024-08-14 14:10:03 +03:00
Danila Gorelko
00bd7e1f78
Remove 'Category list' from autocomplete menu (#252) 2024-08-14 14:03:20 +03:00
Timur Ismagilov
719de9b530 Update version to 1.15 2024-06-29 18:37:15 +03:00
Timur Ismagilov
7d5636486d Delete release.yaml
It hadn't been working for a couple of years already anyway
2024-06-29 18:32:42 +03:00
Timur Ismagilov
922181ee93 Add Cmd+' shortcut for local time and change the format of time 2024-06-29 18:21:31 +03:00
Timur Ismagilov
522640f8df Mention that robots.txt can be redefined 2024-06-29 18:17:55 +03:00
Timur Ismagilov
4bfbb6a2d7 Update README.md 2024-06-29 18:12:23 +03:00
Timur Ismagilov
8f9c5d3677 Change tab size to 3
Implements: https://github.com/bouncepaw/mycorrhiza/issues/237
2024-06-29 18:04:58 +03:00
Timur Ismagilov
f1d4310ec1 Change the mv-categories made-up microformat to mv-tags to match Betula
One day Bouncepaw will learn of “JSON API”
2024-06-11 14:58:59 +03:00
Timur Ismagilov
33a477cf36 Add local time buttons
Implements: #231
2024-06-11 14:47:15 +03:00
Timur Ismagilov
a0ec4f5fbf Hopefully fix some bugs on Windows 2024-06-11 02:52:47 +03:00
Timur Ismagilov
9ef08fb42d Fix the size of globes with bigger default font size 2024-06-10 14:25:01 +03:00
Timur Ismagilov
0b132d33fb Keep the globe on the same line while still breaking link #230 2024-06-10 14:15:03 +03:00
Timur Ismagilov
326ace8e13 Fix go.sum 2024-06-03 21:59:18 +03:00
Timur Ismagilov
e0a0385656 Update Mycomarkup to v5.6.0 2024-06-02 00:49:58 +03:00
Timur Ismagilov
d85c12bae5 Fix hover color of subhypha links in dark theme 2024-06-01 23:57:30 +03:00
Timur Ismagilov
d1bf1f76eb Offer to pass -create-admin option if no admins were found
Implements: #218
2024-06-01 23:43:26 +03:00
Timur Ismagilov
2702b4da63 Update gorilla/feeds and fix JSON Feeds (untested) 2024-06-01 23:31:50 +03:00
Timur Ismagilov
640dd3d972 Change the pencil to the eye and an arrow
Pencil? What?
2024-06-01 23:18:16 +03:00
Timur Ismagilov
a0945b1210 Fix the notorious renaming bug
Fixes: #226 #220
2024-06-01 23:10:09 +03:00
Timur Ismagilov
96820305f2 Remove Russian translation for docs topics
They made no sense because the translation of the docs was deleted a long time ago
2024-06-01 22:48:14 +03:00
Timur Ismagilov
5547cb153d Implement /today and /edit-today
Implements #205
2024-06-01 22:44:27 +03:00
Timur Ismagilov
b691ea04ce Wording in docs 2024-06-01 22:34:30 +03:00
handlerug
b43f2836c1
Make shortcuts work outside of English layout (#227)
The keyboard event is tested two times: first time with the original key
property, second time with the key property derived from the key code.
This is done to support non-English shortcuts (which may be added by
wiki administrators).
2024-05-25 23:39:23 +03:00
Danila Gorelko
afe2f0c9e2
Add clickable diffs to recent changes view (#215)
* Add clickable diffs to recent changes view

* Reuse entries

* Insert non-breaking space

* Show hash only once, remove parenthesis
2024-03-10 22:39:56 +03:00
dependabot[bot]
210615efa2
Bump golang.org/x/crypto from 0.1.0 to 0.17.0 (#212)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.1.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.1.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 01:52:05 +03:00
Timur Ismagilov
7ad8a06f66 Follow up translation to the patch 2023-11-27 22:50:55 +03:00
Jackson
5f592acc55 implement user facing password change page
similar to the admin password change, but with a few changes:
- require current password verification

the following still included:
- empty password check
- confirm password check
2023-11-27 22:33:12 +03:00
Jackson
4629f39e99 implement admin form to change a user's password 2023-11-27 22:33:12 +03:00
Jackson
5ed9e6d9ef move form errors out of change group thing
there are multiple form fields now, so the error could apply to any one
of the forms
2023-11-27 22:33:12 +03:00
Jackson
b41acf1f57 implement changing user password function 2023-11-27 22:33:12 +03:00
Shivram
6c2a3c9745
Fix env var bug in history.silentGitsh (#210) 2023-11-22 20:41:18 +03:00
Timur Ismagilov
13a1019c9d Fix link to [[why mycomarkup]] 2023-11-10 00:49:03 +03:00
Timur Ismagilov
1a77fbbb8c Fix JSON feed MIME type 2023-11-10 00:43:33 +03:00
Timur Ismagilov
103a3a0e7d Disable GPG signing on git commit
Fixes: https://github.com/bouncepaw/mycorrhiza/issues/207
2023-11-10 00:39:55 +03:00
decentral1se
5d8eaef6d7
Use -Ns for curl on healtcheck (#206)
Otherwise, it doesn't work.
2023-10-17 12:10:32 +03:00
Timur Ismagilov
d0be765935 Docs: Fix wrong filename for default.css
Fixes: https://github.com/bouncepaw/mycorrhiza/issues/203
2023-08-12 13:54:20 +05:00
Timur Ismagilov
fe4fd09cee Interwiki: Fix some bugs
* Actually, you could not edit interwiki entries before
* Fix faulty template in English locale
2023-08-08 00:37:14 +05:00
Timur Ismagilov
c2619a6b82 Implement Betula interwiki targets
Implements #198
2023-08-06 02:13:46 +05:00
Timur Ismagilov
353d462bbe Fix recent changes crashing on empty wikis
This is a dirty fix. How come git-handling is so messed?
2023-08-06 02:13:46 +05:00
Timur Ismagilov
eae42c310d Add print-specific styles
Implements #201
2023-08-06 02:13:46 +05:00
Danila Gorelko
6b8d9addc5 Makes category buttons for mobile bigger 2023-05-28 14:33:59 +03:00
Timur Ismagilov
b540b94477 Turn off browser's autocomplete for the category input
The datalist-based existing category suggestions still work.
2023-05-14 13:48:45 +03:00
Timur Ismagilov
2dbcb0dc27 Update Mycomarkup doc 2023-04-07 15:59:56 +03:00
Timur Ismagilov
de94ca4967 Update the screenshot in README.md 2023-04-07 15:55:53 +03:00
Timur Ismagilov
dcf823522a Fix version retrieval procedure 2023-04-07 15:45:09 +03:00
Timur Ismagilov
b7be7b0b3a Update en.myco 2023-04-07 15:43:06 +03:00
V
96d7401f5f
explictly fail if git can't operate (#194)
* explictly fail if git can't operate

* Update revision.go
2023-03-12 10:57:14 +03:00
dependabot[bot]
9a458a78a6 Bump golang.org/x/crypto from 0.0.0-20211108221036-ceb1ce70b4fa to 0.1.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.0.0-20211108221036-ceb1ce70b4fa to 0.1.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-07 08:01:34 +03:00
Timur Ismagilov
3c32e40e59 Fix a Telegram-related bug
I removed my favorite comment:

// Problems is something we put blankets on.
2023-02-25 23:38:51 +03:00
dependabot[bot]
7b0aec0a2e Bump golang.org/x/text from 0.3.7 to 0.3.8
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-23 11:08:40 +03:00
Timur Ismagilov
a9eeccb9b9 Implement category autocomplete
Loosely based on the one used in Betula
2023-02-18 23:04:52 +03:00
Timur Ismagilov
e03d17f9a1 Add mv-categories/mv-category microformat 2023-02-18 22:35:25 +03:00
Timur Ismagilov
dda0cf70c5 Update Mycomarkup to v5.4.0
Including:

* Fixes to external links
* Reducing of ID:s
2023-02-01 00:14:37 +05:00
Timur Ismagilov
cb2431115c Title search: Fix <title> in Russian locale 2023-01-03 00:47:14 +05:00
Timur Ismagilov
a61d9c673c Fix primitive diff not working at all 2023-01-03 00:43:29 +05:00
Timur Ismagilov
dc1d9f1ffb Change the version to 1.14 2022-12-10 13:41:57 +03:00
Timur Ismagilov
19bc10464d Help: Replace the image from Wikicommons to my userpic from lesarbr.es 2022-12-09 19:55:36 +03:00
Timur Ismagilov
7140c79b63 Rename: Leave transclusion on redirection hyphae 2022-12-09 18:43:38 +03:00
Timur Ismagilov
bdb5bfc6ec Search: Make the search bar bigger 2022-12-09 18:38:26 +03:00
Timur Ismagilov
fed4472ae3 Search: Delete the search results description 2022-12-09 18:30:42 +03:00
Timur Ismagilov
b29042a52c Search: Add go to hypha section 2022-12-09 18:27:47 +03:00
Timur Ismagilov
2dab26dafa Capitalize Mycomarkup 2022-12-09 18:11:56 +03:00
Umar Getagazov
62c568414e center search bar with standard grid CSS 2022-12-04 21:54:48 +03:00
Umar Getagazov
40dbbf5376 Highlight primitive diff additions and deletions 2022-11-15 20:38:03 +03:00
Umar Getagazov
4e6adec81a Validate the revision hash on /rev{,-text}/ pages 2022-11-14 23:31:22 +03:00
154 changed files with 3074 additions and 4751 deletions

View File

@ -1,28 +0,0 @@
on:
release:
types: [created]
jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
strategy:
matrix:
# build and publish in parallel a lot of binaries
# https://golang.org/doc/install/source#environment See supported Go OS/Arch pairs here
goos: [linux, darwin, openbsd, windows]
goarch: ["386", amd64, arm64]
exclude:
- goarch: "386"
goos: darwin
- goarch: arm64
goos: windows
steps:
- uses: actions/checkout@v2
- uses: wangyoucao577/go-release-action@v1.17
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
binary_name: "mycorrhiza"
extra_files: LICENSE README.md help/mycorrhiza.1

1
.gitignore vendored
View File

@ -1 +1,2 @@
mycorrhiza
.idea

View File

@ -8,7 +8,7 @@ FROM alpine/git as app
EXPOSE 1737
RUN apk add --no-cache curl
HEALTHCHECK CMD curl -f localhost:1737 || exit 1
HEALTHCHECK CMD curl -Ns localhost:1737 || exit 1
WORKDIR /
RUN mkdir wiki

View File

@ -2,17 +2,17 @@
**Mycorrhiza Wiki** is a lightweight file-system wiki engine that uses Git for keeping history. [Main wiki](https://mycorrhiza.wiki)
<img src="https://mycorrhiza.wiki/binary/release/1.10/screenshot" alt="A screenshot of mycorrhiza.wiki's home page in the Safari browser" width="700">
<img src="https://mycorrhiza.wiki/binary/release/1.15.1/screenshot" alt="A screenshot of mycorrhiza.wiki's home page in the Safari browser" width="700">
## Features
* **No database required.** Everything is stored in plain files. It makes installation super easy, and you can modify the content directly by yourself.
* **No database used.** Everything is stored in plain files. It makes installation super easy, and you can modify the content directly by yourself.
* **Everything is hyphae.** A hypha is a unit of content such as a picture, video or a text article. Hyphae can [transclude] and link each other, forming a tight network of hypertext pages.
* **Hyphae are authored in [Mycomarkup],** a markup language that's designed to be unambigious yet easy to use.
* **Hyphae are authored in [Mycomarkup],** a markup language that's designed to be unambiguous yet easy to use.
* **Categories** let you organize hyphae without any hierarchy restrictions, with all the benefits of a category system.
* **Nesting of hyphae** is also supported if you like hierarchies.
* **History of changes** for textual parts of hyphae. Every change is safely stored in [Git]. Web feeds for recent changes included.
* **History of changes.** Every change is safely stored in [Git]. Web feeds (RSS, Atom, JSON Feed) for recent changes included.
* **Keyboard-driven navigation.** Press `?` to see the list of shortcuts.
* **Support for [authorization].** Both plain username-password pairs and [Telegram]'s login widget are supported.
* **[Open Graph] support.** The most relevant info about a hypha is made available through OG meta tags for consumption by other software.
@ -30,17 +30,21 @@ Compare Mycorrhiza Wiki with other engines on [WikiMatrix](https://www.wikimatri
## Installing
See [the deployment guide](https://mycorrhiza.wiki/hypha/deployment) on the wiki.
See [the deployment guide](https://mycorrhiza.wiki/hypha/deployment) on the wiki. Also, Mycorrhiza might be available in your repositories. See [Repology](https://repology.org/project/mycorrhiza/versions).
## Contributing
## Contributing and community
* [SourceHut](https://sr.ht/~bouncepaw/mycorrhiza)
* [GitHub](https://github.com/bouncepaw/mycorrhiza)
* [#mycorrhiza on irc.libera.chat](irc://irc.libera.chat/#mycorrhiza)
* [Fediverse @mycorrhiza@floss.social](https://floss.social/@mycorrhiza)
* Mirrors:
* [SourceHut](https://sr.ht/~bouncepaw/mycorrhiza)
* [Codeberg](https://codeberg.org/bouncepaw/mycorrhiza)
* [@mycorrhizadev (Russian) in Telegram](https://t.me/mycorrhizadev)
If you want to contribute with code, open a pull request on GitHub or send a patch to the [mailing list](https://lists.sr.ht/~bouncepaw/mycorrhiza-devel).
If you want to report an issue, open an issue on GitHub or contact us directly.
Consider supporting the development on [Boosty](https://boosty.to/bouncepaw).
Check out [Betula](https://betula.mycorrhiza.wiki) as well.

View File

@ -1,140 +0,0 @@
package admin
import (
"fmt"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/gorilla/mux"
"log"
"mime"
"net/http"
"os"
"sort"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
// handlerAdmin provides the admin panel.
func handlerAdmin(w http.ResponseWriter, rq *http.Request) {
w.Header().Set("Content-Type", "text/html;charset=utf-8")
w.WriteHeader(http.StatusOK)
viewPanel(viewutil.MetaFrom(w, rq))
}
// handlerAdminShutdown kills the wiki.
func handlerAdminShutdown(w http.ResponseWriter, rq *http.Request) {
if user.CanProceed(rq, "admin/shutdown") {
log.Println("An admin commanded the wiki to shutdown")
os.Exit(0)
}
}
// handlerAdminReindexUsers reinitialises the user system.
func handlerAdminReindexUsers(w http.ResponseWriter, rq *http.Request) {
user.ReadUsersFromFilesystem()
redirectTo := rq.Referer()
if redirectTo == "" {
redirectTo = "/hypha/" + cfg.UserHypha
}
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
}
func handlerAdminUsers(w http.ResponseWriter, rq *http.Request) {
// Get a sorted list of users
var users []*user.User
for u := range user.YieldUsers() {
users = append(users, u)
}
sort.Slice(users, func(i, j int) bool {
less := users[i].RegisteredAt.Before(users[j].RegisteredAt)
return less
})
viewList(viewutil.MetaFrom(w, rq), users)
}
func handlerAdminUserEdit(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(rq)
u := user.ByName(vars["username"])
if u == nil {
util.HTTP404Page(w, "404 page not found")
return
}
f := util.FormDataFromRequest(rq, []string{"group"})
if rq.Method == http.MethodPost {
oldGroup := u.Group
newGroup := f.Get("group")
if user.ValidGroup(newGroup) {
u.Group = newGroup
if err := user.SaveUserDatabase(); err != nil {
u.Group = oldGroup
log.Println(err)
f = f.WithError(err)
} else {
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
return
}
} else {
f = f.WithError(fmt.Errorf("invalid group %s", newGroup))
}
}
f.Put("group", u.Group)
if f.HasError() {
w.WriteHeader(http.StatusBadRequest)
}
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
viewEditUser(viewutil.MetaFrom(w, rq), f, u)
}
func handlerAdminUserDelete(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(rq)
u := user.ByName(vars["username"])
if u == nil {
util.HTTP404Page(w, "404 page not found")
return
}
f := util.NewFormData()
if rq.Method == http.MethodPost {
f = f.WithError(user.DeleteUser(u.Name))
if !f.HasError() {
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
} else {
log.Println(f.Error())
}
}
if f.HasError() {
w.WriteHeader(http.StatusBadRequest)
}
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
viewDeleteUser(viewutil.MetaFrom(w, rq), f, u)
}
func handlerAdminUserNew(w http.ResponseWriter, rq *http.Request) {
if rq.Method == http.MethodGet {
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
viewNewUser(viewutil.MetaFrom(w, rq), util.NewFormData())
} else if rq.Method == http.MethodPost {
// Create a user
f := util.FormDataFromRequest(rq, []string{"name", "password", "group"})
err := user.Register(f.Get("name"), f.Get("password"), f.Get("group"), "local", true)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
viewNewUser(viewutil.MetaFrom(w, rq), f.WithError(err))
} else {
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
}
}
}

View File

@ -1,122 +0,0 @@
package admin
import (
"embed"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/gorilla/mux"
"net/http"
)
const adminTranslationRu = `
{{define "panel title"}}Панель админстратора{{end}}
{{define "panel safe section title"}}Безопасная секция{{end}}
{{define "panel link about"}}Об этой вики{{end}}
{{define "panel update header"}}Обновить ссылки в верхней панели{{end}}
{{define "panel link user list"}}Список пользователей{{end}}
{{define "panel users"}}Управление пользователями{{end}}
{{define "panel unsafe section title"}}Опасная секция{{end}}
{{define "panel shutdown"}}Выключить вики{{end}}
{{define "panel reindex hyphae"}}Переиндексировать гифы{{end}}
{{define "panel interwiki"}}Интервики{{end}}
{{define "manage users"}}Управление пользователями{{end}}
{{define "create user"}}Создать пользователя{{end}}
{{define "reindex users"}}Переиндексировать пользователей{{end}}
{{define "name"}}Имя{{end}}
{{define "group"}}Группа{{end}}
{{define "registered at"}}Зарегистрирован{{end}}
{{define "actions"}}Действия{{end}}
{{define "edit"}}Изменить{{end}}
{{define "new user"}}Новый пользователь{{end}}
{{define "password"}}Пароль{{end}}
{{define "create"}}Создать{{end}}
{{define "change group"}}Изменить группу{{end}}
{{define "user x"}}Пользователь {{.}}{{end}}
{{define "update"}}Обновить{{end}}
{{define "delete user"}}Удалить пользователя{{end}}
{{define "delete user tip"}}Удаляет пользователя из базы данных. Правки пользователя будут сохранены. Имя пользователя освободится для повторной регистрации.{{end}}
{{define "delete user?"}}Удалить пользователя {{.}}?{{end}}
{{define "delete user warning"}}Вы уверены, что хотите удалить этого пользователя из базы данных? Это действие нельзя отменить.{{end}}
`
var (
//go:embed *.html
fs embed.FS
panelChain, listChain, newUserChain, editUserChain, deleteUserChain viewutil.Chain
)
func Init(rtr *mux.Router) {
rtr.HandleFunc("/shutdown", handlerAdminShutdown).Methods(http.MethodPost)
rtr.HandleFunc("/reindex-users", handlerAdminReindexUsers).Methods(http.MethodPost)
rtr.HandleFunc("/new-user", handlerAdminUserNew).Methods(http.MethodGet, http.MethodPost)
rtr.HandleFunc("/users/{username}/edit", handlerAdminUserEdit).Methods(http.MethodGet, http.MethodPost)
rtr.HandleFunc("/users/{username}/delete", handlerAdminUserDelete).Methods(http.MethodGet, http.MethodPost)
rtr.HandleFunc("/users", handlerAdminUsers)
rtr.HandleFunc("/", handlerAdmin)
panelChain = viewutil.CopyEnRuWith(fs, "view_panel.html", adminTranslationRu)
listChain = viewutil.CopyEnRuWith(fs, "view_user_list.html", adminTranslationRu)
newUserChain = viewutil.CopyEnRuWith(fs, "view_new_user.html", adminTranslationRu)
editUserChain = viewutil.CopyEnRuWith(fs, "view_edit_user.html", adminTranslationRu)
deleteUserChain = viewutil.CopyEnRuWith(fs, "view_delete_user.html", adminTranslationRu)
}
func viewPanel(meta viewutil.Meta) {
viewutil.ExecutePage(meta, panelChain, &viewutil.BaseData{})
}
type listData struct {
*viewutil.BaseData
UserHypha string
Users []*user.User
}
func viewList(meta viewutil.Meta, users []*user.User) {
viewutil.ExecutePage(meta, listChain, listData{
BaseData: &viewutil.BaseData{},
UserHypha: cfg.UserHypha,
Users: users,
})
}
type newUserData struct {
*viewutil.BaseData
Form util.FormData
}
func viewNewUser(meta viewutil.Meta, form util.FormData) {
viewutil.ExecutePage(meta, newUserChain, newUserData{
BaseData: &viewutil.BaseData{},
Form: form,
})
}
type editDeleteUserData struct {
*viewutil.BaseData
Form util.FormData
U *user.User
}
func viewEditUser(meta viewutil.Meta, form util.FormData, u *user.User) {
viewutil.ExecutePage(meta, editUserChain, editDeleteUserData{
BaseData: &viewutil.BaseData{},
Form: form,
U: u,
})
}
func viewDeleteUser(meta viewutil.Meta, form util.FormData, u *user.User) {
viewutil.ExecutePage(meta, deleteUserChain, editDeleteUserData{
BaseData: &viewutil.BaseData{},
Form: form,
U: u,
})
}

View File

@ -1,213 +0,0 @@
{% import "net/http" %}
{% import "sort" %}
{% import "github.com/bouncepaw/mycorrhiza/cfg" %}
{% import "github.com/bouncepaw/mycorrhiza/l18n" %}
{% import "github.com/bouncepaw/mycorrhiza/user" %}
{% func Register(rq *http.Request) %}
{% code
lc := l18n.FromRequest(rq)
%}
<main class="main-width">
<section>
{% if cfg.AllowRegistration %}
<form class="modal" method="post" action="/register?{%s rq.URL.RawQuery %}" id="register-form" enctype="multipart/form-data" autocomplete="off">
<fieldset class="modal__fieldset">
<legend class="modal__title">{%s lc.Get("auth.register_header", &l18n.Replacements{"name": cfg.WikiName}) %}</legend>
<label for="register-form__username">{%s lc.Get("auth.username") %}</label>
<br>
<input type="text" required autofocus id="login-form__username" name="username">
<br>
<label for="login-form__password">{%s lc.Get("auth.password") %}</label>
<br>
<input type="password" required name="password">
<p>{%s lc.Get("auth.password_tip") %}</p>
<p>{%s lc.Get("auth.cookie_tip") %}</p>
<button class="btn" type="submit" value="Register">{%s lc.Get("auth.register_button") %}</button>
<a class="btn btn_weak" href="/{%s rq.URL.RawQuery %}">{%s lc.Get("ui.cancel") %}</a>
</fieldset>
</form>
{%= telegramWidget(lc) %}
{% elseif cfg.UseAuth %}
<p>{%s lc.Get("auth.noregister") %}</p>
<p><a href="/{%s rq.URL.RawQuery %}">← {%s lc.Get("auth.go_back") %}</a></p>
{% else %}
<p>{%s lc.Get("auth.noauth") %}</p>
<p><a href="/{%s rq.URL.RawQuery %}">← {%s lc.Get("auth.go_back") %}</a></p>
{% endif %}
</section>
</main>
{% endfunc %}
{% func Login(lc *l18n.Localizer) %}
<main class="main-width">
<section>
{% if cfg.UseAuth %}
<form class="modal" method="post" action="/login" id="login-form" enctype="multipart/form-data" autocomplete="on">
<fieldset class="modal__fieldset">
<legend class="modal__title">{%s lc.Get("auth.login_header", &l18n.Replacements{"name": cfg.WikiName}) %}</legend>
<label for="login-form__username">{%s lc.Get("auth.username") %}</label>
<br>
<input type="text" required autofocus id="login-form__username" name="username" autocomplete="username">
<br>
<label for="login-form__password">{%s lc.Get("auth.password") %}</label>
<br>
<input type="password" required name="password" autocomplete="current-password">
<p>{%s lc.Get("auth.cookie_tip") %}</p>
<button class="btn" type="submit" value="Log in">{%s lc.Get("auth.login_button") %}</button>
<a class="btn btn_weak" href="/">{%s lc.Get("ui.cancel") %}</a>
</fieldset>
</form>
{%= telegramWidget(lc) %}
{% else %}
<p>{%s lc.Get("auth.noauth") %}</p>
<p><a class="btn btn_weak" href="/">← {%s lc.Get("auth.go_home") %}</a></p>
{% endif %}
</section>
</main>
{% endfunc %}
Telegram auth widget was requested by Yogurt. As you can see, we don't offer user administrators control over it. Of course we don't.
{% func telegramWidget(lc *l18n.Localizer) %}
{% if cfg.TelegramEnabled %}
<p class="telegram-notice">{%s lc.Get("auth.telegram_tip") %}</p>
<script async src="https://telegram.org/js/telegram-widget.js?15" data-telegram-login="{%s cfg.TelegramBotName %}" data-size="medium" data-userpic="false" data-auth-url="{%s cfg.URL %}/telegram-login"></script>
{% endif %}
{% endfunc %}
{% func LoginError(err string, lc *l18n.Localizer) %}
<main class="main-width">
<section>
{% switch err %}
{% case "unknown username" %}
<p class="error">{%s lc.Get("auth.error_username") %}</p>
{% case "wrong password" %}
<p class="error">{%s lc.Get("auth.error_password") %}</p>
{% default %}
<p class="error">{%s err %}</p>
{% endswitch %}
<p><a href="/login">← {%s lc.Get("auth.try_again") %}</a></p>
</section>
</main>
{% endfunc %}
{% func Logout(can bool, lc *l18n.Localizer) %}
<main class="main-width">
<section>
{% if can %}
<h1>{%s lc.Get("auth.logout_header") %}</h1>
<form method="POST" action="/logout">
<input class="btn btn_accent" type="submit" value="{%s lc.Get("auth.logout_button") %}"/>
<a class="btn btn_weak" href="/">{%s lc.Get("auth.go_home") %}</a>
</form>
{% else %}
<p>{%s lc.Get("auth.logout_anon") %}</p>
<p><a href="/login">{%s lc.Get("auth.login_title") %}</a></p>
<p><a href="/">← {%s lc.Get("auth.go_home") %}</a></p>
{% endif %}
</section>
</main>
{% endfunc %}
{% func Lock(lc *l18n.Localizer) %}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>🔒 {%s lc.Get("auth.lock_title") %}</title>
<link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<main class="locked-notice main-width">
<section class="locked-notice__message">
<p class="locked-notice__lock">🔒</p>
<h1 class="locked-notice__title">{%s lc.Get("auth.lock_title") %}</h1>
<form class="locked-notice__login-form" method="post" action="/login" id="login-form" enctype="multipart/form-data" autocomplete="on">
<label for="login-form__username">{%s lc.Get("auth.username") %}</label>
<br>
<input type="text" required autofocus id="login-form__username" name="username" autocomplete="username">
<br>
<label for="login-form__password">{%s lc.Get("auth.password") %}</label>
<br>
<input type="password" required name="password" autocomplete="current-password">
<br>
<button class="btn" type="submit" value="Log in">{%s lc.Get("auth.login_button") %}</button>
</form>
{%= telegramWidget(lc) %}
</section>
</main>
</body>
</html>
{% endfunc %}
{% code
var userListL10n = map[string]L10nEntry{
"heading": En("List of users").Ru("Список пользователей"),
"administrators": En("Administrators").Ru("Администраторы"),
"moderators": En("Moderators").Ru("Модераторы"),
"editors": En("Editors").Ru("Редакторы"),
"readers": En("Readers").Ru("Читатели"),
}
%}
{% func UserList(lc *l18n.Localizer) %}
<main class="main-width user-list">
{% code
var get = func(key string) string {
return userListL10n[key].Get(lc.Locale)
}
var (
admins = make([]string, 0)
moderators = make([]string, 0)
editors = make([]string, 0)
readers = make([]string, 0)
)
for u := range user.YieldUsers() {
switch u.Group {
// What if we place the users into sorted slices?
case "admin":
admins = append(admins, u.Name)
case "moderator":
moderators = append(moderators, u.Name)
case "editor", "trusted":
editors = append(editors, u.Name)
case "reader":
readers = append(readers, u.Name)
}
}
sort.Strings(admins)
sort.Strings(moderators)
sort.Strings(editors)
sort.Strings(readers)
%}
<h1>{%s get("heading") %}</h1>
<section>
<h2>{%s get("administrators") %}</h2>
<ol>{% for _, name := range admins %}
<li><a href="/hypha/{%s cfg.UserHypha %}/{%s name %}">{%s name %}</a></li>
{% endfor %}</ol>
</section>
<section>
<h2>{%s get("moderators") %}</h2>
<ol>{% for _, name := range moderators %}
<li><a href="/hypha/{%s cfg.UserHypha %}/{%s name %}">{%s name %}</a></li>
{% endfor %}</ol>
</section>
<section>
<h2>{%s get("editors") %}</h2>
<ol>{% for _, name := range editors %}
<li><a href="/hypha/{%s cfg.UserHypha %}/{%s name %}">{%s name %}</a></li>
{% endfor %}</ol>
</section>
<section>
<h2>{%s get("readers") %}</h2>
<ol>{% for _, name := range readers %}
<li><a href="/hypha/{%s cfg.UserHypha %}/{%s name %}">{%s name %}</a></li>
{% endfor %}</ol>
</section>
</main>
{% endfunc %}

View File

@ -1,805 +0,0 @@
// Code generated by qtc from "auth.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line auth/auth.qtpl:1
package auth
//line auth/auth.qtpl:1
import "net/http"
//line auth/auth.qtpl:2
import "sort"
//line auth/auth.qtpl:3
import "github.com/bouncepaw/mycorrhiza/cfg"
//line auth/auth.qtpl:4
import "github.com/bouncepaw/mycorrhiza/l18n"
//line auth/auth.qtpl:5
import "github.com/bouncepaw/mycorrhiza/user"
//line auth/auth.qtpl:7
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line auth/auth.qtpl:7
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line auth/auth.qtpl:7
func StreamRegister(qw422016 *qt422016.Writer, rq *http.Request) {
//line auth/auth.qtpl:7
qw422016.N().S(`
`)
//line auth/auth.qtpl:9
lc := l18n.FromRequest(rq)
//line auth/auth.qtpl:10
qw422016.N().S(`
<main class="main-width">
<section>
`)
//line auth/auth.qtpl:13
if cfg.AllowRegistration {
//line auth/auth.qtpl:13
qw422016.N().S(`
<form class="modal" method="post" action="/register?`)
//line auth/auth.qtpl:14
qw422016.E().S(rq.URL.RawQuery)
//line auth/auth.qtpl:14
qw422016.N().S(`" id="register-form" enctype="multipart/form-data" autocomplete="off">
<fieldset class="modal__fieldset">
<legend class="modal__title">`)
//line auth/auth.qtpl:16
qw422016.E().S(lc.Get("auth.register_header", &l18n.Replacements{"name": cfg.WikiName}))
//line auth/auth.qtpl:16
qw422016.N().S(`</legend>
<label for="register-form__username">`)
//line auth/auth.qtpl:18
qw422016.E().S(lc.Get("auth.username"))
//line auth/auth.qtpl:18
qw422016.N().S(`</label>
<br>
<input type="text" required autofocus id="login-form__username" name="username">
<br>
<label for="login-form__password">`)
//line auth/auth.qtpl:22
qw422016.E().S(lc.Get("auth.password"))
//line auth/auth.qtpl:22
qw422016.N().S(`</label>
<br>
<input type="password" required name="password">
<p>`)
//line auth/auth.qtpl:25
qw422016.E().S(lc.Get("auth.password_tip"))
//line auth/auth.qtpl:25
qw422016.N().S(`</p>
<p>`)
//line auth/auth.qtpl:26
qw422016.E().S(lc.Get("auth.cookie_tip"))
//line auth/auth.qtpl:26
qw422016.N().S(`</p>
<button class="btn" type="submit" value="Register">`)
//line auth/auth.qtpl:27
qw422016.E().S(lc.Get("auth.register_button"))
//line auth/auth.qtpl:27
qw422016.N().S(`</button>
<a class="btn btn_weak" href="/`)
//line auth/auth.qtpl:28
qw422016.E().S(rq.URL.RawQuery)
//line auth/auth.qtpl:28
qw422016.N().S(`">`)
//line auth/auth.qtpl:28
qw422016.E().S(lc.Get("ui.cancel"))
//line auth/auth.qtpl:28
qw422016.N().S(`</a>
</fieldset>
</form>
`)
//line auth/auth.qtpl:31
streamtelegramWidget(qw422016, lc)
//line auth/auth.qtpl:31
qw422016.N().S(`
`)
//line auth/auth.qtpl:32
} else if cfg.UseAuth {
//line auth/auth.qtpl:32
qw422016.N().S(`
<p>`)
//line auth/auth.qtpl:33
qw422016.E().S(lc.Get("auth.noregister"))
//line auth/auth.qtpl:33
qw422016.N().S(`</p>
<p><a href="/`)
//line auth/auth.qtpl:34
qw422016.E().S(rq.URL.RawQuery)
//line auth/auth.qtpl:34
qw422016.N().S(`">← `)
//line auth/auth.qtpl:34
qw422016.E().S(lc.Get("auth.go_back"))
//line auth/auth.qtpl:34
qw422016.N().S(`</a></p>
`)
//line auth/auth.qtpl:35
} else {
//line auth/auth.qtpl:35
qw422016.N().S(`
<p>`)
//line auth/auth.qtpl:36
qw422016.E().S(lc.Get("auth.noauth"))
//line auth/auth.qtpl:36
qw422016.N().S(`</p>
<p><a href="/`)
//line auth/auth.qtpl:37
qw422016.E().S(rq.URL.RawQuery)
//line auth/auth.qtpl:37
qw422016.N().S(`">← `)
//line auth/auth.qtpl:37
qw422016.E().S(lc.Get("auth.go_back"))
//line auth/auth.qtpl:37
qw422016.N().S(`</a></p>
`)
//line auth/auth.qtpl:38
}
//line auth/auth.qtpl:38
qw422016.N().S(`
</section>
</main>
`)
//line auth/auth.qtpl:41
}
//line auth/auth.qtpl:41
func WriteRegister(qq422016 qtio422016.Writer, rq *http.Request) {
//line auth/auth.qtpl:41
qw422016 := qt422016.AcquireWriter(qq422016)
//line auth/auth.qtpl:41
StreamRegister(qw422016, rq)
//line auth/auth.qtpl:41
qt422016.ReleaseWriter(qw422016)
//line auth/auth.qtpl:41
}
//line auth/auth.qtpl:41
func Register(rq *http.Request) string {
//line auth/auth.qtpl:41
qb422016 := qt422016.AcquireByteBuffer()
//line auth/auth.qtpl:41
WriteRegister(qb422016, rq)
//line auth/auth.qtpl:41
qs422016 := string(qb422016.B)
//line auth/auth.qtpl:41
qt422016.ReleaseByteBuffer(qb422016)
//line auth/auth.qtpl:41
return qs422016
//line auth/auth.qtpl:41
}
//line auth/auth.qtpl:43
func StreamLogin(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
//line auth/auth.qtpl:43
qw422016.N().S(`
<main class="main-width">
<section>
`)
//line auth/auth.qtpl:46
if cfg.UseAuth {
//line auth/auth.qtpl:46
qw422016.N().S(`
<form class="modal" method="post" action="/login" id="login-form" enctype="multipart/form-data" autocomplete="on">
<fieldset class="modal__fieldset">
<legend class="modal__title">`)
//line auth/auth.qtpl:49
qw422016.E().S(lc.Get("auth.login_header", &l18n.Replacements{"name": cfg.WikiName}))
//line auth/auth.qtpl:49
qw422016.N().S(`</legend>
<label for="login-form__username">`)
//line auth/auth.qtpl:50
qw422016.E().S(lc.Get("auth.username"))
//line auth/auth.qtpl:50
qw422016.N().S(`</label>
<br>
<input type="text" required autofocus id="login-form__username" name="username" autocomplete="username">
<br>
<label for="login-form__password">`)
//line auth/auth.qtpl:54
qw422016.E().S(lc.Get("auth.password"))
//line auth/auth.qtpl:54
qw422016.N().S(`</label>
<br>
<input type="password" required name="password" autocomplete="current-password">
<p>`)
//line auth/auth.qtpl:57
qw422016.E().S(lc.Get("auth.cookie_tip"))
//line auth/auth.qtpl:57
qw422016.N().S(`</p>
<button class="btn" type="submit" value="Log in">`)
//line auth/auth.qtpl:58
qw422016.E().S(lc.Get("auth.login_button"))
//line auth/auth.qtpl:58
qw422016.N().S(`</button>
<a class="btn btn_weak" href="/">`)
//line auth/auth.qtpl:59
qw422016.E().S(lc.Get("ui.cancel"))
//line auth/auth.qtpl:59
qw422016.N().S(`</a>
</fieldset>
</form>
`)
//line auth/auth.qtpl:62
streamtelegramWidget(qw422016, lc)
//line auth/auth.qtpl:62
qw422016.N().S(`
`)
//line auth/auth.qtpl:63
} else {
//line auth/auth.qtpl:63
qw422016.N().S(`
<p>`)
//line auth/auth.qtpl:64
qw422016.E().S(lc.Get("auth.noauth"))
//line auth/auth.qtpl:64
qw422016.N().S(`</p>
<p><a class="btn btn_weak" href="/"> `)
//line auth/auth.qtpl:65
qw422016.E().S(lc.Get("auth.go_home"))
//line auth/auth.qtpl:65
qw422016.N().S(`</a></p>
`)
//line auth/auth.qtpl:66
}
//line auth/auth.qtpl:66
qw422016.N().S(`
</section>
</main>
`)
//line auth/auth.qtpl:69
}
//line auth/auth.qtpl:69
func WriteLogin(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
//line auth/auth.qtpl:69
qw422016 := qt422016.AcquireWriter(qq422016)
//line auth/auth.qtpl:69
StreamLogin(qw422016, lc)
//line auth/auth.qtpl:69
qt422016.ReleaseWriter(qw422016)
//line auth/auth.qtpl:69
}
//line auth/auth.qtpl:69
func Login(lc *l18n.Localizer) string {
//line auth/auth.qtpl:69
qb422016 := qt422016.AcquireByteBuffer()
//line auth/auth.qtpl:69
WriteLogin(qb422016, lc)
//line auth/auth.qtpl:69
qs422016 := string(qb422016.B)
//line auth/auth.qtpl:69
qt422016.ReleaseByteBuffer(qb422016)
//line auth/auth.qtpl:69
return qs422016
//line auth/auth.qtpl:69
}
// Telegram auth widget was requested by Yogurt. As you can see, we don't offer user administrators control over it. Of course we don't.
//line auth/auth.qtpl:72
func streamtelegramWidget(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
//line auth/auth.qtpl:72
qw422016.N().S(`
`)
//line auth/auth.qtpl:73
if cfg.TelegramEnabled {
//line auth/auth.qtpl:73
qw422016.N().S(`
<p class="telegram-notice">`)
//line auth/auth.qtpl:74
qw422016.E().S(lc.Get("auth.telegram_tip"))
//line auth/auth.qtpl:74
qw422016.N().S(`</p>
<script async src="https://telegram.org/js/telegram-widget.js?15" data-telegram-login="`)
//line auth/auth.qtpl:75
qw422016.E().S(cfg.TelegramBotName)
//line auth/auth.qtpl:75
qw422016.N().S(`" data-size="medium" data-userpic="false" data-auth-url="`)
//line auth/auth.qtpl:75
qw422016.E().S(cfg.URL)
//line auth/auth.qtpl:75
qw422016.N().S(`/telegram-login"></script>
`)
//line auth/auth.qtpl:76
}
//line auth/auth.qtpl:76
qw422016.N().S(`
`)
//line auth/auth.qtpl:77
}
//line auth/auth.qtpl:77
func writetelegramWidget(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
//line auth/auth.qtpl:77
qw422016 := qt422016.AcquireWriter(qq422016)
//line auth/auth.qtpl:77
streamtelegramWidget(qw422016, lc)
//line auth/auth.qtpl:77
qt422016.ReleaseWriter(qw422016)
//line auth/auth.qtpl:77
}
//line auth/auth.qtpl:77
func telegramWidget(lc *l18n.Localizer) string {
//line auth/auth.qtpl:77
qb422016 := qt422016.AcquireByteBuffer()
//line auth/auth.qtpl:77
writetelegramWidget(qb422016, lc)
//line auth/auth.qtpl:77
qs422016 := string(qb422016.B)
//line auth/auth.qtpl:77
qt422016.ReleaseByteBuffer(qb422016)
//line auth/auth.qtpl:77
return qs422016
//line auth/auth.qtpl:77
}
//line auth/auth.qtpl:79
func StreamLoginError(qw422016 *qt422016.Writer, err string, lc *l18n.Localizer) {
//line auth/auth.qtpl:79
qw422016.N().S(`
<main class="main-width">
<section>
`)
//line auth/auth.qtpl:82
switch err {
//line auth/auth.qtpl:83
case "unknown username":
//line auth/auth.qtpl:83
qw422016.N().S(`
<p class="error">`)
//line auth/auth.qtpl:84
qw422016.E().S(lc.Get("auth.error_username"))
//line auth/auth.qtpl:84
qw422016.N().S(`</p>
`)
//line auth/auth.qtpl:85
case "wrong password":
//line auth/auth.qtpl:85
qw422016.N().S(`
<p class="error">`)
//line auth/auth.qtpl:86
qw422016.E().S(lc.Get("auth.error_password"))
//line auth/auth.qtpl:86
qw422016.N().S(`</p>
`)
//line auth/auth.qtpl:87
default:
//line auth/auth.qtpl:87
qw422016.N().S(`
<p class="error">`)
//line auth/auth.qtpl:88
qw422016.E().S(err)
//line auth/auth.qtpl:88
qw422016.N().S(`</p>
`)
//line auth/auth.qtpl:89
}
//line auth/auth.qtpl:89
qw422016.N().S(`
<p><a href="/login"> `)
//line auth/auth.qtpl:90
qw422016.E().S(lc.Get("auth.try_again"))
//line auth/auth.qtpl:90
qw422016.N().S(`</a></p>
</section>
</main>
`)
//line auth/auth.qtpl:93
}
//line auth/auth.qtpl:93
func WriteLoginError(qq422016 qtio422016.Writer, err string, lc *l18n.Localizer) {
//line auth/auth.qtpl:93
qw422016 := qt422016.AcquireWriter(qq422016)
//line auth/auth.qtpl:93
StreamLoginError(qw422016, err, lc)
//line auth/auth.qtpl:93
qt422016.ReleaseWriter(qw422016)
//line auth/auth.qtpl:93
}
//line auth/auth.qtpl:93
func LoginError(err string, lc *l18n.Localizer) string {
//line auth/auth.qtpl:93
qb422016 := qt422016.AcquireByteBuffer()
//line auth/auth.qtpl:93
WriteLoginError(qb422016, err, lc)
//line auth/auth.qtpl:93
qs422016 := string(qb422016.B)
//line auth/auth.qtpl:93
qt422016.ReleaseByteBuffer(qb422016)
//line auth/auth.qtpl:93
return qs422016
//line auth/auth.qtpl:93
}
//line auth/auth.qtpl:95
func StreamLogout(qw422016 *qt422016.Writer, can bool, lc *l18n.Localizer) {
//line auth/auth.qtpl:95
qw422016.N().S(`
<main class="main-width">
<section>
`)
//line auth/auth.qtpl:98
if can {
//line auth/auth.qtpl:98
qw422016.N().S(`
<h1>`)
//line auth/auth.qtpl:99
qw422016.E().S(lc.Get("auth.logout_header"))
//line auth/auth.qtpl:99
qw422016.N().S(`</h1>
<form method="POST" action="/logout">
<input class="btn btn_accent" type="submit" value="`)
//line auth/auth.qtpl:101
qw422016.E().S(lc.Get("auth.logout_button"))
//line auth/auth.qtpl:101
qw422016.N().S(`"/>
<a class="btn btn_weak" href="/">`)
//line auth/auth.qtpl:102
qw422016.E().S(lc.Get("auth.go_home"))
//line auth/auth.qtpl:102
qw422016.N().S(`</a>
</form>
`)
//line auth/auth.qtpl:104
} else {
//line auth/auth.qtpl:104
qw422016.N().S(`
<p>`)
//line auth/auth.qtpl:105
qw422016.E().S(lc.Get("auth.logout_anon"))
//line auth/auth.qtpl:105
qw422016.N().S(`</p>
<p><a href="/login">`)
//line auth/auth.qtpl:106
qw422016.E().S(lc.Get("auth.login_title"))
//line auth/auth.qtpl:106
qw422016.N().S(`</a></p>
<p><a href="/"> `)
//line auth/auth.qtpl:107
qw422016.E().S(lc.Get("auth.go_home"))
//line auth/auth.qtpl:107
qw422016.N().S(`</a></p>
`)
//line auth/auth.qtpl:108
}
//line auth/auth.qtpl:108
qw422016.N().S(`
</section>
</main>
`)
//line auth/auth.qtpl:111
}
//line auth/auth.qtpl:111
func WriteLogout(qq422016 qtio422016.Writer, can bool, lc *l18n.Localizer) {
//line auth/auth.qtpl:111
qw422016 := qt422016.AcquireWriter(qq422016)
//line auth/auth.qtpl:111
StreamLogout(qw422016, can, lc)
//line auth/auth.qtpl:111
qt422016.ReleaseWriter(qw422016)
//line auth/auth.qtpl:111
}
//line auth/auth.qtpl:111
func Logout(can bool, lc *l18n.Localizer) string {
//line auth/auth.qtpl:111
qb422016 := qt422016.AcquireByteBuffer()
//line auth/auth.qtpl:111
WriteLogout(qb422016, can, lc)
//line auth/auth.qtpl:111
qs422016 := string(qb422016.B)
//line auth/auth.qtpl:111
qt422016.ReleaseByteBuffer(qb422016)
//line auth/auth.qtpl:111
return qs422016
//line auth/auth.qtpl:111
}
//line auth/auth.qtpl:113
func StreamLock(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
//line auth/auth.qtpl:113
qw422016.N().S(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>🔒 `)
//line auth/auth.qtpl:119
qw422016.E().S(lc.Get("auth.lock_title"))
//line auth/auth.qtpl:119
qw422016.N().S(`</title>
<link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<main class="locked-notice main-width">
<section class="locked-notice__message">
<p class="locked-notice__lock">🔒</p>
<h1 class="locked-notice__title">`)
//line auth/auth.qtpl:127
qw422016.E().S(lc.Get("auth.lock_title"))
//line auth/auth.qtpl:127
qw422016.N().S(`</h1>
<form class="locked-notice__login-form" method="post" action="/login" id="login-form" enctype="multipart/form-data" autocomplete="on">
<label for="login-form__username">`)
//line auth/auth.qtpl:129
qw422016.E().S(lc.Get("auth.username"))
//line auth/auth.qtpl:129
qw422016.N().S(`</label>
<br>
<input type="text" required autofocus id="login-form__username" name="username" autocomplete="username">
<br>
<label for="login-form__password">`)
//line auth/auth.qtpl:133
qw422016.E().S(lc.Get("auth.password"))
//line auth/auth.qtpl:133
qw422016.N().S(`</label>
<br>
<input type="password" required name="password" autocomplete="current-password">
<br>
<button class="btn" type="submit" value="Log in">`)
//line auth/auth.qtpl:137
qw422016.E().S(lc.Get("auth.login_button"))
//line auth/auth.qtpl:137
qw422016.N().S(`</button>
</form>
`)
//line auth/auth.qtpl:139
streamtelegramWidget(qw422016, lc)
//line auth/auth.qtpl:139
qw422016.N().S(`
</section>
</main>
</body>
</html>
`)
//line auth/auth.qtpl:144
}
//line auth/auth.qtpl:144
func WriteLock(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
//line auth/auth.qtpl:144
qw422016 := qt422016.AcquireWriter(qq422016)
//line auth/auth.qtpl:144
StreamLock(qw422016, lc)
//line auth/auth.qtpl:144
qt422016.ReleaseWriter(qw422016)
//line auth/auth.qtpl:144
}
//line auth/auth.qtpl:144
func Lock(lc *l18n.Localizer) string {
//line auth/auth.qtpl:144
qb422016 := qt422016.AcquireByteBuffer()
//line auth/auth.qtpl:144
WriteLock(qb422016, lc)
//line auth/auth.qtpl:144
qs422016 := string(qb422016.B)
//line auth/auth.qtpl:144
qt422016.ReleaseByteBuffer(qb422016)
//line auth/auth.qtpl:144
return qs422016
//line auth/auth.qtpl:144
}
//line auth/auth.qtpl:147
var userListL10n = map[string]L10nEntry{
"heading": En("List of users").Ru("Список пользователей"),
"administrators": En("Administrators").Ru("Администраторы"),
"moderators": En("Moderators").Ru("Модераторы"),
"editors": En("Editors").Ru("Редакторы"),
"readers": En("Readers").Ru("Читатели"),
}
//line auth/auth.qtpl:156
func StreamUserList(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
//line auth/auth.qtpl:156
qw422016.N().S(`
<main class="main-width user-list">
`)
//line auth/auth.qtpl:159
var get = func(key string) string {
return userListL10n[key].Get(lc.Locale)
}
var (
admins = make([]string, 0)
moderators = make([]string, 0)
editors = make([]string, 0)
readers = make([]string, 0)
)
for u := range user.YieldUsers() {
switch u.Group {
// What if we place the users into sorted slices?
case "admin":
admins = append(admins, u.Name)
case "moderator":
moderators = append(moderators, u.Name)
case "editor", "trusted":
editors = append(editors, u.Name)
case "reader":
readers = append(readers, u.Name)
}
}
sort.Strings(admins)
sort.Strings(moderators)
sort.Strings(editors)
sort.Strings(readers)
//line auth/auth.qtpl:186
qw422016.N().S(`
<h1>`)
//line auth/auth.qtpl:187
qw422016.E().S(get("heading"))
//line auth/auth.qtpl:187
qw422016.N().S(`</h1>
<section>
<h2>`)
//line auth/auth.qtpl:189
qw422016.E().S(get("administrators"))
//line auth/auth.qtpl:189
qw422016.N().S(`</h2>
<ol>`)
//line auth/auth.qtpl:190
for _, name := range admins {
//line auth/auth.qtpl:190
qw422016.N().S(`
<li><a href="/hypha/`)
//line auth/auth.qtpl:191
qw422016.E().S(cfg.UserHypha)
//line auth/auth.qtpl:191
qw422016.N().S(`/`)
//line auth/auth.qtpl:191
qw422016.E().S(name)
//line auth/auth.qtpl:191
qw422016.N().S(`">`)
//line auth/auth.qtpl:191
qw422016.E().S(name)
//line auth/auth.qtpl:191
qw422016.N().S(`</a></li>
`)
//line auth/auth.qtpl:192
}
//line auth/auth.qtpl:192
qw422016.N().S(`</ol>
</section>
<section>
<h2>`)
//line auth/auth.qtpl:195
qw422016.E().S(get("moderators"))
//line auth/auth.qtpl:195
qw422016.N().S(`</h2>
<ol>`)
//line auth/auth.qtpl:196
for _, name := range moderators {
//line auth/auth.qtpl:196
qw422016.N().S(`
<li><a href="/hypha/`)
//line auth/auth.qtpl:197
qw422016.E().S(cfg.UserHypha)
//line auth/auth.qtpl:197
qw422016.N().S(`/`)
//line auth/auth.qtpl:197
qw422016.E().S(name)
//line auth/auth.qtpl:197
qw422016.N().S(`">`)
//line auth/auth.qtpl:197
qw422016.E().S(name)
//line auth/auth.qtpl:197
qw422016.N().S(`</a></li>
`)
//line auth/auth.qtpl:198
}
//line auth/auth.qtpl:198
qw422016.N().S(`</ol>
</section>
<section>
<h2>`)
//line auth/auth.qtpl:201
qw422016.E().S(get("editors"))
//line auth/auth.qtpl:201
qw422016.N().S(`</h2>
<ol>`)
//line auth/auth.qtpl:202
for _, name := range editors {
//line auth/auth.qtpl:202
qw422016.N().S(`
<li><a href="/hypha/`)
//line auth/auth.qtpl:203
qw422016.E().S(cfg.UserHypha)
//line auth/auth.qtpl:203
qw422016.N().S(`/`)
//line auth/auth.qtpl:203
qw422016.E().S(name)
//line auth/auth.qtpl:203
qw422016.N().S(`">`)
//line auth/auth.qtpl:203
qw422016.E().S(name)
//line auth/auth.qtpl:203
qw422016.N().S(`</a></li>
`)
//line auth/auth.qtpl:204
}
//line auth/auth.qtpl:204
qw422016.N().S(`</ol>
</section>
<section>
<h2>`)
//line auth/auth.qtpl:207
qw422016.E().S(get("readers"))
//line auth/auth.qtpl:207
qw422016.N().S(`</h2>
<ol>`)
//line auth/auth.qtpl:208
for _, name := range readers {
//line auth/auth.qtpl:208
qw422016.N().S(`
<li><a href="/hypha/`)
//line auth/auth.qtpl:209
qw422016.E().S(cfg.UserHypha)
//line auth/auth.qtpl:209
qw422016.N().S(`/`)
//line auth/auth.qtpl:209
qw422016.E().S(name)
//line auth/auth.qtpl:209
qw422016.N().S(`">`)
//line auth/auth.qtpl:209
qw422016.E().S(name)
//line auth/auth.qtpl:209
qw422016.N().S(`</a></li>
`)
//line auth/auth.qtpl:210
}
//line auth/auth.qtpl:210
qw422016.N().S(`</ol>
</section>
</main>
`)
//line auth/auth.qtpl:213
}
//line auth/auth.qtpl:213
func WriteUserList(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
//line auth/auth.qtpl:213
qw422016 := qt422016.AcquireWriter(qq422016)
//line auth/auth.qtpl:213
StreamUserList(qw422016, lc)
//line auth/auth.qtpl:213
qt422016.ReleaseWriter(qw422016)
//line auth/auth.qtpl:213
}
//line auth/auth.qtpl:213
func UserList(lc *l18n.Localizer) string {
//line auth/auth.qtpl:213
qb422016 := qt422016.AcquireByteBuffer()
//line auth/auth.qtpl:213
WriteUserList(qb422016, lc)
//line auth/auth.qtpl:213
qs422016 := string(qb422016.B)
//line auth/auth.qtpl:213
qt422016.ReleaseByteBuffer(qb422016)
//line auth/auth.qtpl:213
return qs422016
//line auth/auth.qtpl:213
}

View File

@ -1,22 +0,0 @@
package auth
type L10nEntry struct {
_en string
_ru string
}
func En(v string) L10nEntry {
return L10nEntry{_en: v}
}
func (e L10nEntry) Ru(v string) L10nEntry {
e._ru = v
return e
}
func (e L10nEntry) Get(lang string) string {
if lang == "ru" && e._ru != "" {
return e._ru
}
return e._en
}

View File

@ -1,224 +0,0 @@
package auth
import (
"errors"
"fmt"
"io"
"log"
"mime"
"net/http"
"strings"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/gorilla/mux"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/l18n"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
func InitAuth(r *mux.Router) {
r.HandleFunc("/user-list", handlerUserList)
r.HandleFunc("/lock", handlerLock)
// The check below saves a lot of extra checks and lines of codes in other places in this file.
if !cfg.UseAuth {
return
}
if cfg.AllowRegistration {
r.HandleFunc("/register", handlerRegister).Methods(http.MethodPost, http.MethodGet)
}
if cfg.TelegramEnabled {
r.HandleFunc("/telegram-login", handlerTelegramLogin)
}
r.HandleFunc("/login", handlerLogin)
r.HandleFunc("/logout", handlerLogout)
}
func handlerUserList(w http.ResponseWriter, rq *http.Request) {
lc := l18n.FromRequest(rq)
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(viewutil.Base(viewutil.MetaFrom(w, rq), lc.Get("ui.users_title"), UserList(lc), map[string]string{})))
}
func handlerLock(w http.ResponseWriter, rq *http.Request) {
_, _ = io.WriteString(w, Lock(l18n.FromRequest(rq)))
}
// handlerRegister displays the register form (GET) or registers the user (POST).
func handlerRegister(w http.ResponseWriter, rq *http.Request) {
lc := l18n.FromRequest(rq)
util.PrepareRq(rq)
if rq.Method == http.MethodGet {
_, _ = io.WriteString(
w,
viewutil.Base(
viewutil.MetaFrom(w, rq),
lc.Get("auth.register_title"),
Register(rq),
map[string]string{},
),
)
return
}
var (
username = rq.PostFormValue("username")
password = rq.PostFormValue("password")
err = user.Register(username, password, "editor", "local", false)
)
if err != nil {
log.Printf("Failed to register %s: %s", username, err.Error())
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
w.WriteHeader(http.StatusBadRequest)
_, _ = io.WriteString(
w,
viewutil.Base(
viewutil.MetaFrom(w, rq),
lc.Get("auth.register_title"),
fmt.Sprintf(
`<main class="main-width"><p>%s</p><p><a href="/register">%s<a></p></main>`,
err.Error(),
lc.Get("auth.try_again"),
),
map[string]string{},
),
)
return
}
log.Printf("Successfully registered %s", username)
if err := user.LoginDataHTTP(w, username, password); err != nil {
return
}
http.Redirect(w, rq, "/"+rq.URL.RawQuery, http.StatusSeeOther)
}
// handlerLogout shows the logout form (GET) or logs the user out (POST).
func handlerLogout(w http.ResponseWriter, rq *http.Request) {
if rq.Method == http.MethodGet {
var (
u = user.FromRequest(rq)
can = u != nil
lc = l18n.FromRequest(rq)
)
w.Header().Set("Content-Type", "text/html;charset=utf-8")
if can {
log.Println("User", u.Name, "tries to log out")
w.WriteHeader(http.StatusOK)
} else {
log.Println("Unknown user tries to log out")
w.WriteHeader(http.StatusForbidden)
}
_, _ = io.WriteString(
w,
viewutil.Base(viewutil.MetaFrom(w, rq), lc.Get("auth.logout_title"), Logout(can, lc), map[string]string{}),
)
} else if rq.Method == http.MethodPost {
user.LogoutFromRequest(w, rq)
http.Redirect(w, rq, "/", http.StatusSeeOther)
}
}
// handlerLogin shows the login form (GET) or logs the user in (POST).
func handlerLogin(w http.ResponseWriter, rq *http.Request) {
lc := l18n.FromRequest(rq)
if rq.Method == http.MethodGet {
w.Header().Set("Content-Type", "text/html;charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(
w,
viewutil.Base(
viewutil.MetaFrom(w, rq),
lc.Get("auth.login_title"),
Login(lc),
map[string]string{},
),
)
} else if rq.Method == http.MethodPost {
var (
username = util.CanonicalName(rq.PostFormValue("username"))
password = rq.PostFormValue("password")
err = user.LoginDataHTTP(w, username, password)
)
if err != nil {
w.Header().Set("Content-Type", "text/html;charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
_, _ = io.WriteString(w, viewutil.Base(viewutil.MetaFrom(w, rq), err.Error(), LoginError(err.Error(), lc), map[string]string{}))
return
}
http.Redirect(w, rq, "/", http.StatusSeeOther)
}
}
func handlerTelegramLogin(w http.ResponseWriter, rq *http.Request) {
// Note there is no lock here.
lc := l18n.FromRequest(rq)
w.Header().Set("Content-Type", "text/html;charset=utf-8")
rq.ParseForm()
var (
values = rq.URL.Query()
username = strings.ToLower(values.Get("username"))
seemsValid = user.TelegramAuthParamsAreValid(values)
err = user.Register(
username,
"", // Password matters not
"editor",
"telegram",
false,
)
)
if user.HasUsername(username) && user.ByName(username).Source == "telegram" {
// Problems is something we put blankets on.
err = nil
}
if !seemsValid {
err = errors.New("Wrong parameters")
}
if err != nil {
log.Printf("Failed to register %s using Telegram: %s", username, err.Error())
w.WriteHeader(http.StatusBadRequest)
_, _ = io.WriteString(
w,
viewutil.Base(
viewutil.MetaFrom(w, rq),
lc.Get("ui.error"),
fmt.Sprintf(
`<main class="main-width"><p>%s</p><p>%s</p><p><a href="/login">%s<a></p></main>`,
lc.Get("auth.error_telegram"),
err.Error(),
lc.Get("auth.go_login"),
),
map[string]string{},
),
)
return
}
errmsg := user.LoginDataHTTP(w, username, "")
if errmsg != nil {
log.Printf("Failed to login %s using Telegram: %s", username, err.Error())
w.WriteHeader(http.StatusBadRequest)
_, _ = io.WriteString(
w,
viewutil.Base(
viewutil.MetaFrom(w, rq),
"Error",
fmt.Sprintf(
`<main class="main-width"><p>%s</p><p>%s</p><p><a href="/login">%s<a></p></main>`,
lc.Get("auth.error_telegram"),
err.Error(),
lc.Get("auth.go_login"),
),
map[string]string{},
),
)
return
}
log.Printf("Authorize %s from Telegram", username)
http.Redirect(w, rq, "/", http.StatusSeeOther)
}

View File

@ -1,85 +0,0 @@
package backlinks
import (
"embed"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/util"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/gorilla/mux"
"net/http"
"sort"
)
func InitHandlers(rtr *mux.Router) {
rtr.PathPrefix("/backlinks/").HandlerFunc(handlerBacklinks)
rtr.PathPrefix("/orphans").HandlerFunc(handlerOrphans)
chainBacklinks = viewutil.CopyEnRuWith(fs, "view_backlinks.html", ruTranslation)
chainOrphans = viewutil.CopyEnRuWith(fs, "view_orphans.html", ruTranslation)
}
// handlerBacklinks lists all backlinks to a hypha.
func handlerBacklinks(w http.ResponseWriter, rq *http.Request) {
var (
hyphaName = util.HyphaNameFromRq(rq, "backlinks")
backlinks []string
)
for b := range yieldHyphaBacklinks(hyphaName) {
backlinks = append(backlinks, b)
}
viewBacklinks(viewutil.MetaFrom(w, rq), hyphaName, backlinks)
}
func handlerOrphans(w http.ResponseWriter, rq *http.Request) {
var orphans []string
for h := range hyphae.YieldExistingHyphae() {
if BacklinksCount(h.CanonicalName()) == 0 {
orphans = append(orphans, h.CanonicalName())
}
}
sort.Strings(orphans)
viewOrphans(viewutil.MetaFrom(w, rq), orphans)
}
var (
//go:embed *.html
fs embed.FS
ruTranslation = `
{{define "backlinks to text"}}Обратные ссылки на {{.}}{{end}}
{{define "backlinks to link"}}Обратные ссылки на <a href="/hypha/{{.}}">{{beautifulName .}}</a>{{end}}
{{define "description"}}Ниже перечислены гифы, на которых есть ссылка на эту гифу, трансклюзия этой гифы или эта гифа вставлена как изображение.{{end}}
{{define "orphaned hyphae"}}Гифы-сироты{{end}}
{{define "orphan description"}}Ниже перечислены гифы без ссылок на них.{{end}}
`
chainBacklinks viewutil.Chain
chainOrphans viewutil.Chain
)
type backlinksData struct {
*viewutil.BaseData
HyphaName string
Backlinks []string
}
func viewBacklinks(meta viewutil.Meta, hyphaName string, backlinks []string) {
viewutil.ExecutePage(meta, chainBacklinks, backlinksData{
BaseData: &viewutil.BaseData{
Addr: "/backlinks/" + hyphaName,
},
HyphaName: hyphaName,
Backlinks: backlinks,
})
}
type orphansData struct {
*viewutil.BaseData
Orphans []string
}
func viewOrphans(meta viewutil.Meta, orphans []string) {
viewutil.ExecutePage(meta, chainOrphans, orphansData{
BaseData: &viewutil.BaseData{
Addr: "/orphans",
},
Orphans: orphans,
})
}

View File

@ -1,36 +0,0 @@
{{define "category card"}}
{{if or .GivenPermissionToModify (len .Categories)}}
{{$hyphaName := .HyphaName}}
{{$givenPermission := .GivenPermissionToModify}}
<aside class="layout-card categories-card">
<h2 class="layout-card__title">{{block `categories` .}}Categories{{end}}</h2>
<ul class="categories-card__entries">
{{range .Categories}}
<li class="categories-card__entry">
<a class="categories-card__link" href="/category/{{.}}">{{beautifulName .}}</a>
<form method="POST" action="/remove-from-category" class="categories-card__remove-form">
<input type="hidden" name="cat" value="{{.}}">
<input type="hidden" name="hypha" value="{{$hyphaName}}">
<input type="hidden" name="redirect-to" value="/hypha/{{$hyphaName}}">
{{if $givenPermission}}
<input type="submit" value="x" class="btn categories-card__btn"
title="{{block `remove from category title` .}}Remove the hypha from this category{{end}}">
{{end}}
</form>
</li>
{{end}}
{{if .GivenPermissionToModify}}
<li class="categories-card__entry categories-card__add-to-cat">
<form method="POST" action="/add-to-category" class="categories-card__add-form">
<input type="text" name="cat" id="_cat-name"
placeholder="{{block `placeholder` .}}Category name...{{end}}">
<input type="hidden" name="hypha" value="{{$hyphaName}}">
<input type="hidden" name="redirect-to" value="/hypha/{{$hyphaName}}">
<input type="submit" class="btn categories-card__btn" value="+"
title="{{block `add to category title` .}}Add the hypha to this category{{end}}">
</form>
</li>
{{end}}
</ul>
</aside>
{{end}}{{end}}

View File

@ -1,37 +0,0 @@
{{define "edit category x"}}Edit category {{beautifulName .}}{{end}}
{{define "title"}}{{template "edit category x" .CatName}}{{end}}
{{define "body"}}
<main class="main-width category">
<h1>{{block "edit category heading" .CatName}}Edit category <a href="/category/{{.}}">{{beautifulName .}}</a>{{end}}</h1>
{{if len .Hyphae | not}}
<p>{{block "empty cat" .}}This category is empty{{end}}</p>
{{end}}
{{if .GivenPermissionToModify}}
<h2>{{block "add to category title" .}}Add a hypha to the category{{end}}</h2>
<form method="POST" action="/add-to-category" class="add-to-category">
<input type="text" name="hypha" id="_hypha-name"
placeholder="{{block `hypha name` .}}Hypha name{{end}}">
<input type="hidden" name="cat" value="{{.CatName}}">
<input type="hidden" name="redirect-to" value="/category/{{.CatName}}">
<input type="submit" class="btn" value="{{block `add` .}}Add{{end}}">
</form>
{{if len .Hyphae}}
<h2>{{block "remove hyphae" .}}Remove hyphae from the category{{end}}</h2>
<form method="POST" action="/remove-from-category" class="multi-remove-from-category">
<ol>
{{range .Hyphae}}
<li>
<input type="checkbox" name="_{{.}}" id="_{{.}}">
<label for="_{{.}}"><a href="/hypha/{{.}}">{{beautifulName .}}</a></label>
</li>
{{end}}
</ol>
<input type="hidden" name="cat" value="{{.CatName}}">
<input type="hidden" name="redirect-to" value="/edit-category/{{.CatName}}">
<input type="submit" class="btn" value="{{block `remove` .}}Remove{{end}}">
</form>
{{end}}{{end}}
</main>
{{end}}

View File

@ -1,107 +0,0 @@
package categories
import (
"embed"
"github.com/bouncepaw/mycorrhiza/viewutil"
"log"
"sort"
"strings"
)
const ruTranslation = `
{{define "empty cat"}}Эта категория пуста.{{end}}
{{define "cat"}}Категория{{end}}
{{define "hypha name"}}Название гифы{{end}}
{{define "categories"}}Категории{{end}}
{{define "placeholder"}}Название категории...{{end}}
{{define "remove from category title"}}Убрать гифу из этой категории{{end}}
{{define "add to category title"}}Добавить гифу в эту категорию{{end}}
{{define "category list"}}Список категорий{{end}}
{{define "no categories"}}В этой вики нет категорий.{{end}}
{{define "category x"}}Категория {{. | beautifulName}}{{end}}
{{define "edit category x"}}Редактирование категории {{beautifulName .}}{{end}}
{{define "edit category heading"}}Редактирование категории <a href="/category/{{.}}">{{beautifulName .}}</a>{{end}}
{{define "add"}}Добавить{{end}}
{{define "remove hyphae"}}Убрать гифы из этой категории{{end}}
{{define "remove"}}Убрать{{end}}
{{define "edit"}}Редактировать{{end}}
`
var (
//go:embed *.html
fs embed.FS
viewListChain, viewPageChain, viewCardChain, viewEditChain viewutil.Chain
)
func prepareViews() {
viewCardChain = viewutil.CopyEnRuWith(fs, "view_card.html", ruTranslation)
viewListChain = viewutil.CopyEnRuWith(fs, "view_list.html", ruTranslation)
viewPageChain = viewutil.CopyEnRuWith(fs, "view_page.html", ruTranslation)
viewEditChain = viewutil.CopyEnRuWith(fs, "view_edit.html", ruTranslation)
}
type cardData struct {
HyphaName string
Categories []string
GivenPermissionToModify bool
}
// CategoryCard is the little sidebar that is shown nearby the hypha view.
func CategoryCard(meta viewutil.Meta, hyphaName string) string {
var buf strings.Builder
err := viewCardChain.Get(meta).ExecuteTemplate(&buf, "category card", cardData{
HyphaName: hyphaName,
Categories: CategoriesWithHypha(hyphaName),
GivenPermissionToModify: meta.U.CanProceed("add-to-category"),
})
if err != nil {
log.Println(err)
}
return buf.String()
}
type catData struct {
*viewutil.BaseData
CatName string
Hyphae []string
GivenPermissionToModify bool
}
func categoryEdit(meta viewutil.Meta, catName string) {
viewutil.ExecutePage(meta, viewEditChain, catData{
BaseData: &viewutil.BaseData{
Addr: "/edit-category/" + catName,
},
CatName: catName,
Hyphae: hyphaeInCategory(catName),
GivenPermissionToModify: meta.U.CanProceed("add-to-category"),
})
}
func categoryPage(meta viewutil.Meta, catName string) {
viewutil.ExecutePage(meta, viewPageChain, catData{
BaseData: &viewutil.BaseData{
Addr: "/category/" + catName,
},
CatName: catName,
Hyphae: hyphaeInCategory(catName),
GivenPermissionToModify: meta.U.CanProceed("add-to-category"),
})
}
type listData struct {
*viewutil.BaseData
Categories []string
}
func categoryList(meta viewutil.Meta) {
cats := listOfCategories()
sort.Strings(cats)
viewutil.ExecutePage(meta, viewListChain, listData{
BaseData: &viewutil.BaseData{
Addr: "/category",
},
Categories: cats,
})
}

43
flag.go
View File

@ -3,26 +3,27 @@ package main
import (
"bufio"
_ "embed"
"errors"
"flag"
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"golang.org/x/term"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/internal/user"
"github.com/bouncepaw/mycorrhiza/internal/version"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/version"
"golang.org/x/term"
)
// CLI options are read and parsed here.
// printHelp prints the help message.
func printHelp() {
fmt.Fprintf(
_, _ = fmt.Fprintf(
flag.CommandLine.Output(),
"Usage: %s WIKI_PATH\n",
os.Args[0],
@ -31,7 +32,7 @@ func printHelp() {
}
// parseCliArgs parses CLI options and sets several important global variables. Call it early.
func parseCliArgs() {
func parseCliArgs() error {
var createAdminName string
var versionFlag bool
@ -42,31 +43,38 @@ func parseCliArgs() {
flag.Parse()
if versionFlag {
fmt.Println("Mycorrhiza Wiki", version.Long)
slog.Info("Running Mycorrhiza Wiki", "version", version.Long)
os.Exit(0)
}
args := flag.Args()
if len(args) == 0 {
log.Fatal("error: pass a wiki directory")
slog.Error("Pass a wiki directory")
return errors.New("wiki directory not passed")
}
wikiDir, err := filepath.Abs(args[0])
if err != nil {
log.Fatal(err)
slog.Error("Failed to take absolute filepath of wiki directory",
"path", args[0], "err", err)
return err
}
cfg.WikiDir = wikiDir
if createAdminName != "" {
createAdminCommand(createAdminName)
if err := createAdminCommand(createAdminName); err != nil {
os.Exit(1)
}
os.Exit(0)
}
return nil
}
func createAdminCommand(name string) {
func createAdminCommand(name string) error {
if err := files.PrepareWikiRoot(); err != nil {
log.Fatal(err)
slog.Error("Failed to prepare wiki root", "err", err)
return err
}
cfg.UseAuth = true
cfg.AllowRegistration = true
@ -74,11 +82,14 @@ func createAdminCommand(name string) {
password, err := askPass("Password")
if err != nil {
log.Fatal(err)
slog.Error("Failed to prompt password", "err", err)
return err
}
if err := user.Register(name, password, "admin", "local", true); err != nil {
log.Fatal(err)
slog.Error("Failed to register admin", "err", err)
return err
}
return nil
}
func askPass(prompt string) (string, error) {

24
go.mod
View File

@ -1,26 +1,22 @@
module github.com/bouncepaw/mycorrhiza
go 1.19
go 1.21
require (
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.2.1
github.com/go-ini/ini v1.63.2
github.com/gorilla/feeds v1.1.1
github.com/gorilla/mux v1.8.0
github.com/valyala/quicktemplate v1.7.0
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/text v0.3.7
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.6.0
github.com/go-ini/ini v1.67.0
github.com/gorilla/feeds v1.2.0
github.com/gorilla/mux v1.8.1
golang.org/x/crypto v0.31.0
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
)
require (
github.com/kr/pretty v0.2.1 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 // indirect
golang.org/x/sys v0.28.0 // indirect
)
// Use this trick to test local Mycomarkup changes, replace the path with yours,
// but do not commit the change to the path:
// replace git.sr.ht/~bouncepaw/mycomarkup/v5 v5.2.1 => "/Users/bouncepaw/GolandProjects/mycomarkup"
// replace git.sr.ht/~bouncepaw/mycomarkup/v5 v5.6.0 => "/Users/bouncepaw/src/mycomarkup"

66
go.sum
View File

@ -1,54 +1,32 @@
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.2.1 h1:pq4oM0YaGfSITLMk1/UCtOa0F+rqENX9PLBa6KKd6aE=
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.2.1/go.mod h1:TCzFBqW11En4EjLfcQtJu8C/Ro7FIFR8vZ+nM9f6Q28=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.6.0 h1:zAZwMF+6x8U/nunpqPRVYoDiqVUMBHI04PG8GsDrFOk=
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.6.0/go.mod h1:TCzFBqW11En4EjLfcQtJu8C/Ro7FIFR8vZ+nM9f6Q28=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ini/ini v1.63.2 h1:kwN3umicd2HF3Tgvap4um1ZG52/WyKT9GGdPx0CJk6Y=
github.com/go-ini/ini v1.63.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd h1:zVFyTKZN/Q7mNRWSs1GOYnHM9NiFSJ54YVRsD0rNWT4=
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 h1:T6tyxxvHMj2L1R2kZg0uNMpS8ZhB9lRa9XRGTCSA65w=
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
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=

View File

@ -1,11 +1,8 @@
= Help
This is documentation for **Mycorrhiza Wiki** 1.13.
This is documentation for Mycorrhiza Wiki 1.15.1. Choose a topic from the list.
//Choose a topic from the list.//
The documentation is incomplete. If you want to contribute to the documentation, open a pull request or an issue on [[https://github.com/bouncepaw/mycorrhiza | GitHub]] or [[https://lists.sr.ht/~bouncepaw/mycorrhiza-devel | send a patch]].
Currently, the documentation does not cover everything there is to cover, but it will probably change in the future.
If you want to contribute to the documentation, open a pull request or an issue on [[https://github.com/bouncepaw/mycorrhiza | GitHub]].
=> https://mycorrhiza.wiki | Official wiki
=> https://mycorrhiza.wiki | Official wiki
=> https://floss.social/@mycorrhiza | Fediverse

View File

@ -9,8 +9,9 @@ You can edit all of the files manually, if you want, just do your best to not br
* `wiki.git/` is the Git repository of the wiki, it has all hyphae in it. You can edit it directly, but do not forget to make Git commits with your changes and [[/reindex]] you wiki afterwards.
* `static` holds static data. You can access data there from your wiki with addresses like `/static/image.png`.
** `static/favicon.ico` is your wiki's favicon, accessed at [[/favicon.ico]] by browsers.
** `static/common.css` redefines the engine's default style, if exists. You probably don't need to use it.
** `static/default.css` redefines the engine's default style, if exists. You probably don't need to use it.
** `static/custom.css` is loaded after the main style. If you want to make visual changes to your wiki, this is probably where you should do that.
** `static/robots.txt` redefines default `robots.txt` file.
* `categories.json` contains the information about all categories in your wiki.
* `users.json` stores users' information. The passwords are not stored, only their hashes are, this is safe. Their tokens are stored in `cache/tokens.json`.
* `interwiki.json` holds the interwiki configuration.

View File

@ -1,5 +1,5 @@
= Interwiki
**Interwiki** is a means of bringing wikis together, making a federation of them. In Mycorrhiza, one aspect of interwiki is supported: the interwiki links, or the **interlinks**. Most other wiki systems support it too. Interwiki links are shown in green.
**Interwiki** is a means of bringing wikis together, making a federation of them. In Mycorrhiza, one aspect of interwiki is supported: the interwiki links, or the **interlinks**. Most other wiki systems support them too. Interwiki links are shown in green.
In Mycomarkup, you can address a different wiki by prefixing the link target with a name and a `>` character. For example:
```myco

View File

@ -6,7 +6,7 @@ You can upload any media file, but only those listed below will be displayed on
* **Images:** jpg, gif, png, webp, svg, ico
* **Video:** ogg, webm, mp4
* **Audio:** ogg, webm, mp3
* **Audio:** ogg, webm, mp3, flac, wav
== How to upload media?
For non-existent hyphae, upload a file in the //Upload media// section.

View File

@ -86,7 +86,7 @@ Links to [[hypha | a Hypha]] and [[https://example.org | some website]].
```}
* Links to [[hypha | a Hypha]] and [[https://example.org | some website]].
Since hypha names are case-insensitive, these links are basically the same: `[[hypha]]`, `[[Hypha]]`, `[[HYPHA]]`.
Since hypha names are case-insensitive, these links are basically the same: `[\[hypha]]`, `[\[Hypha]]`, `[\[HYPHA]]`.
=== Rocket link
**Rocket links** are special links. They take up a whole line. They are not consistent with usual inline links. They were taken from [[https://gemini.circumlunar.space/docs/gemtext.gmi | gemtext]].
@ -163,22 +163,22 @@ You can write a description for the image and specify its size.
* {```
img {
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg {
https://bouncepaw.com/mushroom.jpg
https://bouncepaw.com/mushroom.jpg {
Description //here//
}
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 100 { Size }
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 50*50
https://bouncepaw.com/mushroom.jpg | 100 { Size }
https://bouncepaw.com/mushroom.jpg | 50*50
}
```}
* {
img {
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg {
https://bouncepaw.com/mushroom.jpg
https://bouncepaw.com/mushroom.jpg {
Description //here//
}
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 100 { Size }
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 50*50 { Square }
https://bouncepaw.com/mushroom.jpg | 100 { Size }
https://bouncepaw.com/mushroom.jpg | 50*50 { Square }
}
}
@ -205,25 +205,25 @@ Specify the layout after `img` and before `{`. If you do not write any of them,
```
img grid {
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
https://bouncepaw.com/mushroom.jpg
https://bouncepaw.com/mushroom.jpg
}
img side {
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 200
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 200
https://bouncepaw.com/mushroom.jpg | 200
https://bouncepaw.com/mushroom.jpg | 200
}
This text is wrapped.
```
img grid {
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
https://bouncepaw.com/mushroom.jpg
https://bouncepaw.com/mushroom.jpg
}
img side {
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 200
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 200
https://bouncepaw.com/mushroom.jpg | 200
https://bouncepaw.com/mushroom.jpg | 200
}
This text is wrapped.
@ -425,4 +425,4 @@ This is an actual transclusion of a hypha below. It will fail if your wiki does
Recursive transclusion is also supported but it is limited to three iterations.
== See also
=> https://mycorrhiza.wiki/hypha/essay/why_mycomarkup | Why it was created
=> https://mycorrhiza.wiki/hypha/why_mycomarkup | Why it was created

4
help/en/today.myco Normal file
View File

@ -0,0 +1,4 @@
= Today links
Some people use Mycorrhiza to keep their diaries. They put each day's notes into separate hyphae, usually named in the ISO format, for example, `2024-06-01` for the 1st of June, 2024. This date format is especially handy because it aligns with the way Mycorrhiza sorts hyphae, so the days are in order.
Links [[/today]] and [[/edit-today]] are special links that redirect to or edit the “today” hypha. Put them on your home hypha or on [[/help/en/top_bar | the top bar]].

View File

@ -11,38 +11,39 @@
</article>
</main>
<aside class="help-topics layout-card">
<h2 class="layout-card__title">{{block "topics" .}}Help topics{{end}}</h2>
<h2 class="layout-card__title">Help topics</h2>
<ul class="help-topics__list">
<li><a href="/help/en">{{block "main" .}}Main{{end}}</a></li>
<li><a href="/help/en/hypha">{{block "hypha" .}}Hypha{{end}}</a>
<li><a href="/help/en">Main</a></li>
<li><a href="/help/en/hypha">Hypha</a>
<ul>
<li><a href="/help/en/media">{{block "media" .}}Media{{end}}</a></li>
<li><a href="/help/en/media">Media</a></li>
</ul>
</li>
<li><a href="/help/en/mycomarkup">{{block "mycomarkup" .}}Mycomarkup{{end}}</a></li>
<li><a href="/help/en/category">{{block "category" .}}Categories{{end}}</a></li>
<li><a href="/help/en/rename">{{block "rename" .}}Renaming{{end}}</a></li>
<li>{{block "interface" .}}Interface{{end}}
<li><a href="/help/en/mycomarkup">Mycomarkup</a></li>
<li><a href="/help/en/category">Categories</a></li>
<li><a href="/help/en/rename">Renaming</a></li>
<li>Interface
<ul>
<li><a href="/help/en/prevnext">{{block "prevnext" .}}Previous/next{{end}}</a></li>
<li><a href="/help/en/top_bar">{{block "top_bar" .}}Top bar{{end}}</a></li>
<li><a href="/help/en/prevnext">Previous/next</a></li>
<li><a href="/help/en/top_bar">Top bar</a></li>
</ul>
</li>
<li>{{block "special pages" .}}Special pages{{end}}
<li>Special pages
<ul>
<li><a href="/help/en/recent_changes">{{block "recent_changes" .}}Recent changes{{end}}</a></li>
<li><a href="/help/en/feeds">{{block "feeds" .}}Feeds{{end}}</a></li>
<li><a href="/help/en/orphans">{{block "orphans" .}}Orphaned hyphae{{end}}</a></li>
<li><a href="/help/en/recent_changes">Recent changes</a></li>
<li><a href="/help/en/feeds">Feeds</a></li>
<li><a href="/help/en/orphans">Orphaned hyphae</a></li>
<li><a href="/help/en/today">Today links</a></li>
</ul>
</li>
<li>{{block "configuration" .}}Configuration (for administrators){{end}}
<li>Configuration (for administrators)
<ul>
<li><a href="/help/en/config_file">{{block "config_file" .}}Configuration file{{end}}</a></li>
<li><a href="/help/en/lock">{{block "lock" .}}Lock{{end}}</a></li>
<li><a href="/help/en/whitelist">{{block "whitelist" .}}Whitelist{{end}}</a></li>
<li><a href="/help/en/telegram">{{block "telegram" .}}Telegram authentication{{end}}</a></li>
<li><a href="/help/en/interwiki">{{block "interwiki" .}}Interwiki{{end}}</a></li>
<li><a href="/help/en/file_structure">{{block "file structure" .}}File structure{{end}}</a></li>
<li><a href="/help/en/config_file">Configuration file</a></li>
<li><a href="/help/en/lock">Lock</a></li>
<li><a href="/help/en/whitelist">Whitelist</a></li>
<li><a href="/help/en/telegram">Telegram authentication</a></li>
<li><a href="/help/en/interwiki">Interwiki</a></li>
<li><a href="/help/en/file_structure">File structure</a></li>
</ul>
</li>
</ul>

View File

@ -2,15 +2,17 @@ package help
// stuff.go is used for meta stuff about the wiki or all hyphae at once.
import (
"git.sr.ht/~bouncepaw/mycomarkup/v5"
"github.com/bouncepaw/mycorrhiza/mycoopts"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/gorilla/mux"
"io"
"net/http"
"strings"
"github.com/bouncepaw/mycorrhiza/mycoopts"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
"git.sr.ht/~bouncepaw/mycomarkup/v5"
"git.sr.ht/~bouncepaw/mycomarkup/v5/mycocontext"
"github.com/gorilla/mux"
)
var (

View File

@ -7,7 +7,7 @@ import (
"strings"
"time"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/gorilla/feeds"
)
@ -121,6 +121,7 @@ func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
Created: grp[len(grp)-1].Time, // earliest revision
Updated: grp[0].Time, // latest revision
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
Content: grp.descriptionForFeed(opts.order),
}
}

View File

@ -4,12 +4,12 @@ package history
import (
"bytes"
"fmt"
"log"
"log/slog"
"os/exec"
"path/filepath"
"regexp"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/util"
)
@ -21,12 +21,14 @@ var renameMsgPattern = regexp.MustCompile(`^Rename (.*) to .*`)
var gitEnv = []string{"GIT_COMMITTER_NAME=wikimind", "GIT_COMMITTER_EMAIL=wikimind@mycorrhiza"}
// Start finds git and initializes git credentials.
func Start() {
func Start() error {
path, err := exec.LookPath("git")
if err != nil {
log.Fatal("Could not find the git executable. Check your $PATH.")
slog.Error("Could not find the Git executable. Check your $PATH.")
return err
}
gitpath = path
return nil
}
// InitGitRepo checks a Git repository and initializes it if necessary.
@ -44,7 +46,7 @@ func InitGitRepo() {
}
}
if !isGitRepo {
log.Println("Initializing Git repo at", files.HyphaeDir())
slog.Info("Initializing Git repo", "path", files.HyphaeDir())
gitsh("init")
gitsh("config", "core.quotePath", "false")
}
@ -60,7 +62,7 @@ func gitsh(args ...string) (out bytes.Buffer, err error) {
b, err := cmd.CombinedOutput()
if err != nil {
log.Println("gitsh:", err)
slog.Info("Git command failed", "err", err, "output", string(b))
}
return *bytes.NewBuffer(b), err
}
@ -69,7 +71,7 @@ func gitsh(args ...string) (out bytes.Buffer, err error) {
func silentGitsh(args ...string) (out bytes.Buffer, err error) {
cmd := exec.Command(gitpath, args...)
cmd.Dir = files.HyphaeDir()
cmd.Env = gitEnv
cmd.Env = append(cmd.Environ(), gitEnv...)
b, err := cmd.CombinedOutput()
return *bytes.NewBuffer(b), err
@ -77,7 +79,9 @@ func silentGitsh(args ...string) (out bytes.Buffer, err error) {
// Rename renames from `from` to `to` using `git mv`.
func Rename(from, to string) error {
log.Println(util.ShorterPath(from), util.ShorterPath(to))
slog.Info("Renaming file with git mv",
"from", util.ShorterPath(from),
"to", util.ShorterPath(to))
_, err := gitsh("mv", "--force", from, to)
return err
}

View File

@ -3,20 +3,22 @@ package histweb
import (
"embed"
"encoding/hex"
"fmt"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/util"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/gorilla/mux"
"log"
"html/template"
"log/slog"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/util"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
"github.com/gorilla/mux"
)
func InitHandlers(rtr *mux.Router) {
@ -39,15 +41,7 @@ func handlerPrimitiveDiff(w http.ResponseWriter, rq *http.Request) {
util.PrepareRq(rq)
shorterURL := strings.TrimPrefix(rq.URL.Path, "/primitive-diff/")
revHash, slug, found := strings.Cut(shorterURL, "/")
if !found || len(revHash) < 7 || len(slug) < 1 {
http.Error(w, "403 bad request", http.StatusBadRequest)
return
}
paddedRevHash := revHash
if len(paddedRevHash)%2 != 0 {
paddedRevHash = paddedRevHash[:len(paddedRevHash)-1]
}
if _, err := hex.DecodeString(paddedRevHash); err != nil {
if !found || !util.IsRevHash(revHash) || len(slug) < 1 {
http.Error(w, "403 bad request", http.StatusBadRequest)
return
}
@ -89,7 +83,9 @@ func handlerHistory(w http.ResponseWriter, rq *http.Request) {
if err == nil {
list = history.WithRevisions(hyphaName, revs)
}
log.Println("Found", len(revs), "revisions for", hyphaName)
// TODO: extra log, not needed?
slog.Info("Found revisions", "hyphaName", hyphaName, "n", len(revs), "err", err)
historyView(viewutil.MetaFrom(w, rq), hyphaName, list)
}
@ -122,7 +118,7 @@ func handlerRecentChangesAtom(w http.ResponseWriter, rq *http.Request) {
}
func handlerRecentChangesJSON(w http.ResponseWriter, rq *http.Request) {
genericHandlerOfFeeds(w, rq, history.RecentChangesJSON, "JSON feed", "application/json")
genericHandlerOfFeeds(w, rq, history.RecentChangesJSON, "JSON feed", "application/feed+json")
}
var (
@ -134,6 +130,7 @@ var (
{{define "diff for at title"}}Разница для {{beautifulName .HyphaName}} для {{.Hash}}{{end}}
{{define "diff for at heading"}}Разница для <a href="/hypha/{{.HyphaName}}">{{beautifulName .HyphaName}}</a> для {{.Hash}}{{end}}
{{define "no text diff available"}}Нет текстовой разницы.{{end}}
{{define "count pre"}}Отобразить{{end}}
{{define "count post"}}свежих правок.{{end}}
@ -167,15 +164,49 @@ type primitiveDiffData struct {
*viewutil.BaseData
HyphaName string
Hash string
Text string
Text template.HTML
}
func primitiveDiff(meta viewutil.Meta, h hyphae.Hypha, hash, text string) {
hunks := history.SplitPrimitiveDiff(text)
if len(hunks) > 0 {
var buf strings.Builder
for _, hunk := range hunks {
lines := strings.Split(hunk, "\n")
buf.WriteString(`<pre class="codeblock">`)
for i, line := range lines {
line = strings.Trim(line, "\r")
var class string
if len(line) > 0 {
switch line[0] {
case '+':
class = "primitive-diff__addition"
case '-':
class = "primitive-diff__deletion"
case '@':
class = "primitive-diff__context"
}
}
if i > 0 {
buf.WriteString("\n")
}
line = template.HTMLEscapeString(line)
fmt.Fprintf(&buf, `<code class="%s">%s</code>`,
class, line)
}
buf.WriteString(`</pre>`)
}
text = buf.String()
} else if text != "" {
text = template.HTMLEscapeString(text)
text = fmt.Sprintf(
`<pre class="codeblock"><code>%s</code></pre>`, text)
}
viewutil.ExecutePage(meta, chainPrimitiveDiff, primitiveDiffData{
BaseData: &viewutil.BaseData{},
HyphaName: h.CanonicalName(),
Hash: hash,
Text: text,
Text: template.HTML(text),
})
}

View File

@ -1,11 +1,12 @@
{{define "diff for at title"}}Diff of {{beautifulName .HyphaName}} at {{.Hash}}{{end}}
{{define "diff for at heading"}}Diff of <a href="/hypha/{{.HyphaName}}">{{beautifulName .HyphaName}}</a> at {{.Hash}}{{end}}
{{define "no text diff available"}}No text diff available.{{end}}
{{define "title"}}{{template "diff for at title" .}}{{end}}
{{define "body"}}
<main class="main-width">
<article>
<h1>{{template "diff for at heading" .}}</h1>
<pre class="codeblock"><code>{{.Text}}</code></pre>
{{if .Text}}{{.Text}}{{else}}{{template "no text diff available" .}}{{end}}
</article>
</main>
{{end}}
{{end}}

View File

@ -24,7 +24,9 @@
<time class="recent-changes__entry__time">
{{ $time.Format "15:04 UTC" }}
</time>
<span class="recent-changes__entry__message">{{$entry.Hash}}</span>
<span class="recent-changes__entry__message">
{{$entry.HyphaeDiffsHTML}}
</span>
{{ if $entry.Username | ne "anon" }}
<span class="recent-changes__entry__author">
&mdash; <a href="/hypha/{{$userHypha}}/{{$entry.Username}}" rel="author">{{$entry.Username}}</a>

View File

@ -8,7 +8,7 @@ import (
"path/filepath"
"sync"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/internal/user"
"github.com/bouncepaw/mycorrhiza/util"
)
@ -118,6 +118,7 @@ func (hop *Op) Apply() *Op {
"commit",
"--author='"+hop.name+" <"+hop.email+">'",
"--message="+hop.userMsg,
"--no-gpg-sign",
)
gitMutex.Unlock()
return hop

View File

@ -2,18 +2,72 @@ package history
import (
"fmt"
"log"
"html"
"log/slog"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
)
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
// WithRevisions returns an HTML representation of `revs` that is meant to be inserted in a history page.
func WithRevisions(hyphaName string, revs []Revision) string {
var buf strings.Builder
for _, grp := range groupRevisionsByMonth(revs) {
currentYear := grp[0].Time.Year()
currentMonth := grp[0].Time.Month()
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
buf.WriteString(fmt.Sprintf(
`<section class="history__month">
<a href="#%s" class="history__month-anchor">
<h2 id="%s" class="history__month-title">%d %s</h2>
</a>
<ul class="history__entries">`,
sectionId, sectionId, currentYear, currentMonth.String(),
))
for _, rev := range grp {
buf.WriteString(fmt.Sprintf(
`<li class="history__entry">
<a class="history-entry" href="/rev/%s/%s">
<time class="history-entry__time">%s</time>
</a>
<span class="history-entry__hash"><a href="/primitive-diff/%s/%s">%s</a></span>
<span class="history-entry__msg">%s</span>`,
rev.Hash, hyphaName,
rev.timeToDisplay(),
rev.Hash, hyphaName, rev.Hash,
html.EscapeString(rev.Message),
))
if rev.Username != "anon" {
buf.WriteString(fmt.Sprintf(
`<span class="history-entry__author">by <a href="/hypha/%s/%s" rel="author">%s</a></span>`,
cfg.UserHypha, rev.Username, rev.Username,
))
}
buf.WriteString("</li>\n")
}
buf.WriteString(`</ul></section>`)
}
return buf.String()
}
// Revision represents a revision of a hypha.
type Revision struct {
Hash string
// Hash is usually short.
Hash string
// Username is extracted from email.
Username string
Time time.Time
Message string
@ -21,13 +75,74 @@ type Revision struct {
hyphaeAffectedBuf []string
}
// HyphaeDiffsHTML returns a comma-separated list of diffs links of current revision for every affected file as HTML string.
func (rev Revision) HyphaeDiffsHTML() string {
entries := rev.hyphaeAffected()
if len(entries) == 1 {
return fmt.Sprintf(
`<a href="/primitive-diff/%s/%s">%s</a>`,
rev.Hash, entries[0], rev.Hash,
)
}
var buf strings.Builder
for i, hyphaName := range entries {
if i > 0 {
buf.WriteString(`<span aria-hidden="true">, </span>`)
}
buf.WriteString(`<a href="/primitive-diff/`)
buf.WriteString(rev.Hash)
buf.WriteString(`/`)
buf.WriteString(hyphaName)
buf.WriteString(`">`)
if i == 0 {
buf.WriteString(rev.Hash)
buf.WriteString("&nbsp;")
}
buf.WriteString(hyphaName)
buf.WriteString(`</a>`)
}
return buf.String()
}
// descriptionForFeed generates a good enough HTML contents for a web feed.
func (rev *Revision) descriptionForFeed() string {
return fmt.Sprintf(
`<p><b>%s</b> (by %s at %s)</p>
<p>Hyphae affected: %s</p>
<pre><code>%s</code></pre>`,
rev.Message, rev.Username, rev.TimeString(),
rev.HyphaeLinksHTML(),
rev.textDiff(),
)
}
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
func (rev Revision) HyphaeLinksHTML() string {
var buf strings.Builder
for i, hyphaName := range rev.hyphaeAffected() {
if i > 0 {
buf.WriteString(`<span aria-hidden="true">, <span>`)
}
urlSafeHyphaName := url.PathEscape(hyphaName)
buf.WriteString(fmt.Sprintf(`<a href="/hypha/%s">%s</a>`, urlSafeHyphaName, hyphaName))
}
return buf.String()
}
// gitLog calls `git log` and parses the results.
func gitLog(args ...string) ([]Revision, error) {
args = append([]string{
"log", "--abbrev-commit", "--no-merges",
"--pretty=format:%h\t%ae\t%at\t%s",
}, args...)
args = append(args, "--")
out, err := silentGitsh(args...)
if strings.Contains(out.String(), "bad revision 'HEAD'") {
// Then we have no recent changes! It's a hack.
return nil, nil
}
if err != nil {
return nil, err
}
@ -63,11 +178,17 @@ func (stream *recentChangesStream) next(n int) []Revision {
// currHash is the last revision from the last call, so skip it
args = append(args, "--skip=1", stream.currHash)
}
// I don't think this can fail, so ignore the error
res, _ := gitLog(args...)
res, err := gitLog(args...)
if err != nil {
// TODO: return error
slog.Error("Failed to git log", "err", err)
os.Exit(1)
}
if len(res) != 0 {
stream.currHash = res[len(res)-1].Hash
}
return res
}
@ -94,14 +215,14 @@ func (stream recentChangesStream) iterator() func() (Revision, bool) {
func RecentChanges(n int) []Revision {
stream := newRecentChangesStream()
revs := stream.next(n)
log.Printf("Found %d recent changes", len(revs))
slog.Info("Found recent changes", "n", len(revs))
return revs
}
// Revisions returns slice of revisions for the given hypha name, ordered most recent first.
func Revisions(hyphaName string) ([]Revision, error) {
revs, err := gitLog("--", hyphaName+".*")
log.Printf("Found %d revisions for %s\n", len(revs), hyphaName)
slog.Info("Found revisions", "hyphaName", hyphaName, "n", len(revs), "err", err)
return revs, err
}
@ -252,3 +373,21 @@ func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
}
return out.String(), err
}
// SplitPrimitiveDiff splits a primitive diff of a single file into hunks.
func SplitPrimitiveDiff(text string) (result []string) {
idx := strings.Index(text, "@@ -")
if idx < 0 {
return
}
text = text[idx:]
for {
idx = strings.Index(text, "\n@@ -")
if idx < 0 {
result = append(result, text)
return
}
result = append(result, text[:idx+1])
text = text[idx+1:]
}
}

View File

@ -1,55 +0,0 @@
{% import "fmt" %}
{% import "github.com/bouncepaw/mycorrhiza/cfg" %}
HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
{% func (rev Revision) HyphaeLinksHTML() %}
{% stripspace %}
{% for i, hyphaName := range rev.hyphaeAffected() %}
{% if i > 0 %}
<span aria-hidden="true">, </span>
{% endif %}
<a href="/hypha/{%s hyphaName %}">{%s hyphaName %}</a>
{% endfor %}
{% endstripspace %}
{% endfunc %}
descriptionForFeed generates a good enough HTML contents for a web feed.
{% func (rev *Revision) descriptionForFeed() %}
<p><b>{%s rev.Message %}</b> (by {%s rev.Username %} at {%s rev.TimeString() %})</p>
<p>Hyphae affected: {%= rev.HyphaeLinksHTML() %}</p>
<pre><code>{%s rev.textDiff() %}</code></pre>
{% endfunc %}
WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
{% func WithRevisions(hyphaName string, revs []Revision) %}
{% for _, grp := range groupRevisionsByMonth(revs) %}
{% code
currentYear := grp[0].Time.Year()
currentMonth := grp[0].Time.Month()
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
%}
<section class="history__month">
<a href="#{%s sectionId %}" class="history__month-anchor">
<h2 id="{%s sectionId %}" class="history__month-title">{%d currentYear %} {%s currentMonth.String() %}</h2>
</a>
<ul class="history__entries">
{% for _, rev := range grp %}
{%= rev.asHistoryEntry(hyphaName) %}
{% endfor %}
</ul>
</section>
{% endfor %}
{% endfunc %}
{% func (rev *Revision) asHistoryEntry(hyphaName string) %}
<li class="history__entry">
<a class="history-entry" href="/rev/{%s rev.Hash %}/{%s hyphaName %}">
<time class="history-entry__time">{%s rev.timeToDisplay() %}</time>
</a>
<span class="history-entry__hash"><a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">{%s rev.Hash %}</a></span>
<span class="history-entry__msg">{%s rev.Message %}</span>
{% if rev.Username != "anon" %}
<span class="history-entry__author">by <a href="/hypha/{%s cfg.UserHypha %}/{%s rev.Username %}" rel="author">{%s rev.Username %}</a></span>
{% endif %}
</li>
{% endfunc %}

View File

@ -1,326 +0,0 @@
// Code generated by qtc from "view.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line history/view.qtpl:1
package history
//line history/view.qtpl:1
import "fmt"
//line history/view.qtpl:2
import "github.com/bouncepaw/mycorrhiza/cfg"
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
//line history/view.qtpl:5
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line history/view.qtpl:5
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line history/view.qtpl:5
func (rev Revision) StreamHyphaeLinksHTML(qw422016 *qt422016.Writer) {
//line history/view.qtpl:5
qw422016.N().S(`
`)
//line history/view.qtpl:7
for i, hyphaName := range rev.hyphaeAffected() {
//line history/view.qtpl:8
if i > 0 {
//line history/view.qtpl:8
qw422016.N().S(`<span aria-hidden="true">, </span>`)
//line history/view.qtpl:10
}
//line history/view.qtpl:10
qw422016.N().S(`<a href="/hypha/`)
//line history/view.qtpl:11
qw422016.E().S(hyphaName)
//line history/view.qtpl:11
qw422016.N().S(`">`)
//line history/view.qtpl:11
qw422016.E().S(hyphaName)
//line history/view.qtpl:11
qw422016.N().S(`</a>`)
//line history/view.qtpl:12
}
//line history/view.qtpl:13
qw422016.N().S(`
`)
//line history/view.qtpl:14
}
//line history/view.qtpl:14
func (rev Revision) WriteHyphaeLinksHTML(qq422016 qtio422016.Writer) {
//line history/view.qtpl:14
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:14
rev.StreamHyphaeLinksHTML(qw422016)
//line history/view.qtpl:14
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:14
}
//line history/view.qtpl:14
func (rev Revision) HyphaeLinksHTML() string {
//line history/view.qtpl:14
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:14
rev.WriteHyphaeLinksHTML(qb422016)
//line history/view.qtpl:14
qs422016 := string(qb422016.B)
//line history/view.qtpl:14
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:14
return qs422016
//line history/view.qtpl:14
}
// descriptionForFeed generates a good enough HTML contents for a web feed.
//line history/view.qtpl:17
func (rev *Revision) streamdescriptionForFeed(qw422016 *qt422016.Writer) {
//line history/view.qtpl:17
qw422016.N().S(`
<p><b>`)
//line history/view.qtpl:18
qw422016.E().S(rev.Message)
//line history/view.qtpl:18
qw422016.N().S(`</b> (by `)
//line history/view.qtpl:18
qw422016.E().S(rev.Username)
//line history/view.qtpl:18
qw422016.N().S(` at `)
//line history/view.qtpl:18
qw422016.E().S(rev.TimeString())
//line history/view.qtpl:18
qw422016.N().S(`)</p>
<p>Hyphae affected: `)
//line history/view.qtpl:19
rev.StreamHyphaeLinksHTML(qw422016)
//line history/view.qtpl:19
qw422016.N().S(`</p>
<pre><code>`)
//line history/view.qtpl:20
qw422016.E().S(rev.textDiff())
//line history/view.qtpl:20
qw422016.N().S(`</code></pre>
`)
//line history/view.qtpl:21
}
//line history/view.qtpl:21
func (rev *Revision) writedescriptionForFeed(qq422016 qtio422016.Writer) {
//line history/view.qtpl:21
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:21
rev.streamdescriptionForFeed(qw422016)
//line history/view.qtpl:21
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:21
}
//line history/view.qtpl:21
func (rev *Revision) descriptionForFeed() string {
//line history/view.qtpl:21
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:21
rev.writedescriptionForFeed(qb422016)
//line history/view.qtpl:21
qs422016 := string(qb422016.B)
//line history/view.qtpl:21
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:21
return qs422016
//line history/view.qtpl:21
}
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
//line history/view.qtpl:24
func StreamWithRevisions(qw422016 *qt422016.Writer, hyphaName string, revs []Revision) {
//line history/view.qtpl:24
qw422016.N().S(`
`)
//line history/view.qtpl:25
for _, grp := range groupRevisionsByMonth(revs) {
//line history/view.qtpl:25
qw422016.N().S(`
`)
//line history/view.qtpl:27
currentYear := grp[0].Time.Year()
currentMonth := grp[0].Time.Month()
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
//line history/view.qtpl:30
qw422016.N().S(`
<section class="history__month">
<a href="#`)
//line history/view.qtpl:32
qw422016.E().S(sectionId)
//line history/view.qtpl:32
qw422016.N().S(`" class="history__month-anchor">
<h2 id="`)
//line history/view.qtpl:33
qw422016.E().S(sectionId)
//line history/view.qtpl:33
qw422016.N().S(`" class="history__month-title">`)
//line history/view.qtpl:33
qw422016.N().D(currentYear)
//line history/view.qtpl:33
qw422016.N().S(` `)
//line history/view.qtpl:33
qw422016.E().S(currentMonth.String())
//line history/view.qtpl:33
qw422016.N().S(`</h2>
</a>
<ul class="history__entries">
`)
//line history/view.qtpl:36
for _, rev := range grp {
//line history/view.qtpl:36
qw422016.N().S(`
`)
//line history/view.qtpl:37
rev.streamasHistoryEntry(qw422016, hyphaName)
//line history/view.qtpl:37
qw422016.N().S(`
`)
//line history/view.qtpl:38
}
//line history/view.qtpl:38
qw422016.N().S(`
</ul>
</section>
`)
//line history/view.qtpl:41
}
//line history/view.qtpl:41
qw422016.N().S(`
`)
//line history/view.qtpl:42
}
//line history/view.qtpl:42
func WriteWithRevisions(qq422016 qtio422016.Writer, hyphaName string, revs []Revision) {
//line history/view.qtpl:42
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:42
StreamWithRevisions(qw422016, hyphaName, revs)
//line history/view.qtpl:42
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:42
}
//line history/view.qtpl:42
func WithRevisions(hyphaName string, revs []Revision) string {
//line history/view.qtpl:42
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:42
WriteWithRevisions(qb422016, hyphaName, revs)
//line history/view.qtpl:42
qs422016 := string(qb422016.B)
//line history/view.qtpl:42
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:42
return qs422016
//line history/view.qtpl:42
}
//line history/view.qtpl:44
func (rev *Revision) streamasHistoryEntry(qw422016 *qt422016.Writer, hyphaName string) {
//line history/view.qtpl:44
qw422016.N().S(`
<li class="history__entry">
<a class="history-entry" href="/rev/`)
//line history/view.qtpl:46
qw422016.E().S(rev.Hash)
//line history/view.qtpl:46
qw422016.N().S(`/`)
//line history/view.qtpl:46
qw422016.E().S(hyphaName)
//line history/view.qtpl:46
qw422016.N().S(`">
<time class="history-entry__time">`)
//line history/view.qtpl:47
qw422016.E().S(rev.timeToDisplay())
//line history/view.qtpl:47
qw422016.N().S(`</time>
</a>
<span class="history-entry__hash"><a href="/primitive-diff/`)
//line history/view.qtpl:49
qw422016.E().S(rev.Hash)
//line history/view.qtpl:49
qw422016.N().S(`/`)
//line history/view.qtpl:49
qw422016.E().S(hyphaName)
//line history/view.qtpl:49
qw422016.N().S(`">`)
//line history/view.qtpl:49
qw422016.E().S(rev.Hash)
//line history/view.qtpl:49
qw422016.N().S(`</a></span>
<span class="history-entry__msg">`)
//line history/view.qtpl:50
qw422016.E().S(rev.Message)
//line history/view.qtpl:50
qw422016.N().S(`</span>
`)
//line history/view.qtpl:51
if rev.Username != "anon" {
//line history/view.qtpl:51
qw422016.N().S(`
<span class="history-entry__author">by <a href="/hypha/`)
//line history/view.qtpl:52
qw422016.E().S(cfg.UserHypha)
//line history/view.qtpl:52
qw422016.N().S(`/`)
//line history/view.qtpl:52
qw422016.E().S(rev.Username)
//line history/view.qtpl:52
qw422016.N().S(`" rel="author">`)
//line history/view.qtpl:52
qw422016.E().S(rev.Username)
//line history/view.qtpl:52
qw422016.N().S(`</a></span>
`)
//line history/view.qtpl:53
}
//line history/view.qtpl:53
qw422016.N().S(`
</li>
`)
//line history/view.qtpl:55
}
//line history/view.qtpl:55
func (rev *Revision) writeasHistoryEntry(qq422016 qtio422016.Writer, hyphaName string) {
//line history/view.qtpl:55
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:55
rev.streamasHistoryEntry(qw422016, hyphaName)
//line history/view.qtpl:55
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:55
}
//line history/view.qtpl:55
func (rev *Revision) asHistoryEntry(hyphaName string) string {
//line history/view.qtpl:55
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:55
rev.writeasHistoryEntry(qb422016, hyphaName)
//line history/view.qtpl:55
qs422016 := string(qb422016.B)
//line history/view.qtpl:55
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:55
return qs422016
//line history/view.qtpl:55
}

View File

@ -1,17 +1,18 @@
package main
import (
"log"
"errors"
"log/slog"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
)
func serveHTTP(handler http.Handler) {
func serveHTTP(handler http.Handler) (err error) {
server := &http.Server{
ReadTimeout: 300 * time.Second,
WriteTimeout: 300 * time.Second,
@ -20,35 +21,51 @@ func serveHTTP(handler http.Handler) {
}
if strings.HasPrefix(cfg.ListenAddr, "/") {
startUnixSocketServer(server, cfg.ListenAddr)
err = startUnixSocketServer(server, cfg.ListenAddr)
} else {
server.Addr = cfg.ListenAddr
startHTTPServer(server)
err = startHTTPServer(server)
}
return err
}
func startUnixSocketServer(server *http.Server, socketFile string) {
os.Remove(socketFile)
listener, err := net.Listen("unix", socketFile)
func startUnixSocketServer(server *http.Server, socketPath string) error {
err := os.Remove(socketPath)
if err != nil {
log.Fatalf("Failed to start a server: %v", err)
}
defer listener.Close()
if err := os.Chmod(socketFile, 0666); err != nil {
log.Fatalf("Failed to set socket permissions: %v", err)
slog.Warn("Failed to clean up old socket", "err", err)
}
log.Printf("Listening on Unix socket %s", cfg.ListenAddr)
if err := server.Serve(listener); err != http.ErrServerClosed {
log.Fatalf("Failed to start a server: %v", err)
listener, err := net.Listen("unix", socketPath)
if err != nil {
slog.Error("Failed to start the server", "err", err)
return err
}
defer func(listener net.Listener) {
_ = listener.Close()
}(listener)
if err := os.Chmod(socketPath, 0666); err != nil {
slog.Error("Failed to set socket permissions", "err", err)
return err
}
slog.Info("Listening Unix socket", "addr", socketPath)
if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
slog.Error("Failed to start the server", "err", err)
return err
}
return nil
}
func startHTTPServer(server *http.Server) {
log.Printf("Listening on %s", server.Addr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Failed to start a server: %v", err)
func startHTTPServer(server *http.Server) error {
slog.Info("Listening over HTTP", "addr", server.Addr)
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
slog.Error("Failed to start the server", "err", err)
return err
}
return nil
}

View File

@ -2,70 +2,19 @@ package hypview
import (
"embed"
"github.com/bouncepaw/mycorrhiza/backlinks"
"html/template"
"log"
"log/slog"
"strings"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
)
var (
//go:embed *.html
fs embed.FS
ruTranslation = `
{{define "editing hypha"}}Редактирование {{beautifulName .}}{{end}}
{{define "editing [[hypha]]"}}Редактирование <a href="/hypha/{{.}}">{{beautifulName .}}</a>{{end}}
{{define "creating [[hypha]]"}}Создание <a href="/hypha/{{.}}">{{beautifulName .}}</a>{{end}}
{{define "you're creating a new hypha"}}Вы создаёте новую гифу.{{end}}
{{define "describe your changes"}}Опишите ваши правки{{end}}
{{define "save"}}Сохранить{{end}}
{{define "preview"}}Предпросмотр{{end}}
{{define "previewing hypha"}}Предпросмотр «{{beautifulName .}}»{{end}}
{{define "preview tip"}}Заметьте, эта гифа ещё не сохранена. Вот её предпросмотр:{{end}}
{{define "markup"}}Разметка{{end}}
{{define "link"}}Ссылка{{end}}
{{define "link title"}}Текст{{end}}
{{define "heading"}}Заголовок{{end}}
{{define "bold"}}Жирный{{end}}
{{define "italic"}}Курсив{{end}}
{{define "highlight"}}Выделение{{end}}
{{define "underline"}}Подчеркивание{{end}}
{{define "mono"}}Моноширинный{{end}}
{{define "super"}}Надстрочный{{end}}
{{define "sub"}}Подстрочный{{end}}
{{define "strike"}}Зачёркнутый{{end}}
{{define "rocket"}}Ссылка-ракета{{end}}
{{define "transclude"}}Трансклюзия{{end}}
{{define "hr"}}Гориз. черта{{end}}
{{define "code"}}Код-блок{{end}}
{{define "bullets"}}Маркир. список{{end}}
{{define "numbers"}}Нумер. список{{end}}
{{define "mycomarkup help"}}<a href="/help/en/mycomarkup" class="shy-link">Подробнее</a> о микоразметке{{end}}
{{define "actions"}}Действия{{end}}
{{define "current date"}}Текущая дата{{end}}
{{define "current time"}}Текущее время{{end}}
{{define "selflink"}}Ссылка на вас{{end}}
{{define "empty heading"}}Эта гифа не существует{{end}}
{{define "empty no rights"}}У вас нет прав для создания новых гиф. Вы можете:{{end}}
{{define "empty log in"}}Войти в свою учётную запись, если она у вас есть{{end}}
{{define "empty register"}}Создать новую учётную запись{{end}}
{{define "write a text"}}Написать текст{{end}}
{{define "write a text tip"}}Напишите заметку, дневник, статью, рассказ или иной текст с помощью <a href="/help/en/mycomarkup" class="shy-link">микоразметки</a>. Сохраняется полная история правок документа.{{end}}
{{define "write a text writing conventions"}}Не забывайте следовать правилам оформления этой вики, если они имеются.{{end}}
{{define "write a text btn"}}Создать{{end}}
{{define "upload a media"}}Загрузить медиа{{end}}
{{define "upload a media tip"}}Загрузите изображение, видео или аудио. Распространённые форматы можно просматривать из браузера, остальные можно только скачать и просмотреть локально. Позже вы можете дописать пояснение к этому медиа.{{end}}
{{define "upload a media btn"}}Загрузить{{end}}
{{define "delete hypha?"}}Удалить {{beautifulName .}}?{{end}}
{{define "delete [[hypha]]?"}}Удалить <a href="/hypha/{{.}}">{{beautifulName .}}</a>?{{end}}
{{define "want to delete?"}}Вы действительно хотите удалить эту гифу?{{end}}
{{define "delete tip"}}Нельзя отменить удаление гифы, но её история останется доступной.{{end}}
{{define "rename hypha?"}}Переименовать {{beautifulName .}}?{{end}}
{{define "rename [[hypha]]?"}}Переименовать <a href="/hypha/{{.}}">{{beautifulName .}}</a>?{{end}}
{{define "new name"}}Новое название:{{end}}
@ -73,48 +22,15 @@ var (
{{define "rename tip"}}Переименовывайте аккуратно. <a href="/help/en/rename">Документация на английском.</a>{{end}}
{{define "leave redirection"}}Оставить перенаправление{{end}}
{{define "remove media from x?"}}Убрать медиа у {{beautifulName .}}?{{end}}
{{define "remove media from [[x]]?"}}Убрать медиа у <a href="/hypha/{{.HyphaName}}">{{beautifulName .HyphaName}}</a>?{{end}}
{{define "remove media for real?"}}Вы точно хотите убрать медиа у гифы «{{beautifulName .HyphaName}}»?{{end}}
`
chainNaviTitle viewutil.Chain
chainEditHypha viewutil.Chain
chainEmptyHypha viewutil.Chain
chainDeleteHypha viewutil.Chain
chainRenameHypha viewutil.Chain
chainRemoveMedia viewutil.Chain
)
func Init() {
chainNaviTitle = viewutil.CopyEnRuWith(fs, "view_navititle.html", "")
chainEditHypha = viewutil.CopyEnRuWith(fs, "view_edit.html", ruTranslation)
chainEmptyHypha = viewutil.CopyEnRuWith(fs, "view_empty_hypha.html", ruTranslation)
chainDeleteHypha = viewutil.CopyEnRuWith(fs, "view_delete.html", ruTranslation)
chainRenameHypha = viewutil.CopyEnRuWith(fs, "view_rename.html", ruTranslation)
chainRemoveMedia = viewutil.CopyEnRuWith(fs, "view_remove_media.html", ruTranslation)
}
type editData struct {
*viewutil.BaseData
HyphaName string
IsNew bool
Content string
Message string
Preview template.HTML
}
func EditHypha(meta viewutil.Meta, hyphaName string, isNew bool, content string, message string, preview template.HTML) {
viewutil.ExecutePage(meta, chainEditHypha, editData{
BaseData: &viewutil.BaseData{
Addr: "/edit/" + hyphaName,
EditScripts: cfg.EditScripts,
},
HyphaName: hyphaName,
IsNew: isNew,
Content: content,
Message: message,
Preview: preview,
})
}
type renameData struct {
@ -133,49 +49,6 @@ func RenameHypha(meta viewutil.Meta, hyphaName string) {
})
}
type deleteRemoveMediaData struct {
*viewutil.BaseData
HyphaName string
}
func DeleteHypha(meta viewutil.Meta, hyphaName string) {
viewutil.ExecutePage(meta, chainDeleteHypha, deleteRemoveMediaData{
BaseData: &viewutil.BaseData{
Addr: "/delete/" + hyphaName,
},
HyphaName: hyphaName,
})
}
func RemoveMedia(meta viewutil.Meta, hyphaName string) {
viewutil.ExecutePage(meta, chainRemoveMedia, deleteRemoveMediaData{
BaseData: &viewutil.BaseData{
Addr: "/remove-media/" + hyphaName,
},
HyphaName: hyphaName,
})
}
type emptyHyphaData struct {
Meta viewutil.Meta
HyphaName string
AllowRegistration bool
UseAuth bool
}
func EmptyHypha(meta viewutil.Meta, hyphaName string) string {
var buf strings.Builder
if err := chainEmptyHypha.Get(meta).ExecuteTemplate(&buf, "empty hypha card", emptyHyphaData{
Meta: meta,
HyphaName: hyphaName,
AllowRegistration: cfg.AllowRegistration,
UseAuth: cfg.UseAuth,
}); err != nil {
log.Println(err)
}
return buf.String()
}
type naviTitleData struct {
HyphaNameParts []string
HyphaNamePartsWithParents []string
@ -183,7 +56,7 @@ type naviTitleData struct {
HomeHypha string
}
func NaviTitle(meta viewutil.Meta, hyphaName string) string {
func NaviTitle(meta viewutil.Meta, hyphaName string) template.HTML {
parts, partsWithParents := naviTitleify(hyphaName)
var buf strings.Builder
err := chainNaviTitle.Get(meta).ExecuteTemplate(&buf, "navititle", naviTitleData{
@ -193,9 +66,9 @@ func NaviTitle(meta viewutil.Meta, hyphaName string) string {
HomeHypha: cfg.HomeHypha,
})
if err != nil {
log.Println(err)
slog.Error("Failed to render NaviTitle properly; using nevertheless", "err", err)
}
return buf.String()
return template.HTML(buf.String())
}
func naviTitleify(hyphaName string) ([]string, []string) {

View File

@ -1,50 +0,0 @@
{% import "github.com/bouncepaw/mycorrhiza/backlinks" %}
{% import "github.com/bouncepaw/mycorrhiza/cfg" %}
{% import "github.com/bouncepaw/mycorrhiza/hyphae" %}
{% import "github.com/bouncepaw/mycorrhiza/user" %}
{% import "github.com/bouncepaw/mycorrhiza/util" %}
{% import "github.com/bouncepaw/mycorrhiza/viewutil" %}
{% func hyphaInfoEntry(h hyphae.Hypha, u *user.User, action string, hasToExist bool, displayText string) %}
{% code flag := true %}
{% switch h.(type) %}
{% case *hyphae.EmptyHypha %}
{% code flag = !hasToExist %}
{% endswitch %}
{% if u.CanProceed(action) && flag %}
<li class="hypha-info__entry hypha-info__entry_{%s action %}">
<a class="hypha-info__link" href="/{%s action %}/{%s h.CanonicalName() %}">{%s displayText %}</a>
</li>
{% endif %}
{% endfunc %}
{% func hyphaInfo(meta viewutil.Meta, h hyphae.Hypha) %}
{% code
u := meta.U
lc := meta.Lc
backs := backlinks.BacklinksCount(h.CanonicalName())
%}
<nav class="hypha-info">
<ul class="hypha-info__list">
{%= hyphaInfoEntry(h, u, "history", false, lc.Get("ui.history_link")) %}
{%= hyphaInfoEntry(h, u, "rename", true, lc.Get("ui.rename_link")) %}
{%= hyphaInfoEntry(h, u, "delete", true, lc.Get("ui.delete_link")) %}
{%= hyphaInfoEntry(h, u, "text", true, lc.Get("ui.text_link")) %}
{% switch h := h.(type) %}
{% case *hyphae.TextualHypha %}
{%= hyphaInfoEntry(h, u, "media", true, lc.Get("ui.media_link_for_textual")) %}
{% default %}
{%= hyphaInfoEntry(h, u, "media", true, lc.Get("ui.media_link")) %}
{% endswitch %}
{%= hyphaInfoEntry(h, u, "backlinks", false, lc.GetPlural("ui.backlinks_link", backs)) %}
</ul>
</nav>
{% endfunc %}
{% func commonScripts() %}
{% for _, scriptPath := range cfg.CommonScripts %}
<script src="{%s scriptPath %}"></script>
{% endfor %}
{% endfunc %}
{% func beautifulLink(hyphaName string) %}<a href="/hypha/{%s= hyphaName %}">{%s util.BeautifulName(hyphaName) %}</a>{% endfunc %}

View File

@ -1,311 +0,0 @@
// Code generated by qtc from "nav.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line hypview/nav.qtpl:1
package hypview
//line hypview/nav.qtpl:1
import "github.com/bouncepaw/mycorrhiza/backlinks"
//line hypview/nav.qtpl:2
import "github.com/bouncepaw/mycorrhiza/cfg"
//line hypview/nav.qtpl:3
import "github.com/bouncepaw/mycorrhiza/hyphae"
//line hypview/nav.qtpl:4
import "github.com/bouncepaw/mycorrhiza/user"
//line hypview/nav.qtpl:5
import "github.com/bouncepaw/mycorrhiza/util"
//line hypview/nav.qtpl:6
import "github.com/bouncepaw/mycorrhiza/viewutil"
//line hypview/nav.qtpl:8
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line hypview/nav.qtpl:8
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line hypview/nav.qtpl:8
func streamhyphaInfoEntry(qw422016 *qt422016.Writer, h hyphae.Hypha, u *user.User, action string, hasToExist bool, displayText string) {
//line hypview/nav.qtpl:8
qw422016.N().S(`
`)
//line hypview/nav.qtpl:9
flag := true
//line hypview/nav.qtpl:9
qw422016.N().S(`
`)
//line hypview/nav.qtpl:10
switch h.(type) {
//line hypview/nav.qtpl:11
case *hyphae.EmptyHypha:
//line hypview/nav.qtpl:11
qw422016.N().S(`
`)
//line hypview/nav.qtpl:12
flag = !hasToExist
//line hypview/nav.qtpl:12
qw422016.N().S(`
`)
//line hypview/nav.qtpl:13
}
//line hypview/nav.qtpl:13
qw422016.N().S(`
`)
//line hypview/nav.qtpl:14
if u.CanProceed(action) && flag {
//line hypview/nav.qtpl:14
qw422016.N().S(`
<li class="hypha-info__entry hypha-info__entry_`)
//line hypview/nav.qtpl:15
qw422016.E().S(action)
//line hypview/nav.qtpl:15
qw422016.N().S(`">
<a class="hypha-info__link" href="/`)
//line hypview/nav.qtpl:16
qw422016.E().S(action)
//line hypview/nav.qtpl:16
qw422016.N().S(`/`)
//line hypview/nav.qtpl:16
qw422016.E().S(h.CanonicalName())
//line hypview/nav.qtpl:16
qw422016.N().S(`">`)
//line hypview/nav.qtpl:16
qw422016.E().S(displayText)
//line hypview/nav.qtpl:16
qw422016.N().S(`</a>
</li>
`)
//line hypview/nav.qtpl:18
}
//line hypview/nav.qtpl:18
qw422016.N().S(`
`)
//line hypview/nav.qtpl:19
}
//line hypview/nav.qtpl:19
func writehyphaInfoEntry(qq422016 qtio422016.Writer, h hyphae.Hypha, u *user.User, action string, hasToExist bool, displayText string) {
//line hypview/nav.qtpl:19
qw422016 := qt422016.AcquireWriter(qq422016)
//line hypview/nav.qtpl:19
streamhyphaInfoEntry(qw422016, h, u, action, hasToExist, displayText)
//line hypview/nav.qtpl:19
qt422016.ReleaseWriter(qw422016)
//line hypview/nav.qtpl:19
}
//line hypview/nav.qtpl:19
func hyphaInfoEntry(h hyphae.Hypha, u *user.User, action string, hasToExist bool, displayText string) string {
//line hypview/nav.qtpl:19
qb422016 := qt422016.AcquireByteBuffer()
//line hypview/nav.qtpl:19
writehyphaInfoEntry(qb422016, h, u, action, hasToExist, displayText)
//line hypview/nav.qtpl:19
qs422016 := string(qb422016.B)
//line hypview/nav.qtpl:19
qt422016.ReleaseByteBuffer(qb422016)
//line hypview/nav.qtpl:19
return qs422016
//line hypview/nav.qtpl:19
}
//line hypview/nav.qtpl:21
func streamhyphaInfo(qw422016 *qt422016.Writer, meta viewutil.Meta, h hyphae.Hypha) {
//line hypview/nav.qtpl:21
qw422016.N().S(`
`)
//line hypview/nav.qtpl:23
u := meta.U
lc := meta.Lc
backs := backlinks.BacklinksCount(h.CanonicalName())
//line hypview/nav.qtpl:26
qw422016.N().S(`
<nav class="hypha-info">
<ul class="hypha-info__list">
`)
//line hypview/nav.qtpl:29
streamhyphaInfoEntry(qw422016, h, u, "history", false, lc.Get("ui.history_link"))
//line hypview/nav.qtpl:29
qw422016.N().S(`
`)
//line hypview/nav.qtpl:30
streamhyphaInfoEntry(qw422016, h, u, "rename", true, lc.Get("ui.rename_link"))
//line hypview/nav.qtpl:30
qw422016.N().S(`
`)
//line hypview/nav.qtpl:31
streamhyphaInfoEntry(qw422016, h, u, "delete", true, lc.Get("ui.delete_link"))
//line hypview/nav.qtpl:31
qw422016.N().S(`
`)
//line hypview/nav.qtpl:32
streamhyphaInfoEntry(qw422016, h, u, "text", true, lc.Get("ui.text_link"))
//line hypview/nav.qtpl:32
qw422016.N().S(`
`)
//line hypview/nav.qtpl:33
switch h := h.(type) {
//line hypview/nav.qtpl:34
case *hyphae.TextualHypha:
//line hypview/nav.qtpl:34
qw422016.N().S(`
`)
//line hypview/nav.qtpl:35
streamhyphaInfoEntry(qw422016, h, u, "media", true, lc.Get("ui.media_link_for_textual"))
//line hypview/nav.qtpl:35
qw422016.N().S(`
`)
//line hypview/nav.qtpl:36
default:
//line hypview/nav.qtpl:36
qw422016.N().S(`
`)
//line hypview/nav.qtpl:37
streamhyphaInfoEntry(qw422016, h, u, "media", true, lc.Get("ui.media_link"))
//line hypview/nav.qtpl:37
qw422016.N().S(`
`)
//line hypview/nav.qtpl:38
}
//line hypview/nav.qtpl:38
qw422016.N().S(`
`)
//line hypview/nav.qtpl:39
streamhyphaInfoEntry(qw422016, h, u, "backlinks", false, lc.GetPlural("ui.backlinks_link", backs))
//line hypview/nav.qtpl:39
qw422016.N().S(`
</ul>
</nav>
`)
//line hypview/nav.qtpl:42
}
//line hypview/nav.qtpl:42
func writehyphaInfo(qq422016 qtio422016.Writer, meta viewutil.Meta, h hyphae.Hypha) {
//line hypview/nav.qtpl:42
qw422016 := qt422016.AcquireWriter(qq422016)
//line hypview/nav.qtpl:42
streamhyphaInfo(qw422016, meta, h)
//line hypview/nav.qtpl:42
qt422016.ReleaseWriter(qw422016)
//line hypview/nav.qtpl:42
}
//line hypview/nav.qtpl:42
func hyphaInfo(meta viewutil.Meta, h hyphae.Hypha) string {
//line hypview/nav.qtpl:42
qb422016 := qt422016.AcquireByteBuffer()
//line hypview/nav.qtpl:42
writehyphaInfo(qb422016, meta, h)
//line hypview/nav.qtpl:42
qs422016 := string(qb422016.B)
//line hypview/nav.qtpl:42
qt422016.ReleaseByteBuffer(qb422016)
//line hypview/nav.qtpl:42
return qs422016
//line hypview/nav.qtpl:42
}
//line hypview/nav.qtpl:44
func streamcommonScripts(qw422016 *qt422016.Writer) {
//line hypview/nav.qtpl:44
qw422016.N().S(`
`)
//line hypview/nav.qtpl:45
for _, scriptPath := range cfg.CommonScripts {
//line hypview/nav.qtpl:45
qw422016.N().S(`
<script src="`)
//line hypview/nav.qtpl:46
qw422016.E().S(scriptPath)
//line hypview/nav.qtpl:46
qw422016.N().S(`"></script>
`)
//line hypview/nav.qtpl:47
}
//line hypview/nav.qtpl:47
qw422016.N().S(`
`)
//line hypview/nav.qtpl:48
}
//line hypview/nav.qtpl:48
func writecommonScripts(qq422016 qtio422016.Writer) {
//line hypview/nav.qtpl:48
qw422016 := qt422016.AcquireWriter(qq422016)
//line hypview/nav.qtpl:48
streamcommonScripts(qw422016)
//line hypview/nav.qtpl:48
qt422016.ReleaseWriter(qw422016)
//line hypview/nav.qtpl:48
}
//line hypview/nav.qtpl:48
func commonScripts() string {
//line hypview/nav.qtpl:48
qb422016 := qt422016.AcquireByteBuffer()
//line hypview/nav.qtpl:48
writecommonScripts(qb422016)
//line hypview/nav.qtpl:48
qs422016 := string(qb422016.B)
//line hypview/nav.qtpl:48
qt422016.ReleaseByteBuffer(qb422016)
//line hypview/nav.qtpl:48
return qs422016
//line hypview/nav.qtpl:48
}
//line hypview/nav.qtpl:50
func streambeautifulLink(qw422016 *qt422016.Writer, hyphaName string) {
//line hypview/nav.qtpl:50
qw422016.N().S(`<a href="/hypha/`)
//line hypview/nav.qtpl:50
qw422016.N().S(hyphaName)
//line hypview/nav.qtpl:50
qw422016.N().S(`">`)
//line hypview/nav.qtpl:50
qw422016.E().S(util.BeautifulName(hyphaName))
//line hypview/nav.qtpl:50
qw422016.N().S(`</a>`)
//line hypview/nav.qtpl:50
}
//line hypview/nav.qtpl:50
func writebeautifulLink(qq422016 qtio422016.Writer, hyphaName string) {
//line hypview/nav.qtpl:50
qw422016 := qt422016.AcquireWriter(qq422016)
//line hypview/nav.qtpl:50
streambeautifulLink(qw422016, hyphaName)
//line hypview/nav.qtpl:50
qt422016.ReleaseWriter(qw422016)
//line hypview/nav.qtpl:50
}
//line hypview/nav.qtpl:50
func beautifulLink(hyphaName string) string {
//line hypview/nav.qtpl:50
qb422016 := qt422016.AcquireByteBuffer()
//line hypview/nav.qtpl:50
writebeautifulLink(qb422016, hyphaName)
//line hypview/nav.qtpl:50
qs422016 := string(qb422016.B)
//line hypview/nav.qtpl:50
qt422016.ReleaseByteBuffer(qb422016)
//line hypview/nav.qtpl:50
return qs422016
//line hypview/nav.qtpl:50
}

View File

@ -1,161 +0,0 @@
{% import "net/http" %}
{% import "strings" %}
{% import "path" %}
{% import "os" %}
{% import "github.com/bouncepaw/mycorrhiza/cfg" %}
{% import "github.com/bouncepaw/mycorrhiza/hyphae" %}
{% import "github.com/bouncepaw/mycorrhiza/categories" %}
{% import "github.com/bouncepaw/mycorrhiza/l18n" %}
{% import "github.com/bouncepaw/mycorrhiza/mimetype" %}
{% import "github.com/bouncepaw/mycorrhiza/tree" %}
{% import "github.com/bouncepaw/mycorrhiza/user" %}
{% import "github.com/bouncepaw/mycorrhiza/util" %}
{% import "github.com/bouncepaw/mycorrhiza/viewutil" %}
{% func MediaMenu(rq *http.Request, h hyphae.Hypha, u *user.User) %}
{% code
lc := l18n.FromRequest(rq)
%}
<main class="main-width media-tab">
<h1>{%s= lc.Get("ui.media_title", &l18n.Replacements{"name": beautifulLink(h.CanonicalName())}) %}</h1>
{% switch h.(type) %}
{% case *hyphae.MediaHypha %}
<p class="explanation">{%s lc.Get("ui.media_tip") %} <a href="/help/en/media" class="shy-link">{%s lc.Get("ui.media_what_is") %}</a></p>
{% default %}
<p class="explanation">{%s lc.Get("ui.media_empty") %} <a href="/help/en/media" class="shy-link">{%s lc.Get("ui.media_what_is") %}</a></p>
{% endswitch %}
<section class="amnt-grid">
{% switch h := h.(type) %}
{% case *hyphae.MediaHypha %}
{% code
mime := mimetype.FromExtension(path.Ext(h.MediaFilePath()))
fileinfo, err := os.Stat(h.MediaFilePath()) %}
{% if err == nil %}
<fieldset class="amnt-menu-block">
<legend class="modal__title modal__title_small">{%s lc.Get("ui.media_stat") %}</legend>
<p class="modal__confirmation-msg"><b>{%s lc.Get("ui.media_stat_size") %}</b> {%s lc.GetPlural64("ui.media_size_value", fileinfo.Size())%}</p>
<p><b>{%s lc.Get("ui.media_stat_mime") %}</b> {%s mime %}</p>
</fieldset>
{% endif %}
{% if strings.HasPrefix(mime, "image/") %}
<fieldset class="amnt-menu-block">
<legend class="modal__title modal__title_small">{%s lc.Get("ui.media_include") %}</legend>
<p class="modal__confirmation-msg">{%s lc.Get("ui.media_include_tip") %}</p>
<pre class="codeblock"><code>img { {%s h.CanonicalName() %} }</code></pre>
</fieldset>
{% endif %}
{% endswitch %}
{% if u.CanProceed("upload-binary") %}
<form action="/upload-binary/{%s h.CanonicalName() %}"
method="post" enctype="multipart/form-data"
class="upload-binary modal amnt-menu-block">
<fieldset class="modal__fieldset">
<legend class="modal__title modal__title_small">{%s lc.Get("ui.media_new") %}</legend>
<p class="modal__confirmation-msg">{%s lc.Get("ui.media_new_tip") %}</p>
<label for="upload-binary__input"></label>
<input type="file" id="upload-binary__input" name="binary">
<button type="submit" class="btn stick-to-bottom" value="Upload">{%s lc.Get("ui.media_upload")%}</button>
</fieldset>
</form>
{% endif %}
{% switch h := h.(type) %}
{% case *hyphae.MediaHypha %}
{% if u.CanProceed("remove-media") %}
<form action="/remove-media/{%s h.CanonicalName() %}" method="post" class="modal amnt-menu-block" method="POST">
<fieldset class="modal__fieldset">
<legend class="modal__title modal__title_small">{%s lc.Get("ui.media_remove") %}</legend>
<p class="modal__confirmation-msg">{%s lc.Get("ui.media_remove_tip") %}</p>
<button type="submit" class="btn" value="Remove media">{%s lc.Get("ui.media_remove_button") %}</button>
</fieldset>
</form>
{% endif %}
{% endswitch %}
</section>
</main>
{% endfunc %}
If `contents` == "", a helpful message is shown instead.
If you rename .prevnext, change the docs too.
{% func Hypha(meta viewutil.Meta, h hyphae.Hypha, contents string) %}
{% code
subhyphae, prevHyphaName, nextHyphaName := tree.Tree(h.CanonicalName())
lc := meta.Lc
%}
<main class="main-width">
<section id="hypha">
{% if meta.U.CanProceed("edit") %}
<div class="btn btn_navititle">
<a class="btn__link_navititle" href="/edit/{%s h.CanonicalName() %}">{%s lc.Get("ui.edit_link") %}</a>
</div>
{% endif %}
{% if cfg.UseAuth && util.IsProfileName(h.CanonicalName()) && meta.U.Name == strings.TrimPrefix(h.CanonicalName(), cfg.UserHypha + "/") %}
<div class="btn btn_navititle">
<a class="btn__link_navititle" href="/logout">{%s lc.Get("ui.logout_link") %}</a>
</div>
{% if meta.U.Group == "admin" %}
<div class="btn btn_navititle">
<a class="btn__link_navititle" href="/admin">{%s lc.Get("ui.admin_panel") %}<a>
</div>
{% endif %}
{% endif %}
{%s= NaviTitle(meta, h.CanonicalName()) %}
{% switch h.(type) %}
{% case *hyphae.EmptyHypha %}
{%s= EmptyHypha(meta, h.CanonicalName()) %}
{% default %}
{%s= contents %}
{% endswitch %}
</section>
<section class="prevnext">
{% if prevHyphaName != "" %}
<a class="prevnext__el prevnext__prev" href="/hypha/{%s prevHyphaName %}" rel="prev">← {%s util.BeautifulName(path.Base(prevHyphaName)) %}</a>
{% endif %}
{% if nextHyphaName != "" %}
<a class="prevnext__el prevnext__next" href="/hypha/{%s nextHyphaName %}" rel="next">{%s util.BeautifulName(path.Base(nextHyphaName)) %} →</a>
{% endif %}
</section>
{% if strings.TrimSpace(subhyphae) != "" %}
<section class="subhyphae">
<h2 class="subhyphae__title">{%s lc.Get("ui.subhyphae") %}</h2>
<nav class="subhyphae__nav">
<ul class="subhyphae__list">
{%s= subhyphae %}
</ul>
</nav>
</section>
{% endif %}
<section id="hypha-bottom">
{%= hyphaInfo(meta, h) %}
</section>
</main>
{%s= categories.CategoryCard(meta, h.CanonicalName()) %}
{%= viewScripts() %}
{% endfunc %}
{% func Revision(meta viewutil.Meta, h hyphae.Hypha, contents, revHash string) %}
<main class="main-width">
<section>
<p>{%s meta.Lc.Get("ui.revision_warning") %} <a href="/rev-text/{%s revHash %}/{%s h.CanonicalName() %}">{%s meta.Lc.Get("ui.revision_link") %}</a></p>
{%s= NaviTitle(meta, h.CanonicalName()) %}
{%s= contents %}
</section>
</main>
{%= viewScripts() %}
{% endfunc %}
{% func viewScripts() %}
{% for _, scriptPath := range cfg.ViewScripts %}
<script src="{%s scriptPath %}"></script>
{% endfor %}
{% endfunc %}

View File

@ -1,651 +0,0 @@
// Code generated by qtc from "readers.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line hypview/readers.qtpl:1
package hypview
//line hypview/readers.qtpl:1
import "net/http"
//line hypview/readers.qtpl:2
import "strings"
//line hypview/readers.qtpl:3
import "path"
//line hypview/readers.qtpl:4
import "os"
//line hypview/readers.qtpl:6
import "github.com/bouncepaw/mycorrhiza/cfg"
//line hypview/readers.qtpl:7
import "github.com/bouncepaw/mycorrhiza/hyphae"
//line hypview/readers.qtpl:8
import "github.com/bouncepaw/mycorrhiza/categories"
//line hypview/readers.qtpl:9
import "github.com/bouncepaw/mycorrhiza/l18n"
//line hypview/readers.qtpl:10
import "github.com/bouncepaw/mycorrhiza/mimetype"
//line hypview/readers.qtpl:11
import "github.com/bouncepaw/mycorrhiza/tree"
//line hypview/readers.qtpl:12
import "github.com/bouncepaw/mycorrhiza/user"
//line hypview/readers.qtpl:13
import "github.com/bouncepaw/mycorrhiza/util"
//line hypview/readers.qtpl:14
import "github.com/bouncepaw/mycorrhiza/viewutil"
//line hypview/readers.qtpl:16
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line hypview/readers.qtpl:16
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line hypview/readers.qtpl:16
func StreamMediaMenu(qw422016 *qt422016.Writer, rq *http.Request, h hyphae.Hypha, u *user.User) {
//line hypview/readers.qtpl:16
qw422016.N().S(`
`)
//line hypview/readers.qtpl:18
lc := l18n.FromRequest(rq)
//line hypview/readers.qtpl:19
qw422016.N().S(`
<main class="main-width media-tab">
<h1>`)
//line hypview/readers.qtpl:21
qw422016.N().S(lc.Get("ui.media_title", &l18n.Replacements{"name": beautifulLink(h.CanonicalName())}))
//line hypview/readers.qtpl:21
qw422016.N().S(`</h1>
`)
//line hypview/readers.qtpl:22
switch h.(type) {
//line hypview/readers.qtpl:23
case *hyphae.MediaHypha:
//line hypview/readers.qtpl:23
qw422016.N().S(`
<p class="explanation">`)
//line hypview/readers.qtpl:24
qw422016.E().S(lc.Get("ui.media_tip"))
//line hypview/readers.qtpl:24
qw422016.N().S(` <a href="/help/en/media" class="shy-link">`)
//line hypview/readers.qtpl:24
qw422016.E().S(lc.Get("ui.media_what_is"))
//line hypview/readers.qtpl:24
qw422016.N().S(`</a></p>
`)
//line hypview/readers.qtpl:25
default:
//line hypview/readers.qtpl:25
qw422016.N().S(`
<p class="explanation">`)
//line hypview/readers.qtpl:26
qw422016.E().S(lc.Get("ui.media_empty"))
//line hypview/readers.qtpl:26
qw422016.N().S(` <a href="/help/en/media" class="shy-link">`)
//line hypview/readers.qtpl:26
qw422016.E().S(lc.Get("ui.media_what_is"))
//line hypview/readers.qtpl:26
qw422016.N().S(`</a></p>
`)
//line hypview/readers.qtpl:27
}
//line hypview/readers.qtpl:27
qw422016.N().S(`
<section class="amnt-grid">
`)
//line hypview/readers.qtpl:30
switch h := h.(type) {
//line hypview/readers.qtpl:31
case *hyphae.MediaHypha:
//line hypview/readers.qtpl:31
qw422016.N().S(`
`)
//line hypview/readers.qtpl:33
mime := mimetype.FromExtension(path.Ext(h.MediaFilePath()))
fileinfo, err := os.Stat(h.MediaFilePath())
//line hypview/readers.qtpl:34
qw422016.N().S(`
`)
//line hypview/readers.qtpl:35
if err == nil {
//line hypview/readers.qtpl:35
qw422016.N().S(`
<fieldset class="amnt-menu-block">
<legend class="modal__title modal__title_small">`)
//line hypview/readers.qtpl:37
qw422016.E().S(lc.Get("ui.media_stat"))
//line hypview/readers.qtpl:37
qw422016.N().S(`</legend>
<p class="modal__confirmation-msg"><b>`)
//line hypview/readers.qtpl:38
qw422016.E().S(lc.Get("ui.media_stat_size"))
//line hypview/readers.qtpl:38
qw422016.N().S(`</b> `)
//line hypview/readers.qtpl:38
qw422016.E().S(lc.GetPlural64("ui.media_size_value", fileinfo.Size()))
//line hypview/readers.qtpl:38
qw422016.N().S(`</p>
<p><b>`)
//line hypview/readers.qtpl:39
qw422016.E().S(lc.Get("ui.media_stat_mime"))
//line hypview/readers.qtpl:39
qw422016.N().S(`</b> `)
//line hypview/readers.qtpl:39
qw422016.E().S(mime)
//line hypview/readers.qtpl:39
qw422016.N().S(`</p>
</fieldset>
`)
//line hypview/readers.qtpl:41
}
//line hypview/readers.qtpl:41
qw422016.N().S(`
`)
//line hypview/readers.qtpl:43
if strings.HasPrefix(mime, "image/") {
//line hypview/readers.qtpl:43
qw422016.N().S(`
<fieldset class="amnt-menu-block">
<legend class="modal__title modal__title_small">`)
//line hypview/readers.qtpl:45
qw422016.E().S(lc.Get("ui.media_include"))
//line hypview/readers.qtpl:45
qw422016.N().S(`</legend>
<p class="modal__confirmation-msg">`)
//line hypview/readers.qtpl:46
qw422016.E().S(lc.Get("ui.media_include_tip"))
//line hypview/readers.qtpl:46
qw422016.N().S(`</p>
<pre class="codeblock"><code>img { `)
//line hypview/readers.qtpl:47
qw422016.E().S(h.CanonicalName())
//line hypview/readers.qtpl:47
qw422016.N().S(` }</code></pre>
</fieldset>
`)
//line hypview/readers.qtpl:49
}
//line hypview/readers.qtpl:49
qw422016.N().S(`
`)
//line hypview/readers.qtpl:50
}
//line hypview/readers.qtpl:50
qw422016.N().S(`
`)
//line hypview/readers.qtpl:52
if u.CanProceed("upload-binary") {
//line hypview/readers.qtpl:52
qw422016.N().S(`
<form action="/upload-binary/`)
//line hypview/readers.qtpl:53
qw422016.E().S(h.CanonicalName())
//line hypview/readers.qtpl:53
qw422016.N().S(`"
method="post" enctype="multipart/form-data"
class="upload-binary modal amnt-menu-block">
<fieldset class="modal__fieldset">
<legend class="modal__title modal__title_small">`)
//line hypview/readers.qtpl:57
qw422016.E().S(lc.Get("ui.media_new"))
//line hypview/readers.qtpl:57
qw422016.N().S(`</legend>
<p class="modal__confirmation-msg">`)
//line hypview/readers.qtpl:58
qw422016.E().S(lc.Get("ui.media_new_tip"))
//line hypview/readers.qtpl:58
qw422016.N().S(`</p>
<label for="upload-binary__input"></label>
<input type="file" id="upload-binary__input" name="binary">
<button type="submit" class="btn stick-to-bottom" value="Upload">`)
//line hypview/readers.qtpl:62
qw422016.E().S(lc.Get("ui.media_upload"))
//line hypview/readers.qtpl:62
qw422016.N().S(`</button>
</fieldset>
</form>
`)
//line hypview/readers.qtpl:65
}
//line hypview/readers.qtpl:65
qw422016.N().S(`
`)
//line hypview/readers.qtpl:68
switch h := h.(type) {
//line hypview/readers.qtpl:69
case *hyphae.MediaHypha:
//line hypview/readers.qtpl:69
qw422016.N().S(`
`)
//line hypview/readers.qtpl:70
if u.CanProceed("remove-media") {
//line hypview/readers.qtpl:70
qw422016.N().S(`
<form action="/remove-media/`)
//line hypview/readers.qtpl:71
qw422016.E().S(h.CanonicalName())
//line hypview/readers.qtpl:71
qw422016.N().S(`" method="post" class="modal amnt-menu-block" method="POST">
<fieldset class="modal__fieldset">
<legend class="modal__title modal__title_small">`)
//line hypview/readers.qtpl:73
qw422016.E().S(lc.Get("ui.media_remove"))
//line hypview/readers.qtpl:73
qw422016.N().S(`</legend>
<p class="modal__confirmation-msg">`)
//line hypview/readers.qtpl:74
qw422016.E().S(lc.Get("ui.media_remove_tip"))
//line hypview/readers.qtpl:74
qw422016.N().S(`</p>
<button type="submit" class="btn" value="Remove media">`)
//line hypview/readers.qtpl:75
qw422016.E().S(lc.Get("ui.media_remove_button"))
//line hypview/readers.qtpl:75
qw422016.N().S(`</button>
</fieldset>
</form>
`)
//line hypview/readers.qtpl:78
}
//line hypview/readers.qtpl:78
qw422016.N().S(`
`)
//line hypview/readers.qtpl:79
}
//line hypview/readers.qtpl:79
qw422016.N().S(`
</section>
</main>
`)
//line hypview/readers.qtpl:83
}
//line hypview/readers.qtpl:83
func WriteMediaMenu(qq422016 qtio422016.Writer, rq *http.Request, h hyphae.Hypha, u *user.User) {
//line hypview/readers.qtpl:83
qw422016 := qt422016.AcquireWriter(qq422016)
//line hypview/readers.qtpl:83
StreamMediaMenu(qw422016, rq, h, u)
//line hypview/readers.qtpl:83
qt422016.ReleaseWriter(qw422016)
//line hypview/readers.qtpl:83
}
//line hypview/readers.qtpl:83
func MediaMenu(rq *http.Request, h hyphae.Hypha, u *user.User) string {
//line hypview/readers.qtpl:83
qb422016 := qt422016.AcquireByteBuffer()
//line hypview/readers.qtpl:83
WriteMediaMenu(qb422016, rq, h, u)
//line hypview/readers.qtpl:83
qs422016 := string(qb422016.B)
//line hypview/readers.qtpl:83
qt422016.ReleaseByteBuffer(qb422016)
//line hypview/readers.qtpl:83
return qs422016
//line hypview/readers.qtpl:83
}
// If `contents` == "", a helpful message is shown instead.
//
// If you rename .prevnext, change the docs too.
//line hypview/readers.qtpl:88
func StreamHypha(qw422016 *qt422016.Writer, meta viewutil.Meta, h hyphae.Hypha, contents string) {
//line hypview/readers.qtpl:88
qw422016.N().S(`
`)
//line hypview/readers.qtpl:90
subhyphae, prevHyphaName, nextHyphaName := tree.Tree(h.CanonicalName())
lc := meta.Lc
//line hypview/readers.qtpl:92
qw422016.N().S(`
<main class="main-width">
<section id="hypha">
`)
//line hypview/readers.qtpl:95
if meta.U.CanProceed("edit") {
//line hypview/readers.qtpl:95
qw422016.N().S(`
<div class="btn btn_navititle">
<a class="btn__link_navititle" href="/edit/`)
//line hypview/readers.qtpl:97
qw422016.E().S(h.CanonicalName())
//line hypview/readers.qtpl:97
qw422016.N().S(`">`)
//line hypview/readers.qtpl:97
qw422016.E().S(lc.Get("ui.edit_link"))
//line hypview/readers.qtpl:97
qw422016.N().S(`</a>
</div>
`)
//line hypview/readers.qtpl:99
}
//line hypview/readers.qtpl:99
qw422016.N().S(`
`)
//line hypview/readers.qtpl:101
if cfg.UseAuth && util.IsProfileName(h.CanonicalName()) && meta.U.Name == strings.TrimPrefix(h.CanonicalName(), cfg.UserHypha+"/") {
//line hypview/readers.qtpl:101
qw422016.N().S(`
<div class="btn btn_navititle">
<a class="btn__link_navititle" href="/logout">`)
//line hypview/readers.qtpl:103
qw422016.E().S(lc.Get("ui.logout_link"))
//line hypview/readers.qtpl:103
qw422016.N().S(`</a>
</div>
`)
//line hypview/readers.qtpl:105
if meta.U.Group == "admin" {
//line hypview/readers.qtpl:105
qw422016.N().S(`
<div class="btn btn_navititle">
<a class="btn__link_navititle" href="/admin">`)
//line hypview/readers.qtpl:107
qw422016.E().S(lc.Get("ui.admin_panel"))
//line hypview/readers.qtpl:107
qw422016.N().S(`<a>
</div>
`)
//line hypview/readers.qtpl:109
}
//line hypview/readers.qtpl:109
qw422016.N().S(`
`)
//line hypview/readers.qtpl:110
}
//line hypview/readers.qtpl:110
qw422016.N().S(`
`)
//line hypview/readers.qtpl:112
qw422016.N().S(NaviTitle(meta, h.CanonicalName()))
//line hypview/readers.qtpl:112
qw422016.N().S(`
`)
//line hypview/readers.qtpl:113
switch h.(type) {
//line hypview/readers.qtpl:114
case *hyphae.EmptyHypha:
//line hypview/readers.qtpl:114
qw422016.N().S(`
`)
//line hypview/readers.qtpl:115
qw422016.N().S(EmptyHypha(meta, h.CanonicalName()))
//line hypview/readers.qtpl:115
qw422016.N().S(`
`)
//line hypview/readers.qtpl:116
default:
//line hypview/readers.qtpl:116
qw422016.N().S(`
`)
//line hypview/readers.qtpl:117
qw422016.N().S(contents)
//line hypview/readers.qtpl:117
qw422016.N().S(`
`)
//line hypview/readers.qtpl:118
}
//line hypview/readers.qtpl:118
qw422016.N().S(`
</section>
<section class="prevnext">
`)
//line hypview/readers.qtpl:121
if prevHyphaName != "" {
//line hypview/readers.qtpl:121
qw422016.N().S(`
<a class="prevnext__el prevnext__prev" href="/hypha/`)
//line hypview/readers.qtpl:122
qw422016.E().S(prevHyphaName)
//line hypview/readers.qtpl:122
qw422016.N().S(`" rel="prev">← `)
//line hypview/readers.qtpl:122
qw422016.E().S(util.BeautifulName(path.Base(prevHyphaName)))
//line hypview/readers.qtpl:122
qw422016.N().S(`</a>
`)
//line hypview/readers.qtpl:123
}
//line hypview/readers.qtpl:123
qw422016.N().S(`
`)
//line hypview/readers.qtpl:124
if nextHyphaName != "" {
//line hypview/readers.qtpl:124
qw422016.N().S(`
<a class="prevnext__el prevnext__next" href="/hypha/`)
//line hypview/readers.qtpl:125
qw422016.E().S(nextHyphaName)
//line hypview/readers.qtpl:125
qw422016.N().S(`" rel="next">`)
//line hypview/readers.qtpl:125
qw422016.E().S(util.BeautifulName(path.Base(nextHyphaName)))
//line hypview/readers.qtpl:125
qw422016.N().S(` </a>
`)
//line hypview/readers.qtpl:126
}
//line hypview/readers.qtpl:126
qw422016.N().S(`
</section>
`)
//line hypview/readers.qtpl:128
if strings.TrimSpace(subhyphae) != "" {
//line hypview/readers.qtpl:128
qw422016.N().S(`
<section class="subhyphae">
<h2 class="subhyphae__title">`)
//line hypview/readers.qtpl:130
qw422016.E().S(lc.Get("ui.subhyphae"))
//line hypview/readers.qtpl:130
qw422016.N().S(`</h2>
<nav class="subhyphae__nav">
<ul class="subhyphae__list">
`)
//line hypview/readers.qtpl:133
qw422016.N().S(subhyphae)
//line hypview/readers.qtpl:133
qw422016.N().S(`
</ul>
</nav>
</section>
`)
//line hypview/readers.qtpl:137
}
//line hypview/readers.qtpl:137
qw422016.N().S(`
<section id="hypha-bottom">
`)
//line hypview/readers.qtpl:139
streamhyphaInfo(qw422016, meta, h)
//line hypview/readers.qtpl:139
qw422016.N().S(`
</section>
</main>
`)
//line hypview/readers.qtpl:142
qw422016.N().S(categories.CategoryCard(meta, h.CanonicalName()))
//line hypview/readers.qtpl:142
qw422016.N().S(`
`)
//line hypview/readers.qtpl:143
streamviewScripts(qw422016)
//line hypview/readers.qtpl:143
qw422016.N().S(`
`)
//line hypview/readers.qtpl:144
}
//line hypview/readers.qtpl:144
func WriteHypha(qq422016 qtio422016.Writer, meta viewutil.Meta, h hyphae.Hypha, contents string) {
//line hypview/readers.qtpl:144
qw422016 := qt422016.AcquireWriter(qq422016)
//line hypview/readers.qtpl:144
StreamHypha(qw422016, meta, h, contents)
//line hypview/readers.qtpl:144
qt422016.ReleaseWriter(qw422016)
//line hypview/readers.qtpl:144
}
//line hypview/readers.qtpl:144
func Hypha(meta viewutil.Meta, h hyphae.Hypha, contents string) string {
//line hypview/readers.qtpl:144
qb422016 := qt422016.AcquireByteBuffer()
//line hypview/readers.qtpl:144
WriteHypha(qb422016, meta, h, contents)
//line hypview/readers.qtpl:144
qs422016 := string(qb422016.B)
//line hypview/readers.qtpl:144
qt422016.ReleaseByteBuffer(qb422016)
//line hypview/readers.qtpl:144
return qs422016
//line hypview/readers.qtpl:144
}
//line hypview/readers.qtpl:146
func StreamRevision(qw422016 *qt422016.Writer, meta viewutil.Meta, h hyphae.Hypha, contents, revHash string) {
//line hypview/readers.qtpl:146
qw422016.N().S(`
<main class="main-width">
<section>
<p>`)
//line hypview/readers.qtpl:149
qw422016.E().S(meta.Lc.Get("ui.revision_warning"))
//line hypview/readers.qtpl:149
qw422016.N().S(` <a href="/rev-text/`)
//line hypview/readers.qtpl:149
qw422016.E().S(revHash)
//line hypview/readers.qtpl:149
qw422016.N().S(`/`)
//line hypview/readers.qtpl:149
qw422016.E().S(h.CanonicalName())
//line hypview/readers.qtpl:149
qw422016.N().S(`">`)
//line hypview/readers.qtpl:149
qw422016.E().S(meta.Lc.Get("ui.revision_link"))
//line hypview/readers.qtpl:149
qw422016.N().S(`</a></p>
`)
//line hypview/readers.qtpl:150
qw422016.N().S(NaviTitle(meta, h.CanonicalName()))
//line hypview/readers.qtpl:150
qw422016.N().S(`
`)
//line hypview/readers.qtpl:151
qw422016.N().S(contents)
//line hypview/readers.qtpl:151
qw422016.N().S(`
</section>
</main>
`)
//line hypview/readers.qtpl:154
streamviewScripts(qw422016)
//line hypview/readers.qtpl:154
qw422016.N().S(`
`)
//line hypview/readers.qtpl:155
}
//line hypview/readers.qtpl:155
func WriteRevision(qq422016 qtio422016.Writer, meta viewutil.Meta, h hyphae.Hypha, contents, revHash string) {
//line hypview/readers.qtpl:155
qw422016 := qt422016.AcquireWriter(qq422016)
//line hypview/readers.qtpl:155
StreamRevision(qw422016, meta, h, contents, revHash)
//line hypview/readers.qtpl:155
qt422016.ReleaseWriter(qw422016)
//line hypview/readers.qtpl:155
}
//line hypview/readers.qtpl:155
func Revision(meta viewutil.Meta, h hyphae.Hypha, contents, revHash string) string {
//line hypview/readers.qtpl:155
qb422016 := qt422016.AcquireByteBuffer()
//line hypview/readers.qtpl:155
WriteRevision(qb422016, meta, h, contents, revHash)
//line hypview/readers.qtpl:155
qs422016 := string(qb422016.B)
//line hypview/readers.qtpl:155
qt422016.ReleaseByteBuffer(qb422016)
//line hypview/readers.qtpl:155
return qs422016
//line hypview/readers.qtpl:155
}
//line hypview/readers.qtpl:157
func streamviewScripts(qw422016 *qt422016.Writer) {
//line hypview/readers.qtpl:157
qw422016.N().S(`
`)
//line hypview/readers.qtpl:158
for _, scriptPath := range cfg.ViewScripts {
//line hypview/readers.qtpl:158
qw422016.N().S(`
<script src="`)
//line hypview/readers.qtpl:159
qw422016.E().S(scriptPath)
//line hypview/readers.qtpl:159
qw422016.N().S(`"></script>
`)
//line hypview/readers.qtpl:160
}
//line hypview/readers.qtpl:160
qw422016.N().S(`
`)
//line hypview/readers.qtpl:161
}
//line hypview/readers.qtpl:161
func writeviewScripts(qq422016 qtio422016.Writer) {
//line hypview/readers.qtpl:161
qw422016 := qt422016.AcquireWriter(qq422016)
//line hypview/readers.qtpl:161
streamviewScripts(qw422016)
//line hypview/readers.qtpl:161
qt422016.ReleaseWriter(qw422016)
//line hypview/readers.qtpl:161
}
//line hypview/readers.qtpl:161
func viewScripts() string {
//line hypview/readers.qtpl:161
qb422016 := qt422016.AcquireByteBuffer()
//line hypview/readers.qtpl:161
writeviewScripts(qb422016)
//line hypview/readers.qtpl:161
qs422016 := string(qb422016.B)
//line hypview/readers.qtpl:161
qt422016.ReleaseByteBuffer(qb422016)
//line hypview/readers.qtpl:161
return qs422016
//line hypview/readers.qtpl:161
}

View File

@ -1,32 +0,0 @@
{{define "empty hypha card"}}
<section class="non-existent-hypha">
<h2 class="non-existent-hypha__title">{{block "empty heading" .}}This hypha does not exist{{end}}</h2>
{{if and .UseAuth (eq .Meta.U.Group "anon")}}
<p>{{block "empty no rights" .}}You are not authorized to create new hyphae. Here is what you can do:{{end}}</p>
<ul>
<li><a href="/login">{{block "empty log in" .}}Log in to your account, if you have one{{end}}</a></li>
{{if .AllowRegistration}}<li><a href="/register">{{block "empty register" .}}Register a new account{{end}}</a></li>{{end}}
</ul>
{{else}}
<div class="non-existent-hypha__ways">
<section class="non-existent-hypha__way">
<h3 class="non-existent-hypha__subtitle">📝 {{block "write a text" .}}Write a text{{end}}</h3>
<p>{{block "write a text tip" .}}Write a note, a diary, an article, a story or anything textual using <a href="/help/en/mycomarkup" class="shy-link">Mycomarkup</a>. Full history of edits to the document will be saved.{{end}}</p>
<p>{{block "write a text writing conventions" .}}Make sure to follow this wiki's writing conventions if there are any.{{end}}</p>
<a class="btn btn_accent stick-to-bottom" href="/edit/{{.HyphaName}}">{{block "write a text btn" .}}Create{{end}}</a>
</section>
<section class="non-existent-hypha__way">
<h3 class="non-existent-hypha__subtitle">🖼 {{block "upload a media" .}}Upload a media{{end}}</h3>
<p>{{block "upload a media tip" .}}Upload a picture, a video or an audio. Most common formats can be viewed from the browser, others can only be downloaded and viewed locally. You can write a description for the media later.{{end}}</p>
<form action="/upload-binary/{{.HyphaName}}"
method="post" enctype="multipart/form-data"
class="upload-binary">
<input type="file" id="upload-binary__input" name="binary">
<button type="submit" class="btn stick-to-bottom" value="Upload">{{block "upload a media btn" .}}Upload{{end}}</button>
</form>
</section>
</div>
{{end}}
</section>
{{end}}

View File

@ -1,22 +0,0 @@
{{define "remove media from x?"}}Remove media from {{beautifulName .}}?{{end}}
{{define "title"}}{{template "remove media from x?" .HyphaName}}{{end}}
{{define "body"}}
<main class="main-width">
<form class="modal" action="/remove-media/{{.HyphaName}}" method="post">
<fieldset class="modal__fieldset">
<legend class="modal__title">
{{block "remove media from [[x]]?" .}}Remove media from <a href="/hypha/{{.HyphaName}}">{{beautifulName .HyphaName}}</a>?{{end}}
</legend>
<p class="modal__confirmation-msg">
{{block "remove media for real?" .}}Do you really want to remove media from hypha {{beautifulName .HyphaName}}?{{end}}
</p>
<button type="submit" value="Confirm" class="btn" autofocus>
{{template "confirm"}}
</button>
<a href="/hypha/{%s hyphaName %}" class="btn btn_weak">
{{template "cancel"}}
</a>
</fieldset>
</form>
</main>
{{end}}

View File

@ -2,10 +2,11 @@
package backlinks
import (
"log"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"log/slog"
"os"
"sort"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/util"
)
@ -61,6 +62,25 @@ func BacklinksCount(hyphaName string) int {
return 0
}
func BacklinksFor(hyphaName string) []string {
var backlinks []string
for b := range yieldHyphaBacklinks(hyphaName) {
backlinks = append(backlinks, b)
}
return backlinks
}
func Orphans() []string {
var orphans []string
for h := range hyphae.YieldExistingHyphae() {
if BacklinksCount(h.CanonicalName()) == 0 {
orphans = append(orphans, h.CanonicalName())
}
}
sort.Strings(orphans)
return orphans
}
// Using set here seems like the most appropriate solution
type linkSet map[string]struct{}
@ -88,7 +108,7 @@ func fetchText(h hyphae.Hypha) string {
text, err := os.ReadFile(path)
if err != nil {
log.Println(err)
slog.Error("Failed to read file", "path", path, "err", err, "hyphaName", h.CanonicalName())
return ""
}
return string(text)

View File

@ -1,12 +1,13 @@
package backlinks
import (
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/mycoopts"
"git.sr.ht/~bouncepaw/mycomarkup/v5"
"git.sr.ht/~bouncepaw/mycomarkup/v5/links"
"git.sr.ht/~bouncepaw/mycomarkup/v5/mycocontext"
"git.sr.ht/~bouncepaw/mycomarkup/v5/tools"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/mycoopts"
)
// UpdateBacklinksAfterEdit is a creation/editing hook for backlinks index

View File

@ -23,8 +23,8 @@ package categories
import "sync"
// listOfCategories returns unsorted names of all categories.
func listOfCategories() (categoryList []string) {
// ListOfCategories returns unsorted names of all categories.
func ListOfCategories() (categoryList []string) {
mutex.RLock()
for cat, _ := range categoryToHyphae {
categoryList = append(categoryList, cat)
@ -44,8 +44,8 @@ func CategoriesWithHypha(hyphaName string) (categoryList []string) {
}
}
// hyphaeInCategory returns what hyphae are in the category. If the returned slice is empty, the category does not exist, and vice versa. The category name must be canonical.
func hyphaeInCategory(catName string) (hyphaList []string) {
// HyphaeInCategory returns what hyphae are in the category. If the returned slice is empty, the category does not exist, and vice versa. The category name must be canonical.
func HyphaeInCategory(catName string) (hyphaList []string) {
mutex.RLock()
defer mutex.RUnlock()
if node, ok := categoryToHyphae[catName]; ok {
@ -75,8 +75,8 @@ func AddHyphaToCategory(hyphaName, catName string) {
go saveToDisk()
}
// removeHyphaFromCategory removes the hypha from the category and updates the records on the disk. If the hypha is not in the category, nothing happens. Pass canonical names.
func removeHyphaFromCategory(hyphaName, catName string) {
// RemoveHyphaFromCategory removes the hypha from the category and updates the records on the disk. If the hypha is not in the category, nothing happens. Pass canonical names.
func RemoveHyphaFromCategory(hyphaName, catName string) {
mutex.Lock()
if node, ok := hyphaToCategories[hyphaName]; ok {
node.removeCategory(catName)

View File

@ -2,25 +2,25 @@ package categories
import (
"encoding/json"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/util"
"golang.org/x/exp/slices"
"log"
"log/slog"
"os"
"slices"
"sort"
"sync"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/util"
)
var categoryToHyphae = map[string]*categoryNode{}
var hyphaToCategories = map[string]*hyphaNode{}
// Init initializes the category system. Call it after the Structure is initialized. This function might terminate the program in case of a bad mood or filesystem faults.
func Init() {
var (
record, err = readCategoriesFromDisk()
)
func Init() error {
record, err := readCategoriesFromDisk()
if err != nil {
log.Fatalln(err)
slog.Error("Failed to read categories from disk", "err", err)
return err
}
for _, cat := range record.Categories {
@ -45,7 +45,8 @@ func Init() {
}
}
log.Println("Found", len(categoryToHyphae), "categories")
slog.Info("Indexed categories", "n", len(categoryToHyphae))
return nil
}
type categoryNode struct {
@ -122,9 +123,7 @@ func readCategoriesFromDisk() (catFileRecord, error) {
var fileMutex sync.Mutex
func saveToDisk() {
var (
record catFileRecord
)
var record catFileRecord
for name, node := range categoryToHyphae {
record.Categories = append(record.Categories, catRecord{
Name: name,
@ -133,13 +132,16 @@ func saveToDisk() {
}
data, err := json.MarshalIndent(record, "", "\t")
if err != nil {
log.Fatalln(err) // Better fail now, than later
slog.Error("Failed to marshal categories record", "err", err)
os.Exit(1) // Better fail now, than later
}
// TODO: make the data safer somehow?? Back it up before overwriting?
fileMutex.Lock()
err = os.WriteFile(files.CategoriesJSON(), data, 0666)
if err != nil {
log.Fatalln(err)
slog.Error("Failed to write categories.json", "err", err)
os.Exit(1)
}
fileMutex.Unlock()
}

View File

@ -109,22 +109,22 @@ type Telegram struct {
// configuration. Call it sometime during the initialization.
func ReadConfigFile(path string) error {
cfg := &Config{
WikiName: "Mycorrhiza Wiki",
NaviTitleIcon: "🍄",
WikiName: "Lyxi's Vault",
NaviTitleIcon: "🦇",
Hyphae: Hyphae{
HomeHypha: "home",
UserHypha: "u",
HeaderLinksHypha: "",
HeaderLinksHypha: "u/alyxbatte/header",
RedirectionCategory: "redirection",
},
Network: Network{
ListenAddr: "127.0.0.1:1737",
ListenAddr: "0.0.0.0:1737",
URL: "",
},
Authorization: Authorization{
UseAuth: false,
UseAuth: true,
AllowRegistration: false,
RegistrationLimit: 0,
RegistrationLimit: 1,
Locked: false,
UseWhiteList: false,
WhiteList: []string{},

View File

@ -6,8 +6,8 @@ import (
"os"
"path/filepath"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/static"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/web/static"
)
var paths struct {
@ -25,7 +25,7 @@ var paths struct {
// A separate function is needed to easily know where a general storage path is
// needed rather than a concrete Git or the whole wiki storage path, so that we
// could easily refactor things later if we'll ever support different storages.
func HyphaeDir() string { return paths.gitRepo }
func HyphaeDir() string { return filepath.ToSlash(paths.gitRepo) }
// GitRepo returns the path to the Git repository of the wiki.
func GitRepo() string { return paths.gitRepo }

View File

@ -1,9 +1,10 @@
package hyphae
import (
"github.com/bouncepaw/mycorrhiza/util"
"os"
"path/filepath"
"github.com/bouncepaw/mycorrhiza/util"
)
// ExistingHypha is not EmptyHypha. *MediaHypha and *TextualHypha implement this interface.

View File

@ -1,11 +1,11 @@
package hyphae
import (
"log"
"log/slog"
"os"
"path/filepath"
"github.com/bouncepaw/mycorrhiza/mimetype"
"github.com/bouncepaw/mycorrhiza/internal/mimetype"
)
// Index finds all hypha files in the full `path` and saves them to the hypha storage.
@ -27,11 +27,10 @@ func Index(path string) {
switch foundHypha := foundHypha.(type) {
case *TextualHypha: // conflict! overwrite
storedHypha.mycoFilePath = foundHypha.mycoFilePath
log.Printf(
"File collision for hypha %s, using %s rather than %s\n",
foundHypha.CanonicalName(),
foundHypha.TextFilePath(),
storedHypha.TextFilePath(),
slog.Info("File collision",
"hypha", foundHypha.CanonicalName(),
"usingFile", foundHypha.TextFilePath(),
"insteadOf", storedHypha.TextFilePath(),
)
case *MediaHypha: // no conflict
Insert(ExtendTextualToMedia(storedHypha, foundHypha.mediaFilePath))
@ -43,16 +42,16 @@ func Index(path string) {
storedHypha.mycoFilePath = foundHypha.mycoFilePath
case *MediaHypha: // conflict! overwrite
storedHypha.mediaFilePath = foundHypha.mediaFilePath
log.Printf(
"File collision for hypha %s, using %s rather than %s\n",
foundHypha.CanonicalName(),
foundHypha.MediaFilePath(),
storedHypha.MediaFilePath(),
slog.Info("File collision",
"hypha", foundHypha.CanonicalName(),
"usingFile", foundHypha.MediaFilePath(),
"insteadOf", storedHypha.MediaFilePath(),
)
}
}
}
log.Println("Indexed", Count(), "hyphae")
slog.Info("Indexed hyphae", "n", Count())
}
// indexHelper finds all hypha files in the full `path` and sends them to the
@ -61,7 +60,8 @@ func Index(path string) {
func indexHelper(path string, nestLevel uint, ch chan ExistingHypha) {
nodes, err := os.ReadDir(path)
if err != nil {
log.Fatal(err)
slog.Error("Failed to read directory", "path", path, "err", err)
os.Exit(1)
}
for _, node := range nodes {
@ -73,7 +73,7 @@ func indexHelper(path string, nestLevel uint, ch chan ExistingHypha) {
}
var (
hyphaPartPath = filepath.Join(path, node.Name())
hyphaPartPath = filepath.ToSlash(filepath.Join(path, node.Name()))
hyphaName, isText, skip = mimetype.DataFromFilename(hyphaPartPath)
)
if !skip {

View File

@ -1,9 +1,10 @@
package hyphae
import (
"github.com/bouncepaw/mycorrhiza/files"
"path/filepath"
"sync"
"github.com/bouncepaw/mycorrhiza/internal/files"
)
type MediaHypha struct {

View File

@ -1,11 +1,13 @@
package migration
import (
"git.sr.ht/~bouncepaw/mycomarkup/v5/tools"
"github.com/bouncepaw/mycorrhiza/files"
"io/ioutil"
"log"
"log/slog"
"os"
"github.com/bouncepaw/mycorrhiza/internal/files"
"git.sr.ht/~bouncepaw/mycomarkup/v5/tools"
)
var headingMarkerPath string
@ -29,7 +31,8 @@ func shouldMigrateHeadings() bool {
return true
}
if err != nil {
log.Fatalln("When checking if heading migration is needed:", err.Error())
slog.Error("Failed to check if heading migration is needed", "err", err)
os.Exit(1)
}
_ = file.Close()
return false
@ -42,6 +45,7 @@ func createHeadingMarker() {
0766,
)
if err != nil {
log.Fatalln(err)
slog.Error("Failed to create heading migration marker", "err", err)
os.Exit(1)
}
}

View File

@ -8,13 +8,14 @@
package migration
import (
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/user"
"io"
"log"
"log/slog"
"os"
"strings"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/user"
)
func genericLineMigrator(
@ -36,7 +37,8 @@ func genericLineMigrator(
file, err := os.OpenFile(hypha.TextFilePath(), os.O_RDWR, 0766)
if err != nil {
hop.WithErrAbort(err)
log.Fatal("Something went wrong when opening ", hypha.TextFilePath(), ": ", err.Error())
slog.Error("Failed to open text part file", "path", hypha.TextFilePath(), "err", err)
os.Exit(1)
}
var buf strings.Builder
@ -44,7 +46,7 @@ func genericLineMigrator(
if err != nil {
hop.WithErrAbort(err)
_ = file.Close()
log.Fatal("Something went wrong when reading ", hypha.TextFilePath(), ": ", err.Error())
slog.Error("Failed to read text part file", "path", hypha.TextFilePath(), "err", err)
}
var (
@ -58,21 +60,24 @@ func genericLineMigrator(
if err != nil {
hop.WithErrAbort(err)
_ = file.Close()
log.Fatal("Something went wrong when truncating ", hypha.TextFilePath(), ": ", err.Error())
slog.Error("Failed to truncate text part file", "path", hypha.TextFilePath(), "err", err)
os.Exit(1)
}
_, err = file.Seek(0, 0)
if err != nil {
hop.WithErrAbort(err)
_ = file.Close()
log.Fatal("Something went wrong when seeking in ", hypha.TextFilePath(), ": ", err.Error())
slog.Error("Failed to seek in text part file", "path", hypha.TextFilePath(), "err", err)
os.Exit(1)
}
_, err = file.WriteString(newText)
if err != nil {
hop.WithErrAbort(err)
_ = file.Close()
log.Fatal("Something went wrong when writing to ", hypha.TextFilePath(), ": ", err.Error())
slog.Error("Failed to write to text part file", "path", hypha.TextFilePath(), "err", err)
os.Exit(1)
}
}
_ = file.Close()
@ -84,8 +89,8 @@ func genericLineMigrator(
}
if hop.WithFiles(mycoFiles...).Apply().HasErrors() {
log.Fatal(commitErrorMessage, hop.FirstErrorText())
slog.Error(commitErrorMessage + hop.FirstErrorText())
}
log.Println("Migrated", len(mycoFiles), "Mycomarkup documents")
slog.Info("Migrated Mycomarkup documents", "n", len(mycoFiles))
}

View File

@ -1,11 +1,13 @@
package migration
import (
"git.sr.ht/~bouncepaw/mycomarkup/v5/tools"
"github.com/bouncepaw/mycorrhiza/files"
"io/ioutil"
"log"
"log/slog"
"os"
"github.com/bouncepaw/mycorrhiza/internal/files"
"git.sr.ht/~bouncepaw/mycomarkup/v5/tools"
)
var rocketMarkerPath string
@ -33,7 +35,8 @@ func shouldMigrateRockets() bool {
return true
}
if err != nil {
log.Fatalln("When checking if rocket migration is needed:", err.Error())
slog.Error("Failed to check if rocket migration is needed", "err", err)
os.Exit(1)
}
_ = file.Close()
return false
@ -46,6 +49,7 @@ func createRocketLinkMarker() {
0766,
)
if err != nil {
log.Fatalln(err)
slog.Error("Failed to create rocket link migration marker")
os.Exit(1)
}
}

View File

@ -40,20 +40,33 @@ func DataFromFilename(fullPath string) (name string, isText bool, skip bool) {
var mapMime2Ext = map[string]string{
"application/octet-stream": "bin",
"image/jpeg": "jpg",
"image/gif": "gif",
"image/png": "png",
"image/webp": "webp",
"image/svg+xml": "svg",
"image/x-icon": "ico",
"application/ogg": "ogg",
"video/webm": "webm",
"audio/mp3": "mp3",
"video/mp4": "mp4",
"image/jpeg": "jpg",
"image/gif": "gif",
"image/png": "png",
"image/webp": "webp",
"image/svg+xml": "svg",
"image/x-icon": "ico",
"application/ogg": "ogg",
"video/webm": "webm",
"audio/mp3": "mp3",
"audio/mpeg": "mp3",
"audio/mpeg3": "mp3",
"video/mp4": "mp4",
"audio/flac": "flac",
"audio/wav": "wav",
"audio/vnd.wav": "wav",
"audio/vnd.wave": "wav",
"audio/wave": "wav",
"audio/x-pn-wav": "wav",
"audio/x-wav": "wav",
}
var mapExt2Mime = map[string]string{
".bin": "application/octet-stream",
".bin": "application/octet-stream",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
@ -61,8 +74,12 @@ var mapExt2Mime = map[string]string{
".webp": "image/webp",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".ogg": "application/ogg",
".webm": "video/webm",
".mp3": "audio/mp3",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".flac": "audio/flac",
"wav": "audio/wav",
}

View File

@ -3,9 +3,9 @@ package shroom
import (
"errors"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/user"
"github.com/bouncepaw/mycorrhiza/l18n"
"github.com/bouncepaw/mycorrhiza/user"
)
// TODO: get rid of this abomination

View File

@ -2,11 +2,12 @@ package shroom
import (
"fmt"
"github.com/bouncepaw/mycorrhiza/backlinks"
"github.com/bouncepaw/mycorrhiza/categories"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
"github.com/bouncepaw/mycorrhiza/internal/categories"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/user"
)
// Delete deletes the hypha and makes a history record about that.

View File

@ -1,14 +1,16 @@
package shroom
import (
"os"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/mycoopts"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
"git.sr.ht/~bouncepaw/mycomarkup/v5"
"git.sr.ht/~bouncepaw/mycomarkup/v5/blocks"
"git.sr.ht/~bouncepaw/mycomarkup/v5/mycocontext"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/mycoopts"
"github.com/bouncepaw/mycorrhiza/viewutil"
"os"
)
// SetHeaderLinks initializes header links by reading the configured hypha, if there is any, or resorting to default values.

36
internal/shroom/log.go Normal file
View File

@ -0,0 +1,36 @@
package shroom
import (
"log/slog"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/user"
)
func rejectRenameLog(h hyphae.Hypha, u *user.User, errmsg string) {
slog.Info("Reject rename",
"hyphaName", h.CanonicalName(),
"username", u.Name,
"errmsg", errmsg)
}
func rejectRemoveMediaLog(h hyphae.Hypha, u *user.User, errmsg string) {
slog.Info("Reject remove media",
"hyphaName", h.CanonicalName(),
"username", u.Name,
"errmsg", errmsg)
}
func rejectEditLog(h hyphae.Hypha, u *user.User, errmsg string) {
slog.Info("Reject edit",
"hyphaName", h.CanonicalName(),
"username", u.Name,
"errmsg", errmsg)
}
func rejectUploadMediaLog(h hyphae.Hypha, u *user.User, errmsg string) {
slog.Info("Reject upload media",
"hyphaName", h.CanonicalName(),
"username", u.Name,
"errmsg", errmsg)
}

View File

@ -3,16 +3,18 @@ package shroom
import (
"errors"
"fmt"
"github.com/bouncepaw/mycorrhiza/backlinks"
"github.com/bouncepaw/mycorrhiza/categories"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/files"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
"github.com/bouncepaw/mycorrhiza/internal/categories"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/user"
"github.com/bouncepaw/mycorrhiza/util"
)
@ -41,8 +43,10 @@ func Rename(oldHypha hyphae.ExistingHypha, newName string, recursive bool, leave
var (
re = regexp.MustCompile(`(?i)` + oldHypha.CanonicalName())
replaceName = func(str string) string {
// Can we drop that util.CanonicalName?
return re.ReplaceAllString(util.CanonicalName(str), newName)
namepart := strings.TrimPrefix(str, files.HyphaeDir())
// Can we drop that util.CanonicalName?:
replaced := re.ReplaceAllString(util.CanonicalName(namepart), newName)
return path.Join(files.HyphaeDir(), replaced)
}
hyphaeToRename = findHyphaeToRename(oldHypha, recursive)
renameMap, err = renamingPairs(hyphaeToRename, replaceName)
@ -75,7 +79,7 @@ func Rename(oldHypha hyphae.ExistingHypha, newName string, recursive bool, leave
for _, h := range hyphaeToRename {
var (
oldName = h.CanonicalName()
newName = replaceName(oldName)
newName = re.ReplaceAllString(oldName, newName)
)
hyphae.RenameHyphaTo(h, newName, replaceName)
backlinks.UpdateBacklinksAfterRename(h, oldName)
@ -93,9 +97,13 @@ func Rename(oldHypha hyphae.ExistingHypha, newName string, recursive bool, leave
return nil
}
const redirectionTemplate = `=> %[1]s | 👁 %[2]s
<= %[1]s | full
`
func leaveRedirection(oldName, newName string, hop *history.Op) error {
var (
text = fmt.Sprintf("=> %s | ✏️ %s\n", newName, util.BeautifulName(newName))
text = fmt.Sprintf(redirectionTemplate, newName, util.BeautifulName(newName))
emptyHypha = hyphae.ByName(oldName)
)
switch emptyHypha := emptyHypha.(type) {

View File

@ -3,7 +3,7 @@ package shroom
import (
"strings"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/util"
)

View File

@ -4,8 +4,8 @@ import (
"fmt"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/user"
)
// RemoveMedia removes media from the media hypha and makes a history record about that. If it only had media, the hypha will be deleted. If it also had text, the hypha will become textual.

View File

@ -4,17 +4,19 @@ import (
"bytes"
"errors"
"fmt"
"github.com/bouncepaw/mycorrhiza/backlinks"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/mimetype"
"github.com/bouncepaw/mycorrhiza/user"
"io"
"log"
"log/slog"
"mime/multipart"
"os"
"path/filepath"
"strings"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/mimetype"
"github.com/bouncepaw/mycorrhiza/internal/user"
)
func historyMessageForTextUpload(h hyphae.Hypha, userMessage string) string {
@ -78,7 +80,9 @@ func UploadText(h hyphae.Hypha, data []byte, userMessage string, u *user.User) e
switch h := h.(type) {
case *hyphae.EmptyHypha:
H := hyphae.ExtendEmptyToTextual(h, filepath.Join(files.HyphaeDir(), h.CanonicalName()+".myco"))
parts := []string{files.HyphaeDir()}
parts = append(parts, strings.Split(h.CanonicalName()+".myco", "\\")...)
H := hyphae.ExtendEmptyToTextual(h, filepath.Join(parts...))
err := writeTextToDisk(H, data, hop)
if err != nil {
@ -139,7 +143,8 @@ func writeMediaToDisk(h hyphae.Hypha, mime string, data []byte) (string, error)
var (
ext = mimetype.ToExtension(mime)
// That's where the file will go
uploadedFilePath = filepath.Join(files.HyphaeDir(), h.CanonicalName()+ext)
uploadedFilePath = filepath.Join(append([]string{files.HyphaeDir()}, strings.Split(h.CanonicalName()+ext, "\\")...)...)
)
if err := os.MkdirAll(filepath.Dir(uploadedFilePath), 0777); err != nil {
@ -196,7 +201,7 @@ func UploadBinary(h hyphae.Hypha, mime string, file multipart.File, u *user.User
if err := history.Rename(prevFilePath, uploadedFilePath); err != nil {
return err
}
log.Printf("Move %s to %s\n", prevFilePath, uploadedFilePath)
slog.Info("Move file", "from", prevFilePath, "to", uploadedFilePath)
h.SetMediaFilePath(uploadedFilePath)
}
}

View File

@ -1,14 +1,19 @@
package tree
import (
"github.com/bouncepaw/mycorrhiza/hyphae"
"fmt"
"html/template"
"io"
"path"
"sort"
"strings"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/util"
)
// Tree returns the subhypha matrix as HTML and names of the next and previous hyphae (or empty strings).
func Tree(hyphaName string) (childrenHTML, prev, next string) {
func Tree(hyphaName string) (childrenHTML template.HTML, prev, next string) {
var (
root = child{hyphaName, true, make([]child, 0)}
descendantPrefix = hyphaName + "/"
@ -44,6 +49,41 @@ type child struct {
children []child
}
/*
Subhyphae links are recursive. It may end up looking like that if drawn with
pseudographics:
Foo The presented hyphae are ./foo and ./foo/bar
Bar
*/
func childHTML(c *child, w io.Writer) {
sort.Slice(c.children, func(i, j int) bool {
return c.children[i].name < c.children[j].name
})
_, _ = io.WriteString(w, "<li class=\"subhyphae__entry\">\n<a class=\"subhyphae__link")
if !c.exists {
_, _ = io.WriteString(w, " wikilink_new")
}
_, _ = io.WriteString(w, fmt.Sprintf(
"\" href=\"/hypha/%s\">%s</a>\n",
c.name,
util.BeautifulName(path.Base(c.name)),
))
if len(c.children) > 0 {
_, _ = io.WriteString(w, "<ul>\n")
for _, child := range c.children {
childHTML(&child, w)
}
_, _ = io.WriteString(w, "</ul>\n")
}
_, _ = io.WriteString(w, "</li>\n")
}
func addHyphaToChild(hyphaName, subPath string, child *child) {
// when hyphaName = "root/a/b", subPath = "a/b", and child.name = "root"
// addHyphaToChild("root/a/b", "b", child{"root/a"})
@ -78,12 +118,13 @@ func findOrCreateSubchild(name string, baseChild *child) *child {
return &baseChild.children[len(baseChild.children)-1]
}
func subhyphaeMatrix(children []child) (html string) {
func subhyphaeMatrix(children []child) template.HTML {
sort.Slice(children, func(i, j int) bool {
return children[i].name < children[j].name
})
var buf strings.Builder
for _, child := range children {
html += childHTML(&child)
childHTML(&child, &buf)
}
return html
return template.HTML(buf.String())
}

View File

@ -3,11 +3,11 @@ package user
import (
"encoding/json"
"errors"
"log"
"log/slog"
"os"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/util"
)
@ -32,19 +32,23 @@ func usersFromFile() []*User {
return users
}
if err != nil {
log.Fatal(err)
slog.Error("Failed to read users.json", "err", err)
os.Exit(1)
}
err = json.Unmarshal(contents, &users)
if err != nil {
log.Fatal(err)
slog.Error("Failed to unmarshal users.json contents", "err", err)
os.Exit(1)
}
for _, u := range users {
u.Name = util.CanonicalName(u.Name)
if u.Source == "" {
u.Source = "local"
}
}
log.Println("Found", len(users), "users")
slog.Info("Indexed users", "n", len(users))
return users
}
@ -63,20 +67,22 @@ func readTokensToUsers() {
return
}
if err != nil {
log.Fatal(err)
slog.Error("Failed to read tokens.json", "err", err)
os.Exit(1)
}
var tmp map[string]string
err = json.Unmarshal(contents, &tmp)
if err != nil {
log.Fatal(err)
slog.Error("Failed to unmarshal tokens.json contents", "err", err)
os.Exit(1)
}
for token, username := range tmp {
tokens.Store(token, username)
// commenceSession(username, token)
}
log.Println("Found", len(tmp), "active sessions")
slog.Info("Indexed active sessions", "n", len(tmp))
}
// SaveUserDatabase stores current user credentials into JSON file by configured path.
@ -94,13 +100,13 @@ func dumpUserCredentials() error {
blob, err := json.MarshalIndent(userList, "", "\t")
if err != nil {
log.Println(err)
slog.Error("Failed to marshal users.json", "err", err)
return err
}
err = os.WriteFile(files.UserCredentialsJSON(), blob, 0666)
if err != nil {
log.Println(err)
slog.Error("Failed to write users.json", "err", err)
return err
}
@ -119,11 +125,11 @@ func dumpTokens() {
blob, err := json.MarshalIndent(tmp, "", "\t")
if err != nil {
log.Println(err)
slog.Error("Failed to marshal tokens.json", "err", err)
return
}
err = os.WriteFile(files.TokensJSON(), blob, 0666)
if err != nil {
log.Println("an error occurred in dumpTokens function:", err)
slog.Error("Failed to write tokens.json", "err", err)
}
}

View File

@ -6,16 +6,16 @@ import (
"encoding/hex"
"errors"
"fmt"
"log"
"log/slog"
"net/http"
"sort"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/util"
"golang.org/x/crypto/bcrypt"
)
// CanProceed returns `true` if the user in `rq` has enough rights to access `route`.
@ -59,7 +59,7 @@ func Register(username, password, group, source string, force bool) error {
return fmt.Errorf("username %s is already taken", username)
case !force && cfg.RegistrationLimit > 0 && Count() >= cfg.RegistrationLimit:
return fmt.Errorf("reached the limit of registered users (%d)", cfg.RegistrationLimit)
case password == "":
case password == "" && source != "telegram":
return fmt.Errorf("password must not be empty")
}
@ -79,6 +79,11 @@ func Register(username, password, group, source string, force bool) error {
return SaveUserDatabase()
}
var (
ErrUnknownUsername = errors.New("unknown username")
ErrWrongPassword = errors.New("wrong password")
)
// LoginDataHTTP logs such user in and returns string representation of an error if there is any.
//
// The HTTP parameters are used for setting header status (bad request, if it is bad) and saving a cookie.
@ -86,17 +91,17 @@ func LoginDataHTTP(w http.ResponseWriter, username, password string) error {
w.Header().Set("Content-Type", "text/html;charset=utf-8")
if !HasUsername(username) {
w.WriteHeader(http.StatusBadRequest)
log.Println("Unknown username", username, "was entered")
return errors.New("unknown username")
slog.Info("Unknown username entered", "username", username)
return ErrUnknownUsername
}
if !CredentialsOK(username, password) {
w.WriteHeader(http.StatusBadRequest)
log.Println("A wrong password was entered for username", username)
return errors.New("wrong password")
slog.Info("Wrong password entered", "username", username)
return ErrWrongPassword
}
token, err := AddSession(username)
if err != nil {
log.Println(err)
slog.Error("Failed to add session", "username", username, "err", err)
w.WriteHeader(http.StatusBadRequest)
return err
}
@ -109,7 +114,7 @@ func AddSession(username string) (string, error) {
token, err := util.RandomString(16)
if err == nil {
commenceSession(username, token)
log.Println("New token for", username, "is", token)
slog.Info("Added session", "username", username)
}
return token, err
}

View File

@ -1,12 +1,14 @@
package user
import (
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"golang.org/x/crypto/bcrypt"
)
@ -36,8 +38,8 @@ var minimalRights = map[string]int{
"upload-binary": 1,
"rename": 1,
"upload-text": 1,
"add-to-category": 2,
"remove-from-category": 2,
"add-to-category": 1,
"remove-from-category": 1,
"remove-media": 2,
"update-header-links": 3,
"delete": 3,
@ -135,6 +137,20 @@ func (user *User) ShowLockMaybe(w http.ResponseWriter, rq *http.Request) bool {
return false
}
// Sets a new password for the user.
func (user *User) ChangePassword(password string) error {
if user.Source != "local" {
return fmt.Errorf("Only local users can change their passwords.")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
user.Password = string(hash)
return SaveUserDatabase()
}
// IsValidUsername checks if the given username is valid.
func IsValidUsername(username string) bool {
for _, r := range username {

View File

@ -1,6 +1,9 @@
package user
import "sync"
import (
"sort"
"sync"
)
var users sync.Map
var tokens sync.Map
@ -9,7 +12,7 @@ var tokens sync.Map
func YieldUsers() chan *User {
ch := make(chan *User)
go func(ch chan *User) {
users.Range(func(_, v interface{}) bool {
users.Range(func(_, v any) bool {
ch <- v.(*User)
return true
})
@ -38,6 +41,15 @@ func Count() (i uint64) {
return i
}
func HasAnyAdmins() bool {
for u := range YieldUsers() {
if u.Group == "admin" {
return true
}
}
return false
}
// HasUsername checks whether the desired user exists
func HasUsername(username string) bool {
_, has := users.Load(username)
@ -90,3 +102,24 @@ func terminateSession(token string) {
tokens.Delete(token)
dumpTokens()
}
func UsersInGroups() (admins []string, moderators []string, editors []string, readers []string) {
for u := range YieldUsers() {
switch u.Group {
// What if we place the users into sorted slices?
case "admin":
admins = append(admins, u.Name)
case "moderator":
moderators = append(moderators, u.Name)
case "editor", "trusted":
editors = append(editors, u.Name)
case "reader":
readers = append(readers, u.Name)
}
}
sort.Strings(admins)
sort.Strings(moderators)
sort.Strings(editors)
sort.Strings(readers)
return
}

View File

@ -15,7 +15,7 @@ var Long string
// Short is the human-friendly x.y.z part of the long version string.
var Short string
var versionRegexp = regexp.MustCompile(`This is documentation for \*\*Mycorrhiza Wiki\*\* (.*).`)
var versionRegexp = regexp.MustCompile(`This is documentation for Mycorrhiza Wiki (.*)\. `)
func init() {
if b, err := help.Get("en"); err == nil {

View File

@ -4,29 +4,62 @@ package interwiki
import (
"encoding/json"
"errors"
"git.sr.ht/~bouncepaw/mycomarkup/v5/options"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/util"
"log"
"log/slog"
"os"
"sync"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/util"
"git.sr.ht/~bouncepaw/mycomarkup/v5/options"
)
func Init() {
var (
record, err = readInterwiki()
)
func Init() error {
record, err := readInterwiki()
if err != nil {
log.Fatalln(err)
slog.Error("Failed to read interwiki", "err", err)
return err
}
for _, wiki := range record {
wiki := wiki // This line is required
wiki.canonize()
if err := wiki.canonize(); err != nil {
return err
}
if err := addEntry(&wiki); err != nil {
log.Fatalln(err.Error())
slog.Error("Failed to add interwiki entry", "err", err)
return err
}
}
log.Printf("Loaded %d interwiki entries\n", len(listOfEntries))
slog.Info("Indexed interwiki map", "n", len(listOfEntries))
return nil
}
func dropEmptyStrings(ss []string) (clean []string) {
for _, s := range ss {
if s != "" {
clean = append(clean, s)
}
}
return clean
}
// difference returns the elements in `a` that aren't in `b`.
// Taken from https://stackoverflow.com/a/45428032
// CC BY-SA 4.0, no changes made
func difference(a, b []string) []string {
mb := make(map[string]struct{}, len(b))
for _, x := range b {
mb[x] = struct{}{}
}
var diff []string
for _, x := range a {
if _, found := mb[x]; !found {
diff = append(diff, x)
}
}
return diff
}
func areNamesFree(names []string) (bool, string) {
@ -40,30 +73,68 @@ func areNamesFree(names []string) (bool, string) {
var mutex sync.Mutex
func addEntry(wiki *Wiki) error {
func replaceEntry(oldWiki *Wiki, newWiki *Wiki) error {
diff := difference(
append(newWiki.Aliases, newWiki.Name),
append(oldWiki.Aliases, oldWiki.Name),
)
if ok, name := areNamesFree(diff); !ok {
return errors.New(name)
}
deleteEntry(oldWiki)
return addEntry(newWiki)
}
func deleteEntry(wiki *Wiki) {
mutex.Lock()
defer mutex.Unlock()
var (
// non-empty names only
names = func(names []string) []string {
var result []string
for _, name := range names {
if name != "" {
result = append(result, name)
}
// I'm being fancy here. Come on, the code here is already a mess.
// Let me have some fun.
var wg sync.WaitGroup
wg.Add(2)
go func() {
names := append(wiki.Aliases, wiki.Name)
for _, name := range names {
name := name // I guess we need that
delete(entriesByName, name)
}
wg.Done()
}()
go func() {
for i, w := range listOfEntries {
i, w := i, w
if w.Name == wiki.Name {
// Drop ith element.
listOfEntries[i] = listOfEntries[len(listOfEntries)-1]
listOfEntries = listOfEntries[:len(listOfEntries)-1]
break
}
return result
}(append(wiki.Aliases, wiki.Name))
}
wg.Done()
}()
wg.Wait()
}
// TODO: There is something clearly wrong with error-returning in this function.
func addEntry(wiki *Wiki) error {
mutex.Lock()
defer mutex.Unlock()
wiki.Aliases = dropEmptyStrings(wiki.Aliases)
var (
names = append(wiki.Aliases, wiki.Name)
ok, name = areNamesFree(names)
)
if !ok {
log.Printf("There are multiple uses of the same name %s\n", name)
switch {
case !ok:
slog.Error("There are multiple uses of the same name", "name", name)
return errors.New(name)
}
if len(names) == 0 {
log.Println("No names passed for a new interwiki entry")
// There is something clearly wrong with error-returning in this function.
case len(names) == 0:
slog.Error("No names passed for a new interwiki entry")
return errors.New("")
}
@ -112,10 +183,13 @@ func readInterwiki() ([]Wiki, error) {
func saveInterwikiJson() {
// Trust me, wiki crashing when an admin takes an administrative action totally makes sense.
if data, err := json.MarshalIndent(listOfEntries, "", "\t"); err != nil {
log.Fatalln(err)
slog.Error("Failed to marshal interwiki entries", "err", err)
os.Exit(1)
} else if err = os.WriteFile(files.InterwikiJSON(), data, 0666); err != nil {
log.Fatalln(err)
} else {
log.Println("Saved interwiki.json")
slog.Error("Failed to write interwiki.json", "err", err)
os.Exit(1)
}
slog.Info("Saved interwiki.json")
}

View File

@ -5,6 +5,8 @@
{{define "aliases (,)"}}Aliases (separated by commas):{{end}}
{{define "engine"}}Engine:{{end}}
{{define "engine/mycorrhiza"}}Mycorrhiza{{end}}
{{define "engine/betula"}}Betula{{end}}
{{define "engine/agora"}}Agora{{end}}
{{define "engine/generic"}}Generic (any website){{end}}
{{define "url"}}URL{{end}}
{{define "link href format"}}Link href attribute format string:{{end}}
@ -53,7 +55,7 @@
<form method="post" action="/interwiki/modify-entry/{{.Name}}">
<p>
<label for="name{{$i}}" class="required-field">{{template "name"}}</label>
<input type="text" id="name" name="name{{$i}}" required
<input type="text" id="name{{$i}}" name="name" required
value="{{.Name}}">
</p>
<p>
@ -70,7 +72,8 @@
<label for="engine{{$i}}" class="required-field">{{template "engine"}}</label>
<select name="engine" id="engine{{$i}}" required>
<option value="mycorrhiza" {{if eq .Engine "mycorrhiza"}}selected{{end}}>{{template "engine/mycorrhiza"}} 🍄</option>
<option value="agora" {{if eq .Engine "agora"}}selected{{end}}>Agora ἀ</option>
<option value="betula" {{if eq .Engine "betula"}}selected{{end}}>{{template "engine/betula"}} 🌳</option>
<option value="agora" {{if eq .Engine "agora"}}selected{{end}}>{{template "engine/agora"}} ἀ</option>
<option value="generic" {{if eq .Engine "generic"}}selected{{end}}>{{template "engine/generic"}}</option>
</select>
</p>
@ -109,7 +112,8 @@
<label for="engine" class="required-field">{{template "engine"}}</label>
<select name="engine" id="engine" required>
<option value="mycorrhiza">{{template "engine/mycorrhiza"}} 🍄</option>
<option value="agora">Agora ἀ</option>
<option value="betula">{{template "engine/betula"}} 🌳</option>
<option value="agora">{{template "engine/agora"}} ἀ</option>
<option value="generic">{{template "engine/generic"}}</option>
</select>
</p>
@ -123,7 +127,7 @@
<input type="url" id="img-src-format" name="img-src-format"
placeholder="https://wiki.example.org/media/{NAME}">
</p>
<input type="submit" class="btn">
<input type="submit" class="btn" value="Add entry">
</form>
{{end}}

View File

@ -2,7 +2,7 @@
<main class="main-width">
<h1>{{block "heading" .}}Name taken{{end}}</h1>
<p>{{block "tip" .TakenName}}Name <kbd>{{.}}</kbd> is already taken, please choose a different one.{{end}}</p>
<form method="post" action="/interwiki/add-entry">
<form method="post" action="/interwiki/{{.Action}}">
<p>
<label for="name" class="required-field">Name:</label>
<input type="text" id="name" name="name" required
@ -22,6 +22,7 @@
<label for="engine" class="required-field">Engine:</label>
<select name="engine" id="engine" required>
<option value="mycorrhiza" {{if eq .Engine "mycorrhiza"}}selected{{end}}>Mycorrhiza 🍄</option>
<option value="betula" {{if eq .Engine "betula"}}selected{{end}}>Betula 🌳</option>
<option value="agora" {{if eq .Engine "agora"}}selected{{end}}>Agora ἀ</option>
<option value="generic" {{if eq .Engine "generic"}}selected{{end}}>Generic (any website)</option>
</select>

View File

@ -2,10 +2,13 @@ package interwiki
import (
"embed"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/gorilla/mux"
"log/slog"
"net/http"
"strings"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
"github.com/gorilla/mux"
)
var (
@ -18,6 +21,8 @@ var (
{{define "aliases (,)"}}Псевдонимы (разделённые запятыми):{{end}}
{{define "engine"}}Движок:{{end}}
{{define "engine/mycorrhiza"}}Микориза{{end}}
{{define "engine/betula"}}Бетула{{end}}
{{define "engine/agora"}}Агора{{end}}
{{define "engine/generic"}}Любой сайт{{end}}
{{define "link href format"}}Строка форматирования атрибута href ссылки:{{end}}
{{define "img src format"}}Строка форматирования атрибута src изображения:{{end}}
@ -35,9 +40,10 @@ func InitHandlers(rtr *mux.Router) {
chainNameTaken = viewutil.CopyEnRuWith(fs, "view_name_taken.html", ruTranslation)
rtr.HandleFunc("/interwiki", handlerInterwiki)
rtr.HandleFunc("/interwiki/add-entry", handlerAddEntry).Methods(http.MethodPost)
rtr.HandleFunc("/interwiki/modify-entry/{target}", handlerModifyEntry).Methods(http.MethodPost)
}
func handlerAddEntry(w http.ResponseWriter, rq *http.Request) {
func readInterwikiEntryFromRequest(rq *http.Request) Wiki {
wiki := Wiki{
Name: rq.PostFormValue("name"),
Aliases: strings.Split(rq.PostFormValue("aliases"), ","),
@ -47,8 +53,43 @@ func handlerAddEntry(w http.ResponseWriter, rq *http.Request) {
Engine: WikiEngine(rq.PostFormValue("engine")),
}
wiki.canonize()
return wiki
}
func handlerModifyEntry(w http.ResponseWriter, rq *http.Request) {
var (
oldData *Wiki
ok bool
name = mux.Vars(rq)["target"]
newData = readInterwikiEntryFromRequest(rq)
)
if oldData, ok = entriesByName[name]; !ok {
slog.Info("Could not modify entry",
"name", name,
"reason", "does not exist")
viewutil.HandlerNotFound(w, rq)
return
}
if err := replaceEntry(oldData, &newData); err != nil {
slog.Info("Could not modify entry",
"name", name,
"reason", "one of the proposed aliases or the name is taken",
"err", err)
viewNameTaken(viewutil.MetaFrom(w, rq), oldData, err.Error(), "modify-entry/"+name)
return
}
saveInterwikiJson()
slog.Info("Modified entry", "name", name)
http.Redirect(w, rq, "/interwiki", http.StatusSeeOther)
}
func handlerAddEntry(w http.ResponseWriter, rq *http.Request) {
wiki := readInterwikiEntryFromRequest(rq)
if err := addEntry(&wiki); err != nil {
viewNameTaken(viewutil.MetaFrom(w, rq), &wiki, err.Error())
viewNameTaken(viewutil.MetaFrom(w, rq), &wiki, err.Error(), "add-entry")
return
}
saveInterwikiJson()
@ -59,13 +100,15 @@ type nameTakenData struct {
*viewutil.BaseData
*Wiki
TakenName string
Action string
}
func viewNameTaken(meta viewutil.Meta, wiki *Wiki, takenName string) {
func viewNameTaken(meta viewutil.Meta, wiki *Wiki, takenName, action string) {
viewutil.ExecutePage(meta, chainNameTaken, nameTakenData{
BaseData: &viewutil.BaseData{},
Wiki: wiki,
TakenName: takenName,
Action: action,
})
}

View File

@ -1,9 +1,11 @@
package interwiki
import (
"errors"
"fmt"
"log/slog"
"github.com/bouncepaw/mycorrhiza/util"
"log"
)
// WikiEngine is an enumeration of supported interwiki targets.
@ -11,6 +13,7 @@ type WikiEngine string
const (
Mycorrhiza WikiEngine = "mycorrhiza"
Betula WikiEngine = "betula"
Agora WikiEngine = "agora"
// Generic is any website.
Generic WikiEngine = "generic"
@ -18,7 +21,7 @@ const (
func (we WikiEngine) Valid() bool {
switch we {
case Mycorrhiza, Agora, Generic:
case Mycorrhiza, Betula, Agora, Generic:
return true
}
return false
@ -46,14 +49,20 @@ type Wiki struct {
Engine WikiEngine `json:"engine"`
}
func (w *Wiki) canonize() {
func (w *Wiki) canonize() error {
switch {
case w.Name == "":
log.Fatalln("Cannot have a wiki in the interwiki map with no name")
slog.Error("A site in the interwiki map has no name")
return errors.New("site with no name")
case w.URL == "":
log.Fatalf("Wiki %s has no URL\n", w.Name)
slog.Error("Site in the interwiki map has no URL", "name", w.Name)
return errors.New("site with no URL")
case !w.Engine.Valid():
log.Fatalf("Unknown engine %s for wiki %s\n", w.Engine, w.Name)
slog.Error("Site in the interwiki map has an unknown engine",
"siteName", w.Name,
"engine", w.Engine,
)
return errors.New("unknown engine")
}
w.Name = util.CanonicalName(w.Name)
@ -65,6 +74,8 @@ func (w *Wiki) canonize() {
switch w.Engine {
case Mycorrhiza:
w.LinkHrefFormat = fmt.Sprintf("%s/hypha/{NAME}", w.URL)
case Betula:
w.LinkHrefFormat = fmt.Sprintf("%s/{BETULA-NAME}", w.URL)
case Agora:
w.LinkHrefFormat = fmt.Sprintf("%s/node/{NAME}", w.URL)
default:
@ -80,4 +91,6 @@ func (w *Wiki) canonize() {
w.ImgSrcFormat = fmt.Sprintf("%s/{NAME}", w.URL)
}
}
return nil
}

View File

@ -3,33 +3,29 @@
"password": "Password",
"register_title": "Register",
"register_header": "Register on {{.name}}",
"register_header": "",
"register_button": "Register",
"login_title": "Login",
"login_header": "Log in to {{.name}}",
"login_button": "Log in",
"logout_title": "Logout?",
"logout_title": "",
"logout_header": "Log out?",
"logout_button": "Confirm",
"logout_anon": "You cannot log out because you are not logged in.",
"logout_anon": "",
"lock_title": "Locked",
"password_tip": "The server stores your password in an encrypted form; even administrators cannot read it.",
"cookie_tip": "By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you. You will stay logged in until you log out.",
"telegram_tip": "You can log in using Telegram. It only works if you have set your @username in Telegram and this username is free on this wiki.",
"password_tip": "",
"cookie_tip": "",
"telegram_tip": "",
"noauth": "Authentication is disabled. You can make edits anonymously.",
"noauth": "",
"noregister": "Registrations are currently closed. Administrators can make an account for you by hand; contact them.",
"error_username": "Unknown username.",
"error_password": "Wrong password.",
"error_telegram": "Could not authorize using Telegram.",
"error_username": "",
"error_password": "",
"error_telegram": "",
"go_back": "Go back",
"go_home": "Go home",
"go_home": "",
"go_login": "Go to the login page",
"try_again": "Try again"
}

View File

@ -4,18 +4,6 @@
"title_search": "Search by title",
"admin_panel": "Admin panel",
"edit_link": "Edit text",
"logout_link": "Log out",
"history_link": "View history",
"rename_link": "Rename",
"delete_link": "Delete",
"text_link": "View markup",
"media_link": "Manage media",
"media_link_for_textual": "Turn to media hypha",
"backlinks_link": "{{.n}} backlink%s",
"backlinks_link+one": "",
"backlinks_link+other": "s",
"subhyphae": "Subhyphae",
"random_no_hyphae": "There are no hyphae",
@ -57,9 +45,9 @@
"diff_title": "Diff of {{.name}} at {{.rev}}",
"revision_title": "{{.name}} at {{.rev}}",
"revision_warning": "Please note that viewing media is not supported in history for now.",
"revision_link": "Get Mycomarkup source of this revision",
"revision_title": "",
"revision_warning": "",
"revision_link": "",
"revision_no_text": "This hypha had no text at this revision.",
"about_title": "About {{.name}}",
@ -76,25 +64,6 @@
"media_noaudio": "Your browser does not support audio.",
"media_noaudio_link": "Download audio",
"media_title": "Media of {{.name}}",
"media_empty": "This hypha has no media, you can upload it here.",
"media_tip": "You can manage the hypha's media on this page.",
"media_what_is": "What is media?",
"media_upload": "Upload",
"media_stat": "Stat",
"media_stat_size": "File size:",
"media_size_value": "{{.n}} byte%s",
"media_size_value+one": "",
"media_size_value+other": "s",
"media_stat_mime": "MIME type:",
"media_include": "Include",
"media_include_tip": "This media is an image. To include it in a hypha, use a syntax like this:",
"media_new": "media",
"media_new_tip": "You can upload a new media. Please do not upload too big pictures unless you need to because may not want to wait for big pictures to load.",
"media_remove": "Remove media",
"media_remove_tip": "Please note that you don't have to remove media before uploading a new media.",
"media_remove_button": "Remove media",
"confirm": "Confirm",
"cancel": "Cancel"
}

View File

@ -21,7 +21,7 @@ import (
"encoding/json"
"fmt"
"io/fs"
"log"
"log/slog"
"net/http"
"path/filepath"
"strings"
@ -78,7 +78,7 @@ func init() {
var strings map[string]string
if err := json.Unmarshal(contents, &strings); err != nil {
log.Fatalf("error while parsing %s: %v", path, err)
slog.Error("Failed to unmarshal localization file", "path", path, "err", err)
}
for key, value := range strings {

View File

@ -2,31 +2,31 @@
"username": "Логин",
"password": "Пароль",
"register_title": "Регистрация",
"register_header": "Регистрация на «{{.name}}»",
"register_button": "Зарегистрироваться",
"register_title": "",
"register_header": "",
"register_button": "",
"login_title": "Вход",
"login_header": "Вход в «{{.name}}»",
"login_button": "Войти",
"login_header": "",
"login_button": "",
"logout_title": "Выйти?",
"logout_header": "Выйти?",
"logout_title": "",
"logout_header": "?",
"logout_button": "Подтвердить",
"logout_anon": "Вы не можете выйти, потому что ещё не вошли.",
"logout_anon": "",
"lock_title": "Доступ закрыт",
"lock_title": "",
"password_tip": "Сервер хранит ваш пароль в зашифрованном виде, даже администраторы не смогут его прочесть.",
"cookie_tip": "Отправляя эту форму, вы разрешаете вики хранить cookie в вашем браузере. Это позволит движку связывать ваши правки с вашей учётной записью. Вы будете авторизованы, пока не выйдете из учётной записи.",
"password_tip": "",
"cookie_tip": "",
"telegram_tip": "Вы можете войти с помощью Телеграм. Это сработает, если у вашего профиля есть @имя, и оно не занято в этой вики.",
"noauth": "Аутентификация отключена. Вы можете делать правки анонимно.",
"noauth": "",
"noregister": "Регистрация в текущее время недоступна. Администраторы могут вручную создать вам учётную запись, свяжитесь с ними.",
"error_username": "Неизвестное имя пользователя.",
"error_username": "",
"error_password": "Неверный пароль.",
"error_telegram": "Не удалось авторизоваться через Телеграм.",
"error_telegram": "",
"go_back": "Назад",
"go_home": "Домой",

View File

@ -29,7 +29,7 @@
"bullets": "Маркир. список",
"numbers": "Нумер. список",
"help": "{{.link}} о микоразметке",
"help": "{{.link}} о Микоразметке",
"help_link": "Подробнее",
"selflink": "Ссылка на вас",

View File

@ -8,14 +8,14 @@
"backlinks_heading": "Обратные ссылки на {{.hypha_link}}",
"backlinks_desc": "Ниже перечислены гифы, на которых есть ссылка на эту гифу, трансклюзия этой гифы или эта гифа вставлена как изображение.",
"edit_link": "Редактировать",
"logout_link": "Выйти",
"history_link": "История",
"rename_link": "Переименовать",
"delete_link": "Удалить",
"text_link": "Посмотреть разметку",
"media_link": "Медиа",
"media_link_for_textual": "Превратить в медиа-гифу",
"edit_link": "",
"logout_link": "",
"history_link": "",
"rename_link": "",
"delete_link": "",
"text_link": "",
"media_link": "",
"media_link_for_textual": "",
"backlinks_link": "{{.n}} %s сюда",
"backlinks_link+one": "ссылка",
"backlinks_link+few": "ссылки",
@ -59,9 +59,9 @@
"ask_really": "Вы действительно хотите {{.verb}} гифу «{{.name}}»?",
"ask_remove_media_verb": "убрать медиа",
"revision_title": "{{.name}} из {{.rev}}",
"revision_warning": "Обратите внимание, просмотр медиа в истории пока что недоступен.",
"revision_link": "Посмотреть код микоразметки для этой ревизии",
"revision_title": "",
"revision_warning": "",
"revision_link": "",
"revision_no_text": "В этой ревизии гифы не было текста.",
"about_title": "О {{.name}}",
@ -78,26 +78,6 @@
"media_noaudio": "Ваш браузер не поддерживает аудио.",
"media_noaudio_link": "Скачать аудио",
"media_title": "Медиа «{{.name}}»",
"media_empty": "Эта гифа не имеет медиа, здесь вы можете его загрузить.",
"media_tip": "На этой странице вы можете управлять медиа.",
"media_what_is": "Что такое медиа?",
"media_upload": "Загрузить",
"media_stat": "Свойства",
"media_stat_size": "Размер файла:",
"media_size_value": "{{.n}} %s",
"media_size_value+one": "байт",
"media_size_value+few": "байта",
"media_size_value+many": "байт",
"media_stat_mime": "MIME-тип:",
"media_include": "Добавление",
"media_include_tip": "Это медиа изображение. Чтобы добавить его в текст гифы, используйте синтаксис ниже:",
"media_new": "Прикрепить",
"media_new_tip": "Вы можете загрузить новое медиа. Пожалуйста, не загружайте слишком большие изображения без необходимости, чтобы впоследствии не ждать её долгую загрузку.",
"media_remove": "Открепить",
"media_remove_tip": "Заметьте, чтобы заменить медиа, вам не нужно его перед этим откреплять.",
"media_remove_button": "Открепить",
"confirm": "Применить",
"cancel": "Отмена"
}

72
main.go
View File

@ -1,65 +1,79 @@
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
//
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=tree
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=history
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=mycoopts
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=auth
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=hypview
package main
import (
"github.com/bouncepaw/mycorrhiza/backlinks"
"github.com/bouncepaw/mycorrhiza/categories"
"github.com/bouncepaw/mycorrhiza/interwiki"
"github.com/bouncepaw/mycorrhiza/migration"
"github.com/bouncepaw/mycorrhiza/version"
"github.com/bouncepaw/mycorrhiza/viewutil"
"log"
"log/slog"
"os"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/shroom"
"github.com/bouncepaw/mycorrhiza/static"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
"github.com/bouncepaw/mycorrhiza/internal/categories"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/migration"
"github.com/bouncepaw/mycorrhiza/internal/shroom"
"github.com/bouncepaw/mycorrhiza/internal/user"
"github.com/bouncepaw/mycorrhiza/internal/version"
"github.com/bouncepaw/mycorrhiza/interwiki"
"github.com/bouncepaw/mycorrhiza/web"
"github.com/bouncepaw/mycorrhiza/web/static"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
)
func main() {
parseCliArgs()
if err := parseCliArgs(); err != nil {
os.Exit(1)
}
if err := files.PrepareWikiRoot(); err != nil {
log.Fatal(err)
slog.Error("Failed to prepare wiki root", "err", err)
os.Exit(1)
}
if err := cfg.ReadConfigFile(files.ConfigPath()); err != nil {
log.Fatal(err)
slog.Error("Failed to read config", "err", err)
os.Exit(1)
}
log.Println("Running Mycorrhiza Wiki", version.Short)
if err := os.Chdir(files.HyphaeDir()); err != nil {
log.Fatal(err)
slog.Error("Failed to chdir to hyphae dir",
"err", err, "hyphaeDir", files.HyphaeDir())
os.Exit(1)
}
log.Println("Wiki directory is", cfg.WikiDir)
slog.Info("Running Mycorrhiza Wiki",
"version", version.Short, "wikiDir", cfg.WikiDir)
// Init the subsystems:
// TODO: keep all crashes in main rather than somewhere there
viewutil.Init()
hyphae.Index(files.HyphaeDir())
backlinks.IndexBacklinks()
go backlinks.RunBacklinksConveyor()
user.InitUserDatabase()
history.Start()
if err := history.Start(); err != nil {
os.Exit(1)
}
history.InitGitRepo()
migration.MigrateRocketsMaybe()
migration.MigrateHeadingsMaybe()
shroom.SetHeaderLinks()
categories.Init()
interwiki.Init()
if err := categories.Init(); err != nil {
os.Exit(1)
}
if err := interwiki.Init(); err != nil {
os.Exit(1)
}
// Static files:
static.InitFS(files.StaticFiles())
serveHTTP(web.Handler())
if !user.HasAnyAdmins() {
slog.Error("Your wiki has no admin yet. Run Mycorrhiza with -create-admin <username> option to create an admin.")
}
err := serveHTTP(web.Handler())
if err != nil {
os.Exit(1)
}
}

View File

@ -1,13 +1,15 @@
package misc
import (
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/l18n"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/version"
"log"
"log/slog"
"os"
"strings"
"text/template" // sic!
"text/template" // sic! TODO: make it html/template after the template library migration
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/user"
"github.com/bouncepaw/mycorrhiza/internal/version"
"github.com/bouncepaw/mycorrhiza/l18n"
)
type L10nEntry struct {
@ -95,7 +97,8 @@ func AboutHTML(lc *l18n.Localizer) string {
}
temp, err := template.New("about wiki").Funcs(template.FuncMap{"get": get}).Parse(aboutTemplateString)
if err != nil {
log.Fatalln(err)
slog.Error("Failed to parse About template", "err", err)
os.Exit(1)
}
data := aboutData
data.Version = version.Short
@ -112,7 +115,8 @@ func AboutHTML(lc *l18n.Localizer) string {
var out strings.Builder
err = temp.Execute(&out, data)
if err != nil {
log.Println(err)
slog.Error("Failed to execute About template", "err", err)
os.Exit(1)
}
return out.String()
}

View File

@ -3,7 +3,7 @@ package misc
import (
"io"
"log"
"log/slog"
"math/rand"
"mime"
"net/http"
@ -11,16 +11,16 @@ import (
"github.com/gorilla/mux"
"github.com/bouncepaw/mycorrhiza/backlinks"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/internal/shroom"
"github.com/bouncepaw/mycorrhiza/internal/user"
"github.com/bouncepaw/mycorrhiza/l18n"
"github.com/bouncepaw/mycorrhiza/shroom"
"github.com/bouncepaw/mycorrhiza/static"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/bouncepaw/mycorrhiza/web/static"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
)
func InitAssetHandlers(rtr *mux.Router) {
@ -73,11 +73,11 @@ func handlerReindex(w http.ResponseWriter, rq *http.Request) {
if ok := user.CanProceed(rq, "reindex"); !ok {
var lc = l18n.FromRequest(rq)
viewutil.HttpErr(viewutil.MetaFrom(w, rq), http.StatusForbidden, cfg.HomeHypha, lc.Get("ui.reindex_no_rights"))
log.Println("Rejected", rq.URL)
slog.Info("No rights to reindex")
return
}
hyphae.ResetCount()
log.Println("Reindexing hyphae in", files.HyphaeDir())
slog.Info("Reindexing hyphae", "hyphaeDir", files.HyphaeDir())
hyphae.Index(files.HyphaeDir())
backlinks.IndexBacklinks()
http.Redirect(w, rq, "/", http.StatusSeeOther)
@ -89,9 +89,10 @@ func handlerUpdateHeaderLinks(w http.ResponseWriter, rq *http.Request) {
if ok := user.CanProceed(rq, "update-header-links"); !ok {
var lc = l18n.FromRequest(rq)
viewutil.HttpErr(viewutil.MetaFrom(w, rq), http.StatusForbidden, cfg.HomeHypha, lc.Get("ui.header_no_rights"))
log.Println("Rejected", rq.URL)
slog.Info("No rights to update header links")
return
}
slog.Info("Updated header links")
shroom.SetHeaderLinks()
http.Redirect(w, rq, "/", http.StatusSeeOther)
}
@ -133,7 +134,7 @@ func handlerAbout(w http.ResponseWriter, rq *http.Request) {
map[string]string{},
))
if err != nil {
log.Println(err)
slog.Error("Failed to write About template", "err", err)
}
}
@ -148,7 +149,7 @@ func handlerStyle(w http.ResponseWriter, rq *http.Request) {
}
_, err = io.Copy(w, file)
if err != nil {
log.Println(err)
slog.Error("Failed to write stylesheet; proceeding anyway", "err", err)
}
_ = file.Close()
}
@ -163,7 +164,7 @@ func handlerRobotsTxt(w http.ResponseWriter, rq *http.Request) {
}
_, err = io.Copy(w, file)
if err != nil {
log.Println()
slog.Error("Failed to write robots.txt; proceeding anyway", "err", err)
}
_ = file.Close()
}
@ -172,12 +173,14 @@ func handlerTitleSearch(w http.ResponseWriter, rq *http.Request) {
util.PrepareRq(rq)
_ = rq.ParseForm()
var (
query = rq.FormValue("q")
results []string
query = rq.FormValue("q")
hyphaName = util.CanonicalName(query)
_, nameFree = hyphae.AreFreeNames(hyphaName)
results []string
)
for hyphaName := range shroom.YieldHyphaNamesContainingString(query) {
results = append(results, hyphaName)
}
w.WriteHeader(http.StatusOK)
viewTitleSearch(viewutil.MetaFrom(w, rq), query, results)
viewTitleSearch(viewutil.MetaFrom(w, rq), query, hyphaName, !nameFree, results)
}

View File

@ -3,8 +3,10 @@
{{define "body"}}
<main class="main-width">
<h1>{{block "search results for" .Query}}Search results for {{.}}{{end}}</h1>
{{if .MatchedHyphaName}}
<p>{{block "go to hypha" .}}Go to hypha <a class="wikilink{{if .HasExactMatch | not}} wikilink_new{{end}}" href="/hypha/{{.MatchedHyphaName}}">{{beautifulName .MatchedHyphaName}}</a>.{{end}}</p>
{{end}}
{{if len .Results}}
<p>{{block "search desc" .}}Every hypha name has been compared with the query. Hyphae that have matched the query are listed below.{{end}}</p>
<ol>
{{range .Results}}
<li>

View File

@ -2,8 +2,9 @@ package misc
import (
"embed"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/viewutil"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
)
var (
@ -12,11 +13,11 @@ var (
chainList, chainTitleSearch viewutil.Chain
ruTranslation = `
{{define "list of hyphae"}}Список гиф{{end}}
{{define "search:"}}Поиск:{{end}}
{{define "search:"}}Поиск: {{.}}{{end}}
{{define "search results for"}}Результаты поиска для «{{.}}»{{end}}
{{define "search desc"}}Название каждой из существующих гиф сопоставлено с запросом. Подходящие гифы приведены ниже.{{end}}
{{define "search no results"}}Ничего не найдено{{end}}
{{define "search no results"}}Ничего не найдено.{{end}}
{{define "x total"}}{{.}} всего.{{end}}
{{define "go to hypha"}}Перейти к гифе <a class="wikilink{{if .HasExactMatch | not}} wikilink_new{{end}}" href="/hypha/{{.MatchedHyphaName}}">{{beautifulName .MatchedHyphaName}}</a>.{{end}}
`
)
@ -46,14 +47,18 @@ func viewList(meta viewutil.Meta, entries []listDatum) {
type titleSearchData struct {
*viewutil.BaseData
Query string
Results []string
Query string
Results []string
MatchedHyphaName string
HasExactMatch bool
}
func viewTitleSearch(meta viewutil.Meta, query string, results []string) {
func viewTitleSearch(meta viewutil.Meta, query string, hyphaName string, hasExactMatch bool, results []string) {
viewutil.ExecutePage(meta, chainTitleSearch, titleSearchData{
BaseData: &viewutil.BaseData{},
Query: query,
Results: results,
BaseData: &viewutil.BaseData{},
Query: query,
Results: results,
MatchedHyphaName: hyphaName,
HasExactMatch: hasExactMatch,
})
}

View File

@ -2,11 +2,17 @@ package mycoopts
import (
"errors"
"git.sr.ht/~bouncepaw/mycomarkup/v5/options"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/hyphae"
"fmt"
"html"
"path/filepath"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
"github.com/bouncepaw/mycorrhiza/interwiki"
"github.com/bouncepaw/mycorrhiza/l18n"
"github.com/bouncepaw/mycorrhiza/util"
"git.sr.ht/~bouncepaw/mycomarkup/v5/options"
)
func MarkupOptions(hyphaName string) options.Options {
@ -52,3 +58,58 @@ func MarkupOptions(hyphaName string) options.Options {
ImgSrcFormatForInterwikiPrefix: interwiki.ImgSrcFormatFor,
}.FillTheRest()
}
func mediaRaw(h *hyphae.MediaHypha) string {
return Media(h, l18n.New("en", "en"))
}
func Media(h *hyphae.MediaHypha, lc *l18n.Localizer) string {
name := html.EscapeString(h.CanonicalName())
switch filepath.Ext(h.MediaFilePath()) {
case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
return fmt.Sprintf(
`<div class="binary-container binary-container_with-img">
<a href="/binary/%s"><img src="/binary/%s"/></a>
</div>`,
name, name,
)
case ".ogg", ".webm", ".mp4":
return fmt.Sprintf(
`<div class="binary-container binary-container_with-video">
<video controls>
<source src="/binary/%s"/>
<p>%s <a href="/binary/%s">%s</a></p>
</video>
</div>`,
name,
html.EscapeString(lc.Get("ui.media_novideo")),
name,
html.EscapeString(lc.Get("ui.media_novideo_link")),
)
case ".mp3", ".wav", ".flac":
return fmt.Sprintf(
`<div class="binary-container binary-container_with-audio">
<audio controls>
<source src="/binary/%s"/>
<p>%s <a href="/binary/%s">%s</a></p>
</audio>
</div>`,
name,
html.EscapeString(lc.Get("ui.media_noaudio")),
name,
html.EscapeString(lc.Get("ui.media_noaudio_link")),
)
default:
return fmt.Sprintf(
`<div class="binary-container binary-container_with-nothing">
<p><a href="/binary/%s">%s</a></p>
</div>`,
name,
html.EscapeString(lc.Get("ui.media_download")),
)
}
}

View File

@ -1,37 +0,0 @@
{% import "path/filepath" %}
{% import "github.com/bouncepaw/mycorrhiza/hyphae" %}
{% import "github.com/bouncepaw/mycorrhiza/l18n" %}
{% func mediaRaw(h *hyphae.MediaHypha) %}{%= Media(h, l18n.New("en", "en")) %}{% endfunc %}
{% func Media(h *hyphae.MediaHypha, lc *l18n.Localizer) %}
{% switch filepath.Ext(h.MediaFilePath()) %}
{% case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico" %}
<div class="binary-container binary-container_with-img">
<a href="/binary/{%s= h.CanonicalName() %}"><img src="/binary/{%s= h.CanonicalName() %}"/></a>
</div>
{% case ".ogg", ".webm", ".mp4" %}
<div class="binary-container binary-container_with-video">
<video controls>
<source src="/binary/{%s= h.CanonicalName() %}"/>
<p>{%s lc.Get("ui.media_novideo") %} <a href="/binary/{%s= h.CanonicalName() %}">{%s lc.Get("ui.media_novideo_link") %}</a></p>
</video>
</div>
{% case ".mp3" %}
<div class="binary-container binary-container_with-audio">
<audio controls>
<source src="/binary/{%s= h.CanonicalName() %}"/>
<p>{%s lc.Get("ui.media_noaudio") %} <a href="/binary/{%s= h.CanonicalName() %}">{%s lc.Get("ui.media_noaudio_link") %}</a></p>
</audio>
</div>
{% default %}
<div class="binary-container binary-container_with-nothing">
<p><a href="/binary/{%s= h.CanonicalName() %}">{%s lc.Get("ui.media_download") %}</a></p>
</div>
{% endswitch %}
{% endfunc %}

View File

@ -1,190 +0,0 @@
// Code generated by qtc from "view.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line mycoopts/view.qtpl:1
package mycoopts
//line mycoopts/view.qtpl:1
import "path/filepath"
//line mycoopts/view.qtpl:3
import "github.com/bouncepaw/mycorrhiza/hyphae"
//line mycoopts/view.qtpl:4
import "github.com/bouncepaw/mycorrhiza/l18n"
//line mycoopts/view.qtpl:6
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line mycoopts/view.qtpl:6
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line mycoopts/view.qtpl:6
func streammediaRaw(qw422016 *qt422016.Writer, h *hyphae.MediaHypha) {
//line mycoopts/view.qtpl:6
StreamMedia(qw422016, h, l18n.New("en", "en"))
//line mycoopts/view.qtpl:6
}
//line mycoopts/view.qtpl:6
func writemediaRaw(qq422016 qtio422016.Writer, h *hyphae.MediaHypha) {
//line mycoopts/view.qtpl:6
qw422016 := qt422016.AcquireWriter(qq422016)
//line mycoopts/view.qtpl:6
streammediaRaw(qw422016, h)
//line mycoopts/view.qtpl:6
qt422016.ReleaseWriter(qw422016)
//line mycoopts/view.qtpl:6
}
//line mycoopts/view.qtpl:6
func mediaRaw(h *hyphae.MediaHypha) string {
//line mycoopts/view.qtpl:6
qb422016 := qt422016.AcquireByteBuffer()
//line mycoopts/view.qtpl:6
writemediaRaw(qb422016, h)
//line mycoopts/view.qtpl:6
qs422016 := string(qb422016.B)
//line mycoopts/view.qtpl:6
qt422016.ReleaseByteBuffer(qb422016)
//line mycoopts/view.qtpl:6
return qs422016
//line mycoopts/view.qtpl:6
}
//line mycoopts/view.qtpl:8
func StreamMedia(qw422016 *qt422016.Writer, h *hyphae.MediaHypha, lc *l18n.Localizer) {
//line mycoopts/view.qtpl:8
qw422016.N().S(`
`)
//line mycoopts/view.qtpl:9
switch filepath.Ext(h.MediaFilePath()) {
//line mycoopts/view.qtpl:11
case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
//line mycoopts/view.qtpl:11
qw422016.N().S(`
<div class="binary-container binary-container_with-img">
<a href="/binary/`)
//line mycoopts/view.qtpl:13
qw422016.N().S(h.CanonicalName())
//line mycoopts/view.qtpl:13
qw422016.N().S(`"><img src="/binary/`)
//line mycoopts/view.qtpl:13
qw422016.N().S(h.CanonicalName())
//line mycoopts/view.qtpl:13
qw422016.N().S(`"/></a>
</div>
`)
//line mycoopts/view.qtpl:16
case ".ogg", ".webm", ".mp4":
//line mycoopts/view.qtpl:16
qw422016.N().S(`
<div class="binary-container binary-container_with-video">
<video controls>
<source src="/binary/`)
//line mycoopts/view.qtpl:19
qw422016.N().S(h.CanonicalName())
//line mycoopts/view.qtpl:19
qw422016.N().S(`"/>
<p>`)
//line mycoopts/view.qtpl:20
qw422016.E().S(lc.Get("ui.media_novideo"))
//line mycoopts/view.qtpl:20
qw422016.N().S(` <a href="/binary/`)
//line mycoopts/view.qtpl:20
qw422016.N().S(h.CanonicalName())
//line mycoopts/view.qtpl:20
qw422016.N().S(`">`)
//line mycoopts/view.qtpl:20
qw422016.E().S(lc.Get("ui.media_novideo_link"))
//line mycoopts/view.qtpl:20
qw422016.N().S(`</a></p>
</video>
</div>
`)
//line mycoopts/view.qtpl:24
case ".mp3":
//line mycoopts/view.qtpl:24
qw422016.N().S(`
<div class="binary-container binary-container_with-audio">
<audio controls>
<source src="/binary/`)
//line mycoopts/view.qtpl:27
qw422016.N().S(h.CanonicalName())
//line mycoopts/view.qtpl:27
qw422016.N().S(`"/>
<p>`)
//line mycoopts/view.qtpl:28
qw422016.E().S(lc.Get("ui.media_noaudio"))
//line mycoopts/view.qtpl:28
qw422016.N().S(` <a href="/binary/`)
//line mycoopts/view.qtpl:28
qw422016.N().S(h.CanonicalName())
//line mycoopts/view.qtpl:28
qw422016.N().S(`">`)
//line mycoopts/view.qtpl:28
qw422016.E().S(lc.Get("ui.media_noaudio_link"))
//line mycoopts/view.qtpl:28
qw422016.N().S(`</a></p>
</audio>
</div>
`)
//line mycoopts/view.qtpl:32
default:
//line mycoopts/view.qtpl:32
qw422016.N().S(`
<div class="binary-container binary-container_with-nothing">
<p><a href="/binary/`)
//line mycoopts/view.qtpl:34
qw422016.N().S(h.CanonicalName())
//line mycoopts/view.qtpl:34
qw422016.N().S(`">`)
//line mycoopts/view.qtpl:34
qw422016.E().S(lc.Get("ui.media_download"))
//line mycoopts/view.qtpl:34
qw422016.N().S(`</a></p>
</div>
`)
//line mycoopts/view.qtpl:36
}
//line mycoopts/view.qtpl:36
qw422016.N().S(`
`)
//line mycoopts/view.qtpl:37
}
//line mycoopts/view.qtpl:37
func WriteMedia(qq422016 qtio422016.Writer, h *hyphae.MediaHypha, lc *l18n.Localizer) {
//line mycoopts/view.qtpl:37
qw422016 := qt422016.AcquireWriter(qq422016)
//line mycoopts/view.qtpl:37
StreamMedia(qw422016, h, lc)
//line mycoopts/view.qtpl:37
qt422016.ReleaseWriter(qw422016)
//line mycoopts/view.qtpl:37
}
//line mycoopts/view.qtpl:37
func Media(h *hyphae.MediaHypha, lc *l18n.Localizer) string {
//line mycoopts/view.qtpl:37
qb422016 := qt422016.AcquireByteBuffer()
//line mycoopts/view.qtpl:37
WriteMedia(qb422016, h, lc)
//line mycoopts/view.qtpl:37
qs422016 := string(qb422016.B)
//line mycoopts/view.qtpl:37
qt422016.ReleaseByteBuffer(qb422016)
//line mycoopts/view.qtpl:37
return qs422016
//line mycoopts/view.qtpl:37
}

View File

@ -1,21 +0,0 @@
package shroom
import (
"log"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/user"
)
func rejectRenameLog(h hyphae.Hypha, u *user.User, errmsg string) {
log.Printf("Reject rename %s by @%s: %s\n", h.CanonicalName(), u.Name, errmsg)
}
func rejectRemoveMediaLog(h hyphae.Hypha, u *user.User, errmsg string) {
log.Printf("Reject remove media %s by @%s: %s\n", h.CanonicalName(), u.Name, errmsg)
}
func rejectEditLog(h hyphae.Hypha, u *user.User, errmsg string) {
log.Printf("Reject edit %s by @%s: %s\n", h.CanonicalName(), u.Name, errmsg)
}
func rejectUploadMediaLog(h hyphae.Hypha, u *user.User, errmsg string) {
log.Printf("Reject upload media %s by @%s: %s\n", h.CanonicalName(), u.Name, errmsg)
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M15.502 20A6.523 6.523 0 0 1 12 23.502 6.523 6.523 0 0 1 8.498 20h2.26c.326.489.747.912 1.242 1.243.495-.33.916-.754 1.243-1.243h2.259zM18 14.805l2 2.268V19H4v-1.927l2-2.268V9c0-3.483 2.504-6.447 6-7.545C15.496 2.553 18 5.517 18 9v5.805zM17.27 17L16 15.56V9c0-2.318-1.57-4.43-4-5.42C9.57 4.57 8 6.681 8 9v6.56L6.73 17h10.54zM12 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>

Before

Width:  |  Height:  |  Size: 473 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-2.29-2.333A17.9 17.9 0 0 1 8.027 13H4.062a8.008 8.008 0 0 0 5.648 6.667zM10.03 13c.151 2.439.848 4.73 1.97 6.752A15.905 15.905 0 0 0 13.97 13h-3.94zm9.908 0h-3.965a17.9 17.9 0 0 1-1.683 6.667A8.008 8.008 0 0 0 19.938 13zM4.062 11h3.965A17.9 17.9 0 0 1 9.71 4.333 8.008 8.008 0 0 0 4.062 11zm5.969 0h3.938A15.905 15.905 0 0 0 12 4.248 15.905 15.905 0 0 0 10.03 11zm4.259-6.667A17.9 17.9 0 0 1 15.973 11h3.965a8.008 8.008 0 0 0-5.648-6.667z"/></svg>

Before

Width:  |  Height:  |  Size: 627 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="#999" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm17 4.238l-7.928 7.1L4 7.216V19h16V7.238zM4.511 5l7.55 6.662L19.502 5H4.511z"/></svg>

Before

Width:  |  Height:  |  Size: 261 B

View File

@ -1,32 +0,0 @@
{% import "sort" %}
{% import "path" %}
{% import "github.com/bouncepaw/mycorrhiza/util" %}
Subhyphae links are recursive. It may end up looking like that if drawn with
pseudographics:
╔══════════════╗
║Foo ║ The presented hyphae are ./foo and ./foo/bar
║╔════════════╗║
║║Bar ║║
║╚════════════╝║
╚══════════════╝
{% func childHTML(c *child) %}
{% code
sort.Slice(c.children, func(i, j int) bool {
return c.children[i].name < c.children[j].name
})
%}
<li class="subhyphae__entry">
<a class="subhyphae__link {% if !c.exists %}wikilink_new{% endif %}" href="/hypha/{%s c.name %}">
{%s util.BeautifulName(path.Base(c.name)) %}
</a>
{% if len(c.children) > 0 %}
<ul>
{% for _, child := range c.children %}
{%s= childHTML(&child) %}
{% endfor %}
</ul>
{% endif %}
</li>
{% endfunc %}

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