Compare commits

...

12 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
18 changed files with 225 additions and 750 deletions

1
.gitignore vendored
View File

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

View File

@ -2,7 +2,7 @@
**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.15/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

10
go.mod
View File

@ -7,16 +7,14 @@ require (
github.com/go-ini/ini v1.67.0
github.com/gorilla/feeds v1.2.0
github.com/gorilla/mux v1.8.1
github.com/valyala/quicktemplate v1.7.0
golang.org/x/crypto v0.27.0
golang.org/x/term v0.24.0
golang.org/x/text v0.18.0
golang.org/x/crypto v0.31.0
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
)
require (
github.com/stretchr/testify v1.7.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/sys v0.28.0 // indirect
)
// Use this trick to test local Mycomarkup changes, replace the path with yours,

37
go.sum
View File

@ -1,18 +1,13 @@
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/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
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.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
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/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.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=
@ -24,30 +19,14 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
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.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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

@ -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

@ -2,16 +2,67 @@ package history
import (
"fmt"
"html"
"log/slog"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
)
// 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 is usually short.
@ -24,6 +75,62 @@ 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{

View File

@ -1,73 +0,0 @@
{% import "fmt" %}
{% import "github.com/bouncepaw/mycorrhiza/internal/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 %}
HyphaeDiffsHTML returns a comma-separated list of diffs links of current revision for every affected file as HTML string.
{% func (rev Revision) HyphaeDiffsHTML() %}
{% code entries := rev.hyphaeAffected() %}
{% stripspace %}
{% if len(entries) == 1 %}
<a href="/primitive-diff/{%s rev.Hash %}/{%s entries[0] %}">{%s rev.Hash %}</a>
{% else %}
{% for i, hyphaName := range entries %}
{% if i > 0 %}
<span aria-hidden="true">, </span>
{% endif %}
<a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">
{% if i == 0 %}
{%s rev.Hash %}&nbsp;
{% endif %}
{%s hyphaName %}</a>
{% endfor %}
{% endif %}
{% 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 %}
<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>
{% endfor %}
</ul>
</section>
{% endfor %}
{% endfunc %}

View File

@ -1,384 +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/internal/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
}
// HyphaeDiffsHTML returns a comma-separated list of diffs links of current revision for every affected file as HTML string.
//line history/view.qtpl:18
func (rev Revision) StreamHyphaeDiffsHTML(qw422016 *qt422016.Writer) {
//line history/view.qtpl:18
qw422016.N().S(`
`)
//line history/view.qtpl:19
entries := rev.hyphaeAffected()
//line history/view.qtpl:19
qw422016.N().S(`
`)
//line history/view.qtpl:21
if len(entries) == 1 {
//line history/view.qtpl:21
qw422016.N().S(`<a href="/primitive-diff/`)
//line history/view.qtpl:22
qw422016.E().S(rev.Hash)
//line history/view.qtpl:22
qw422016.N().S(`/`)
//line history/view.qtpl:22
qw422016.E().S(entries[0])
//line history/view.qtpl:22
qw422016.N().S(`">`)
//line history/view.qtpl:22
qw422016.E().S(rev.Hash)
//line history/view.qtpl:22
qw422016.N().S(`</a>`)
//line history/view.qtpl:23
} else {
//line history/view.qtpl:24
for i, hyphaName := range entries {
//line history/view.qtpl:25
if i > 0 {
//line history/view.qtpl:25
qw422016.N().S(`<span aria-hidden="true">, </span>`)
//line history/view.qtpl:27
}
//line history/view.qtpl:27
qw422016.N().S(`<a href="/primitive-diff/`)
//line history/view.qtpl:28
qw422016.E().S(rev.Hash)
//line history/view.qtpl:28
qw422016.N().S(`/`)
//line history/view.qtpl:28
qw422016.E().S(hyphaName)
//line history/view.qtpl:28
qw422016.N().S(`">`)
//line history/view.qtpl:29
if i == 0 {
//line history/view.qtpl:30
qw422016.E().S(rev.Hash)
//line history/view.qtpl:30
qw422016.N().S(`&nbsp;`)
//line history/view.qtpl:31
}
//line history/view.qtpl:32
qw422016.E().S(hyphaName)
//line history/view.qtpl:32
qw422016.N().S(`</a>`)
//line history/view.qtpl:33
}
//line history/view.qtpl:34
}
//line history/view.qtpl:35
qw422016.N().S(`
`)
//line history/view.qtpl:36
}
//line history/view.qtpl:36
func (rev Revision) WriteHyphaeDiffsHTML(qq422016 qtio422016.Writer) {
//line history/view.qtpl:36
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:36
rev.StreamHyphaeDiffsHTML(qw422016)
//line history/view.qtpl:36
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:36
}
//line history/view.qtpl:36
func (rev Revision) HyphaeDiffsHTML() string {
//line history/view.qtpl:36
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:36
rev.WriteHyphaeDiffsHTML(qb422016)
//line history/view.qtpl:36
qs422016 := string(qb422016.B)
//line history/view.qtpl:36
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:36
return qs422016
//line history/view.qtpl:36
}
// descriptionForFeed generates a good enough HTML contents for a web feed.
//line history/view.qtpl:39
func (rev *Revision) streamdescriptionForFeed(qw422016 *qt422016.Writer) {
//line history/view.qtpl:39
qw422016.N().S(`
<p><b>`)
//line history/view.qtpl:40
qw422016.E().S(rev.Message)
//line history/view.qtpl:40
qw422016.N().S(`</b> (by `)
//line history/view.qtpl:40
qw422016.E().S(rev.Username)
//line history/view.qtpl:40
qw422016.N().S(` at `)
//line history/view.qtpl:40
qw422016.E().S(rev.TimeString())
//line history/view.qtpl:40
qw422016.N().S(`)</p>
<p>Hyphae affected: `)
//line history/view.qtpl:41
rev.StreamHyphaeLinksHTML(qw422016)
//line history/view.qtpl:41
qw422016.N().S(`</p>
<pre><code>`)
//line history/view.qtpl:42
qw422016.E().S(rev.textDiff())
//line history/view.qtpl:42
qw422016.N().S(`</code></pre>
`)
//line history/view.qtpl:43
}
//line history/view.qtpl:43
func (rev *Revision) writedescriptionForFeed(qq422016 qtio422016.Writer) {
//line history/view.qtpl:43
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:43
rev.streamdescriptionForFeed(qw422016)
//line history/view.qtpl:43
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:43
}
//line history/view.qtpl:43
func (rev *Revision) descriptionForFeed() string {
//line history/view.qtpl:43
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:43
rev.writedescriptionForFeed(qb422016)
//line history/view.qtpl:43
qs422016 := string(qb422016.B)
//line history/view.qtpl:43
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:43
return qs422016
//line history/view.qtpl:43
}
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
//line history/view.qtpl:46
func StreamWithRevisions(qw422016 *qt422016.Writer, hyphaName string, revs []Revision) {
//line history/view.qtpl:46
qw422016.N().S(`
`)
//line history/view.qtpl:47
for _, grp := range groupRevisionsByMonth(revs) {
//line history/view.qtpl:47
qw422016.N().S(`
`)
//line history/view.qtpl:49
currentYear := grp[0].Time.Year()
currentMonth := grp[0].Time.Month()
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
//line history/view.qtpl:52
qw422016.N().S(`
<section class="history__month">
<a href="#`)
//line history/view.qtpl:54
qw422016.E().S(sectionId)
//line history/view.qtpl:54
qw422016.N().S(`" class="history__month-anchor">
<h2 id="`)
//line history/view.qtpl:55
qw422016.E().S(sectionId)
//line history/view.qtpl:55
qw422016.N().S(`" class="history__month-title">`)
//line history/view.qtpl:55
qw422016.N().D(currentYear)
//line history/view.qtpl:55
qw422016.N().S(` `)
//line history/view.qtpl:55
qw422016.E().S(currentMonth.String())
//line history/view.qtpl:55
qw422016.N().S(`</h2>
</a>
<ul class="history__entries">
`)
//line history/view.qtpl:58
for _, rev := range grp {
//line history/view.qtpl:58
qw422016.N().S(`
<li class="history__entry">
<a class="history-entry" href="/rev/`)
//line history/view.qtpl:60
qw422016.E().S(rev.Hash)
//line history/view.qtpl:60
qw422016.N().S(`/`)
//line history/view.qtpl:60
qw422016.E().S(hyphaName)
//line history/view.qtpl:60
qw422016.N().S(`">
<time class="history-entry__time">`)
//line history/view.qtpl:61
qw422016.E().S(rev.timeToDisplay())
//line history/view.qtpl:61
qw422016.N().S(`</time>
</a>
<span class="history-entry__hash"><a href="/primitive-diff/`)
//line history/view.qtpl:63
qw422016.E().S(rev.Hash)
//line history/view.qtpl:63
qw422016.N().S(`/`)
//line history/view.qtpl:63
qw422016.E().S(hyphaName)
//line history/view.qtpl:63
qw422016.N().S(`">`)
//line history/view.qtpl:63
qw422016.E().S(rev.Hash)
//line history/view.qtpl:63
qw422016.N().S(`</a></span>
<span class="history-entry__msg">`)
//line history/view.qtpl:64
qw422016.E().S(rev.Message)
//line history/view.qtpl:64
qw422016.N().S(`</span>
`)
//line history/view.qtpl:65
if rev.Username != "anon" {
//line history/view.qtpl:65
qw422016.N().S(`
<span class="history-entry__author">by <a href="/hypha/`)
//line history/view.qtpl:66
qw422016.E().S(cfg.UserHypha)
//line history/view.qtpl:66
qw422016.N().S(`/`)
//line history/view.qtpl:66
qw422016.E().S(rev.Username)
//line history/view.qtpl:66
qw422016.N().S(`" rel="author">`)
//line history/view.qtpl:66
qw422016.E().S(rev.Username)
//line history/view.qtpl:66
qw422016.N().S(`</a></span>
`)
//line history/view.qtpl:67
}
//line history/view.qtpl:67
qw422016.N().S(`
</li>
`)
//line history/view.qtpl:69
}
//line history/view.qtpl:69
qw422016.N().S(`
</ul>
</section>
`)
//line history/view.qtpl:72
}
//line history/view.qtpl:72
qw422016.N().S(`
`)
//line history/view.qtpl:73
}
//line history/view.qtpl:73
func WriteWithRevisions(qq422016 qtio422016.Writer, hyphaName string, revs []Revision) {
//line history/view.qtpl:73
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:73
StreamWithRevisions(qw422016, hyphaName, revs)
//line history/view.qtpl:73
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:73
}
//line history/view.qtpl:73
func WithRevisions(hyphaName string, revs []Revision) string {
//line history/view.qtpl:73
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:73
WriteWithRevisions(qb422016, hyphaName, revs)
//line history/view.qtpl:73
qs422016 := string(qb422016.B)
//line history/view.qtpl:73
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:73
return qs422016
//line history/view.qtpl:73
}

View File

@ -32,7 +32,7 @@ func serveHTTP(handler http.Handler) (err error) {
func startUnixSocketServer(server *http.Server, socketPath string) error {
err := os.Remove(socketPath)
if err != nil {
return err
slog.Warn("Failed to clean up old socket", "err", err)
}
listener, err := net.Listen("unix", socketPath)

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

@ -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

@ -1,10 +1,10 @@
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
//
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=history
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=mycoopts
package main
import (
"log/slog"
"os"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
"github.com/bouncepaw/mycorrhiza/internal/categories"
@ -19,8 +19,6 @@ import (
"github.com/bouncepaw/mycorrhiza/web"
"github.com/bouncepaw/mycorrhiza/web/static"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
"log/slog"
"os"
)
func main() {

View File

@ -2,10 +2,14 @@ package mycoopts
import (
"errors"
"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"
@ -54,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/internal/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/internal/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

@ -258,8 +258,8 @@ func handlerHypha(w http.ResponseWriter, rq *http.Request) {
if err == nil {
ctx, _ := mycocontext.ContextFromStringInput(string(fileContentsT), mycoopts.MarkupOptions(hyphaName))
getOpenGraph, descVisitor, imgVisitor := tools.OpenGraphVisitors(ctx)
openGraph = template.HTML(getOpenGraph())
ast := mycomarkup.BlockTree(ctx, descVisitor, imgVisitor)
openGraph = template.HTML(getOpenGraph())
contents = template.HTML(mycomarkup.BlocksToHTML(ctx, ast))
}
switch h := h.(type) {

View File

@ -317,7 +317,7 @@ kbd {
top: 0;
left: 50%;
width: 100%;
max-width: 800px;
max-width: 1000px;
margin: 96px auto;
padding: 24px;
transform: translate(-50%, 0);
@ -354,7 +354,7 @@ kbd {
margin: 0;
padding: 8px;
border: none;
background: url(/web/static/icon/x.svg) no-repeat 8px 8px / 16px 16px;
background: url(/static/icon/x.svg) no-repeat 8px 8px / 16px 16px;
width: 32px;
height: 32px;
cursor: pointer;

View File

@ -285,8 +285,8 @@ rrh.shortcuts.addGroup(new ShortcutGroup('Common', null, [
if (document.body.dataset.rrhAddr.startsWith('/hypha')) {
rrh.shortcuts.addGroup(new ShortcutGroup('Hypha', null, [
new Shortcut('', $$('article .wikilink'), 'First 9 hyphas links'),
new Shortcut(['p', 'Alt+ArrowLeft', 'Ctrl+Alt+ArrowLeft'], $('.prevnext__prev'), 'Previous hypha'),
new Shortcut(['n', 'Alt+ArrowRight', 'Ctrl+Alt+ArrowRight'], $('.prevnext__next'), 'Next hypha'),
new Shortcut(['p', 'Alt+Shift+ArrowLeft', 'Ctrl+Alt+ArrowLeft'], $('.prevnext__prev'), 'Previous hypha'),
new Shortcut(['n', 'Alt+Shift+ArrowRight', 'Ctrl+Alt+ArrowRight'], $('.prevnext__next'), 'Next hypha'),
new Shortcut(['s', 'Alt+ArrowUp', 'Ctrl+Alt+ArrowUp'], $$('.navi-title a').slice(1, -1).slice(-1)[0], 'Parent hypha'),
new Shortcut(['c', 'Alt+ArrowDown', 'Ctrl+Alt+ArrowDown'], $('.subhyphae__link'), 'First child hypha'),
new Shortcut(['e', isMac ? 'Meta+Enter' : 'Ctrl+Enter'], $('.btn__link_navititle[href^="/edit/"]'), 'Edit this hypha'),