diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 2da5de3..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -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 diff --git a/README.md b/README.md index 1df37af..a282d28 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ ## 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,14 +30,16 @@ 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. -## 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). diff --git a/categories/view_list.html b/categories/view_list.html index e4e3573..71321dc 100644 --- a/categories/view_list.html +++ b/categories/view_list.html @@ -1,12 +1,12 @@ {{define "category list"}}Category list{{end}} {{define "title"}}{{template "category list"}}{{end}} {{define "body"}} -
+

{{template "title"}}

{{if len .Categories}}
    {{range .Categories}} -
  1. +
  2. {{beautifulName .}}
  3. {{end}} diff --git a/help/en.myco b/help/en.myco index e24d118..f82a3b3 100644 --- a/help/en.myco +++ b/help/en.myco @@ -1,6 +1,6 @@ = Help -This is documentation for Mycorrhiza Wiki 1.14. Choose a topic from the list. +This is documentation for Mycorrhiza Wiki 1.15. 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]]. diff --git a/help/en/file_structure.myco b/help/en/file_structure.myco index 6798b21..2b0d8fd 100644 --- a/help/en/file_structure.myco +++ b/help/en/file_structure.myco @@ -11,6 +11,7 @@ You can edit all of the files manually, if you want, just do your best to not br ** `static/favicon.ico` is your wiki's favicon, accessed at [[/favicon.ico]] by browsers. ** `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. diff --git a/internal/hyphae/files.go b/internal/hyphae/files.go index ba6f762..a1a3039 100644 --- a/internal/hyphae/files.go +++ b/internal/hyphae/files.go @@ -3,6 +3,7 @@ package hyphae import ( "github.com/bouncepaw/mycorrhiza/internal/mimetype" "log" + "log/slog" "os" "path/filepath" ) @@ -26,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)) @@ -42,11 +42,11 @@ 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(), ) } } diff --git a/internal/shroom/upload.go b/internal/shroom/upload.go index 1876837..8750ca9 100644 --- a/internal/shroom/upload.go +++ b/internal/shroom/upload.go @@ -4,23 +4,24 @@ import ( "bytes" "errors" "fmt" + "github.com/bouncepaw/mycorrhiza/backlinks" + "github.com/bouncepaw/mycorrhiza/files" "github.com/bouncepaw/mycorrhiza/history" - "github.com/bouncepaw/mycorrhiza/internal/backlinks" - "github.com/bouncepaw/mycorrhiza/internal/files" - hyphae2 "github.com/bouncepaw/mycorrhiza/internal/hyphae" - "github.com/bouncepaw/mycorrhiza/internal/mimetype" - "github.com/bouncepaw/mycorrhiza/internal/user" + "github.com/bouncepaw/mycorrhiza/hyphae" + "github.com/bouncepaw/mycorrhiza/mimetype" + "github.com/bouncepaw/mycorrhiza/user" "io" "log" "mime/multipart" "os" "path/filepath" + "strings" ) -func historyMessageForTextUpload(h hyphae2.Hypha, userMessage string) string { +func historyMessageForTextUpload(h hyphae.Hypha, userMessage string) string { var verb string switch h.(type) { - case *hyphae2.EmptyHypha: + case *hyphae.EmptyHypha: verb = "Create" default: verb = "Edit" @@ -32,8 +33,8 @@ func historyMessageForTextUpload(h hyphae2.Hypha, userMessage string) string { return fmt.Sprintf("%s ‘%s’: %s", verb, h.CanonicalName(), userMessage) } -func writeTextToDisk(h hyphae2.ExistingHypha, data []byte, hop *history.Op) error { - if err := hyphae2.WriteToMycoFile(h, data); err != nil { +func writeTextToDisk(h hyphae.ExistingHypha, data []byte, hop *history.Op) error { + if err := hyphae.WriteToMycoFile(h, data); err != nil { return err } hop.WithFiles(h.TextFilePath()) @@ -42,7 +43,7 @@ func writeTextToDisk(h hyphae2.ExistingHypha, data []byte, hop *history.Op) erro } // UploadText edits the hypha's text part and makes a history record about that. -func UploadText(h hyphae2.Hypha, data []byte, userMessage string, u *user.User) error { +func UploadText(h hyphae.Hypha, data []byte, userMessage string, u *user.User) error { hop := history. Operation(history.TypeEditText). WithMsg(historyMessageForTextUpload(h, userMessage)). @@ -56,13 +57,13 @@ func UploadText(h hyphae2.Hypha, data []byte, userMessage string, u *user.User) } // Hypha name exploit check - if !hyphae2.IsValidName(h.CanonicalName()) { + if !hyphae.IsValidName(h.CanonicalName()) { // We check for the name only. I suppose the filepath would be valid as well. hop.Abort() return errors.New("invalid hypha name") } - oldText, err := hyphae2.FetchMycomarkupFile(h) + oldText, err := hyphae.FetchMycomarkupFile(h) if err != nil { hop.Abort() return err @@ -77,8 +78,10 @@ func UploadText(h hyphae2.Hypha, data []byte, userMessage string, u *user.User) // At this point, we have a savable user-generated Mycomarkup document. Gotta save it. switch h := h.(type) { - case *hyphae2.EmptyHypha: - H := hyphae2.ExtendEmptyToTextual(h, filepath.Join(files.HyphaeDir(), h.CanonicalName()+".myco")) + case *hyphae.EmptyHypha: + 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 { @@ -86,9 +89,9 @@ func UploadText(h hyphae2.Hypha, data []byte, userMessage string, u *user.User) return err } - hyphae2.Insert(H) + hyphae.Insert(H) backlinks.UpdateBacklinksAfterEdit(H, "") - case *hyphae2.MediaHypha: + case *hyphae.MediaHypha: // TODO: that []byte(...) part should be removed if bytes.Equal(data, []byte(oldText)) { // No changes! Just like cancel button @@ -103,8 +106,8 @@ func UploadText(h hyphae2.Hypha, data []byte, userMessage string, u *user.User) } backlinks.UpdateBacklinksAfterEdit(h, oldText) - case *hyphae2.TextualHypha: - oldText, err := hyphae2.FetchMycomarkupFile(h) + case *hyphae.TextualHypha: + oldText, err := hyphae.FetchMycomarkupFile(h) if err != nil { hop.Abort() return err @@ -130,16 +133,17 @@ func UploadText(h hyphae2.Hypha, data []byte, userMessage string, u *user.User) return nil } -func historyMessageForMediaUpload(h hyphae2.Hypha, mime string) string { +func historyMessageForMediaUpload(h hyphae.Hypha, mime string) string { return fmt.Sprintf("Upload media for ‘%s’ with type ‘%s’", h.CanonicalName(), mime) } // writeMediaToDisk saves the given data with the given mime type for the given hypha to the disk and returns the path to the saved file and an error, if any. -func writeMediaToDisk(h hyphae2.Hypha, mime string, data []byte) (string, error) { +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 { @@ -153,7 +157,7 @@ func writeMediaToDisk(h hyphae2.Hypha, mime string, data []byte) (string, error) } // UploadBinary edits the hypha's media part and makes a history record about that. -func UploadBinary(h hyphae2.Hypha, mime string, file multipart.File, u *user.User) error { +func UploadBinary(h hyphae.Hypha, mime string, file multipart.File, u *user.User) error { // Privilege check if !u.CanProceed("upload-binary") { @@ -162,7 +166,7 @@ func UploadBinary(h hyphae2.Hypha, mime string, file multipart.File, u *user.Use } // Hypha name exploit check - if !hyphae2.IsValidName(h.CanonicalName()) { + if !hyphae.IsValidName(h.CanonicalName()) { // We check for the name only. I suppose the filepath would be valid as well. return errors.New("invalid hypha name") } @@ -185,12 +189,12 @@ func UploadBinary(h hyphae2.Hypha, mime string, file multipart.File, u *user.Use } switch h := h.(type) { - case *hyphae2.EmptyHypha: - H := hyphae2.ExtendEmptyToMedia(h, uploadedFilePath) - hyphae2.Insert(H) - case *hyphae2.TextualHypha: - hyphae2.Insert(hyphae2.ExtendTextualToMedia(h, uploadedFilePath)) - case *hyphae2.MediaHypha: // If this is not the first media the hypha gets + case *hyphae.EmptyHypha: + H := hyphae.ExtendEmptyToMedia(h, uploadedFilePath) + hyphae.Insert(H) + case *hyphae.TextualHypha: + hyphae.Insert(hyphae.ExtendTextualToMedia(h, uploadedFilePath)) + case *hyphae.MediaHypha: // If this is not the first media the hypha gets prevFilePath := h.MediaFilePath() if prevFilePath != uploadedFilePath { if err := history.Rename(prevFilePath, uploadedFilePath); err != nil { diff --git a/web/static/default.css b/web/static/default.css index 50375c9..45b86b8 100644 --- a/web/static/default.css +++ b/web/static/default.css @@ -106,13 +106,11 @@ textarea {font-size:16px; font-family: inherit; line-height: 150%; } main h1:not(.navi-title) {font-size:1.7rem;} blockquote { margin: 0; padding-left: .75rem; } -.wikilink_external::before { display: inline-block; width: 18px; height: 16px; vertical-align: sub; } -/* .wikilink_external { padding-left: 16px; } */ -.wikilink_gopher::before { content: url("/static/icon/gopher-proto.svg"); } -.wikilink_http::before, .wikilink_https::before { content: url("/static/icon/http-proto.svg"); } -/* .wikilink_https { background: transparent url("/static/icon/http-proto.svg") center left no-repeat; } */ -.wikilink_gemini::before { content: url("/static/icon/gemini-proto.svg"); } -.wikilink_mailto::before { content: url("/static/icon/mailto-proto.svg"); } +.wikilink_external { padding-left: 1.2rem; } +.wikilink_gopher { background: transparent left no-repeat url("/static/icon/gopher-proto.svg"); } +.wikilink_http, .wikilink_https { background: transparent left no-repeat url("/static/icon/http-proto.svg"); } +.wikilink_gemini { background: transparent left no-repeat url("/static/icon/gemini-proto.svg"); } +.wikilink_mailto { background: transparent left no-repeat url("/static/icon/mailto-proto.svg"); } article { overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; line-height: 150%; } main h1 { margin: .5rem 0 0 0; } @@ -124,7 +122,7 @@ article p { margin: .5rem 0; } article ul, ol { padding-left: 1.5rem; margin: .5rem 0; } article code { padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; font-family: 'Menlo', 'PT Mono', monospace; } article pre.codeblock { padding:.5rem; white-space: pre-wrap; border-radius: .25rem;} -.codeblock code {padding:0; font-size:15px;} +.codeblock code {padding:0; font-size:15px; tab-size: 3; } .transclusion { border-radius: .25rem; margin-bottom: .25rem; clear: both; } .transclusion_failed { padding: 0 .5rem; } .transclusion__content > *:not(.binary-container) {margin: 0.5rem; } diff --git a/web/static/icon/gemini-proto.svg b/web/static/icon/gemini-proto.svg index f2a6414..65c98b1 100644 --- a/web/static/icon/gemini-proto.svg +++ b/web/static/icon/gemini-proto.svg @@ -1 +1 @@ - + diff --git a/web/static/icon/gopher-proto.svg b/web/static/icon/gopher-proto.svg index 3ee0c59..a497c8c 100644 --- a/web/static/icon/gopher-proto.svg +++ b/web/static/icon/gopher-proto.svg @@ -1,4 +1,4 @@ - + + + diff --git a/web/static/icon/mailto-proto.svg b/web/static/icon/mailto-proto.svg index 863ccc1..4b78405 100644 --- a/web/static/icon/mailto-proto.svg +++ b/web/static/icon/mailto-proto.svg @@ -1 +1,2 @@ - + + diff --git a/web/static/shortcuts.js b/web/static/shortcuts.js index e1ed6ad..9dcd577 100644 --- a/web/static/shortcuts.js +++ b/web/static/shortcuts.js @@ -329,7 +329,8 @@ if (document.body.dataset.rrhAddr.startsWith('/edit')) { new Shortcut(isMac ? 'Meta+Shift+x' : 'Ctrl+X', wrapStrikethrough, 'Strikethrough', { force: true }), new Shortcut(isMac ? 'Meta+k' : 'Ctrl+k', wrapLink, 'Inline link', { force: true }), // Apparently, ⌘; conflicts with a Safari's hotkey. Whatever. - new Shortcut(isMac ? 'Meta+;' : 'Ctrl+;', insertDate, 'Insert date UTC', { force: true }), + new Shortcut(isMac ? 'Meta+;' : 'Ctrl+;', insertDateUTC, 'Insert date UTC', { force: true }), + new Shortcut(isMac ? "Meta+'" : "Ctrl+'", insertTimeLocal, 'Insert local time', { force: true }) ])) } } diff --git a/web/static/toolbar.js b/web/static/toolbar.js index 0fdd5c9..1785f63 100644 --- a/web/static/toolbar.js +++ b/web/static/toolbar.js @@ -87,16 +87,30 @@ const insertHorizontalBar = textInserter('\n----\n'), insertBulletedList = textInserter('\n* '), insertNumberedList = textInserter('\n*. ') -function insertDate() { +function insertDateUTC() { let date = new Date().toISOString().split('T')[0] textInserter(date)() } function insertTimeUTC() { - let time = new Date().toISOString().substring(11, 19) + " UTC" + let time = new Date().toISOString().substring(11, 16) + " UTC " textInserter(time)() } +function len2(n) { + return n < 10 ? `0${n}` : `${n}` +} + +function insertDateLocal() { + let d = new Date() + textInserter(`${d.getFullYear()}-${len2(d.getMonth() + 1)}-${len2(d.getDate())}`)() +} + +function insertTimeLocal() { + let d = new Date() + textInserter(`${len2(d.getHours())}:${len2(d.getMinutes())} `)() +} + function insertUserlink() { const userlink = document.querySelector('.auth-links__user-link') const userHypha = userlink.getAttribute('href').substring(7) // no /hypha/ @@ -124,8 +138,10 @@ const buttonsHandlers = { codeblock: insertCodeblock, bulletedlist: insertBulletedList, numberedlist: insertNumberedList, - date: insertDate, - time: insertTimeUTC, + 'date-utc': insertDateUTC, + 'time-utc': insertTimeUTC, + 'date-local': insertDateLocal, + 'time-local': insertTimeLocal, 'user-link': insertUserlink, } for (const key of Object.keys(buttonsHandlers)) { diff --git a/web/static/view.js b/web/static/view.js index 06fc1ec..909a092 100644 --- a/web/static/view.js +++ b/web/static/view.js @@ -35,7 +35,7 @@ wrapper.appendChild(hamburgerSection); return Array .from(new DOMParser() .parseFromString(html, 'text/html') - .querySelectorAll('.mv-category .p-name')) + .querySelectorAll('.mv-tags .p-name')) .map(a => a.innerText); }); diff --git a/web/views/hypha-edit.html b/web/views/hypha-edit.html index 1bec2fe..e375363 100644 --- a/web/views/hypha-edit.html +++ b/web/views/hypha-edit.html @@ -32,8 +32,10 @@

    {{block "actions" .}}Actions{{end}}

    - - + + + + {{if .Meta.U.Group | ne "anon"}} {{end}}