mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-07 02:10:26 +00:00
Merge branch 'master' into mycomarkup-3-migrate-and-test
This commit is contained in:
commit
dbb45b2ded
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
mycorrhiza
|
mycorrhiza
|
||||||
|
config.mk
|
||||||
|
10
Makefile
10
Makefile
@ -1,13 +1,15 @@
|
|||||||
WIKI=~/src/example-wiki
|
.POSIX:
|
||||||
|
include config.example.mk
|
||||||
|
-include config.mk
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
./mycorrhiza ${WIKI}
|
./mycorrhiza ${WIKIPATH}
|
||||||
|
|
||||||
config_run: build
|
config_run: build
|
||||||
./mycorrhiza ${WIKI}
|
./mycorrhiza ${WIKIPATH}
|
||||||
|
|
||||||
devconfig_run: build
|
devconfig_run: build
|
||||||
./mycorrhiza ${WIKI}
|
./mycorrhiza ${WIKIPATH}
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go generate
|
go generate
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
# 🍄 Mycorrhiza Wiki
|
# 🍄 Mycorrhiza Wiki
|
||||||
|
**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.4/screenshot" alt="A screenshot of mycorrhiza.wiki's home page in the Safari browser" width="600">
|
<img src="https://mycorrhiza.wiki/binary/release/1.4/screenshot" alt="A screenshot of mycorrhiza.wiki's home page in the Safari browser" width="600">
|
||||||
|
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/bouncepaw/mycorrhiza)](https://goreportcard.com/report/github.com/bouncepaw/mycorrhiza)
|
|
||||||
|
|
||||||
**Mycorrhiza Wiki** is a lightweight file-system wiki engine that uses Git for keeping history.
|
|
||||||
|
|
||||||
[👉 Main wiki](https://mycorrhiza.wiki)
|
|
||||||
|
|
||||||
## Features
|
## 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 required.** Everything is stored in plain files. It makes installation super easy, and you can modify the content directly by yourself.
|
||||||
|
@ -97,8 +97,8 @@ type Authorization struct {
|
|||||||
|
|
||||||
// Telegram is the section of Config that sets Telegram authorization.
|
// Telegram is the section of Config that sets Telegram authorization.
|
||||||
type Telegram struct {
|
type Telegram struct {
|
||||||
TelegramBotToken string `comment:"Token of your bot.`
|
TelegramBotToken string `comment:"Token of your bot."`
|
||||||
TelegramBotName string `comment:"Username of your bot, sans @.`
|
TelegramBotName string `comment:"Username of your bot, sans @."`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadConfigFile reads a config on the given path and stores the
|
// ReadConfigFile reads a config on the given path and stores the
|
||||||
|
1
config.example.mk
Normal file
1
config.example.mk
Normal file
@ -0,0 +1 @@
|
|||||||
|
WIKIPATH=~/src/example-wiki
|
2
flag.go
2
flag.go
@ -74,7 +74,7 @@ func createAdminCommand(name string) {
|
|||||||
user.InitUserDatabase()
|
user.InitUserDatabase()
|
||||||
log.SetOutput(wr)
|
log.SetOutput(wr)
|
||||||
|
|
||||||
handle := int(syscall.Stdin)
|
handle := syscall.Stdin
|
||||||
if !term.IsTerminal(handle) {
|
if !term.IsTerminal(handle) {
|
||||||
log.Fatal("error: not a terminal")
|
log.Fatal("error: not a terminal")
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -9,7 +9,7 @@ require (
|
|||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
github.com/valyala/quicktemplate v1.6.3
|
github.com/valyala/quicktemplate v1.7.0
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||||
|
25
go.sum
25
go.sum
@ -3,6 +3,7 @@ github.com/bouncepaw/mycomarkup/v3 v3.2.1 h1:Gtot9+3Ds0rqQ+T9BvBIMYgYn13vL9L1yoz
|
|||||||
github.com/bouncepaw/mycomarkup/v3 v3.2.1/go.mod h1:BpiGUVsYCgRZCDxF0iIdc08LJokm/Ab36S/Hif0J6D0=
|
github.com/bouncepaw/mycomarkup/v3 v3.2.1/go.mod h1:BpiGUVsYCgRZCDxF0iIdc08LJokm/Ab36S/Hif0J6D0=
|
||||||
github.com/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU=
|
github.com/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU=
|
||||||
github.com/go-ini/ini v1.62.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.62.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
|
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||||
@ -11,8 +12,8 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
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 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
@ -24,20 +25,21 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
|
|||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||||
github.com/valyala/quicktemplate v1.6.3 h1:O7EuMwuH7Q94U2CXD6sOX8AYHqQqWtmIk690IhmpkKA=
|
github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
|
||||||
github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
|
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@ -46,9 +48,14 @@ golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlA
|
|||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Help
|
# Help
|
||||||
|
|
||||||
This is documentation for **Mycorrhiza Wiki** version **1.5** ([[https://mycorrhiza.wiki | official wiki]]).
|
This is documentation for **Mycorrhiza Wiki** version **1.6** ([[https://mycorrhiza.wiki | official wiki]]).
|
||||||
|
|
||||||
**Choose a topic from the list.**
|
**Choose a topic from the list.**
|
||||||
|
|
||||||
|
39
help/en/feeds.myco
Normal file
39
help/en/feeds.myco
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Help: Feeds
|
||||||
|
Mycorrhiza Wiki has RSS, Atom, and JSON feeds to track the latest changes on the wiki.
|
||||||
|
These feeds are linked on the [[/recent-changes | recent changes page]].
|
||||||
|
|
||||||
|
## Options
|
||||||
|
These feeds have options to combine related changes into groups:
|
||||||
|
* {
|
||||||
|
**period** Can be set to lengths of time like `5m`, `24h`, etc.
|
||||||
|
Edits by the same author that happen within this time of each other can be grouped into one item in the feed.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
**same** Can be set to `author`, `message`, or `none`.
|
||||||
|
Edits will only be grouped together if they have the same author or message. By default, edits need to have the same author and message. If it is `none`, all edits can be grouped.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
**order** Can be set to `old-to-now` (default) or `new-to-old`.
|
||||||
|
This determines what order edits in groups will be shown in in your feed.
|
||||||
|
}
|
||||||
|
|
||||||
|
If none of these options are set, changes will never be grouped.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
URLs for feeds using these options look like this:
|
||||||
|
* {
|
||||||
|
`/recent-changes-rss?period=1h`
|
||||||
|
Changes within one hour of each other with the same author and message will be grouped together.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?period=1h&order=new-to-old`
|
||||||
|
Same as the last one, but the groups will be shown in the opposite order.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?period=1h&same=none`
|
||||||
|
Changes within one hour of each other will be grouped together, even with different authors and messages.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?same=author&same=message`
|
||||||
|
Changes with the same author and message will be grouped together no matter how much time passes between them.
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
# Справка
|
# Справка
|
||||||
|
|
||||||
Это документация к **вики-движку Микориза** версии **1.5** ([[https://mycorrhiza.wiki | официальная вики]]).
|
Это документация к **вики-движку Микориза** версии **1.6** ([[https://mycorrhiza.wiki | официальная вики]]).
|
||||||
|
|
||||||
**Выберите тему из списка.**
|
**Выберите тему из списка.**
|
||||||
|
|
||||||
|
41
help/ru/feeds.myco
Normal file
41
help/ru/feeds.myco
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Справка: Ленты
|
||||||
|
В Микоризе можно подписаться на недавние изменения на вики через RSS, Atom или JSON-ленту.
|
||||||
|
|
||||||
|
Ссылки на все три ленты есть на [[/recent-changes | странице недавних изменений]].
|
||||||
|
|
||||||
|
## Опции
|
||||||
|
Можно настроить группировку правок в лентах при помощи опций в URL:
|
||||||
|
* {
|
||||||
|
**period** — период, например: `5m`, `24h`...
|
||||||
|
|
||||||
|
Правки от одного и того же автора, которые совершены в течение этого времени будут обьединены в одну запись в ленте.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
**same** — свойство, по которому обьединяются правки в ленте: `author`, `message` или `none`.
|
||||||
|
|
||||||
|
По умолчанию, у правок должны быть одинаковые авторы и описания. Вы можете сделать так, чтобы обьединялись только по автору (`author`) или только по описанию (`message`). Если задать `none`, все правки могут быть сгруппированы.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
**order** — порядок: от старого к новому `old-to-now` (по умолчанию) или от нового к старому `new-to-old`.
|
||||||
|
}
|
||||||
|
|
||||||
|
Если ничего не настраивать, то правки не будут группироваться.
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
URL для лент с использованием этих опций выглядят так:
|
||||||
|
* {
|
||||||
|
`/recent-changes-rss?period=1h`
|
||||||
|
Правки за час от одного автора с одинаковым описанием будут сгруппированы.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?period=1h&order=new-to-old`
|
||||||
|
То же самое, но в обратном порядке.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?period=1h&same=none`
|
||||||
|
Любые правки в течение одного часа будут обьединены.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?same=author&same=message`
|
||||||
|
Правки от одного автора и с одинаковым описаниемм будут группироваться независимо от времени между ними.
|
||||||
|
}
|
318
history/feed.go
Normal file
318
history/feed.go
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
package history
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
|
||||||
|
"github.com/gorilla/feeds"
|
||||||
|
)
|
||||||
|
|
||||||
|
const changeGroupMaxSize = 30
|
||||||
|
|
||||||
|
func recentChangesFeed(opts FeedOptions) *feeds.Feed {
|
||||||
|
feed := &feeds.Feed{
|
||||||
|
Title: cfg.WikiName + " (recent changes)",
|
||||||
|
Link: &feeds.Link{Href: cfg.URL},
|
||||||
|
Description: fmt.Sprintf("List of %d recent changes on the wiki", changeGroupMaxSize),
|
||||||
|
Updated: time.Now(),
|
||||||
|
}
|
||||||
|
revs := newRecentChangesStream()
|
||||||
|
groups := groupRevisions(revs, opts)
|
||||||
|
for _, grp := range groups {
|
||||||
|
item := grp.feedItem(opts)
|
||||||
|
feed.Add(&item)
|
||||||
|
}
|
||||||
|
return feed
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChangesRSS creates recent changes feed in RSS format.
|
||||||
|
func RecentChangesRSS(opts FeedOptions) (string, error) {
|
||||||
|
return recentChangesFeed(opts).ToRss()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChangesAtom creates recent changes feed in Atom format.
|
||||||
|
func RecentChangesAtom(opts FeedOptions) (string, error) {
|
||||||
|
return recentChangesFeed(opts).ToAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChangesJSON creates recent changes feed in JSON format.
|
||||||
|
func RecentChangesJSON(opts FeedOptions) (string, error) {
|
||||||
|
return recentChangesFeed(opts).ToJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// revisionGroup is a slice of revisions, ordered most recent first.
|
||||||
|
type revisionGroup []Revision
|
||||||
|
|
||||||
|
func newRevisionGroup(rev Revision) revisionGroup {
|
||||||
|
return revisionGroup([]Revision{rev})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp *revisionGroup) addRevision(rev Revision) {
|
||||||
|
*grp = append(*grp, rev)
|
||||||
|
}
|
||||||
|
|
||||||
|
// orderedIndex returns the ith revision in the group following the given order.
|
||||||
|
func (grp *revisionGroup) orderedIndex(i int, order feedGroupOrder) *Revision {
|
||||||
|
switch order {
|
||||||
|
case newToOld:
|
||||||
|
return &(*grp)[i]
|
||||||
|
case oldToNew:
|
||||||
|
return &(*grp)[len(*grp)-1-i]
|
||||||
|
}
|
||||||
|
// unreachable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
|
||||||
|
var (
|
||||||
|
currentYear int
|
||||||
|
currentMonth time.Month
|
||||||
|
)
|
||||||
|
for _, rev := range revs {
|
||||||
|
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
|
||||||
|
currentYear = rev.Time.Year()
|
||||||
|
currentMonth = rev.Time.Month()
|
||||||
|
res = append(res, newRevisionGroup(rev))
|
||||||
|
} else {
|
||||||
|
res[len(res)-1].addRevision(rev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupRevisions groups revisions for a feed.
|
||||||
|
// It returns the first changeGroupMaxSize (30) groups.
|
||||||
|
// The grouping parameter determines when two revisions will be grouped.
|
||||||
|
func groupRevisions(revs recentChangesStream, opts FeedOptions) (res []revisionGroup) {
|
||||||
|
nextRev := revs.iterator()
|
||||||
|
rev, empty := nextRev()
|
||||||
|
if empty {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
currGroup := newRevisionGroup(rev)
|
||||||
|
for rev, done := nextRev(); !done; rev, done = nextRev() {
|
||||||
|
if opts.canGroup(currGroup, rev) {
|
||||||
|
currGroup.addRevision(rev)
|
||||||
|
} else {
|
||||||
|
res = append(res, currGroup)
|
||||||
|
if len(res) == changeGroupMaxSize {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
currGroup = newRevisionGroup(rev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no more revisions, haven't added the last group yet
|
||||||
|
return append(res, currGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
|
||||||
|
title, author := grp.titleAndAuthor(opts.order)
|
||||||
|
return feeds.Item{
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Id: grp[len(grp)-1].Hash,
|
||||||
|
Description: grp.descriptionForFeed(opts.order),
|
||||||
|
Created: grp[len(grp)-1].Time, // earliest revision
|
||||||
|
Updated: grp[0].Time, // latest revision
|
||||||
|
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// titleAndAuthor creates a title and author for a feed item.
|
||||||
|
// If all messages and authors are the same (or there's just one rev), "message by author"
|
||||||
|
// If all authors are the same, "num edits (first message, ...) by author"
|
||||||
|
// Else (even if all messages are the same), "num edits (first message, ...)"
|
||||||
|
func (grp revisionGroup) titleAndAuthor(order feedGroupOrder) (title string, author *feeds.Author) {
|
||||||
|
allMessagesSame := true
|
||||||
|
allAuthorsSame := true
|
||||||
|
for _, rev := range grp[1:] {
|
||||||
|
if rev.Message != grp[0].Message {
|
||||||
|
allMessagesSame = false
|
||||||
|
}
|
||||||
|
if rev.Username != grp[0].Username {
|
||||||
|
allAuthorsSame = false
|
||||||
|
}
|
||||||
|
if !allMessagesSame && !allAuthorsSame {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allMessagesSame && allAuthorsSame {
|
||||||
|
title = grp[0].Message
|
||||||
|
} else {
|
||||||
|
title = fmt.Sprintf("%d edits (%s, ...)", len(grp), grp.orderedIndex(0, order).Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if allAuthorsSame {
|
||||||
|
title += fmt.Sprintf(" by %s", grp[0].Username)
|
||||||
|
author = &feeds.Author{Name: grp[0].Username}
|
||||||
|
} else {
|
||||||
|
author = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return title, author
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp revisionGroup) descriptionForFeed(order feedGroupOrder) string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
for i := 0; i < len(grp); i++ {
|
||||||
|
desc := grp.orderedIndex(i, order).descriptionForFeed()
|
||||||
|
builder.WriteString(desc)
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type feedOptionParserState struct {
|
||||||
|
isAnythingSet bool
|
||||||
|
conds []groupingCondition
|
||||||
|
order feedGroupOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
// feedGrouping represents a set of conditions that must all be satisfied for revisions to be grouped.
|
||||||
|
// If there are no conditions, revisions will never be grouped.
|
||||||
|
type FeedOptions struct {
|
||||||
|
conds []groupingCondition
|
||||||
|
order feedGroupOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFeedOptions(query url.Values) (FeedOptions, error) {
|
||||||
|
parser := feedOptionParserState{}
|
||||||
|
|
||||||
|
err := parser.parseFeedGroupingPeriod(query)
|
||||||
|
if err != nil {
|
||||||
|
return FeedOptions{}, err
|
||||||
|
}
|
||||||
|
err = parser.parseFeedGroupingSame(query)
|
||||||
|
if err != nil {
|
||||||
|
return FeedOptions{}, err
|
||||||
|
}
|
||||||
|
err = parser.parseFeedGroupingOrder(query)
|
||||||
|
if err != nil {
|
||||||
|
return FeedOptions{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var conds []groupingCondition
|
||||||
|
if parser.isAnythingSet {
|
||||||
|
conds = parser.conds
|
||||||
|
} else {
|
||||||
|
// if no options are applied, do no grouping instead of using the default options
|
||||||
|
conds = nil
|
||||||
|
}
|
||||||
|
return FeedOptions{conds: conds, order: parser.order}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (parser *feedOptionParserState) parseFeedGroupingPeriod(query url.Values) error {
|
||||||
|
if query["period"] != nil {
|
||||||
|
parser.isAnythingSet = true
|
||||||
|
period, err := time.ParseDuration(query.Get("period"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parser.conds = append(parser.conds, periodGroupingCondition{period})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (parser *feedOptionParserState) parseFeedGroupingSame(query url.Values) error {
|
||||||
|
if same := query["same"]; same != nil {
|
||||||
|
parser.isAnythingSet = true
|
||||||
|
if len(same) == 1 && same[0] == "none" {
|
||||||
|
// same=none adds no condition
|
||||||
|
parser.conds = append(parser.conds, sameGroupingCondition{})
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// handle same=author, same=author&same=message, etc.
|
||||||
|
cond := sameGroupingCondition{}
|
||||||
|
for _, sameCond := range same {
|
||||||
|
switch sameCond {
|
||||||
|
case "author":
|
||||||
|
if cond.author {
|
||||||
|
return errors.New("set same=author twice")
|
||||||
|
}
|
||||||
|
cond.author = true
|
||||||
|
case "message":
|
||||||
|
if cond.message {
|
||||||
|
return errors.New("set same=message twice")
|
||||||
|
}
|
||||||
|
cond.message = true
|
||||||
|
default:
|
||||||
|
return errors.New("unknown same option " + sameCond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parser.conds = append(parser.conds, cond)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// same defaults to both author and message
|
||||||
|
// but this won't be applied if no grouping options are set
|
||||||
|
parser.conds = append(parser.conds, sameGroupingCondition{author: true, message: true})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type feedGroupOrder int
|
||||||
|
|
||||||
|
const (
|
||||||
|
newToOld feedGroupOrder = iota
|
||||||
|
oldToNew feedGroupOrder = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func (parser *feedOptionParserState) parseFeedGroupingOrder(query url.Values) error {
|
||||||
|
if order := query["order"]; order != nil {
|
||||||
|
parser.isAnythingSet = true
|
||||||
|
switch query.Get("order") {
|
||||||
|
case "old-to-new":
|
||||||
|
parser.order = oldToNew
|
||||||
|
case "new-to-old":
|
||||||
|
parser.order = newToOld
|
||||||
|
default:
|
||||||
|
return errors.New("unknown order option " + query.Get("order"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parser.order = oldToNew
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// canGroup determines whether a revision can be added to a group.
|
||||||
|
func (opts FeedOptions) canGroup(grp revisionGroup, rev Revision) bool {
|
||||||
|
if len(opts.conds) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cond := range opts.conds {
|
||||||
|
if !cond.canGroup(grp, rev) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupingCondition interface {
|
||||||
|
canGroup(grp revisionGroup, rev Revision) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// periodGroupingCondition will group two revisions if they are within period of each other.
|
||||||
|
type periodGroupingCondition struct {
|
||||||
|
period time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cond periodGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
|
||||||
|
return grp[len(grp)-1].Time.Sub(rev.Time) < cond.period
|
||||||
|
}
|
||||||
|
|
||||||
|
type sameGroupingCondition struct {
|
||||||
|
author bool
|
||||||
|
message bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c sameGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
|
||||||
|
return (!c.author || grp[0].Username == rev.Username) &&
|
||||||
|
(!c.message || grp[0].Message == rev.Message)
|
||||||
|
}
|
@ -4,14 +4,10 @@ package history
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/files"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
@ -54,131 +50,6 @@ func InitGitRepo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
|
|
||||||
type Revision struct {
|
|
||||||
Hash string
|
|
||||||
Username string
|
|
||||||
Time time.Time
|
|
||||||
Message string
|
|
||||||
filesAffectedBuf []string
|
|
||||||
hyphaeAffectedBuf []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// filesAffected tells what files have been affected by the revision.
|
|
||||||
func (rev *Revision) filesAffected() (filenames []string) {
|
|
||||||
if nil != rev.filesAffectedBuf {
|
|
||||||
return rev.filesAffectedBuf
|
|
||||||
}
|
|
||||||
// List of files affected by this revision, one per line.
|
|
||||||
out, err := silentGitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
|
|
||||||
// There's an error? Well, whatever, let's just assign an empty slice, who cares.
|
|
||||||
if err != nil {
|
|
||||||
rev.filesAffectedBuf = []string{}
|
|
||||||
} else {
|
|
||||||
rev.filesAffectedBuf = strings.Split(out.String(), "\n")
|
|
||||||
}
|
|
||||||
return rev.filesAffectedBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine what hyphae were affected by this revision
|
|
||||||
func (rev *Revision) hyphaeAffected() (hyphae []string) {
|
|
||||||
if nil != rev.hyphaeAffectedBuf {
|
|
||||||
return rev.hyphaeAffectedBuf
|
|
||||||
}
|
|
||||||
hyphae = make([]string, 0)
|
|
||||||
var (
|
|
||||||
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
|
|
||||||
set = make(map[string]bool)
|
|
||||||
isNewName = func(hyphaName string) bool {
|
|
||||||
if _, present := set[hyphaName]; present {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
set[hyphaName] = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
filesAffected = rev.filesAffected()
|
|
||||||
)
|
|
||||||
for _, filename := range filesAffected {
|
|
||||||
if strings.IndexRune(filename, '.') >= 0 {
|
|
||||||
dotPos := strings.LastIndexByte(filename, '.')
|
|
||||||
hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
|
|
||||||
if isNewName(hyphaName) {
|
|
||||||
hyphae = append(hyphae, hyphaName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rev.hyphaeAffectedBuf = hyphae
|
|
||||||
return hyphae
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeString returns a human readable time representation.
|
|
||||||
func (rev Revision) TimeString() string {
|
|
||||||
return rev.Time.Format(time.RFC822)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
|
||||||
func (rev Revision) HyphaeLinksHTML() (html string) {
|
|
||||||
hyphae := rev.hyphaeAffected()
|
|
||||||
for i, hyphaName := range hyphae {
|
|
||||||
if i > 0 {
|
|
||||||
html += `<span aria-hidden="true">, </span>`
|
|
||||||
}
|
|
||||||
html += fmt.Sprintf(`<a href="/hypha/%[1]s">%[1]s</a>`, hyphaName)
|
|
||||||
}
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
// descriptionForFeed generates a good enough HTML contents for a web feed.
|
|
||||||
func (rev *Revision) descriptionForFeed() (htmlDesc string) {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<p>%s</p>
|
|
||||||
<p><b>Hyphae affected:</b> %s</p>
|
|
||||||
<pre><code>%s</code></pre>`, rev.Message, rev.HyphaeLinksHTML(), html.EscapeString(rev.textDiff()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// textDiff generates a good enough diff to display in a web feed. It is not html-escaped.
|
|
||||||
func (rev *Revision) textDiff() (diff string) {
|
|
||||||
filenames, ok := rev.mycoFiles()
|
|
||||||
if !ok {
|
|
||||||
return "No text changes"
|
|
||||||
}
|
|
||||||
for _, filename := range filenames {
|
|
||||||
text, err := PrimitiveDiffAtRevision(filename, rev.Hash)
|
|
||||||
if err != nil {
|
|
||||||
diff += "\nAn error has occurred with " + filename + "\n"
|
|
||||||
}
|
|
||||||
diff += text + "\n"
|
|
||||||
}
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// mycoFiles returns filenames of .myco file. It is not ok if there are no myco files.
|
|
||||||
func (rev *Revision) mycoFiles() (filenames []string, ok bool) {
|
|
||||||
filenames = []string{}
|
|
||||||
for _, filename := range rev.filesAffected() {
|
|
||||||
if strings.HasSuffix(filename, ".myco") {
|
|
||||||
filenames = append(filenames, filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filenames, len(filenames) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try and guess what link is the most important by looking at the message.
|
|
||||||
func (rev *Revision) bestLink() string {
|
|
||||||
var (
|
|
||||||
revs = rev.hyphaeAffected()
|
|
||||||
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
|
|
||||||
)
|
|
||||||
switch {
|
|
||||||
case renameRes != nil:
|
|
||||||
return "/hypha/" + renameRes[1]
|
|
||||||
case len(revs) == 0:
|
|
||||||
return ""
|
|
||||||
default:
|
|
||||||
return "/hypha/" + revs[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// I pronounce it as [gɪt͡ʃ].
|
// I pronounce it as [gɪt͡ʃ].
|
||||||
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
||||||
func gitsh(args ...string) (out bytes.Buffer, err error) {
|
func gitsh(args ...string) (out bytes.Buffer, err error) {
|
||||||
@ -204,16 +75,6 @@ func silentGitsh(args ...string) (out bytes.Buffer, err error) {
|
|||||||
return *bytes.NewBuffer(b), err
|
return *bytes.NewBuffer(b), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted.
|
|
||||||
func unixTimestampAsTime(ts string) *time.Time {
|
|
||||||
i, err := strconv.ParseInt(ts, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tm := time.Unix(i, 0)
|
|
||||||
return &tm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename renames from `from` to `to` using `git mv`.
|
// Rename renames from `from` to `to` using `git mv`.
|
||||||
func Rename(from, to string) error {
|
func Rename(from, to string) error {
|
||||||
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
||||||
|
@ -1,199 +0,0 @@
|
|||||||
package history
|
|
||||||
|
|
||||||
// information.go
|
|
||||||
// Things related to gathering existing information.
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/files"
|
|
||||||
|
|
||||||
"github.com/gorilla/feeds"
|
|
||||||
)
|
|
||||||
|
|
||||||
func recentChangesFeed() *feeds.Feed {
|
|
||||||
feed := &feeds.Feed{
|
|
||||||
Title: "Recent changes",
|
|
||||||
Link: &feeds.Link{Href: cfg.URL},
|
|
||||||
Description: "List of 30 recent changes on the wiki",
|
|
||||||
Author: &feeds.Author{Name: "Wikimind", Email: "wikimind@mycorrhiza"},
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
out, err = silentGitsh(
|
|
||||||
"log", "--oneline", "--no-merges",
|
|
||||||
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
|
|
||||||
"--max-count=30",
|
|
||||||
)
|
|
||||||
revs []Revision
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
for _, line := range strings.Split(out.String(), "\n") {
|
|
||||||
revs = append(revs, parseRevisionLine(line))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("Found %d recent changes", len(revs))
|
|
||||||
for _, rev := range revs {
|
|
||||||
feed.Add(&feeds.Item{
|
|
||||||
Title: rev.Message,
|
|
||||||
Author: &feeds.Author{Name: rev.Username},
|
|
||||||
Id: rev.Hash,
|
|
||||||
Description: rev.descriptionForFeed(),
|
|
||||||
Created: rev.Time,
|
|
||||||
Updated: rev.Time,
|
|
||||||
Link: &feeds.Link{Href: cfg.URL + rev.bestLink()},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return feed
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentChangesRSS creates recent changes feed in RSS format.
|
|
||||||
func RecentChangesRSS() (string, error) {
|
|
||||||
return recentChangesFeed().ToRss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentChangesAtom creates recent changes feed in Atom format.
|
|
||||||
func RecentChangesAtom() (string, error) {
|
|
||||||
return recentChangesFeed().ToAtom()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentChangesJSON creates recent changes feed in JSON format.
|
|
||||||
func RecentChangesJSON() (string, error) {
|
|
||||||
return recentChangesFeed().ToJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentChanges gathers an arbitrary number of latest changes in form of revisions slice.
|
|
||||||
func RecentChanges(n int) []Revision {
|
|
||||||
var (
|
|
||||||
out, err = silentGitsh(
|
|
||||||
"log", "--oneline", "--no-merges",
|
|
||||||
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
|
|
||||||
"--max-count="+strconv.Itoa(n),
|
|
||||||
)
|
|
||||||
revs []Revision
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
for _, line := range strings.Split(out.String(), "\n") {
|
|
||||||
revs = append(revs, parseRevisionLine(line))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("Found %d recent changes", len(revs))
|
|
||||||
return revs
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileChanged tells you if the file has been changed.
|
|
||||||
func FileChanged(path string) bool {
|
|
||||||
_, err := gitsh("diff", "--exit-code", path)
|
|
||||||
return err != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revisions returns slice of revisions for the given hypha name.
|
|
||||||
func Revisions(hyphaName string) ([]Revision, error) {
|
|
||||||
var (
|
|
||||||
out, err = silentGitsh(
|
|
||||||
"log", "--oneline", "--no-merges",
|
|
||||||
// Hash, author email, author time, commit msg separated by tab
|
|
||||||
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
|
|
||||||
"--", hyphaName+".*",
|
|
||||||
)
|
|
||||||
revs []Revision
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
for _, line := range strings.Split(out.String(), "\n") {
|
|
||||||
if line != "" {
|
|
||||||
revs = append(revs, parseRevisionLine(line))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("Found %d revisions for ‘%s’\n", len(revs), hyphaName)
|
|
||||||
return revs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
|
|
||||||
func WithRevisions(hyphaName string, revs []Revision) (html string) {
|
|
||||||
var (
|
|
||||||
currentYear int
|
|
||||||
currentMonth time.Month
|
|
||||||
)
|
|
||||||
for i, rev := range revs {
|
|
||||||
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
|
|
||||||
currentYear = rev.Time.Year()
|
|
||||||
currentMonth = rev.Time.Month()
|
|
||||||
if i != 0 {
|
|
||||||
html += `
|
|
||||||
</ul>
|
|
||||||
</section>`
|
|
||||||
}
|
|
||||||
html += fmt.Sprintf(`
|
|
||||||
<section class="history__month">
|
|
||||||
<a href="#%[1]d-%[2]d" class="history__month-anchor">
|
|
||||||
<h2 id="%[1]d-%[2]d" class="history__month-title">%[3]s</h2>
|
|
||||||
</a>
|
|
||||||
<ul class="history__entries">`,
|
|
||||||
currentYear, currentMonth,
|
|
||||||
strconv.Itoa(currentYear)+" "+rev.Time.Month().String())
|
|
||||||
}
|
|
||||||
html += rev.asHistoryEntry(hyphaName)
|
|
||||||
}
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rev *Revision) asHistoryEntry(hyphaName string) (html string) {
|
|
||||||
author := ""
|
|
||||||
if rev.Username != "anon" {
|
|
||||||
author = fmt.Sprintf(`
|
|
||||||
<span class="history-entry__author">by <a href="/hypha/%[1]s/%[2]s" rel="author">%[2]s</span>`, cfg.UserHypha, rev.Username)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
<li class="history__entry">
|
|
||||||
<a class="history-entry" href="/rev/%[3]s/%[1]s">
|
|
||||||
<time class="history-entry__time">%[2]s</time>
|
|
||||||
<span class="history-entry__hash"><a href="/primitive-diff/%[3]s/%[1]s">%[3]s</a></span>
|
|
||||||
<span class="history-entry__msg">%[4]s</span>
|
|
||||||
</a>%[5]s
|
|
||||||
</li>
|
|
||||||
`, hyphaName, rev.timeToDisplay(), rev.Hash, rev.Message, author)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return time like mm-dd 13:42
|
|
||||||
func (rev *Revision) timeToDisplay() string {
|
|
||||||
D := rev.Time.Day()
|
|
||||||
h, m, _ := rev.Time.Clock()
|
|
||||||
return fmt.Sprintf("%02d — %02d:%02d", D, h, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This regex is wrapped in "". For some reason, these quotes appear at some time and we have to get rid of them.
|
|
||||||
var revisionLinePattern = regexp.MustCompile("\"(.*)\t(.*)@.*\t(.*)\t(.*)\"")
|
|
||||||
|
|
||||||
func parseRevisionLine(line string) Revision {
|
|
||||||
results := revisionLinePattern.FindStringSubmatch(line)
|
|
||||||
return Revision{
|
|
||||||
Hash: results[1],
|
|
||||||
Username: results[2],
|
|
||||||
Time: *unixTimestampAsTime(results[3]),
|
|
||||||
Message: results[4],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileAtRevision shows how the file with the given file path looked at the commit with the hash. It may return an error if git fails.
|
|
||||||
func FileAtRevision(filepath, hash string) (string, error) {
|
|
||||||
out, err := gitsh("show", hash+":"+strings.TrimPrefix(filepath, files.HyphaeDir()+"/"))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return out.String(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrimitiveDiffAtRevision generates a plain-text diff for the given filepath at the commit with the given hash. It may return an error if git fails.
|
|
||||||
func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
|
|
||||||
out, err := silentGitsh("diff", "--unified=1", "--no-color", hash+"~", hash, "--", filepath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return out.String(), err
|
|
||||||
}
|
|
@ -95,9 +95,7 @@ func (hop *Op) WithFilesRenamed(pairs map[string]string) *Op {
|
|||||||
hop.Errs = append(hop.Errs, err)
|
hop.Errs = append(hop.Errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := Rename(from, to); err != nil {
|
hop.gitop("mv", "--force", from, to)
|
||||||
hop.Errs = append(hop.Errs, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hop
|
return hop
|
||||||
|
254
history/revision.go
Normal file
254
history/revision.go
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
package history
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
|
||||||
|
type Revision struct {
|
||||||
|
Hash string
|
||||||
|
Username string
|
||||||
|
Time time.Time
|
||||||
|
Message string
|
||||||
|
filesAffectedBuf []string
|
||||||
|
hyphaeAffectedBuf []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...)
|
||||||
|
out, err := silentGitsh(args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outStr := out.String()
|
||||||
|
if outStr == "" {
|
||||||
|
// if there are no commits to return
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var revs []Revision
|
||||||
|
for _, line := range strings.Split(outStr, "\n") {
|
||||||
|
revs = append(revs, parseRevisionLine(line))
|
||||||
|
}
|
||||||
|
return revs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type recentChangesStream struct {
|
||||||
|
currHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRecentChangesStream() recentChangesStream {
|
||||||
|
// next returns the next n revisions from the stream, ordered most recent first.
|
||||||
|
// If there are less than n revisions remaining, it will return only those.
|
||||||
|
return recentChangesStream{currHash: ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stream *recentChangesStream) next(n int) []Revision {
|
||||||
|
args := []string{"--max-count=" + strconv.Itoa(n)}
|
||||||
|
if stream.currHash == "" {
|
||||||
|
args = append(args, "HEAD")
|
||||||
|
} else {
|
||||||
|
// 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...)
|
||||||
|
if len(res) != 0 {
|
||||||
|
stream.currHash = res[len(res)-1].Hash
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// recentChangesIterator returns a function that returns successive revisions from the stream.
|
||||||
|
// It buffers revisions to avoid calling git every time.
|
||||||
|
func (stream recentChangesStream) iterator() func() (Revision, bool) {
|
||||||
|
var buf []Revision
|
||||||
|
return func() (Revision, bool) {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// no real reason to choose 30, just needs some large number
|
||||||
|
buf = stream.next(30)
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// revs has no revisions left
|
||||||
|
return Revision{}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rev := buf[0]
|
||||||
|
buf = buf[1:]
|
||||||
|
return rev, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChanges gathers an arbitrary number of latest changes in form of revisions slice, ordered most recent first.
|
||||||
|
func RecentChanges(n int) []Revision {
|
||||||
|
stream := newRecentChangesStream()
|
||||||
|
revs := stream.next(n)
|
||||||
|
log.Printf("Found %d recent changes", 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)
|
||||||
|
return revs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileChanged tells you if the file has been changed since the last commit.
|
||||||
|
func FileChanged(path string) bool {
|
||||||
|
_, err := gitsh("diff", "--exit-code", path)
|
||||||
|
return err != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return time like dd — 13:42
|
||||||
|
func (rev *Revision) timeToDisplay() string {
|
||||||
|
D := rev.Time.Day()
|
||||||
|
h, m, _ := rev.Time.Clock()
|
||||||
|
return fmt.Sprintf("%02d — %02d:%02d", D, h, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var revisionLinePattern = regexp.MustCompile("(.*)\t(.*)@.*\t(.*)\t(.*)")
|
||||||
|
|
||||||
|
// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted.
|
||||||
|
func unixTimestampAsTime(ts string) *time.Time {
|
||||||
|
i, err := strconv.ParseInt(ts, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tm := time.Unix(i, 0)
|
||||||
|
return &tm
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRevisionLine(line string) Revision {
|
||||||
|
results := revisionLinePattern.FindStringSubmatch(line)
|
||||||
|
return Revision{
|
||||||
|
Hash: results[1],
|
||||||
|
Username: results[2],
|
||||||
|
Time: *unixTimestampAsTime(results[3]),
|
||||||
|
Message: results[4],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filesAffected tells what files have been affected by the revision.
|
||||||
|
func (rev *Revision) filesAffected() (filenames []string) {
|
||||||
|
if nil != rev.filesAffectedBuf {
|
||||||
|
return rev.filesAffectedBuf
|
||||||
|
}
|
||||||
|
// List of files affected by this revision, one per line.
|
||||||
|
out, err := silentGitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
|
||||||
|
// There's an error? Well, whatever, let's just assign an empty slice, who cares.
|
||||||
|
if err != nil {
|
||||||
|
rev.filesAffectedBuf = []string{}
|
||||||
|
} else {
|
||||||
|
rev.filesAffectedBuf = strings.Split(out.String(), "\n")
|
||||||
|
}
|
||||||
|
return rev.filesAffectedBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine what hyphae were affected by this revision
|
||||||
|
func (rev *Revision) hyphaeAffected() (hyphae []string) {
|
||||||
|
if nil != rev.hyphaeAffectedBuf {
|
||||||
|
return rev.hyphaeAffectedBuf
|
||||||
|
}
|
||||||
|
hyphae = make([]string, 0)
|
||||||
|
var (
|
||||||
|
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
|
||||||
|
set = make(map[string]bool)
|
||||||
|
isNewName = func(hyphaName string) bool {
|
||||||
|
if _, present := set[hyphaName]; present {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
set[hyphaName] = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
filesAffected = rev.filesAffected()
|
||||||
|
)
|
||||||
|
for _, filename := range filesAffected {
|
||||||
|
if strings.IndexRune(filename, '.') >= 0 {
|
||||||
|
dotPos := strings.LastIndexByte(filename, '.')
|
||||||
|
hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
|
||||||
|
if isNewName(hyphaName) {
|
||||||
|
hyphae = append(hyphae, hyphaName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rev.hyphaeAffectedBuf = hyphae
|
||||||
|
return hyphae
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeString returns a human readable time representation.
|
||||||
|
func (rev Revision) TimeString() string {
|
||||||
|
return rev.Time.Format(time.RFC822)
|
||||||
|
}
|
||||||
|
|
||||||
|
// textDiff generates a good enough diff to display in a web feed. It is not html-escaped.
|
||||||
|
func (rev *Revision) textDiff() (diff string) {
|
||||||
|
filenames, ok := rev.mycoFiles()
|
||||||
|
if !ok {
|
||||||
|
return "No text changes"
|
||||||
|
}
|
||||||
|
for _, filename := range filenames {
|
||||||
|
text, err := PrimitiveDiffAtRevision(filename, rev.Hash)
|
||||||
|
if err != nil {
|
||||||
|
diff += "\nAn error has occurred with " + filename + "\n"
|
||||||
|
}
|
||||||
|
diff += text + "\n"
|
||||||
|
}
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
// mycoFiles returns filenames of .myco file. It is not ok if there are no myco files.
|
||||||
|
func (rev *Revision) mycoFiles() (filenames []string, ok bool) {
|
||||||
|
filenames = []string{}
|
||||||
|
for _, filename := range rev.filesAffected() {
|
||||||
|
if strings.HasSuffix(filename, ".myco") {
|
||||||
|
filenames = append(filenames, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filenames, len(filenames) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try and guess what link is the most important by looking at the message.
|
||||||
|
func (rev *Revision) bestLink() string {
|
||||||
|
var (
|
||||||
|
revs = rev.hyphaeAffected()
|
||||||
|
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case renameRes != nil:
|
||||||
|
return "/hypha/" + renameRes[1]
|
||||||
|
case len(revs) == 0:
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return "/hypha/" + revs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileAtRevision shows how the file with the given file path looked at the commit with the hash. It may return an error if git fails.
|
||||||
|
func FileAtRevision(filepath, hash string) (string, error) {
|
||||||
|
out, err := gitsh("show", hash+":"+strings.TrimPrefix(filepath, files.HyphaeDir()+"/"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrimitiveDiffAtRevision generates a plain-text diff for the given filepath at the commit with the given hash. It may return an error if git fails.
|
||||||
|
func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
|
||||||
|
out, err := silentGitsh("diff", "--unified=1", "--no-color", hash+"~", hash, "--", filepath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out.String(), err
|
||||||
|
}
|
55
history/view.qtpl
Normal file
55
history/view.qtpl
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% 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("%d-%d", 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 %}
|
326
history/view.qtpl.go
Normal file
326
history/view.qtpl.go
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
// 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("%d-%d", 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
|
||||||
|
}
|
@ -6,7 +6,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/mimetype"
|
"github.com/bouncepaw/mycorrhiza/mimetype"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Index finds all hypha files in the full `path` and saves them to the hypha storage.
|
// Index finds all hypha files in the full `path` and saves them to the hypha storage.
|
||||||
@ -20,7 +19,7 @@ func Index(path string) {
|
|||||||
}(ch)
|
}(ch)
|
||||||
|
|
||||||
for h := range ch {
|
for h := range ch {
|
||||||
// At this time it is safe to ignore the mutex, because there is only one worker.
|
// It's safe to ignore the mutex because there is a single worker right now.
|
||||||
if oh := ByName(h.Name); oh.Exists {
|
if oh := ByName(h.Name); oh.Exists {
|
||||||
oh.MergeIn(h)
|
oh.MergeIn(h)
|
||||||
} else {
|
} else {
|
||||||
@ -32,7 +31,9 @@ func Index(path string) {
|
|||||||
log.Println("Indexed", Count(), "hyphae")
|
log.Println("Indexed", Count(), "hyphae")
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexHelper finds all hypha files in the full `path` and sends them to the channel. Handling of duplicate entries and attachment and counting them is up to the caller.
|
// indexHelper finds all hypha files in the full `path` and sends them to the
|
||||||
|
// channel. Handling of duplicate entries and attachment and counting them is
|
||||||
|
// up to the caller.
|
||||||
func indexHelper(path string, nestLevel uint, ch chan *Hypha) {
|
func indexHelper(path string, nestLevel uint, ch chan *Hypha) {
|
||||||
nodes, err := os.ReadDir(path)
|
nodes, err := os.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -40,10 +41,10 @@ func indexHelper(path string, nestLevel uint, ch chan *Hypha) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
// If this hypha looks like it can be a hypha path, go deeper. Do not touch the .git and static folders for they have an administrative importance!
|
// If this hypha looks like it can be a hypha path, go deeper. Do not
|
||||||
if node.IsDir() &&
|
// touch the .git and static folders for they have an administrative
|
||||||
util.IsCanonicalName(node.Name()) &&
|
// importance!
|
||||||
node.Name() != ".git" &&
|
if node.IsDir() && IsValidName(node.Name()) && node.Name() != ".git" &&
|
||||||
!(nestLevel == 0 && node.Name() == "static") {
|
!(nestLevel == 0 && node.Name() == "static") {
|
||||||
indexHelper(filepath.Join(path, node.Name()), nestLevel+1, ch)
|
indexHelper(filepath.Join(path, node.Name()), nestLevel+1, ch)
|
||||||
continue
|
continue
|
||||||
|
@ -2,16 +2,31 @@
|
|||||||
package hyphae
|
package hyphae
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bouncepaw/mycorrhiza/files"
|
|
||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HyphaPattern is a pattern which all hyphae must match.
|
// HyphaPattern is a pattern which all hyphae names must match.
|
||||||
var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}]+`)
|
var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}]+`)
|
||||||
|
|
||||||
|
// IsValidName checks for invalid characters and path traversals.
|
||||||
|
func IsValidName(hyphaName string) bool {
|
||||||
|
if !HyphaPattern.MatchString(hyphaName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, segment := range strings.Split(hyphaName, "/") {
|
||||||
|
if segment == ".git" || segment == ".." {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Hypha keeps vital information about a hypha
|
// Hypha keeps vital information about a hypha
|
||||||
type Hypha struct {
|
type Hypha struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
@ -79,7 +79,7 @@ func PathographicSort(src chan string) <-chan string {
|
|||||||
|
|
||||||
// Subhyphae returns slice of subhyphae.
|
// Subhyphae returns slice of subhyphae.
|
||||||
func (h *Hypha) Subhyphae() []*Hypha {
|
func (h *Hypha) Subhyphae() []*Hypha {
|
||||||
hyphae := []*Hypha{}
|
var hyphae []*Hypha
|
||||||
for subh := range YieldExistingHyphae() {
|
for subh := range YieldExistingHyphae() {
|
||||||
if strings.HasPrefix(subh.Name, h.Name+"/") {
|
if strings.HasPrefix(subh.Name, h.Name+"/") {
|
||||||
hyphae = append(hyphae, subh)
|
hyphae = append(hyphae, subh)
|
||||||
|
38
l18n/l18n.go
38
l18n/l18n.go
@ -102,6 +102,7 @@ var localizations = map[string]string{
|
|||||||
"en.help.empty_error_link": "contributing",
|
"en.help.empty_error_link": "contributing",
|
||||||
"en.help.empty_error_title": "This entry does not exist!",
|
"en.help.empty_error_title": "This entry does not exist!",
|
||||||
"en.help.entry_not_found": "Entry not found",
|
"en.help.entry_not_found": "Entry not found",
|
||||||
|
"en.help.feeds": "Feeds",
|
||||||
"en.help.hypha": "Hypha",
|
"en.help.hypha": "Hypha",
|
||||||
"en.help.interface": "Interface",
|
"en.help.interface": "Interface",
|
||||||
"en.help.lock": "Lock",
|
"en.help.lock": "Lock",
|
||||||
@ -123,6 +124,18 @@ var localizations = map[string]string{
|
|||||||
"en.ui.about_title": "About {{.name}}",
|
"en.ui.about_title": "About {{.name}}",
|
||||||
"en.ui.about_usercount": "User count:",
|
"en.ui.about_usercount": "User count:",
|
||||||
"en.ui.about_version": "{{.pre}}Mycorrhiza Wiki{{.post}} version:",
|
"en.ui.about_version": "{{.pre}}Mycorrhiza Wiki{{.post}} version:",
|
||||||
|
"en.ui.act_noattachment": "No attachment",
|
||||||
|
"en.ui.act_noattachment_tip": "Cannot unattach this hypha because it has no attachment",
|
||||||
|
"en.ui.act_norights": "Not enough rights",
|
||||||
|
"en.ui.act_norights_attach": "You must be an editor to attach a hypha",
|
||||||
|
"en.ui.act_norights_delete": "Not enough rights to delete, you must be a moderator",
|
||||||
|
"en.ui.act_norights_edit": "You must be an editor to edit a hypha",
|
||||||
|
"en.ui.act_norights_rename": "Not enough rights to rename, you must be a trusted editor",
|
||||||
|
"en.ui.act_norights_unattach": "Not enough rights to unattach, you must be a trusted editor",
|
||||||
|
"en.ui.act_notexist": "Does not exist",
|
||||||
|
"en.ui.act_notexist_delete": "Cannot delete this hypha because it does not exist",
|
||||||
|
"en.ui.act_notexist_rename": "Cannot rename this hypha because it does not exist",
|
||||||
|
"en.ui.act_notexist_unattach": "Cannot unattach this hypha because it does not exist",
|
||||||
"en.ui.admin_panel": "Admin panel",
|
"en.ui.admin_panel": "Admin panel",
|
||||||
"en.ui.ask_delete": "Delete %s?",
|
"en.ui.ask_delete": "Delete %s?",
|
||||||
"en.ui.ask_delete_tip": "In this version of Mycorrhiza Wiki you cannot undelete a deleted hypha but the history can still be accessed.",
|
"en.ui.ask_delete_tip": "In this version of Mycorrhiza Wiki you cannot undelete a deleted hypha but the history can still be accessed.",
|
||||||
@ -205,8 +218,14 @@ var localizations = map[string]string{
|
|||||||
"en.ui.recent_title+other": "s",
|
"en.ui.recent_title+other": "s",
|
||||||
"en.ui.register": "Register",
|
"en.ui.register": "Register",
|
||||||
"en.ui.reindex_no_rights": "You must be an admin to reindex hyphae.",
|
"en.ui.reindex_no_rights": "You must be an admin to reindex hyphae.",
|
||||||
|
"en.ui.rename_badname": "Invalid name",
|
||||||
|
"en.ui.rename_badname_tip": "Invalid new name. Names cannot contain characters {{.chars}}.",
|
||||||
"en.ui.rename_link": "Rename",
|
"en.ui.rename_link": "Rename",
|
||||||
|
"en.ui.rename_noname": "No name given",
|
||||||
|
"en.ui.rename_noname_tip": "No new name is given",
|
||||||
"en.ui.rename_recurse": "Rename subhyphae too",
|
"en.ui.rename_recurse": "Rename subhyphae too",
|
||||||
|
"en.ui.rename_taken": "Name taken",
|
||||||
|
"en.ui.rename_taken_tip": "Hypha named {{.name}} already exists, cannot rename",
|
||||||
"en.ui.rename_tip": "If you rename this hypha, all incoming links and all relative outcoming links will break. You will also lose all history for the new name. Rename carefully.",
|
"en.ui.rename_tip": "If you rename this hypha, all incoming links and all relative outcoming links will break. You will also lose all history for the new name. Rename carefully.",
|
||||||
"en.ui.rename_to": "New name",
|
"en.ui.rename_to": "New name",
|
||||||
"en.ui.revision_link": "Get Mycomarkup source of this revision",
|
"en.ui.revision_link": "Get Mycomarkup source of this revision",
|
||||||
@ -317,6 +336,7 @@ var localizations = map[string]string{
|
|||||||
"ru.help.empty_error_link": "репозитории",
|
"ru.help.empty_error_link": "репозитории",
|
||||||
"ru.help.empty_error_title": "Этой страницы не существует!",
|
"ru.help.empty_error_title": "Этой страницы не существует!",
|
||||||
"ru.help.entry_not_found": "Запись не найдена",
|
"ru.help.entry_not_found": "Запись не найдена",
|
||||||
|
"ru.help.feeds": "Ленты",
|
||||||
"ru.help.hypha": "Гифа",
|
"ru.help.hypha": "Гифа",
|
||||||
"ru.help.interface": "Интерфейс",
|
"ru.help.interface": "Интерфейс",
|
||||||
"ru.help.lock": "Блокировка",
|
"ru.help.lock": "Блокировка",
|
||||||
@ -338,6 +358,18 @@ var localizations = map[string]string{
|
|||||||
"ru.ui.about_title": "О вики «{{.name}}»",
|
"ru.ui.about_title": "О вики «{{.name}}»",
|
||||||
"ru.ui.about_usercount": "Число пользователей:",
|
"ru.ui.about_usercount": "Число пользователей:",
|
||||||
"ru.ui.about_version": "Версия {{.pre}}Микоризы{{.post}}:",
|
"ru.ui.about_version": "Версия {{.pre}}Микоризы{{.post}}:",
|
||||||
|
"ru.ui.act_noattachment": "Нет вложения",
|
||||||
|
"ru.ui.act_noattachment_tip": "Не могу открепить гифу, потому что в ней нет вложения",
|
||||||
|
"ru.ui.act_norights": "Недостаточно прав",
|
||||||
|
"ru.ui.act_norights_attach": "Вы должны быть редактором, чтобы прикреплять гифы",
|
||||||
|
"ru.ui.act_norights_delete": "Недостаточно прав для удаления, вы должны быть модератором",
|
||||||
|
"ru.ui.act_norights_edit": "Вы должны быть редактором, чтобы редактировать гифы",
|
||||||
|
"ru.ui.act_norights_rename": "Недостаточно прав для переименования, вы должны быть доверенным редактором",
|
||||||
|
"ru.ui.act_norights_unattach": "Недостаточно прав для открепления, вы должны быть доверенным редактором",
|
||||||
|
"ru.ui.act_notexist": "Не существует",
|
||||||
|
"ru.ui.act_notexist_delete": "Нельзя удалить эту гифу, потому что она не существует",
|
||||||
|
"ru.ui.act_notexist_rename": "Нельзя переименовать эту гифу, потому что она не существует",
|
||||||
|
"ru.ui.act_notexist_unattach": "Нельзя открепить эту гифу, потому что она не существует",
|
||||||
"ru.ui.admin_panel": "Администрирование",
|
"ru.ui.admin_panel": "Администрирование",
|
||||||
"ru.ui.ask_delete": "Удалить «%s»?",
|
"ru.ui.ask_delete": "Удалить «%s»?",
|
||||||
"ru.ui.ask_delete_tip": "В этой версии Микоризы нельзя отменить удаление гифы, но её история останется доступной.",
|
"ru.ui.ask_delete_tip": "В этой версии Микоризы нельзя отменить удаление гифы, но её история останется доступной.",
|
||||||
@ -424,8 +456,14 @@ var localizations = map[string]string{
|
|||||||
"ru.ui.recent_title+one": "недавнее изменение",
|
"ru.ui.recent_title+one": "недавнее изменение",
|
||||||
"ru.ui.register": "Регистрация",
|
"ru.ui.register": "Регистрация",
|
||||||
"ru.ui.reindex_no_rights": "Вы должны быть администратором, чтобы переиндексировать гифы.",
|
"ru.ui.reindex_no_rights": "Вы должны быть администратором, чтобы переиндексировать гифы.",
|
||||||
|
"ru.ui.rename_badname": "Некорректное название",
|
||||||
|
"ru.ui.rename_badname_tip": "Новое название некорректно. Названия не могут содержать символы {{.chars}}.",
|
||||||
"ru.ui.rename_link": "Переименовать",
|
"ru.ui.rename_link": "Переименовать",
|
||||||
|
"ru.ui.rename_noname": "Нет названия",
|
||||||
|
"ru.ui.rename_noname_tip": "Не задано новое название",
|
||||||
"ru.ui.rename_recurse": "Также переименовать подгифы",
|
"ru.ui.rename_recurse": "Также переименовать подгифы",
|
||||||
|
"ru.ui.rename_taken": "Название занято",
|
||||||
|
"ru.ui.rename_taken_tip": "Гифа {{.name}} уже существует, не могу переименовать",
|
||||||
"ru.ui.rename_tip": "Если вы переименуете эту гифу, сломаются все ссылки, ведущие на неё, а также исходящие относительные ссылки. Также вы потеряете всю текущую историю для нового названия. Переименовывайте аккуратно.",
|
"ru.ui.rename_tip": "Если вы переименуете эту гифу, сломаются все ссылки, ведущие на неё, а также исходящие относительные ссылки. Также вы потеряете всю текущую историю для нового названия. Переименовывайте аккуратно.",
|
||||||
"ru.ui.rename_to": "Новое название",
|
"ru.ui.rename_to": "Новое название",
|
||||||
"ru.ui.revision_link": "Посмотреть код микоразметки для этой ревизии",
|
"ru.ui.revision_link": "Посмотреть код микоразметки для этой ревизии",
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"sibling_hyphae": "Sibling hyphae",
|
"sibling_hyphae": "Sibling hyphae",
|
||||||
"special_pages": "Special pages",
|
"special_pages": "Special pages",
|
||||||
"recent_changes": "Recent changes",
|
"recent_changes": "Recent changes",
|
||||||
|
"feeds": "Feeds",
|
||||||
"configuration": "Configuration (for administrators)",
|
"configuration": "Configuration (for administrators)",
|
||||||
"lock": "Lock",
|
"lock": "Lock",
|
||||||
"whitelist": "Whitelist",
|
"whitelist": "Whitelist",
|
||||||
|
@ -43,6 +43,25 @@
|
|||||||
"rename_to": "New name",
|
"rename_to": "New name",
|
||||||
"rename_recurse": "Rename subhyphae too",
|
"rename_recurse": "Rename subhyphae too",
|
||||||
"rename_tip": "If you rename this hypha, all incoming links and all relative outcoming links will break. You will also lose all history for the new name. Rename carefully.",
|
"rename_tip": "If you rename this hypha, all incoming links and all relative outcoming links will break. You will also lose all history for the new name. Rename carefully.",
|
||||||
|
"rename_taken": "Name taken",
|
||||||
|
"rename_taken_tip": "Hypha named {{.name}} already exists, cannot rename",
|
||||||
|
"rename_noname": "No name given",
|
||||||
|
"rename_noname_tip": "No new name is given",
|
||||||
|
"rename_badname": "Invalid name",
|
||||||
|
"rename_badname_tip": "Invalid new name. Names cannot contain characters {{.chars}}.",
|
||||||
|
|
||||||
|
"act_noattachment": "No attachment",
|
||||||
|
"act_noattachment_tip": "Cannot unattach this hypha because it has no attachment",
|
||||||
|
"act_norights": "Not enough rights",
|
||||||
|
"act_notexist": "Does not exist",
|
||||||
|
"act_norights_delete": "Not enough rights to delete, you must be a moderator",
|
||||||
|
"act_notexist_delete": "Cannot delete this hypha because it does not exist",
|
||||||
|
"act_norights_rename": "Not enough rights to rename, you must be a trusted editor",
|
||||||
|
"act_notexist_rename": "Cannot rename this hypha because it does not exist",
|
||||||
|
"act_norights_unattach": "Not enough rights to unattach, you must be a trusted editor",
|
||||||
|
"act_notexist_unattach": "Cannot unattach this hypha because it does not exist",
|
||||||
|
"act_norights_edit": "You must be an editor to edit a hypha",
|
||||||
|
"act_norights_attach": "You must be an editor to attach a hypha",
|
||||||
|
|
||||||
"ask_delete": "Delete %s?",
|
"ask_delete": "Delete %s?",
|
||||||
"ask_delete_tip": "In this version of Mycorrhiza Wiki you cannot undelete a deleted hypha but the history can still be accessed.",
|
"ask_delete_tip": "In this version of Mycorrhiza Wiki you cannot undelete a deleted hypha but the history can still be accessed.",
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"sibling_hyphae": "Гифы-сиблинги",
|
"sibling_hyphae": "Гифы-сиблинги",
|
||||||
"special_pages": "Специальные страницы",
|
"special_pages": "Специальные страницы",
|
||||||
"recent_changes": "Недавние изменения",
|
"recent_changes": "Недавние изменения",
|
||||||
|
"feeds": "Ленты",
|
||||||
"configuration": "Конфигурация (для администраторов)",
|
"configuration": "Конфигурация (для администраторов)",
|
||||||
"lock": "Блокировка",
|
"lock": "Блокировка",
|
||||||
"whitelist": "Белый список",
|
"whitelist": "Белый список",
|
||||||
|
@ -45,6 +45,25 @@
|
|||||||
"rename_to": "Новое название",
|
"rename_to": "Новое название",
|
||||||
"rename_recurse": "Также переименовать подгифы",
|
"rename_recurse": "Также переименовать подгифы",
|
||||||
"rename_tip": "Если вы переименуете эту гифу, сломаются все ссылки, ведущие на неё, а также исходящие относительные ссылки. Также вы потеряете всю текущую историю для нового названия. Переименовывайте аккуратно.",
|
"rename_tip": "Если вы переименуете эту гифу, сломаются все ссылки, ведущие на неё, а также исходящие относительные ссылки. Также вы потеряете всю текущую историю для нового названия. Переименовывайте аккуратно.",
|
||||||
|
"rename_taken": "Название занято",
|
||||||
|
"rename_taken_tip": "Гифа {{.name}} уже существует, не могу переименовать",
|
||||||
|
"rename_noname": "Нет названия",
|
||||||
|
"rename_noname_tip": "Не задано новое название",
|
||||||
|
"rename_badname": "Некорректное название",
|
||||||
|
"rename_badname_tip": "Новое название некорректно. Названия не могут содержать символы {{.chars}}.",
|
||||||
|
|
||||||
|
"act_noattachment": "Нет вложения",
|
||||||
|
"act_noattachment_tip": "Не могу открепить гифу, потому что в ней нет вложения",
|
||||||
|
"act_norights": "Недостаточно прав",
|
||||||
|
"act_notexist": "Не существует",
|
||||||
|
"act_norights_delete": "Недостаточно прав для удаления, вы должны быть модератором",
|
||||||
|
"act_norights_rename": "Недостаточно прав для переименования, вы должны быть доверенным редактором",
|
||||||
|
"act_norights_unattach": "Недостаточно прав для открепления, вы должны быть доверенным редактором",
|
||||||
|
"act_norights_edit": "Вы должны быть редактором, чтобы редактировать гифы",
|
||||||
|
"act_norights_attach": "Вы должны быть редактором, чтобы прикреплять гифы",
|
||||||
|
"act_notexist_delete": "Нельзя удалить эту гифу, потому что она не существует",
|
||||||
|
"act_notexist_rename": "Нельзя переименовать эту гифу, потому что она не существует",
|
||||||
|
"act_notexist_unattach": "Нельзя открепить эту гифу, потому что она не существует",
|
||||||
|
|
||||||
"ask_delete": "Удалить «%s»?",
|
"ask_delete": "Удалить «%s»?",
|
||||||
"ask_delete_tip": "В этой версии Микоризы нельзя отменить удаление гифы, но её история останется доступной.",
|
"ask_delete_tip": "В этой версии Микоризы нельзя отменить удаление гифы, но её история останется доступной.",
|
||||||
|
1
main.go
1
main.go
@ -1,5 +1,6 @@
|
|||||||
//go:generate qtc -dir=views
|
//go:generate qtc -dir=views
|
||||||
//go:generate qtc -dir=tree
|
//go:generate qtc -dir=tree
|
||||||
|
//go:generate qtc -dir=history
|
||||||
//go:generate go-localize -input l18n_src -output l18n
|
//go:generate go-localize -input l18n_src -output l18n
|
||||||
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
||||||
package main
|
package main
|
||||||
|
@ -4,32 +4,33 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
"github.com/bouncepaw/mycorrhiza/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func canFactory(
|
func canFactory(
|
||||||
rejectLogger func(*hyphae.Hypha, *user.User, string),
|
rejectLogger func(*hyphae.Hypha, *user.User, string),
|
||||||
action string,
|
action string,
|
||||||
dispatcher func(*hyphae.Hypha, *user.User) (string, string),
|
dispatcher func(*hyphae.Hypha, *user.User, *l18n.Localizer) (string, string),
|
||||||
noRightsMsg string,
|
noRightsMsg string,
|
||||||
notExistsMsg string,
|
notExistsMsg string,
|
||||||
careAboutExistence bool,
|
mustExist bool,
|
||||||
) func(*user.User, *hyphae.Hypha) (string, error) {
|
) func(*user.User, *hyphae.Hypha, *l18n.Localizer) (string, error) {
|
||||||
return func(u *user.User, h *hyphae.Hypha) (string, error) {
|
return func(u *user.User, h *hyphae.Hypha, lc *l18n.Localizer) (string, error) {
|
||||||
if !u.CanProceed(action) {
|
if !u.CanProceed(action) {
|
||||||
rejectLogger(h, u, "no rights")
|
rejectLogger(h, u, "no rights")
|
||||||
return "Not enough rights", errors.New(noRightsMsg)
|
return lc.Get("ui.act_no_rights"), errors.New(lc.Get(noRightsMsg))
|
||||||
}
|
}
|
||||||
|
|
||||||
if careAboutExistence && !h.Exists {
|
if mustExist && !h.Exists {
|
||||||
rejectLogger(h, u, "does not exist")
|
rejectLogger(h, u, "does not exist")
|
||||||
return "Does not exist", errors.New(notExistsMsg)
|
return lc.Get("ui.act_notexist"), errors.New(lc.Get(notExistsMsg))
|
||||||
}
|
}
|
||||||
|
|
||||||
if dispatcher == nil {
|
if dispatcher == nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
errmsg, errtitle := dispatcher(h, u)
|
errmsg, errtitle := dispatcher(h, u, lc)
|
||||||
if errtitle == "" {
|
if errtitle == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
@ -43,8 +44,8 @@ var (
|
|||||||
rejectDeleteLog,
|
rejectDeleteLog,
|
||||||
"delete-confirm",
|
"delete-confirm",
|
||||||
nil,
|
nil,
|
||||||
"Not enough rights to delete, you must be a moderator",
|
"ui.act_norights_delete",
|
||||||
"Cannot delete this hypha because it does not exist",
|
"ui.act_notexist_delete",
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,24 +53,24 @@ var (
|
|||||||
rejectRenameLog,
|
rejectRenameLog,
|
||||||
"rename-confirm",
|
"rename-confirm",
|
||||||
nil,
|
nil,
|
||||||
"Not enough rights to rename, you must be a trusted editor",
|
"ui.act_norights_rename",
|
||||||
"Cannot rename this hypha because it does not exist",
|
"ui.act_notexist_rename",
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
CanUnattach = canFactory(
|
CanUnattach = canFactory(
|
||||||
rejectUnattachLog,
|
rejectUnattachLog,
|
||||||
"unattach-confirm",
|
"unattach-confirm",
|
||||||
func(h *hyphae.Hypha, u *user.User) (errmsg, errtitle string) {
|
func(h *hyphae.Hypha, u *user.User, lc *l18n.Localizer) (errmsg, errtitle string) {
|
||||||
if h.BinaryPath == "" {
|
if h.BinaryPath == "" {
|
||||||
rejectUnattachLog(h, u, "no amnt")
|
rejectUnattachLog(h, u, "no amnt")
|
||||||
return "Cannot unattach this hypha because it has no attachment", "No attachment"
|
return lc.Get("ui.act_noattachment_tip"), lc.Get("ui.act_noattachment")
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", ""
|
return "", ""
|
||||||
},
|
},
|
||||||
"Not enough rights to unattach, you must be a trusted editor",
|
"ui.act_norights_unattach",
|
||||||
"Cannot unattach this hypha because it does not exist",
|
"ui.act_notexist_unattach",
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ var (
|
|||||||
rejectEditLog,
|
rejectEditLog,
|
||||||
"upload-text",
|
"upload-text",
|
||||||
nil,
|
nil,
|
||||||
"You must be an editor to edit a hypha",
|
"ui.act_norights_edit",
|
||||||
"You cannot edit a hypha that does not exist",
|
"You cannot edit a hypha that does not exist",
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@ -86,8 +87,10 @@ var (
|
|||||||
rejectAttachLog,
|
rejectAttachLog,
|
||||||
"upload-binary",
|
"upload-binary",
|
||||||
nil,
|
nil,
|
||||||
"You must be an editor to attach a hypha",
|
"ui.act_norights_attach",
|
||||||
"You cannot attach a hypha that does not exist",
|
"You cannot attach a hypha that does not exist",
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* I've left 'not exists' messages for edit and attach out of translation as they are not used -- chekoopa */
|
||||||
|
@ -5,14 +5,15 @@ import (
|
|||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
"github.com/bouncepaw/mycorrhiza/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeleteHypha deletes hypha and makes a history record about that.
|
// DeleteHypha deletes hypha and makes a history record about that.
|
||||||
func DeleteHypha(u *user.User, h *hyphae.Hypha) (hop *history.Op, errtitle string) {
|
func DeleteHypha(u *user.User, h *hyphae.Hypha, lc *l18n.Localizer) (hop *history.Op, errtitle string) {
|
||||||
hop = history.Operation(history.TypeDeleteHypha)
|
hop = history.Operation(history.TypeDeleteHypha)
|
||||||
|
|
||||||
if errtitle, err := CanDelete(u, h); errtitle != "" {
|
if errtitle, err := CanDelete(u, h, lc); errtitle != "" {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
return hop, errtitle
|
return hop, errtitle
|
||||||
}
|
}
|
||||||
|
@ -7,40 +7,41 @@ import (
|
|||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
"github.com/bouncepaw/mycorrhiza/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func canRenameThisToThat(oh *hyphae.Hypha, nh *hyphae.Hypha, u *user.User) (errtitle string, err error) {
|
func canRenameThisToThat(oh *hyphae.Hypha, nh *hyphae.Hypha, u *user.User, lc *l18n.Localizer) (errtitle string, err error) {
|
||||||
if nh.Exists {
|
if nh.Exists {
|
||||||
rejectRenameLog(oh, u, fmt.Sprintf("name ‘%s’ taken already", nh.Name))
|
rejectRenameLog(oh, u, fmt.Sprintf("name ‘%s’ taken already", nh.Name))
|
||||||
return "Name taken", fmt.Errorf("Hypha named <a href='/hypha/%[1]s'>%[1]s</a> already exists, cannot rename", nh.Name)
|
return lc.Get("ui.rename_taken"), fmt.Errorf(lc.Get("ui.rename_taken_tip", &l18n.Replacements{"name": "<a href='/hypha/%[1]s'>%[1]s</a>"}), nh.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if nh.Name == "" {
|
if nh.Name == "" {
|
||||||
rejectRenameLog(oh, u, "no new name given")
|
rejectRenameLog(oh, u, "no new name given")
|
||||||
return "No name given", errors.New("No new name is given")
|
return lc.Get("ui.rename_noname"), errors.New(lc.Get("ui.rename_noname_tip"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hyphae.HyphaPattern.MatchString(nh.Name) {
|
if !hyphae.IsValidName(nh.Name) {
|
||||||
rejectRenameLog(oh, u, fmt.Sprintf("new name ‘%s’ invalid", nh.Name))
|
rejectRenameLog(oh, u, fmt.Sprintf("new name ‘%s’ invalid", nh.Name))
|
||||||
return "Invalid name", errors.New("Invalid new name. Names cannot contain characters <code>^?!:#@><*|\"\\'&%</code>")
|
return lc.Get("ui.rename_badname"), errors.New(lc.Get("ui.rename_badname_tip", &l18n.Replacements{"chars": "<code>^?!:#@><*|\"\\'&%</code>"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way.
|
// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way.
|
||||||
func RenameHypha(h *hyphae.Hypha, newHypha *hyphae.Hypha, recursive bool, u *user.User) (hop *history.Op, errtitle string) {
|
func RenameHypha(h *hyphae.Hypha, newHypha *hyphae.Hypha, recursive bool, u *user.User, lc *l18n.Localizer) (hop *history.Op, errtitle string) {
|
||||||
newHypha.Lock()
|
newHypha.Lock()
|
||||||
defer newHypha.Unlock()
|
defer newHypha.Unlock()
|
||||||
hop = history.Operation(history.TypeRenameHypha)
|
hop = history.Operation(history.TypeRenameHypha)
|
||||||
|
|
||||||
if errtitle, err := CanRename(u, h); errtitle != "" {
|
if errtitle, err := CanRename(u, h, lc); errtitle != "" {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
return hop, errtitle
|
return hop, errtitle
|
||||||
}
|
}
|
||||||
if errtitle, err := canRenameThisToThat(h, newHypha, u); errtitle != "" {
|
if errtitle, err := canRenameThisToThat(h, newHypha, u, lc); errtitle != "" {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
return hop, errtitle
|
return hop, errtitle
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,15 @@ import (
|
|||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
"github.com/bouncepaw/mycorrhiza/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnattachHypha unattaches hypha and makes a history record about that.
|
// UnattachHypha unattaches hypha and makes a history record about that.
|
||||||
func UnattachHypha(u *user.User, h *hyphae.Hypha) (hop *history.Op, errtitle string) {
|
func UnattachHypha(u *user.User, h *hyphae.Hypha, lc *l18n.Localizer) (hop *history.Op, errtitle string) {
|
||||||
hop = history.Operation(history.TypeUnattachHypha)
|
hop = history.Operation(history.TypeUnattachHypha)
|
||||||
|
|
||||||
if errtitle, err := CanUnattach(u, h); errtitle != "" {
|
if errtitle, err := CanUnattach(u, h, lc); errtitle != "" {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
return hop, errtitle
|
return hop, errtitle
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,13 @@ import (
|
|||||||
"github.com/bouncepaw/mycorrhiza/files"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
"github.com/bouncepaw/mycorrhiza/mimetype"
|
"github.com/bouncepaw/mycorrhiza/mimetype"
|
||||||
"github.com/bouncepaw/mycorrhiza/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UploadText edits a hypha' text part and makes a history record about that.
|
// UploadText edits a hypha' text part and makes a history record about that.
|
||||||
func UploadText(h *hyphae.Hypha, data []byte, message string, u *user.User) (hop *history.Op, errtitle string) {
|
func UploadText(h *hyphae.Hypha, data []byte, message string, u *user.User, lc *l18n.Localizer) (hop *history.Op, errtitle string) {
|
||||||
hop = history.Operation(history.TypeEditText)
|
hop = history.Operation(history.TypeEditText)
|
||||||
var action string
|
var action string
|
||||||
if h.Exists {
|
if h.Exists {
|
||||||
@ -34,7 +35,7 @@ func UploadText(h *hyphae.Hypha, data []byte, message string, u *user.User) (hop
|
|||||||
hop.WithMsg(fmt.Sprintf("%s ‘%s’: %s", action, h.Name, message))
|
hop.WithMsg(fmt.Sprintf("%s ‘%s’: %s", action, h.Name, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
if errtitle, err := CanEdit(u, h); err != nil {
|
if errtitle, err := CanEdit(u, h, lc); err != nil {
|
||||||
return hop.WithErrAbort(err), errtitle
|
return hop.WithErrAbort(err), errtitle
|
||||||
}
|
}
|
||||||
if len(bytes.TrimSpace(data)) == 0 && h.BinaryPath == "" {
|
if len(bytes.TrimSpace(data)) == 0 && h.BinaryPath == "" {
|
||||||
@ -45,7 +46,7 @@ func UploadText(h *hyphae.Hypha, data []byte, message string, u *user.User) (hop
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UploadBinary edits a hypha' attachment and makes a history record about that.
|
// UploadBinary edits a hypha' attachment and makes a history record about that.
|
||||||
func UploadBinary(h *hyphae.Hypha, mime string, file multipart.File, u *user.User) (*history.Op, string) {
|
func UploadBinary(h *hyphae.Hypha, mime string, file multipart.File, u *user.User, lc *l18n.Localizer) (*history.Op, string) {
|
||||||
var (
|
var (
|
||||||
hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload attachment for ‘%s’ with type ‘%s’", h.Name, mime))
|
hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload attachment for ‘%s’ with type ‘%s’", h.Name, mime))
|
||||||
data, err = io.ReadAll(file)
|
data, err = io.ReadAll(file)
|
||||||
@ -54,7 +55,7 @@ func UploadBinary(h *hyphae.Hypha, mime string, file multipart.File, u *user.Use
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return hop.WithErrAbort(err), err.Error()
|
return hop.WithErrAbort(err), err.Error()
|
||||||
}
|
}
|
||||||
if errtitle, err := CanAttach(u, h); err != nil {
|
if errtitle, err := CanAttach(u, h, lc); err != nil {
|
||||||
return hop.WithErrAbort(err), errtitle
|
return hop.WithErrAbort(err), errtitle
|
||||||
}
|
}
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
@ -71,8 +72,7 @@ func uploadHelp(h *hyphae.Hypha, hop *history.Op, ext string, data []byte, u *us
|
|||||||
originalFullPath = &h.TextPath
|
originalFullPath = &h.TextPath
|
||||||
originalText = "" // for backlink update
|
originalText = "" // for backlink update
|
||||||
)
|
)
|
||||||
// Reject if the path is outside the hyphae dir
|
if !isValidPath(fullPath) || !hyphae.IsValidName(h.Name) {
|
||||||
if !strings.HasPrefix(fullPath, files.HyphaeDir()) {
|
|
||||||
err := errors.New("bad path")
|
err := errors.New("bad path")
|
||||||
return hop.WithErrAbort(err), err.Error()
|
return hop.WithErrAbort(err), err.Error()
|
||||||
}
|
}
|
||||||
@ -109,3 +109,7 @@ func uploadHelp(h *hyphae.Hypha, hop *history.Op, ext string, data []byte, u *us
|
|||||||
}
|
}
|
||||||
return hop.WithFiles(fullPath).WithUser(u).Apply(), ""
|
return hop.WithFiles(fullPath).WithUser(u).Apply(), ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidPath(pathname string) bool {
|
||||||
|
return strings.HasPrefix(pathname, files.HyphaeDir())
|
||||||
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
.non-existent-hypha { }
|
.non-existent-hypha__way {
|
||||||
.non-existent-hypha__ways { display: flex; flex-direction: column; width: 100%; margin: 0 0 1rem 0;}
|
margin-top: 0.5rem;
|
||||||
.non-existent-hypha__way { border: 1px #999 solid; border-radius: .25rem; padding: .25rem; }
|
margin-bottom: 2rem;
|
||||||
|
padding-right: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.non-existent-hypha__title { margin-bottom: 1rem; }
|
.non-existent-hypha__title { margin-bottom: 1rem; }
|
||||||
.non-existent-hypha__subtitle { margin: 0; }
|
.non-existent-hypha__subtitle { margin: 0; }
|
||||||
|
|
||||||
@ -32,9 +35,16 @@ header { width: 100%; margin-bottom: 1rem; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 500px) {
|
@media screen and (min-width: 500px) {
|
||||||
.non-existent-hypha__way { flex: 1; margin-right: .5rem; }
|
.non-existent-hypha__way {
|
||||||
.non-existent-hypha__ways { flex-direction: row; }
|
float: left;
|
||||||
.non-existent-hypha__way:last-child { margin-right: 0; }
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.non-existent-hypha__ways::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* No longer a phone but still small screen: center main */
|
/* No longer a phone but still small screen: center main */
|
||||||
@ -80,7 +90,7 @@ textarea {font-size:16px; font-family: inherit; line-height: 150%; }
|
|||||||
|
|
||||||
.edit { min-height: 80vh; }
|
.edit { min-height: 80vh; }
|
||||||
.edit__title { margin-top: 0; }
|
.edit__title { margin-top: 0; }
|
||||||
.edit__preview { border: 2px dashed #ddd; }
|
.edit__preview { border: 2px dashed #ddd; padding: 10px; }
|
||||||
.edit-form__textarea { width: 100%; height: calc(100% - 8rem); min-height: 4rem; }
|
.edit-form__textarea { width: 100%; height: calc(100% - 8rem); min-height: 4rem; }
|
||||||
.edit-form__message { width: 100%; margin: 0.25em 0; }
|
.edit-form__message { width: 100%; margin: 0.25em 0; }
|
||||||
.edit-form__save { font-weight: bold; }
|
.edit-form__save { font-weight: bold; }
|
||||||
@ -250,6 +260,13 @@ table { border: 0; background-color: #444444; color: #ddd; }
|
|||||||
mark { background: rgba(130, 80, 30, 5); color: inherit; }
|
mark { background: rgba(130, 80, 30, 5); color: inherit; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shortcuts
|
||||||
|
*/
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 1.5ch;
|
min-width: 1.5ch;
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
(function () {
|
window.hyphaChanged = false;
|
||||||
window.hyphaChanged = false;
|
let textarea = document.querySelector('.edit-form__textarea');
|
||||||
let textarea = document.querySelector('.edit-form__textarea');
|
let form = document.querySelector('.edit-form');
|
||||||
let form = document.querySelector('.edit-form');
|
|
||||||
|
|
||||||
let warnBeforeClosing = function (ev) {
|
let warnBeforeClosing = function (ev) {
|
||||||
if (!window.hyphaChanged) return;
|
if (!window.hyphaChanged) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
return ev.returnValue = 'Are you sure you want to exit? You have unsaved changes.';
|
return ev.returnValue = 'Are you sure you want to exit? You have unsaved changes.';
|
||||||
};
|
};
|
||||||
|
|
||||||
textarea.addEventListener('input', function () {
|
textarea.addEventListener('input', function () {
|
||||||
window.hyphaChanged = true;
|
window.hyphaChanged = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
form.addEventListener('submit', function () {
|
form.addEventListener('submit', function () {
|
||||||
window.hyphaChanged = false;
|
window.hyphaChanged = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('beforeunload', warnBeforeClosing);
|
window.addEventListener('beforeunload', warnBeforeClosing);
|
||||||
})();
|
|
||||||
|
@ -1,367 +1,367 @@
|
|||||||
(() => {
|
const $ = document.querySelector.bind(document);
|
||||||
const $ = document.querySelector.bind(document);
|
const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args));
|
||||||
const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args));
|
|
||||||
|
|
||||||
const isMac = /Macintosh/.test(window.navigator.userAgent);
|
const isMac = /Macintosh/.test(window.navigator.userAgent);
|
||||||
|
|
||||||
function keyEventToShortcut(event) {
|
function keyEventToShortcut(event) {
|
||||||
let elideShift = event.key.toUpperCase() === event.key && event.shiftKey;
|
let elideShift = event.key.toUpperCase() === event.key && event.shiftKey;
|
||||||
return (event.ctrlKey ? 'Ctrl+' : '') +
|
return (event.ctrlKey ? 'Ctrl+' : '') +
|
||||||
(event.altKey ? 'Alt+' : '') +
|
(event.altKey ? 'Alt+' : '') +
|
||||||
(event.metaKey ? 'Meta+' : '') +
|
(event.metaKey ? 'Meta+' : '') +
|
||||||
(!elideShift && event.shiftKey ? 'Shift+' : '') +
|
(!elideShift && event.shiftKey ? 'Shift+' : '') +
|
||||||
(event.key === ',' ? 'Comma' : event.key === ' ' ? 'Space' : event.key);
|
(event.key === ',' ? 'Comma' : event.key === ' ' ? 'Space' : event.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettifyShortcut(shortcut) {
|
||||||
|
let keys = shortcut.split('+');
|
||||||
|
|
||||||
|
if (isMac) {
|
||||||
|
let cmdIdx = keys.indexOf('Meta');
|
||||||
|
if (cmdIdx !== -1 && keys.length - cmdIdx > 2) {
|
||||||
|
let tmp = keys[cmdIdx + 1];
|
||||||
|
keys[cmdIdx + 1] = 'Meta';
|
||||||
|
keys[cmdIdx] = tmp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prettifyShortcut(shortcut) {
|
let lastKey = keys[keys.length - 1];
|
||||||
let keys = shortcut.split('+');
|
if (!keys.includes('Shift') && lastKey.toUpperCase() === lastKey && lastKey.toLowerCase() !== lastKey) {
|
||||||
|
keys.splice(keys.length - 1, 0, 'Shift');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
let cmdIdx = keys.indexOf('Meta');
|
|
||||||
if (cmdIdx !== -1 && keys.length - cmdIdx > 2) {
|
|
||||||
let tmp = keys[cmdIdx + 1];
|
|
||||||
keys[cmdIdx + 1] = 'Meta';
|
|
||||||
keys[cmdIdx] = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastKey = keys[keys.length - 1];
|
|
||||||
if (!keys.includes('Shift') && lastKey.toUpperCase() === lastKey && lastKey.toLowerCase() !== lastKey) {
|
|
||||||
keys.splice(keys.length - 1, 0, 'Shift');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
if (isMac) {
|
|
||||||
switch (keys[i]) {
|
|
||||||
case 'Ctrl': keys[i] = '⌃'; break;
|
|
||||||
case 'Alt': keys[i] = '⌥'; break;
|
|
||||||
case 'Shift': keys[i] = '⇧'; break;
|
|
||||||
case 'Meta': keys[i] = '⌘'; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === keys.length - 1 && i > 0 && keys[i].length === 1) {
|
|
||||||
keys[i] = keys[i].toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (keys[i]) {
|
switch (keys[i]) {
|
||||||
case 'ArrowLeft': keys[i] = '←'; break;
|
case 'Ctrl': keys[i] = '⌃'; break;
|
||||||
case 'ArrowRight': keys[i] = '→'; break;
|
case 'Alt': keys[i] = '⌥'; break;
|
||||||
case 'ArrowUp': keys[i] = '↑'; break;
|
case 'Shift': keys[i] = '⇧'; break;
|
||||||
case 'ArrowDown': keys[i] = '↓'; break;
|
case 'Meta': keys[i] = '⌘'; break;
|
||||||
case 'Comma': keys[i] = ','; break;
|
|
||||||
case 'Enter': keys[i] = '↩'; break;
|
|
||||||
case ' ': keys[i] = 'Space'; break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keys[i] = `<kbd>${keys[i]}</kbd>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys.join(isMac ? '' : ' + ');
|
if (i === keys.length - 1 && i > 0 && keys[i].length === 1) {
|
||||||
|
keys[i] = keys[i].toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (keys[i]) {
|
||||||
|
case 'ArrowLeft': keys[i] = '←'; break;
|
||||||
|
case 'ArrowRight': keys[i] = '→'; break;
|
||||||
|
case 'ArrowUp': keys[i] = '↑'; break;
|
||||||
|
case 'ArrowDown': keys[i] = '↓'; break;
|
||||||
|
case 'Comma': keys[i] = ','; break;
|
||||||
|
case 'Enter': keys[i] = '↩'; break;
|
||||||
|
case ' ': keys[i] = 'Space'; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys[i] = `<kbd>${keys[i]}</kbd>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTextField(element) {
|
return keys.join(isMac ? '' : ' + ');
|
||||||
let name = element.nodeName.toLowerCase();
|
}
|
||||||
return name === 'textarea' ||
|
|
||||||
name === 'select' ||
|
function isTextField(element) {
|
||||||
(name === 'input' && !['submit', 'reset', 'checkbox', 'radio'].includes(element.type)) ||
|
let name = element.nodeName.toLowerCase();
|
||||||
element.isContentEditable;
|
return name === 'textarea' ||
|
||||||
|
name === 'select' ||
|
||||||
|
(name === 'input' && !['submit', 'reset', 'checkbox', 'radio'].includes(element.type)) ||
|
||||||
|
element.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
let notTextField = event => !(event.target instanceof Node && isTextField(event.target));
|
||||||
|
|
||||||
|
let allShortcuts = [];
|
||||||
|
let shortcutsGroup = null;
|
||||||
|
|
||||||
|
class ShortcutHandler {
|
||||||
|
constructor(element, override, filter = () => true) {
|
||||||
|
this.element = element;
|
||||||
|
this.map = {};
|
||||||
|
this.active = this.map;
|
||||||
|
this.override = override;
|
||||||
|
this.filter = filter;
|
||||||
|
this.timeout = null;
|
||||||
|
|
||||||
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
|
this.resetActive = this.resetActive.bind(this);
|
||||||
|
this.addEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
let notTextField = event => !(event.target instanceof Node && isTextField(event.target));
|
addEventListeners() {
|
||||||
|
this.element.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
let allShortcuts = [];
|
add(text, action, description = null, shownInHelp = true) {
|
||||||
let shortcutsGroup = null;
|
let shortcuts = text.trim().split(',').map(shortcut => shortcut.trim().split(' '));
|
||||||
|
|
||||||
class ShortcutHandler {
|
if (shortcutsGroup && shownInHelp) {
|
||||||
constructor(element, override, filter = () => true) {
|
shortcutsGroup.push({
|
||||||
this.element = element;
|
action,
|
||||||
this.map = {};
|
shortcut: text,
|
||||||
this.active = this.map;
|
description,
|
||||||
this.override = override;
|
})
|
||||||
this.filter = filter;
|
}
|
||||||
|
|
||||||
|
for (let shortcut of shortcuts) {
|
||||||
|
let node = this.map;
|
||||||
|
for (let key of shortcut) {
|
||||||
|
if (!node[key]) {
|
||||||
|
node[key] = {};
|
||||||
|
}
|
||||||
|
node = node[key];
|
||||||
|
if (node.action) {
|
||||||
|
delete node.action;
|
||||||
|
delete node.shortcut;
|
||||||
|
delete node.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.action = action;
|
||||||
|
node.shortcut = shortcut;
|
||||||
|
node.description = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group(...args) {
|
||||||
|
if (typeof args[0] === 'string') this.fakeItem(args.shift());
|
||||||
|
shortcutsGroup = [];
|
||||||
|
|
||||||
|
args[0].bind(this)();
|
||||||
|
|
||||||
|
if (shortcutsGroup && shortcutsGroup.length) allShortcuts.push(shortcutsGroup);
|
||||||
|
shortcutsGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindElement(shortcut, element, ...other) {
|
||||||
|
element = typeof element === 'string' ? $(element) : element;
|
||||||
|
if (!element) return;
|
||||||
|
this.add(shortcut, () => {
|
||||||
|
if (isTextField(element)) {
|
||||||
|
element.focus();
|
||||||
|
} else {
|
||||||
|
element.click();
|
||||||
|
}
|
||||||
|
}, ...other);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindLink(shortcut, link, ...other) {
|
||||||
|
this.add(shortcut, () => window.location.href = link, ...other);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindCollection(prefix, elements, collectionDescription, itemDescription) {
|
||||||
|
this.fakeItem(prefix + ' 1 – 9', collectionDescription);
|
||||||
|
|
||||||
|
if (typeof elements === 'string') {
|
||||||
|
elements = $$(elements);
|
||||||
|
} else if (Array.isArray(elements)) {
|
||||||
|
elements = elements.map(el => typeof el === 'string' ? $(el) : el);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= elements.length && i < 10; i++) {
|
||||||
|
this.bindElement(`${prefix} ${i}`, elements[i - 1], `${itemDescription} #${i}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeItem(shortcut, description = null) {
|
||||||
|
let list = shortcutsGroup || allShortcuts;
|
||||||
|
list.push({
|
||||||
|
shortcut: description ? shortcut : null,
|
||||||
|
description: description || shortcut,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(event) {
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
if (['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) return;
|
||||||
|
if (!this.filter(event)) return;
|
||||||
|
|
||||||
|
let shortcut = keyEventToShortcut(event);
|
||||||
|
|
||||||
|
if (!this.active[shortcut]) {
|
||||||
|
this.resetActive();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.active = this.active[shortcut];
|
||||||
|
if (this.active.action) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.active.action(event);
|
||||||
|
if (this.override) event.preventDefault();
|
||||||
|
this.resetActive();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeout) clearTimeout(this.timeout);
|
||||||
|
this.timeout = window.setTimeout(this.resetActive, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetActive() {
|
||||||
|
this.active = this.map;
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout)
|
||||||
this.timeout = null;
|
this.timeout = null;
|
||||||
|
|
||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
||||||
this.resetActive = this.resetActive.bind(this);
|
|
||||||
this.addEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListeners() {
|
|
||||||
this.element.addEventListener('keydown', this.handleKeyDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
add(text, action, description = null, shownInHelp = true) {
|
|
||||||
let shortcuts = text.trim().split(',').map(shortcut => shortcut.trim().split(' '));
|
|
||||||
|
|
||||||
if (shortcutsGroup && shownInHelp) {
|
|
||||||
shortcutsGroup.push({
|
|
||||||
action,
|
|
||||||
shortcut: text,
|
|
||||||
description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let shortcut of shortcuts) {
|
|
||||||
let node = this.map;
|
|
||||||
for (let key of shortcut) {
|
|
||||||
if (!node[key]) {
|
|
||||||
node[key] = {};
|
|
||||||
}
|
|
||||||
node = node[key];
|
|
||||||
if (node.action) {
|
|
||||||
delete node.action;
|
|
||||||
delete node.shortcut;
|
|
||||||
delete node.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.action = action;
|
|
||||||
node.shortcut = shortcut;
|
|
||||||
node.description = description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group(...args) {
|
|
||||||
if (typeof args[0] === 'string') this.fakeItem(args.shift());
|
|
||||||
shortcutsGroup = [];
|
|
||||||
|
|
||||||
args[0].bind(this)();
|
|
||||||
|
|
||||||
if (shortcutsGroup && shortcutsGroup.length) allShortcuts.push(shortcutsGroup);
|
|
||||||
shortcutsGroup = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindElement(shortcut, element, ...other) {
|
|
||||||
element = typeof element === 'string' ? $(element) : element;
|
|
||||||
if (!element) return;
|
|
||||||
this.add(shortcut, () => {
|
|
||||||
if (isTextField(element)) {
|
|
||||||
element.focus();
|
|
||||||
} else {
|
|
||||||
element.click();
|
|
||||||
}
|
|
||||||
}, ...other);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindLink(shortcut, link, ...other) {
|
|
||||||
this.add(shortcut, () => window.location.href = link, ...other);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindCollection(prefix, elements, collectionDescription, itemDescription) {
|
|
||||||
this.fakeItem(prefix + ' 1 – 9', collectionDescription);
|
|
||||||
|
|
||||||
if (typeof elements === 'string') {
|
|
||||||
elements = $$(elements);
|
|
||||||
} else if (Array.isArray(elements)) {
|
|
||||||
elements = elements.map(el => typeof el === 'string' ? $(el) : el);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i <= elements.length && i < 10; i++) {
|
|
||||||
this.bindElement(`${prefix} ${i}`, elements[i-1], `${itemDescription} #${i}`, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeItem(shortcut, description = null) {
|
|
||||||
let list = shortcutsGroup || allShortcuts;
|
|
||||||
list.push({
|
|
||||||
shortcut: description ? shortcut : null,
|
|
||||||
description: description || shortcut,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown(event) {
|
|
||||||
if (event.defaultPrevented) return;
|
|
||||||
if (['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) return;
|
|
||||||
if (!this.filter(event)) return;
|
|
||||||
|
|
||||||
let shortcut = keyEventToShortcut(event);
|
|
||||||
|
|
||||||
if (!this.active[shortcut]) {
|
|
||||||
this.resetActive();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.active = this.active[shortcut];
|
|
||||||
if (this.active.action) {
|
|
||||||
event.stopPropagation();
|
|
||||||
this.active.action(event);
|
|
||||||
if (this.override) event.preventDefault();
|
|
||||||
this.resetActive();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.timeout) clearTimeout(this.timeout);
|
|
||||||
this.timeout = window.setTimeout(this.resetActive, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetActive() {
|
|
||||||
this.active = this.map;
|
|
||||||
if (this.timeout) {
|
|
||||||
clearTimeout(this.timeout)
|
|
||||||
this.timeout = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ShortcutsHelpDialog {
|
class ShortcutsHelpDialog {
|
||||||
constructor() {
|
constructor() {
|
||||||
let template = $('#dialog-template');
|
let template = $('#dialog-template');
|
||||||
let clonedTemplate = template.content.cloneNode(true);
|
let clonedTemplate = template.content.cloneNode(true);
|
||||||
this.backdrop = clonedTemplate.children[0];
|
this.backdrop = clonedTemplate.children[0];
|
||||||
this.dialog = clonedTemplate.children[1];
|
this.dialog = clonedTemplate.children[1];
|
||||||
|
|
||||||
this.dialog.classList.add('shortcuts-help');
|
this.dialog.classList.add('shortcuts-help');
|
||||||
this.dialog.hidden = true;
|
this.dialog.hidden = true;
|
||||||
this.backdrop.hidden = true;
|
this.backdrop.hidden = true;
|
||||||
|
|
||||||
document.body.appendChild(this.backdrop);
|
document.body.appendChild(this.backdrop);
|
||||||
document.body.appendChild(this.dialog);
|
document.body.appendChild(this.dialog);
|
||||||
|
|
||||||
this.close = this.close.bind(this);
|
this.close = this.close.bind(this);
|
||||||
|
|
||||||
this.dialog.querySelector('.dialog__title').textContent = 'List of shortcuts';
|
this.dialog.querySelector('.dialog__title').textContent = 'List of shortcuts';
|
||||||
this.dialog.querySelector('.dialog__close-button').addEventListener('click', this.close);
|
this.dialog.querySelector('.dialog__close-button').addEventListener('click', this.close);
|
||||||
this.backdrop.addEventListener('click', this.close);
|
this.backdrop.addEventListener('click', this.close);
|
||||||
|
|
||||||
this.shortcuts = new ShortcutHandler(this.dialog, false);
|
this.shortcuts = new ShortcutHandler(this.dialog, false);
|
||||||
this.shortcuts.add('Escape', this.close, null, false);
|
this.shortcuts.add('Escape', this.close, null, false);
|
||||||
|
|
||||||
let shortcutsGroup;
|
let shortcutsGroup;
|
||||||
let shortcutsGroupTemplate = document.createElement('div');
|
let shortcutsGroupTemplate = document.createElement('div');
|
||||||
shortcutsGroupTemplate.className = 'shortcuts-group';
|
shortcutsGroupTemplate.className = 'shortcuts-group';
|
||||||
|
|
||||||
for (let item of allShortcuts) {
|
for (let item of allShortcuts) {
|
||||||
if (item.description && !item.shortcut) {
|
if (item.description && !item.shortcut) {
|
||||||
shortcutsGroup = shortcutsGroupTemplate.cloneNode();
|
shortcutsGroup = shortcutsGroupTemplate.cloneNode();
|
||||||
this.dialog.querySelector('.dialog__content').appendChild(shortcutsGroup);
|
this.dialog.querySelector('.dialog__content').appendChild(shortcutsGroup);
|
||||||
|
|
||||||
let heading = document.createElement('h2');
|
let heading = document.createElement('h2');
|
||||||
heading.className = 'shortcuts-group-heading';
|
heading.className = 'shortcuts-group-heading';
|
||||||
heading.textContent = item.description;
|
heading.textContent = item.description;
|
||||||
shortcutsGroup.appendChild(heading);
|
shortcutsGroup.appendChild(heading);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let list = document.createElement('ul');
|
let list = document.createElement('ul');
|
||||||
list.className = 'shortcuts-list';
|
list.className = 'shortcuts-list';
|
||||||
|
|
||||||
for (let shortcut of item) {
|
for (let shortcut of item) {
|
||||||
let listItem = document.createElement('li');
|
let listItem = document.createElement('li');
|
||||||
listItem.className = 'shortcut-row';
|
listItem.className = 'shortcut-row';
|
||||||
list.appendChild(listItem);
|
list.appendChild(listItem);
|
||||||
|
|
||||||
let descriptionColumn = document.createElement('div')
|
let descriptionColumn = document.createElement('div')
|
||||||
descriptionColumn.className = 'shortcut-row__description';
|
descriptionColumn.className = 'shortcut-row__description';
|
||||||
descriptionColumn.textContent = shortcut.description;
|
descriptionColumn.textContent = shortcut.description;
|
||||||
listItem.appendChild(descriptionColumn);
|
listItem.appendChild(descriptionColumn);
|
||||||
|
|
||||||
let shortcutColumn = document.createElement('div');
|
let shortcutColumn = document.createElement('div');
|
||||||
shortcutColumn.className = 'shortcut-row__keys';
|
shortcutColumn.className = 'shortcut-row__keys';
|
||||||
shortcutColumn.innerHTML = shortcut.shortcut.split(',')
|
shortcutColumn.innerHTML = shortcut.shortcut.split(',')
|
||||||
.map(shortcuts => shortcuts.trim().split(' ').map(prettifyShortcut).join(' '))
|
.map(shortcuts => shortcuts.trim().split(' ').map(prettifyShortcut).join(' '))
|
||||||
.join(' <span class="kbd-or">or</span> ');
|
.join(' <span class="kbd-or">or</span> ');
|
||||||
listItem.appendChild(shortcutColumn);
|
listItem.appendChild(shortcutColumn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shortcutsGroup) {
|
||||||
shortcutsGroup.appendChild(list);
|
shortcutsGroup.appendChild(list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
|
||||||
this.prevActiveElement = document.activeElement;
|
|
||||||
|
|
||||||
document.body.overflow = 'hidden';
|
|
||||||
this.backdrop.hidden = false;
|
|
||||||
this.dialog.hidden = false;
|
|
||||||
this.dialog.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
document.body.overflow = '';
|
|
||||||
this.backdrop.hidden = true;
|
|
||||||
this.dialog.hidden = true;
|
|
||||||
|
|
||||||
if (this.prevActiveElement) {
|
|
||||||
this.prevActiveElement.focus();
|
|
||||||
this.prevActiveElement = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
open() {
|
||||||
let helpDialog = null;
|
this.prevActiveElement = document.activeElement;
|
||||||
let openHelp = () => {
|
|
||||||
if (!helpDialog) helpDialog = new ShortcutsHelpDialog();
|
|
||||||
helpDialog.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
let onEditPage = typeof editTextarea !== 'undefined';
|
document.body.overflow = 'hidden';
|
||||||
|
this.backdrop.hidden = false;
|
||||||
|
this.dialog.hidden = false;
|
||||||
|
this.dialog.focus();
|
||||||
|
}
|
||||||
|
|
||||||
// Global shortcuts work everywhere.
|
close() {
|
||||||
let globalShortcuts = new ShortcutHandler(document, false);
|
document.body.overflow = '';
|
||||||
globalShortcuts.add(isMac ? 'Meta+/' : 'Ctrl+/', openHelp);
|
this.backdrop.hidden = true;
|
||||||
|
this.dialog.hidden = true;
|
||||||
|
|
||||||
// Page shortcuts work everywhere except on text fields.
|
if (this.prevActiveElement) {
|
||||||
let pageShortcuts = new ShortcutHandler(document, false, notTextField);
|
this.prevActiveElement.focus();
|
||||||
pageShortcuts.add('?', openHelp, null, false);
|
this.prevActiveElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Common shortcuts
|
window.addEventListener('load', () => {
|
||||||
pageShortcuts.group('Common', function () {
|
let helpDialog = null;
|
||||||
this.bindCollection('g', '.top-bar__highlight-link', 'First 9 header links', 'Header link');
|
let openHelp = () => {
|
||||||
this.bindLink('g h', '/', 'Home');
|
if (!helpDialog) helpDialog = new ShortcutsHelpDialog();
|
||||||
this.bindLink('g l', '/list/', 'List of hyphae');
|
helpDialog.open();
|
||||||
this.bindLink('g r', '/recent-changes/', 'Recent changes');
|
};
|
||||||
this.bindElement('g u', '.auth-links__user-link', 'Your profile′s hypha');
|
|
||||||
|
let onEditPage = typeof editTextarea !== 'undefined';
|
||||||
|
|
||||||
|
// Global shortcuts work everywhere.
|
||||||
|
let globalShortcuts = new ShortcutHandler(document, false);
|
||||||
|
globalShortcuts.add(isMac ? 'Meta+/' : 'Ctrl+/', openHelp);
|
||||||
|
|
||||||
|
// Page shortcuts work everywhere except on text fields.
|
||||||
|
let pageShortcuts = new ShortcutHandler(document, false, notTextField);
|
||||||
|
pageShortcuts.add('?', openHelp, null, false);
|
||||||
|
|
||||||
|
// Common shortcuts
|
||||||
|
pageShortcuts.group('Common', function () {
|
||||||
|
this.bindCollection('g', '.top-bar__highlight-link', 'First 9 header links', 'Header link');
|
||||||
|
this.bindLink('g h', '/', 'Home');
|
||||||
|
this.bindLink('g l', '/list/', 'List of hyphae');
|
||||||
|
this.bindLink('g r', '/recent-changes/', 'Recent changes');
|
||||||
|
this.bindElement('g u', '.auth-links__user-link', 'Your profile′s hypha');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!onEditPage) {
|
||||||
|
// Hypha shortcuts
|
||||||
|
pageShortcuts.group('Hypha', function () {
|
||||||
|
this.bindCollection('', 'article .wikilink', 'First 9 hypha′s links', 'Hypha link');
|
||||||
|
this.bindElement('p, Alt+ArrowLeft, Ctrl+Alt+ArrowLeft', '.prevnext__prev', 'Next hypha');
|
||||||
|
this.bindElement('n, Alt+ArrowRight, Ctrl+Alt+ArrowRight', '.prevnext__next', 'Previous hypha');
|
||||||
|
this.bindElement('s, Alt+ArrowUp, Ctrl+Alt+ArrowUp', $$('.navi-title a').slice(1, -1).slice(-1)[0], 'Parent hypha');
|
||||||
|
this.bindElement('c, Alt+ArrowDown, Ctrl+Alt+ArrowDown', '.subhyphae__link', 'First child hypha');
|
||||||
|
|
||||||
|
this.bindElement('v', '.hypha-info__link[href^="/hypha/"]', 'Go to hypha′s page');
|
||||||
|
this.bindElement('e, ' + (isMac ? "Meta+Enter" : "Ctrl+Enter"), '.edit-btn__link[href^="/edit/"]', 'Edit this hypha');
|
||||||
|
this.bindElement('a', '.hypha-info__link[href^="/attachment/"]', 'Go to attachment');
|
||||||
|
this.bindElement('h', '.hypha-info__link[href^="/history/"]', 'Go to history');
|
||||||
|
this.bindElement('r', '.hypha-info__link[href^="/rename-ask/"]', 'Rename this hypha');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!onEditPage) {
|
} else {
|
||||||
// Hypha shortcuts
|
// Hypha editor shortcuts. These work only on editor's text area.
|
||||||
pageShortcuts.group('Hypha', function () {
|
let editorShortcuts = new ShortcutHandler(editTextarea, true);
|
||||||
this.bindCollection('', 'article .wikilink', 'First 9 hypha′s links', 'Hypha link');
|
|
||||||
this.bindElement('p, Alt+ArrowLeft, Ctrl+Alt+ArrowLeft', '.prevnext__prev', 'Next hypha');
|
|
||||||
this.bindElement('n, Alt+ArrowRight, Ctrl+Alt+ArrowRight', '.prevnext__next', 'Previous hypha');
|
|
||||||
this.bindElement('s, Alt+ArrowUp, Ctrl+Alt+ArrowUp', $$('.navi-title a').slice(1, -1).slice(-1)[0], 'Parent hypha');
|
|
||||||
this.bindElement('c, Alt+ArrowDown, Ctrl+Alt+ArrowDown', '.subhyphae__link', 'First child hypha');
|
|
||||||
|
|
||||||
this.bindElement('v', '.hypha-info__link[href^="/hypha/"]', 'Go to hypha′s page');
|
let shortcuts = [
|
||||||
this.bindElement('e, ' + (isMac ? "Meta+Enter" : "Ctrl+Enter"), '.edit-btn__link[href^="/edit/"]', 'Edit this hypha');
|
// Win+Linux Mac Action Description
|
||||||
this.bindElement('a', '.hypha-info__link[href^="/attachment/"]', 'Go to attachment');
|
['Ctrl+b', 'Meta+b', wrapBold, 'Format: Bold'],
|
||||||
this.bindElement('h', '.hypha-info__link[href^="/history/"]', 'Go to history');
|
['Ctrl+i', 'Meta+i', wrapItalic, 'Format: Italic'],
|
||||||
this.bindElement('r', '.hypha-info__link[href^="/rename-ask/"]', 'Rename this hypha');
|
['Ctrl+M', 'Meta+Shift+m', wrapMonospace, 'Format: Monospaced'],
|
||||||
});
|
['Ctrl+I', 'Meta+Shift+i', wrapHighlighted, 'Format: Highlight'],
|
||||||
|
['Ctrl+.', 'Meta+.', wrapLifted, 'Format: Superscript'],
|
||||||
|
['Ctrl+Comma', 'Meta+Comma', wrapLowered, 'Format: Subscript'],
|
||||||
|
['Ctrl+X', 'Meta+Shift+x', wrapStrikethrough, 'Format: Strikethrough'],
|
||||||
|
['Ctrl+k', 'Meta+k', wrapLink, 'Format: Inline link'],
|
||||||
|
// Apparently, ⌘; conflicts with a Safari's hotkey. Whatever.
|
||||||
|
['Ctrl+;', 'Meta+;', insertDate, 'Insert date UTC'],
|
||||||
|
];
|
||||||
|
|
||||||
} else {
|
editorShortcuts.group('Editor', function () {
|
||||||
// Hypha editor shortcuts. These work only on editor's text area.
|
for (let shortcut of shortcuts) {
|
||||||
let editorShortcuts = new ShortcutHandler(editTextarea, true);
|
if (isMac) {
|
||||||
|
this.add(shortcut[1], ...shortcut.slice(2))
|
||||||
let shortcuts = [
|
} else {
|
||||||
// Win+Linux Mac Action Description
|
this.add(shortcut[0], ...shortcut.slice(2))
|
||||||
['Ctrl+b', 'Meta+b', wrapBold, 'Format: Bold'],
|
|
||||||
['Ctrl+i', 'Meta+i', wrapItalic, 'Format: Italic'],
|
|
||||||
['Ctrl+M', 'Meta+Shift+m', wrapMonospace, 'Format: Monospaced'],
|
|
||||||
['Ctrl+I', 'Meta+Shift+i', wrapHighlighted, 'Format: Highlight'],
|
|
||||||
['Ctrl+.', 'Meta+.', wrapLifted, 'Format: Superscript'],
|
|
||||||
['Ctrl+Comma', 'Meta+Comma', wrapLowered, 'Format: Subscript'],
|
|
||||||
['Ctrl+X', 'Meta+Shift+x', wrapStrikethrough, 'Format: Strikethrough'],
|
|
||||||
['Ctrl+k', 'Meta+k', wrapLink, 'Format: Inline link'],
|
|
||||||
// Apparently, ⌘; conflicts with a Safari's hotkey. Whatever.
|
|
||||||
['Ctrl+;', 'Meta+;', insertDate, 'Insert date UTC'],
|
|
||||||
];
|
|
||||||
|
|
||||||
editorShortcuts.group('Editor', function () {
|
|
||||||
for (let shortcut of shortcuts) {
|
|
||||||
if (isMac) {
|
|
||||||
this.add(shortcut[1], ...shortcut.slice(2))
|
|
||||||
} else {
|
|
||||||
this.add(shortcut[0], ...shortcut.slice(2))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
editorShortcuts.group(function () {
|
editorShortcuts.group(function () {
|
||||||
this.bindElement(isMac ? 'Meta+Enter' : 'Ctrl+Enter', $('.edit-form__save'), 'Save changes');
|
this.bindElement(isMac ? 'Meta+Enter' : 'Ctrl+Enter', $('.edit-form__save'), 'Save changes');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
@ -44,8 +44,8 @@ function selectionWrapper(cursorPosition, prefix, postfix = null, el = editTexta
|
|||||||
// selection is decorated, so we just cut it
|
// selection is decorated, so we just cut it
|
||||||
removing = true
|
removing = true
|
||||||
result = text.substring(cursorPosition, text.length - cursorPosition)
|
result = text.substring(cursorPosition, text.length - cursorPosition)
|
||||||
} else if ( (prefix == el.value.slice(start-cursorPosition, start)) &&
|
} else if ( (prefix === el.value.slice(start-cursorPosition, start)) &&
|
||||||
(postfix == el.value.slice(end, end+cursorPosition)) ) {
|
(postfix === el.value.slice(end, end+cursorPosition)) ) {
|
||||||
// selection is surrounded by decorations
|
// selection is surrounded by decorations
|
||||||
removing = true
|
removing = true
|
||||||
result = text
|
result = text
|
||||||
|
9
tools.go
Normal file
9
tools.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
//go:build tools
|
||||||
|
// +build tools
|
||||||
|
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/chekoopa/go-localize"
|
||||||
|
_ "github.com/valyala/quicktemplate/qtc"
|
||||||
|
)
|
102
tree/tree.go
102
tree/tree.go
@ -11,22 +11,27 @@ import (
|
|||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findSiblingsAndDescendants(hyphaName string) ([]*sibling, map[string]bool) {
|
func findSiblings(hyphaName string) []*sibling {
|
||||||
hyphaDir := ""
|
parentHyphaName := ""
|
||||||
if hyphaRawDir := path.Dir(hyphaName); hyphaRawDir != "." {
|
if hyphaRawDir := path.Dir(hyphaName); hyphaRawDir != "." {
|
||||||
hyphaDir = hyphaRawDir
|
parentHyphaName = hyphaRawDir
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
siblingsMap = make(map[string]bool)
|
siblingsMap = make(map[string]bool)
|
||||||
siblingCheck = func(h *hyphae.Hypha) hyphae.CheckResult {
|
siblingCheck = func(h *hyphae.Hypha) hyphae.CheckResult {
|
||||||
// I don't like this double comparison, but it is only the way to circumvent some flickups
|
switch {
|
||||||
if strings.HasPrefix(h.Name, hyphaDir) && h.Name != hyphaDir && h.Name != hyphaName {
|
case h.Name == hyphaName, // Hypha is no sibling of itself
|
||||||
|
h.Name == parentHyphaName: // Parent hypha is no sibling of its child
|
||||||
|
return hyphae.CheckContinue
|
||||||
|
}
|
||||||
|
if (parentHyphaName != "" && strings.HasPrefix(h.Name, parentHyphaName+"/")) ||
|
||||||
|
(parentHyphaName == "") {
|
||||||
var (
|
var (
|
||||||
rawSubPath = strings.TrimPrefix(h.Name, hyphaDir)[1:]
|
rawSubPath = strings.TrimPrefix(h.Name, parentHyphaName)[1:]
|
||||||
slashIdx = strings.IndexRune(rawSubPath, '/')
|
slashIdx = strings.IndexRune(rawSubPath, '/')
|
||||||
)
|
)
|
||||||
if slashIdx > -1 {
|
if slashIdx > -1 {
|
||||||
var sibPath = h.Name[:slashIdx+len(hyphaDir)+1]
|
var sibPath = h.Name[:slashIdx+len(parentHyphaName)+1]
|
||||||
if _, exists := siblingsMap[sibPath]; !exists {
|
if _, exists := siblingsMap[sibPath]; !exists {
|
||||||
siblingsMap[sibPath] = false
|
siblingsMap[sibPath] = false
|
||||||
}
|
}
|
||||||
@ -37,20 +42,11 @@ func findSiblingsAndDescendants(hyphaName string) ([]*sibling, map[string]bool)
|
|||||||
return hyphae.CheckContinue
|
return hyphae.CheckContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
descendantsPool = make(map[string]bool, 0)
|
|
||||||
descendantCheck = func(h *hyphae.Hypha) hyphae.CheckResult {
|
|
||||||
if strings.HasPrefix(h.Name, hyphaName+"/") {
|
|
||||||
descendantsPool[h.Name] = true
|
|
||||||
}
|
|
||||||
return hyphae.CheckContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
i7n = hyphae.NewIteration()
|
i7n = hyphae.NewIteration()
|
||||||
)
|
)
|
||||||
siblingsMap[hyphaName] = true
|
siblingsMap[hyphaName] = true
|
||||||
|
|
||||||
i7n.AddCheck(siblingCheck)
|
i7n.AddCheck(siblingCheck)
|
||||||
i7n.AddCheck(descendantCheck)
|
|
||||||
i7n.Ignite()
|
i7n.Ignite()
|
||||||
|
|
||||||
siblings := make([]*sibling, len(siblingsMap))
|
siblings := make([]*sibling, len(siblingsMap))
|
||||||
@ -62,7 +58,7 @@ func findSiblingsAndDescendants(hyphaName string) ([]*sibling, map[string]bool)
|
|||||||
sort.Slice(siblings, func(i, j int) bool {
|
sort.Slice(siblings, func(i, j int) bool {
|
||||||
return siblings[i].name < siblings[j].name
|
return siblings[i].name < siblings[j].name
|
||||||
})
|
})
|
||||||
return siblings, descendantsPool
|
return siblings
|
||||||
}
|
}
|
||||||
|
|
||||||
func countSubhyphae(siblings []*sibling) {
|
func countSubhyphae(siblings []*sibling) {
|
||||||
@ -90,21 +86,22 @@ func Tree(hyphaName string) (siblingsHTML, childrenHTML, prev, next string) {
|
|||||||
children := make([]child, 0)
|
children := make([]child, 0)
|
||||||
I := 0
|
I := 0
|
||||||
// The tree is generated in two iterations of hyphae storage:
|
// The tree is generated in two iterations of hyphae storage:
|
||||||
// 1. Find all siblings (sorted) and descendants' names
|
// 1. Find all siblings (sorted)
|
||||||
// 2. Count how many subhyphae siblings have
|
// 2. Count how many subhyphae siblings have
|
||||||
//
|
//
|
||||||
// We also have to figure out what is going on with the descendants: who is a child of whom. We do that in parallel with (2) because we can.
|
// We also have to figure out what is going on with the descendants: who is a child of whom. We do that in parallel with (2) because we can.
|
||||||
// One of the siblings is the hypha with name `hyphaName`
|
// One of the siblings is the hypha with name `hyphaName`
|
||||||
siblings, descendantsPool := findSiblingsAndDescendants(hyphaName)
|
var siblings []*sibling
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
go func() {
|
go func() {
|
||||||
|
siblings = findSiblings(hyphaName)
|
||||||
countSubhyphae(siblings)
|
countSubhyphae(siblings)
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
children = figureOutChildren(hyphaName, descendantsPool, true).children
|
children = figureOutChildren(hyphaName).children
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@ -132,32 +129,55 @@ type child struct {
|
|||||||
children []child
|
children []child
|
||||||
}
|
}
|
||||||
|
|
||||||
func figureOutChildren(hyphaName string, subhyphaePool map[string]bool, exists bool) child {
|
func figureOutChildren(hyphaName string) child {
|
||||||
var (
|
var (
|
||||||
nestLevel = strings.Count(hyphaName, "/")
|
descPrefix = hyphaName + "/"
|
||||||
adopted = make([]child, 0)
|
child = child{hyphaName, true, make([]child, 0)}
|
||||||
)
|
)
|
||||||
for subhyphaName := range subhyphaePool {
|
|
||||||
subnestLevel := strings.Count(subhyphaName, "/")
|
for desc := range hyphae.YieldExistingHyphae() {
|
||||||
if subnestLevel-1 == nestLevel && path.Dir(subhyphaName) == hyphaName {
|
var descName = desc.Name
|
||||||
delete(subhyphaePool, subhyphaName)
|
if strings.HasPrefix(descName, descPrefix) {
|
||||||
adopted = append(adopted, figureOutChildren(subhyphaName, subhyphaePool, true))
|
var subPath = strings.TrimPrefix(descName, descPrefix)
|
||||||
}
|
addHyphaToChild(descName, subPath, &child)
|
||||||
}
|
|
||||||
for descName := range subhyphaePool {
|
|
||||||
if strings.HasPrefix(descName, hyphaName) {
|
|
||||||
var (
|
|
||||||
rawSubPath = strings.TrimPrefix(descName, hyphaName)[1:]
|
|
||||||
slashIdx = strings.IndexRune(rawSubPath, '/')
|
|
||||||
)
|
|
||||||
if slashIdx > -1 {
|
|
||||||
var sibPath = descName[:slashIdx+len(hyphaName)+1]
|
|
||||||
adopted = append(adopted, figureOutChildren(sibPath, subhyphaePool, false))
|
|
||||||
} // `else` never happens?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return child{hyphaName, exists, adopted}
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
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"})
|
||||||
|
// when hyphaName = "root/a/b", subPath = "b", and child.name = "root/a"
|
||||||
|
// set .exists=true for "root/a/b", and create it if it isn't there already
|
||||||
|
var exists = !strings.Contains(subPath, "/")
|
||||||
|
if exists {
|
||||||
|
var subchild = findOrCreateSubchild(subPath, child)
|
||||||
|
subchild.exists = true
|
||||||
|
} else {
|
||||||
|
var (
|
||||||
|
firstSlash = strings.IndexRune(subPath, '/')
|
||||||
|
firstDir = subPath[:firstSlash]
|
||||||
|
restOfPath = subPath[firstSlash+1:]
|
||||||
|
subchild = findOrCreateSubchild(firstDir, child)
|
||||||
|
)
|
||||||
|
addHyphaToChild(hyphaName, restOfPath, subchild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOrCreateSubchild(name string, baseChild *child) *child {
|
||||||
|
// when name = "a", and baseChild.name = "root"
|
||||||
|
// if baseChild.children contains "root/a", return it
|
||||||
|
// else create it and return that
|
||||||
|
var fullName = baseChild.name + "/" + name
|
||||||
|
for i := range baseChild.children {
|
||||||
|
if baseChild.children[i].name == fullName {
|
||||||
|
return &baseChild.children[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baseChild.children = append(baseChild.children, child{fullName, false, make([]child, 0)})
|
||||||
|
return &baseChild.children[len(baseChild.children)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
type sibling struct {
|
type sibling struct {
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
{% import "path" %}
|
{% import "path" %}
|
||||||
{% import "github.com/bouncepaw/mycorrhiza/util" %}
|
{% import "github.com/bouncepaw/mycorrhiza/util" %}
|
||||||
|
|
||||||
{% func TreeHTML() %}
|
|
||||||
{% endfunc %}
|
|
||||||
|
|
||||||
Subhyphae links are recursive. It may end up looking like that if drawn with
|
Subhyphae links are recursive. It may end up looking like that if drawn with
|
||||||
pseudographics:
|
pseudographics:
|
||||||
╔══════════════╗
|
╔══════════════╗
|
||||||
|
@ -13,53 +13,6 @@ import "path"
|
|||||||
//line tree/view.qtpl:3
|
//line tree/view.qtpl:3
|
||||||
import "github.com/bouncepaw/mycorrhiza/util"
|
import "github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
|
||||||
//line tree/view.qtpl:5
|
|
||||||
import (
|
|
||||||
qtio422016 "io"
|
|
||||||
|
|
||||||
qt422016 "github.com/valyala/quicktemplate"
|
|
||||||
)
|
|
||||||
|
|
||||||
//line tree/view.qtpl:5
|
|
||||||
var (
|
|
||||||
_ = qtio422016.Copy
|
|
||||||
_ = qt422016.AcquireByteBuffer
|
|
||||||
)
|
|
||||||
|
|
||||||
//line tree/view.qtpl:5
|
|
||||||
func StreamTreeHTML(qw422016 *qt422016.Writer) {
|
|
||||||
//line tree/view.qtpl:5
|
|
||||||
qw422016.N().S(`
|
|
||||||
`)
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
}
|
|
||||||
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
func WriteTreeHTML(qq422016 qtio422016.Writer) {
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
StreamTreeHTML(qw422016)
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
qt422016.ReleaseWriter(qw422016)
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
}
|
|
||||||
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
func TreeHTML() string {
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
WriteTreeHTML(qb422016)
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
qs422016 := string(qb422016.B)
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
return qs422016
|
|
||||||
//line tree/view.qtpl:6
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subhyphae links are recursive. It may end up looking like that if drawn with
|
// Subhyphae links are recursive. It may end up looking like that if drawn with
|
||||||
// pseudographics:
|
// pseudographics:
|
||||||
// ╔══════════════╗
|
// ╔══════════════╗
|
||||||
@ -69,183 +22,196 @@ func TreeHTML() string {
|
|||||||
// ║╚════════════╝║
|
// ║╚════════════╝║
|
||||||
// ╚══════════════╝
|
// ╚══════════════╝
|
||||||
|
|
||||||
//line tree/view.qtpl:16
|
//line tree/view.qtpl:13
|
||||||
|
import (
|
||||||
|
qtio422016 "io"
|
||||||
|
|
||||||
|
qt422016 "github.com/valyala/quicktemplate"
|
||||||
|
)
|
||||||
|
|
||||||
|
//line tree/view.qtpl:13
|
||||||
|
var (
|
||||||
|
_ = qtio422016.Copy
|
||||||
|
_ = qt422016.AcquireByteBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
//line tree/view.qtpl:13
|
||||||
func streamchildHTML(qw422016 *qt422016.Writer, c *child) {
|
func streamchildHTML(qw422016 *qt422016.Writer, c *child) {
|
||||||
//line tree/view.qtpl:16
|
//line tree/view.qtpl:13
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:18
|
//line tree/view.qtpl:15
|
||||||
sort.Slice(c.children, func(i, j int) bool {
|
sort.Slice(c.children, func(i, j int) bool {
|
||||||
return c.children[i].name < c.children[j].name
|
return c.children[i].name < c.children[j].name
|
||||||
})
|
})
|
||||||
|
|
||||||
//line tree/view.qtpl:21
|
//line tree/view.qtpl:18
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li class="subhyphae__entry">
|
<li class="subhyphae__entry">
|
||||||
<a class="subhyphae__link `)
|
<a class="subhyphae__link `)
|
||||||
//line tree/view.qtpl:23
|
//line tree/view.qtpl:20
|
||||||
if !c.exists {
|
if !c.exists {
|
||||||
//line tree/view.qtpl:23
|
//line tree/view.qtpl:20
|
||||||
qw422016.N().S(`wikilink_new`)
|
qw422016.N().S(`wikilink_new`)
|
||||||
//line tree/view.qtpl:23
|
//line tree/view.qtpl:20
|
||||||
}
|
}
|
||||||
//line tree/view.qtpl:23
|
//line tree/view.qtpl:20
|
||||||
qw422016.N().S(`" href="/hypha/`)
|
qw422016.N().S(`" href="/hypha/`)
|
||||||
//line tree/view.qtpl:23
|
//line tree/view.qtpl:20
|
||||||
qw422016.E().S(c.name)
|
qw422016.E().S(c.name)
|
||||||
//line tree/view.qtpl:23
|
//line tree/view.qtpl:20
|
||||||
qw422016.N().S(`">
|
qw422016.N().S(`">
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:24
|
//line tree/view.qtpl:21
|
||||||
qw422016.E().S(util.BeautifulName(path.Base(c.name)))
|
qw422016.E().S(util.BeautifulName(path.Base(c.name)))
|
||||||
//line tree/view.qtpl:24
|
//line tree/view.qtpl:21
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</a>
|
</a>
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:26
|
//line tree/view.qtpl:23
|
||||||
if len(c.children) > 0 {
|
if len(c.children) > 0 {
|
||||||
//line tree/view.qtpl:26
|
//line tree/view.qtpl:23
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<ul>
|
<ul>
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:28
|
//line tree/view.qtpl:25
|
||||||
for _, child := range c.children {
|
for _, child := range c.children {
|
||||||
//line tree/view.qtpl:28
|
//line tree/view.qtpl:25
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:29
|
//line tree/view.qtpl:26
|
||||||
qw422016.N().S(childHTML(&child))
|
qw422016.N().S(childHTML(&child))
|
||||||
//line tree/view.qtpl:29
|
//line tree/view.qtpl:26
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:30
|
//line tree/view.qtpl:27
|
||||||
}
|
}
|
||||||
//line tree/view.qtpl:30
|
//line tree/view.qtpl:27
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</ul>
|
</ul>
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:32
|
//line tree/view.qtpl:29
|
||||||
}
|
}
|
||||||
//line tree/view.qtpl:32
|
//line tree/view.qtpl:29
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</li>
|
</li>
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
}
|
}
|
||||||
|
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
func writechildHTML(qq422016 qtio422016.Writer, c *child) {
|
func writechildHTML(qq422016 qtio422016.Writer, c *child) {
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
streamchildHTML(qw422016, c)
|
streamchildHTML(qw422016, c)
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
}
|
}
|
||||||
|
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
func childHTML(c *child) string {
|
func childHTML(c *child) string {
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
writechildHTML(qb422016, c)
|
writechildHTML(qb422016, c)
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
return qs422016
|
return qs422016
|
||||||
//line tree/view.qtpl:34
|
//line tree/view.qtpl:31
|
||||||
}
|
}
|
||||||
|
|
||||||
//line tree/view.qtpl:37
|
//line tree/view.qtpl:34
|
||||||
func streamsiblingHTML(qw422016 *qt422016.Writer, s *sibling) {
|
func streamsiblingHTML(qw422016 *qt422016.Writer, s *sibling) {
|
||||||
//line tree/view.qtpl:37
|
//line tree/view.qtpl:34
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li class="sibling-hyphae__entry">
|
<li class="sibling-hyphae__entry">
|
||||||
<a class="sibling-hyphae__link `)
|
<a class="sibling-hyphae__link `)
|
||||||
//line tree/view.qtpl:39
|
//line tree/view.qtpl:36
|
||||||
if !s.exists {
|
if !s.exists {
|
||||||
//line tree/view.qtpl:39
|
//line tree/view.qtpl:36
|
||||||
qw422016.N().S(`wikilink_new`)
|
qw422016.N().S(`wikilink_new`)
|
||||||
//line tree/view.qtpl:39
|
//line tree/view.qtpl:36
|
||||||
}
|
}
|
||||||
//line tree/view.qtpl:39
|
//line tree/view.qtpl:36
|
||||||
qw422016.N().S(`" href="/hypha/`)
|
qw422016.N().S(`" href="/hypha/`)
|
||||||
//line tree/view.qtpl:39
|
//line tree/view.qtpl:36
|
||||||
qw422016.E().S(s.name)
|
qw422016.E().S(s.name)
|
||||||
//line tree/view.qtpl:39
|
//line tree/view.qtpl:36
|
||||||
qw422016.N().S(`">
|
qw422016.N().S(`">
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:40
|
//line tree/view.qtpl:37
|
||||||
qw422016.E().S(util.BeautifulName(path.Base(s.name)))
|
qw422016.E().S(util.BeautifulName(path.Base(s.name)))
|
||||||
//line tree/view.qtpl:40
|
//line tree/view.qtpl:37
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<span class="sibling-hyphae__count">
|
<span class="sibling-hyphae__count">
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:42
|
//line tree/view.qtpl:39
|
||||||
if s.directSubhyphaeCount > 0 {
|
if s.directSubhyphaeCount > 0 {
|
||||||
//line tree/view.qtpl:42
|
//line tree/view.qtpl:39
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<span class="sibling-hyphae__direct-count">
|
<span class="sibling-hyphae__direct-count">
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:44
|
//line tree/view.qtpl:41
|
||||||
qw422016.N().D(s.directSubhyphaeCount)
|
qw422016.N().D(s.directSubhyphaeCount)
|
||||||
//line tree/view.qtpl:44
|
//line tree/view.qtpl:41
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</span>
|
</span>
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:46
|
//line tree/view.qtpl:43
|
||||||
}
|
}
|
||||||
//line tree/view.qtpl:46
|
//line tree/view.qtpl:43
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:47
|
//line tree/view.qtpl:44
|
||||||
if s.indirectSubhyphaeCount > 0 {
|
if s.indirectSubhyphaeCount > 0 {
|
||||||
//line tree/view.qtpl:47
|
//line tree/view.qtpl:44
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<span class="sibling-hyphae__indirect-count">
|
<span class="sibling-hyphae__indirect-count">
|
||||||
(`)
|
(`)
|
||||||
//line tree/view.qtpl:49
|
//line tree/view.qtpl:46
|
||||||
qw422016.N().D(s.indirectSubhyphaeCount)
|
qw422016.N().D(s.indirectSubhyphaeCount)
|
||||||
//line tree/view.qtpl:49
|
//line tree/view.qtpl:46
|
||||||
qw422016.N().S(`)
|
qw422016.N().S(`)
|
||||||
</span>
|
</span>
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:51
|
//line tree/view.qtpl:48
|
||||||
}
|
}
|
||||||
//line tree/view.qtpl:51
|
//line tree/view.qtpl:48
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
`)
|
`)
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
}
|
}
|
||||||
|
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
func writesiblingHTML(qq422016 qtio422016.Writer, s *sibling) {
|
func writesiblingHTML(qq422016 qtio422016.Writer, s *sibling) {
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
streamsiblingHTML(qw422016, s)
|
streamsiblingHTML(qw422016, s)
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
}
|
}
|
||||||
|
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
func siblingHTML(s *sibling) string {
|
func siblingHTML(s *sibling) string {
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
writesiblingHTML(qb422016, s)
|
writesiblingHTML(qb422016, s)
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
return qs422016
|
return qs422016
|
||||||
//line tree/view.qtpl:55
|
//line tree/view.qtpl:52
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ func SaveUserDatabase() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func dumpUserCredentials() error {
|
func dumpUserCredentials() error {
|
||||||
userList := []*User{}
|
var userList []*User
|
||||||
|
|
||||||
// TODO: lock the map during saving to prevent corruption
|
// TODO: lock the map during saving to prevent corruption
|
||||||
for u := range YieldUsers() {
|
for u := range YieldUsers() {
|
||||||
@ -119,5 +119,8 @@ func dumpTokens() {
|
|||||||
log.Println(err)
|
log.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
os.WriteFile(files.TokensJSON(), blob, 0666)
|
err = os.WriteFile(files.TokensJSON(), blob, 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("an error occurred in dumpTokens function:", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ func Register(username, password, group, source string, force bool) error {
|
|||||||
username = util.CanonicalName(username)
|
username = util.CanonicalName(username)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case !util.IsPossibleUsername(username):
|
case !IsValidUsername(username):
|
||||||
return fmt.Errorf("illegal username ‘%s’", username)
|
return fmt.Errorf("illegal username ‘%s’", username)
|
||||||
case !ValidGroup(group):
|
case !ValidGroup(group):
|
||||||
return fmt.Errorf("invalid group ‘%s’", group)
|
return fmt.Errorf("invalid group ‘%s’", group)
|
||||||
|
35
user/user.go
35
user/user.go
@ -2,6 +2,8 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -9,7 +11,9 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User is a user (duh).
|
var usernamePattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}/]+`)
|
||||||
|
|
||||||
|
// User contains information about a given user required for identification.
|
||||||
type User struct {
|
type User struct {
|
||||||
// Name is a username. It must follow hypha naming rules.
|
// Name is a username. It must follow hypha naming rules.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -93,12 +97,10 @@ func (user *User) CanProceed(route string) bool {
|
|||||||
user.RLock()
|
user.RLock()
|
||||||
defer user.RUnlock()
|
defer user.RUnlock()
|
||||||
|
|
||||||
right, _ := groupRight[user.Group]
|
right := groupRight[user.Group]
|
||||||
minimalRight, _ := minimalRights[route]
|
minimalRight := minimalRights[route]
|
||||||
if right >= minimalRight {
|
|
||||||
return true
|
return right >= minimalRight
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) isCorrectPassword(password string) bool {
|
func (user *User) isCorrectPassword(password string) bool {
|
||||||
@ -117,3 +119,22 @@ func (user *User) ShowLockMaybe(w http.ResponseWriter, rq *http.Request) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValidUsername checks if the given username is valid.
|
||||||
|
func IsValidUsername(username string) bool {
|
||||||
|
return username != "anon" && username != "wikimind" &&
|
||||||
|
usernamePattern.MatchString(strings.TrimSpace(username)) &&
|
||||||
|
usernameIsWhiteListed(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func usernameIsWhiteListed(username string) bool {
|
||||||
|
if !cfg.UseWhiteList {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, allowedUsername := range cfg.WhiteList {
|
||||||
|
if allowedUsername == username {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -20,7 +20,7 @@ func YieldUsers() chan *User {
|
|||||||
|
|
||||||
// ListUsersWithGroup returns a slice with users of desired group.
|
// ListUsersWithGroup returns a slice with users of desired group.
|
||||||
func ListUsersWithGroup(group string) []string {
|
func ListUsersWithGroup(group string) []string {
|
||||||
filtered := []string{}
|
var filtered []string
|
||||||
for u := range YieldUsers() {
|
for u := range YieldUsers() {
|
||||||
if u.Group == group {
|
if u.Group == group {
|
||||||
filtered = append(filtered, u.Name)
|
filtered = append(filtered, u.Name)
|
||||||
|
28
util/util.go
28
util/util.go
@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/bouncepaw/mycorrhiza/files"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycomarkup/v3/util"
|
"github.com/bouncepaw/mycomarkup/v3/util"
|
||||||
@ -65,33 +64,6 @@ func CanonicalName(name string) string {
|
|||||||
return util.CanonicalName(name)
|
return util.CanonicalName(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// hyphaPattern is a pattern which all hypha names must match.
|
|
||||||
var hyphaPattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}]+`)
|
|
||||||
|
|
||||||
var usernamePattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}/]+`)
|
|
||||||
|
|
||||||
// IsCanonicalName checks if the `name` is canonical.
|
|
||||||
func IsCanonicalName(name string) bool {
|
|
||||||
return hyphaPattern.MatchString(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsPossibleUsername is true if the given username is ok. Same as IsCanonicalName, but cannot have / in it and cannot be equal to "anon" or "wikimind"
|
|
||||||
func IsPossibleUsername(username string) bool {
|
|
||||||
return username != "anon" && username != "wikimind" && usernameIsWhiteListed(username) && usernamePattern.MatchString(strings.TrimSpace(username))
|
|
||||||
}
|
|
||||||
|
|
||||||
func usernameIsWhiteListed(username string) bool {
|
|
||||||
if !cfg.UseWhiteList {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, allowedUsername := range cfg.WhiteList {
|
|
||||||
if allowedUsername == username {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// HyphaNameFromRq extracts hypha name from http request. You have to also pass the action which is embedded in the url or several actions. For url /hypha/hypha, the action would be "hypha".
|
// HyphaNameFromRq extracts hypha name from http request. You have to also pass the action which is embedded in the url or several actions. For url /hypha/hypha, the action would be "hypha".
|
||||||
func HyphaNameFromRq(rq *http.Request, actions ...string) string {
|
func HyphaNameFromRq(rq *http.Request, actions ...string) string {
|
||||||
p := rq.URL.Path
|
p := rq.URL.Path
|
||||||
|
@ -176,6 +176,7 @@ It outputs a poorly formatted JSON, but it works and is valid.
|
|||||||
<li>{%s lc.GetWithLocale(lang, "help.special_pages") %}
|
<li>{%s lc.GetWithLocale(lang, "help.special_pages") %}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/help/{%s lang %}/recent_changes">{%s lc.GetWithLocale(lang, "help.recent_changes") %}</a></li>
|
<li><a href="/help/{%s lang %}/recent_changes">{%s lc.GetWithLocale(lang, "help.recent_changes") %}</a></li>
|
||||||
|
<li><a href="/help/{%s lang %}/feeds">{%s lc.GetWithLocale(lang, "help.feeds") %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>{%s lc.GetWithLocale(lang, "help.configuration") %}
|
<li>{%s lc.GetWithLocale(lang, "help.configuration") %}
|
||||||
|
@ -707,41 +707,50 @@ func streamhelpTopicsHTML(qw422016 *qt422016.Writer, lang string, lc *l18n.Local
|
|||||||
//line views/stuff.qtpl:178
|
//line views/stuff.qtpl:178
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.recent_changes"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.recent_changes"))
|
||||||
//line views/stuff.qtpl:178
|
//line views/stuff.qtpl:178
|
||||||
|
qw422016.N().S(`</a></li>
|
||||||
|
<li><a href="/help/`)
|
||||||
|
//line views/stuff.qtpl:179
|
||||||
|
qw422016.E().S(lang)
|
||||||
|
//line views/stuff.qtpl:179
|
||||||
|
qw422016.N().S(`/feeds">`)
|
||||||
|
//line views/stuff.qtpl:179
|
||||||
|
qw422016.E().S(lc.GetWithLocale(lang, "help.feeds"))
|
||||||
|
//line views/stuff.qtpl:179
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>`)
|
<li>`)
|
||||||
//line views/stuff.qtpl:181
|
//line views/stuff.qtpl:182
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.configuration"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.configuration"))
|
||||||
//line views/stuff.qtpl:181
|
//line views/stuff.qtpl:182
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/help/`)
|
<li><a href="/help/`)
|
||||||
//line views/stuff.qtpl:183
|
//line views/stuff.qtpl:184
|
||||||
qw422016.E().S(lang)
|
qw422016.E().S(lang)
|
||||||
//line views/stuff.qtpl:183
|
//line views/stuff.qtpl:184
|
||||||
qw422016.N().S(`/lock">`)
|
qw422016.N().S(`/lock">`)
|
||||||
//line views/stuff.qtpl:183
|
//line views/stuff.qtpl:184
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.lock"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.lock"))
|
||||||
//line views/stuff.qtpl:183
|
//line views/stuff.qtpl:184
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
<li><a href="/help/`)
|
<li><a href="/help/`)
|
||||||
//line views/stuff.qtpl:184
|
//line views/stuff.qtpl:185
|
||||||
qw422016.E().S(lang)
|
qw422016.E().S(lang)
|
||||||
//line views/stuff.qtpl:184
|
//line views/stuff.qtpl:185
|
||||||
qw422016.N().S(`/whitelist">`)
|
qw422016.N().S(`/whitelist">`)
|
||||||
//line views/stuff.qtpl:184
|
//line views/stuff.qtpl:185
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.whitelist"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.whitelist"))
|
||||||
//line views/stuff.qtpl:184
|
//line views/stuff.qtpl:185
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
<li><a href="/help/`)
|
<li><a href="/help/`)
|
||||||
//line views/stuff.qtpl:185
|
//line views/stuff.qtpl:186
|
||||||
qw422016.E().S(lang)
|
qw422016.E().S(lang)
|
||||||
//line views/stuff.qtpl:185
|
//line views/stuff.qtpl:186
|
||||||
qw422016.N().S(`/telegram">`)
|
qw422016.N().S(`/telegram">`)
|
||||||
//line views/stuff.qtpl:185
|
//line views/stuff.qtpl:186
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.telegram"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.telegram"))
|
||||||
//line views/stuff.qtpl:185
|
//line views/stuff.qtpl:186
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
<li>...</li>
|
<li>...</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -749,91 +758,91 @@ func streamhelpTopicsHTML(qw422016 *qt422016.Writer, lang string, lc *l18n.Local
|
|||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
func writehelpTopicsHTML(qq422016 qtio422016.Writer, lang string, lc *l18n.Localizer) {
|
func writehelpTopicsHTML(qq422016 qtio422016.Writer, lang string, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
streamhelpTopicsHTML(qw422016, lang, lc)
|
streamhelpTopicsHTML(qw422016, lang, lc)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
func helpTopicsHTML(lang string, lc *l18n.Localizer) string {
|
func helpTopicsHTML(lang string, lc *l18n.Localizer) string {
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
writehelpTopicsHTML(qb422016, lang, lc)
|
writehelpTopicsHTML(qb422016, lang, lc)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:193
|
//line views/stuff.qtpl:194
|
||||||
func streamhelpTopicBadgeHTML(qw422016 *qt422016.Writer, lang, topic string) {
|
func streamhelpTopicBadgeHTML(qw422016 *qt422016.Writer, lang, topic string) {
|
||||||
//line views/stuff.qtpl:193
|
//line views/stuff.qtpl:194
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<a class="help-topic-badge" href="/help/`)
|
<a class="help-topic-badge" href="/help/`)
|
||||||
//line views/stuff.qtpl:194
|
//line views/stuff.qtpl:195
|
||||||
qw422016.E().S(lang)
|
qw422016.E().S(lang)
|
||||||
//line views/stuff.qtpl:194
|
//line views/stuff.qtpl:195
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:194
|
//line views/stuff.qtpl:195
|
||||||
qw422016.E().S(topic)
|
qw422016.E().S(topic)
|
||||||
//line views/stuff.qtpl:194
|
//line views/stuff.qtpl:195
|
||||||
qw422016.N().S(`">?</a>
|
qw422016.N().S(`">?</a>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
func writehelpTopicBadgeHTML(qq422016 qtio422016.Writer, lang, topic string) {
|
func writehelpTopicBadgeHTML(qq422016 qtio422016.Writer, lang, topic string) {
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
streamhelpTopicBadgeHTML(qw422016, lang, topic)
|
streamhelpTopicBadgeHTML(qw422016, lang, topic)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
func helpTopicBadgeHTML(lang, topic string) string {
|
func helpTopicBadgeHTML(lang, topic string) string {
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
writehelpTopicBadgeHTML(qb422016, lang, topic)
|
writehelpTopicBadgeHTML(qb422016, lang, topic)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:197
|
//line views/stuff.qtpl:198
|
||||||
func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:197
|
//line views/stuff.qtpl:198
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<main class="main-width user-list">
|
<main class="main-width user-list">
|
||||||
<h1>`)
|
<h1>`)
|
||||||
//line views/stuff.qtpl:200
|
//line views/stuff.qtpl:201
|
||||||
qw422016.E().S(lc.Get("ui.users_heading"))
|
qw422016.E().S(lc.Get("ui.users_heading"))
|
||||||
//line views/stuff.qtpl:200
|
//line views/stuff.qtpl:201
|
||||||
qw422016.N().S(`</h1>
|
qw422016.N().S(`</h1>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:202
|
//line views/stuff.qtpl:203
|
||||||
var (
|
var (
|
||||||
admins = make([]string, 0)
|
admins = make([]string, 0)
|
||||||
moderators = make([]string, 0)
|
moderators = make([]string, 0)
|
||||||
@ -853,149 +862,149 @@ func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
|||||||
sort.Strings(moderators)
|
sort.Strings(moderators)
|
||||||
sort.Strings(editors)
|
sort.Strings(editors)
|
||||||
|
|
||||||
//line views/stuff.qtpl:220
|
//line views/stuff.qtpl:221
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<section>
|
<section>
|
||||||
<h2>`)
|
<h2>`)
|
||||||
//line views/stuff.qtpl:222
|
//line views/stuff.qtpl:223
|
||||||
qw422016.E().S(lc.Get("ui.users_admins"))
|
qw422016.E().S(lc.Get("ui.users_admins"))
|
||||||
//line views/stuff.qtpl:222
|
//line views/stuff.qtpl:223
|
||||||
qw422016.N().S(`</h2>
|
qw422016.N().S(`</h2>
|
||||||
<ol>`)
|
<ol>`)
|
||||||
//line views/stuff.qtpl:223
|
//line views/stuff.qtpl:224
|
||||||
for _, name := range admins {
|
for _, name := range admins {
|
||||||
//line views/stuff.qtpl:223
|
//line views/stuff.qtpl:224
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li><a href="/hypha/`)
|
<li><a href="/hypha/`)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.E().S(cfg.UserHypha)
|
qw422016.E().S(cfg.UserHypha)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:225
|
//line views/stuff.qtpl:226
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:225
|
//line views/stuff.qtpl:226
|
||||||
qw422016.N().S(`</ol>
|
qw422016.N().S(`</ol>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>`)
|
<h2>`)
|
||||||
//line views/stuff.qtpl:228
|
//line views/stuff.qtpl:229
|
||||||
qw422016.E().S(lc.Get("ui.users_moderators"))
|
qw422016.E().S(lc.Get("ui.users_moderators"))
|
||||||
//line views/stuff.qtpl:228
|
//line views/stuff.qtpl:229
|
||||||
qw422016.N().S(`</h2>
|
qw422016.N().S(`</h2>
|
||||||
<ol>`)
|
<ol>`)
|
||||||
//line views/stuff.qtpl:229
|
//line views/stuff.qtpl:230
|
||||||
for _, name := range moderators {
|
for _, name := range moderators {
|
||||||
//line views/stuff.qtpl:229
|
//line views/stuff.qtpl:230
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li><a href="/hypha/`)
|
<li><a href="/hypha/`)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.E().S(cfg.UserHypha)
|
qw422016.E().S(cfg.UserHypha)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:231
|
//line views/stuff.qtpl:232
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:231
|
//line views/stuff.qtpl:232
|
||||||
qw422016.N().S(`</ol>
|
qw422016.N().S(`</ol>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>`)
|
<h2>`)
|
||||||
//line views/stuff.qtpl:234
|
//line views/stuff.qtpl:235
|
||||||
qw422016.E().S(lc.Get("ui.users_editors"))
|
qw422016.E().S(lc.Get("ui.users_editors"))
|
||||||
//line views/stuff.qtpl:234
|
//line views/stuff.qtpl:235
|
||||||
qw422016.N().S(`</h2>
|
qw422016.N().S(`</h2>
|
||||||
<ol>`)
|
<ol>`)
|
||||||
//line views/stuff.qtpl:235
|
//line views/stuff.qtpl:236
|
||||||
for _, name := range editors {
|
for _, name := range editors {
|
||||||
//line views/stuff.qtpl:235
|
//line views/stuff.qtpl:236
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li><a href="/hypha/`)
|
<li><a href="/hypha/`)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.E().S(cfg.UserHypha)
|
qw422016.E().S(cfg.UserHypha)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:237
|
//line views/stuff.qtpl:238
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:237
|
//line views/stuff.qtpl:238
|
||||||
qw422016.N().S(`</ol>
|
qw422016.N().S(`</ol>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
func WriteUserListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
func WriteUserListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
StreamUserListHTML(qw422016, lc)
|
StreamUserListHTML(qw422016, lc)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
func UserListHTML(lc *l18n.Localizer) string {
|
func UserListHTML(lc *l18n.Localizer) string {
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
WriteUserListHTML(qb422016, lc)
|
WriteUserListHTML(qb422016, lc)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:243
|
//line views/stuff.qtpl:244
|
||||||
func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:243
|
//line views/stuff.qtpl:244
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<h1>`)
|
<h1>`)
|
||||||
//line views/stuff.qtpl:246
|
//line views/stuff.qtpl:247
|
||||||
qw422016.E().S(lc.Get("ui.list_heading"))
|
qw422016.E().S(lc.Get("ui.list_heading"))
|
||||||
//line views/stuff.qtpl:246
|
//line views/stuff.qtpl:247
|
||||||
qw422016.N().S(`</h1>
|
qw422016.N().S(`</h1>
|
||||||
<p>`)
|
<p>`)
|
||||||
//line views/stuff.qtpl:247
|
//line views/stuff.qtpl:248
|
||||||
qw422016.E().S(lc.GetPlural("ui.list_desc", hyphae.Count()))
|
qw422016.E().S(lc.GetPlural("ui.list_desc", hyphae.Count()))
|
||||||
//line views/stuff.qtpl:247
|
//line views/stuff.qtpl:248
|
||||||
qw422016.N().S(`</p>
|
qw422016.N().S(`</p>
|
||||||
<ul class="hypha-list">
|
<ul class="hypha-list">
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:250
|
//line views/stuff.qtpl:251
|
||||||
hyphaNames := make(chan string)
|
hyphaNames := make(chan string)
|
||||||
sortedHypha := hyphae.PathographicSort(hyphaNames)
|
sortedHypha := hyphae.PathographicSort(hyphaNames)
|
||||||
for hypha := range hyphae.YieldExistingHyphae() {
|
for hypha := range hyphae.YieldExistingHyphae() {
|
||||||
@ -1003,252 +1012,252 @@ func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
|||||||
}
|
}
|
||||||
close(hyphaNames)
|
close(hyphaNames)
|
||||||
|
|
||||||
//line views/stuff.qtpl:256
|
//line views/stuff.qtpl:257
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:257
|
//line views/stuff.qtpl:258
|
||||||
for hyphaName := range sortedHypha {
|
for hyphaName := range sortedHypha {
|
||||||
//line views/stuff.qtpl:257
|
//line views/stuff.qtpl:258
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:258
|
//line views/stuff.qtpl:259
|
||||||
hypha := hyphae.ByName(hyphaName)
|
hypha := hyphae.ByName(hyphaName)
|
||||||
|
|
||||||
//line views/stuff.qtpl:258
|
//line views/stuff.qtpl:259
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li class="hypha-list__entry">
|
<li class="hypha-list__entry">
|
||||||
<a class="hypha-list__link" href="/hypha/`)
|
<a class="hypha-list__link" href="/hypha/`)
|
||||||
//line views/stuff.qtpl:260
|
//line views/stuff.qtpl:261
|
||||||
qw422016.E().S(hypha.Name)
|
qw422016.E().S(hypha.Name)
|
||||||
//line views/stuff.qtpl:260
|
//line views/stuff.qtpl:261
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:260
|
//line views/stuff.qtpl:261
|
||||||
qw422016.E().S(util.BeautifulName(hypha.Name))
|
qw422016.E().S(util.BeautifulName(hypha.Name))
|
||||||
//line views/stuff.qtpl:260
|
//line views/stuff.qtpl:261
|
||||||
qw422016.N().S(`</a>
|
qw422016.N().S(`</a>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:261
|
//line views/stuff.qtpl:262
|
||||||
if hypha.BinaryPath != "" {
|
if hypha.BinaryPath != "" {
|
||||||
//line views/stuff.qtpl:261
|
//line views/stuff.qtpl:262
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<span class="hypha-list__amnt-type">`)
|
<span class="hypha-list__amnt-type">`)
|
||||||
//line views/stuff.qtpl:262
|
//line views/stuff.qtpl:263
|
||||||
qw422016.E().S(filepath.Ext(hypha.BinaryPath)[1:])
|
qw422016.E().S(filepath.Ext(hypha.BinaryPath)[1:])
|
||||||
//line views/stuff.qtpl:262
|
//line views/stuff.qtpl:263
|
||||||
qw422016.N().S(`</span>
|
qw422016.N().S(`</span>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:263
|
//line views/stuff.qtpl:264
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:263
|
//line views/stuff.qtpl:264
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</li>
|
</li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:265
|
//line views/stuff.qtpl:266
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:265
|
//line views/stuff.qtpl:266
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
func WriteHyphaListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
func WriteHyphaListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
StreamHyphaListHTML(qw422016, lc)
|
StreamHyphaListHTML(qw422016, lc)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
func HyphaListHTML(lc *l18n.Localizer) string {
|
func HyphaListHTML(lc *l18n.Localizer) string {
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
WriteHyphaListHTML(qb422016, lc)
|
WriteHyphaListHTML(qb422016, lc)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:271
|
//line views/stuff.qtpl:272
|
||||||
func StreamAboutHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
func StreamAboutHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:271
|
//line views/stuff.qtpl:272
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<section>
|
<section>
|
||||||
<h1>`)
|
<h1>`)
|
||||||
//line views/stuff.qtpl:275
|
//line views/stuff.qtpl:276
|
||||||
qw422016.E().S(lc.Get("ui.about_title", &l18n.Replacements{"name": cfg.WikiName}))
|
qw422016.E().S(lc.Get("ui.about_title", &l18n.Replacements{"name": cfg.WikiName}))
|
||||||
//line views/stuff.qtpl:275
|
//line views/stuff.qtpl:276
|
||||||
qw422016.N().S(`</h1>
|
qw422016.N().S(`</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>`)
|
<li><b>`)
|
||||||
//line views/stuff.qtpl:277
|
//line views/stuff.qtpl:278
|
||||||
qw422016.N().S(lc.Get("ui.about_version", &l18n.Replacements{"pre": "<a href=\"https://mycorrhiza.wiki\">", "post": "</a>"}))
|
qw422016.N().S(lc.Get("ui.about_version", &l18n.Replacements{"pre": "<a href=\"https://mycorrhiza.wiki\">", "post": "</a>"}))
|
||||||
//line views/stuff.qtpl:277
|
//line views/stuff.qtpl:277
|
||||||
qw422016.N().S(`</b> 1.6.0</li>
|
qw422016.N().S(`</b> 1.6.0</li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:278
|
//line views/stuff.qtpl:279
|
||||||
if cfg.UseAuth {
|
if cfg.UseAuth {
|
||||||
//line views/stuff.qtpl:278
|
//line views/stuff.qtpl:279
|
||||||
qw422016.N().S(` <li><b>`)
|
qw422016.N().S(` <li><b>`)
|
||||||
//line views/stuff.qtpl:279
|
//line views/stuff.qtpl:280
|
||||||
qw422016.E().S(lc.Get("ui.about_usercount"))
|
qw422016.E().S(lc.Get("ui.about_usercount"))
|
||||||
//line views/stuff.qtpl:279
|
//line views/stuff.qtpl:280
|
||||||
qw422016.N().S(`</b> `)
|
qw422016.N().S(`</b> `)
|
||||||
//line views/stuff.qtpl:279
|
//line views/stuff.qtpl:280
|
||||||
qw422016.N().DUL(user.Count())
|
qw422016.N().DUL(user.Count())
|
||||||
//line views/stuff.qtpl:279
|
//line views/stuff.qtpl:280
|
||||||
qw422016.N().S(`</li>
|
qw422016.N().S(`</li>
|
||||||
<li><b>`)
|
<li><b>`)
|
||||||
//line views/stuff.qtpl:280
|
//line views/stuff.qtpl:281
|
||||||
qw422016.E().S(lc.Get("ui.about_homepage"))
|
qw422016.E().S(lc.Get("ui.about_homepage"))
|
||||||
//line views/stuff.qtpl:280
|
//line views/stuff.qtpl:281
|
||||||
qw422016.N().S(`</b> <a href="/">`)
|
qw422016.N().S(`</b> <a href="/">`)
|
||||||
//line views/stuff.qtpl:280
|
//line views/stuff.qtpl:281
|
||||||
qw422016.E().S(cfg.HomeHypha)
|
qw422016.E().S(cfg.HomeHypha)
|
||||||
//line views/stuff.qtpl:280
|
//line views/stuff.qtpl:281
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
<li><b>`)
|
<li><b>`)
|
||||||
//line views/stuff.qtpl:281
|
//line views/stuff.qtpl:282
|
||||||
qw422016.E().S(lc.Get("ui.about_admins"))
|
qw422016.E().S(lc.Get("ui.about_admins"))
|
||||||
//line views/stuff.qtpl:281
|
//line views/stuff.qtpl:282
|
||||||
qw422016.N().S(`</b>`)
|
qw422016.N().S(`</b>`)
|
||||||
//line views/stuff.qtpl:281
|
//line views/stuff.qtpl:282
|
||||||
for i, username := range user.ListUsersWithGroup("admin") {
|
for i, username := range user.ListUsersWithGroup("admin") {
|
||||||
//line views/stuff.qtpl:282
|
//line views/stuff.qtpl:283
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
//line views/stuff.qtpl:282
|
//line views/stuff.qtpl:283
|
||||||
qw422016.N().S(`<span aria-hidden="true">, </span>
|
qw422016.N().S(`<span aria-hidden="true">, </span>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:283
|
//line views/stuff.qtpl:284
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:283
|
//line views/stuff.qtpl:284
|
||||||
qw422016.N().S(` <a href="/hypha/`)
|
qw422016.N().S(` <a href="/hypha/`)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.E().S(cfg.UserHypha)
|
qw422016.E().S(cfg.UserHypha)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.E().S(username)
|
qw422016.E().S(username)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.E().S(username)
|
qw422016.E().S(username)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.N().S(`</a>`)
|
qw422016.N().S(`</a>`)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.N().S(`</li>
|
qw422016.N().S(`</li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:285
|
//line views/stuff.qtpl:286
|
||||||
} else {
|
} else {
|
||||||
//line views/stuff.qtpl:285
|
//line views/stuff.qtpl:286
|
||||||
qw422016.N().S(` <li>`)
|
qw422016.N().S(` <li>`)
|
||||||
//line views/stuff.qtpl:286
|
//line views/stuff.qtpl:287
|
||||||
qw422016.E().S(lc.Get("ui.about_noauth"))
|
qw422016.E().S(lc.Get("ui.about_noauth"))
|
||||||
//line views/stuff.qtpl:286
|
//line views/stuff.qtpl:287
|
||||||
qw422016.N().S(`</li>
|
qw422016.N().S(`</li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:287
|
//line views/stuff.qtpl:288
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:287
|
//line views/stuff.qtpl:288
|
||||||
qw422016.N().S(` </ul>
|
qw422016.N().S(` </ul>
|
||||||
<p>`)
|
<p>`)
|
||||||
//line views/stuff.qtpl:289
|
//line views/stuff.qtpl:290
|
||||||
qw422016.N().S(lc.Get("ui.about_hyphae", &l18n.Replacements{"link": "<a href=\"/list\">/list</a>"}))
|
qw422016.N().S(lc.Get("ui.about_hyphae", &l18n.Replacements{"link": "<a href=\"/list\">/list</a>"}))
|
||||||
//line views/stuff.qtpl:289
|
//line views/stuff.qtpl:290
|
||||||
qw422016.N().S(`</p>
|
qw422016.N().S(`</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
func WriteAboutHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
func WriteAboutHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
StreamAboutHTML(qw422016, lc)
|
StreamAboutHTML(qw422016, lc)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
func AboutHTML(lc *l18n.Localizer) string {
|
func AboutHTML(lc *l18n.Localizer) string {
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
WriteAboutHTML(qb422016, lc)
|
WriteAboutHTML(qb422016, lc)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:295
|
//line views/stuff.qtpl:296
|
||||||
func StreamCommonScripts(qw422016 *qt422016.Writer) {
|
func StreamCommonScripts(qw422016 *qt422016.Writer) {
|
||||||
//line views/stuff.qtpl:295
|
//line views/stuff.qtpl:296
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:296
|
//line views/stuff.qtpl:297
|
||||||
for _, scriptPath := range cfg.CommonScripts {
|
for _, scriptPath := range cfg.CommonScripts {
|
||||||
//line views/stuff.qtpl:296
|
//line views/stuff.qtpl:297
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<script src="`)
|
<script src="`)
|
||||||
//line views/stuff.qtpl:297
|
//line views/stuff.qtpl:298
|
||||||
qw422016.E().S(scriptPath)
|
qw422016.E().S(scriptPath)
|
||||||
//line views/stuff.qtpl:297
|
//line views/stuff.qtpl:298
|
||||||
qw422016.N().S(`"></script>
|
qw422016.N().S(`"></script>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:298
|
//line views/stuff.qtpl:299
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:298
|
//line views/stuff.qtpl:299
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
func WriteCommonScripts(qq422016 qtio422016.Writer) {
|
func WriteCommonScripts(qq422016 qtio422016.Writer) {
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
StreamCommonScripts(qw422016)
|
StreamCommonScripts(qw422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
func CommonScripts() string {
|
func CommonScripts() string {
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
WriteCommonScripts(qb422016)
|
WriteCommonScripts(qb422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
}
|
}
|
||||||
|
@ -61,8 +61,14 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// genericHandlerOfFeeds is a helper function for the web feed handlers.
|
// genericHandlerOfFeeds is a helper function for the web feed handlers.
|
||||||
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func() (string, error), name string, contentType string) {
|
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func(history.FeedOptions) (string, error), name string, contentType string) {
|
||||||
if content, err := f(); err != nil {
|
opts, err := history.ParseFeedOptions(rq.URL.Query())
|
||||||
|
var content string
|
||||||
|
if err == nil {
|
||||||
|
content, err = f(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
w.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
fmt.Fprint(w, "An error while generating "+name+": "+err.Error())
|
fmt.Fprint(w, "An error while generating "+name+": "+err.Error())
|
||||||
|
@ -35,7 +35,7 @@ func initMutators(r *mux.Router) {
|
|||||||
|
|
||||||
func factoryHandlerAsker(
|
func factoryHandlerAsker(
|
||||||
actionPath string,
|
actionPath string,
|
||||||
asker func(*user.User, *hyphae.Hypha) (string, error),
|
asker func(*user.User, *hyphae.Hypha, *l18n.Localizer) (string, error),
|
||||||
succTitleKey string,
|
succTitleKey string,
|
||||||
succPageTemplate func(*http.Request, string, bool) string,
|
succPageTemplate func(*http.Request, string, bool) string,
|
||||||
) func(http.ResponseWriter, *http.Request) {
|
) func(http.ResponseWriter, *http.Request) {
|
||||||
@ -47,7 +47,7 @@ func factoryHandlerAsker(
|
|||||||
u = user.FromRequest(rq)
|
u = user.FromRequest(rq)
|
||||||
lc = l18n.FromRequest(rq)
|
lc = l18n.FromRequest(rq)
|
||||||
)
|
)
|
||||||
if errtitle, err := asker(u, h); err != nil {
|
if errtitle, err := asker(u, h, lc); err != nil {
|
||||||
httpErr(
|
httpErr(
|
||||||
w,
|
w,
|
||||||
lc,
|
lc,
|
||||||
@ -112,15 +112,15 @@ func factoryHandlerConfirmer(
|
|||||||
|
|
||||||
var handlerUnattachConfirm = factoryHandlerConfirmer(
|
var handlerUnattachConfirm = factoryHandlerConfirmer(
|
||||||
"unattach-confirm",
|
"unattach-confirm",
|
||||||
func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.Op, string) {
|
func(h *hyphae.Hypha, u *user.User, rq *http.Request) (*history.Op, string) {
|
||||||
return shroom.UnattachHypha(u, h)
|
return shroom.UnattachHypha(u, h, l18n.FromRequest(rq))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
var handlerDeleteConfirm = factoryHandlerConfirmer(
|
var handlerDeleteConfirm = factoryHandlerConfirmer(
|
||||||
"delete-confirm",
|
"delete-confirm",
|
||||||
func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.Op, string) {
|
func(h *hyphae.Hypha, u *user.User, rq *http.Request) (*history.Op, string) {
|
||||||
return shroom.DeleteHypha(u, h)
|
return shroom.DeleteHypha(u, h, l18n.FromRequest(rq))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
|
|||||||
newHypha = hyphae.ByName(newName)
|
newHypha = hyphae.ByName(newName)
|
||||||
recursive = rq.PostFormValue("recursive") == "true"
|
recursive = rq.PostFormValue("recursive") == "true"
|
||||||
)
|
)
|
||||||
hop, errtitle := shroom.RenameHypha(oldHypha, newHypha, recursive, u)
|
hop, errtitle := shroom.RenameHypha(oldHypha, newHypha, recursive, u, lc)
|
||||||
if hop.HasErrors() {
|
if hop.HasErrors() {
|
||||||
httpErr(w, lc, http.StatusInternalServerError, hyphaName,
|
httpErr(w, lc, http.StatusInternalServerError, hyphaName,
|
||||||
errtitle,
|
errtitle,
|
||||||
@ -158,7 +158,7 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
|
|||||||
u = user.FromRequest(rq)
|
u = user.FromRequest(rq)
|
||||||
lc = l18n.FromRequest(rq)
|
lc = l18n.FromRequest(rq)
|
||||||
)
|
)
|
||||||
if errtitle, err := shroom.CanEdit(u, h); err != nil {
|
if errtitle, err := shroom.CanEdit(u, h, lc); err != nil {
|
||||||
httpErr(w, lc, http.StatusInternalServerError, hyphaName,
|
httpErr(w, lc, http.StatusInternalServerError, hyphaName,
|
||||||
errtitle,
|
errtitle,
|
||||||
err.Error())
|
err.Error())
|
||||||
@ -201,7 +201,7 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if action != "Preview" {
|
if action != "Preview" {
|
||||||
hop, errtitle = shroom.UploadText(h, []byte(textData), message, u)
|
hop, errtitle = shroom.UploadText(h, []byte(textData), message, u, lc)
|
||||||
if hop.HasErrors() {
|
if hop.HasErrors() {
|
||||||
httpErr(w, lc, http.StatusForbidden, hyphaName,
|
httpErr(w, lc, http.StatusForbidden, hyphaName,
|
||||||
errtitle,
|
errtitle,
|
||||||
@ -247,7 +247,7 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
|
|||||||
lc.Get("ui.error"),
|
lc.Get("ui.error"),
|
||||||
err.Error())
|
err.Error())
|
||||||
}
|
}
|
||||||
if errtitle, err := shroom.CanAttach(u, h); err != nil {
|
if errtitle, err := shroom.CanAttach(u, h, lc); err != nil {
|
||||||
httpErr(w, lc, http.StatusInternalServerError, hyphaName,
|
httpErr(w, lc, http.StatusInternalServerError, hyphaName,
|
||||||
errtitle,
|
errtitle,
|
||||||
err.Error())
|
err.Error())
|
||||||
@ -264,7 +264,7 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
|
|||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
mime = handler.Header.Get("Content-Type")
|
mime = handler.Header.Get("Content-Type")
|
||||||
hop, errtitle = shroom.UploadBinary(h, mime, file, u)
|
hop, errtitle = shroom.UploadBinary(h, mime, file, u, lc)
|
||||||
)
|
)
|
||||||
|
|
||||||
if hop.HasErrors() {
|
if hop.HasErrors() {
|
||||||
|
Loading…
Reference in New Issue
Block a user