mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-05 17:40: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
|
||||
config.mk
|
||||
|
10
Makefile
10
Makefile
@ -1,13 +1,15 @@
|
||||
WIKI=~/src/example-wiki
|
||||
.POSIX:
|
||||
include config.example.mk
|
||||
-include config.mk
|
||||
|
||||
run: build
|
||||
./mycorrhiza ${WIKI}
|
||||
./mycorrhiza ${WIKIPATH}
|
||||
|
||||
config_run: build
|
||||
./mycorrhiza ${WIKI}
|
||||
./mycorrhiza ${WIKIPATH}
|
||||
|
||||
devconfig_run: build
|
||||
./mycorrhiza ${WIKI}
|
||||
./mycorrhiza ${WIKIPATH}
|
||||
|
||||
build:
|
||||
go generate
|
||||
|
@ -1,13 +1,8 @@
|
||||
# 🍄 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">
|
||||
|
||||
[![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
|
||||
|
||||
* **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.
|
||||
type Telegram struct {
|
||||
TelegramBotToken string `comment:"Token of your bot.`
|
||||
TelegramBotName string `comment:"Username of your bot, sans @.`
|
||||
TelegramBotToken string `comment:"Token of your bot."`
|
||||
TelegramBotName string `comment:"Username of your bot, sans @."`
|
||||
}
|
||||
|
||||
// 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()
|
||||
log.SetOutput(wr)
|
||||
|
||||
handle := int(syscall.Stdin)
|
||||
handle := syscall.Stdin
|
||||
if !term.IsTerminal(handle) {
|
||||
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/kr/pretty v0.2.1 // 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/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
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/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU=
|
||||
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/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
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/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/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@ -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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
||||
github.com/valyala/quicktemplate v1.6.3 h1:O7EuMwuH7Q94U2CXD6sOX8AYHqQqWtmIk690IhmpkKA=
|
||||
github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||
github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
|
||||
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/crypto v0.0.0-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/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-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-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-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-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-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
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/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/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/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
|
||||
|
||||
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.**
|
||||
|
||||
|
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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/files"
|
||||
"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͡ʃ].
|
||||
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
||||
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
|
||||
}
|
||||
|
||||
// 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`.
|
||||
func Rename(from, to string) error {
|
||||
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)
|
||||
continue
|
||||
}
|
||||
if err := Rename(from, to); err != nil {
|
||||
hop.Errs = append(hop.Errs, err)
|
||||
}
|
||||
hop.gitop("mv", "--force", from, to)
|
||||
}
|
||||
}
|
||||
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"
|
||||
|
||||
"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.
|
||||
@ -20,7 +19,7 @@ func Index(path string) {
|
||||
}(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 {
|
||||
oh.MergeIn(h)
|
||||
} else {
|
||||
@ -32,7 +31,9 @@ func Index(path string) {
|
||||
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) {
|
||||
nodes, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
@ -40,10 +41,10 @@ func indexHelper(path string, nestLevel uint, ch chan *Hypha) {
|
||||
}
|
||||
|
||||
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 node.IsDir() &&
|
||||
util.IsCanonicalName(node.Name()) &&
|
||||
node.Name() != ".git" &&
|
||||
// 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 node.IsDir() && IsValidName(node.Name()) && node.Name() != ".git" &&
|
||||
!(nestLevel == 0 && node.Name() == "static") {
|
||||
indexHelper(filepath.Join(path, node.Name()), nestLevel+1, ch)
|
||||
continue
|
||||
|
@ -2,16 +2,31 @@
|
||||
package hyphae
|
||||
|
||||
import (
|
||||
"github.com/bouncepaw/mycorrhiza/files"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"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(`[^?!:#@><*|"'&%{}]+`)
|
||||
|
||||
// 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
|
||||
type Hypha struct {
|
||||
sync.RWMutex
|
||||
|
@ -79,7 +79,7 @@ func PathographicSort(src chan string) <-chan string {
|
||||
|
||||
// Subhyphae returns slice of subhyphae.
|
||||
func (h *Hypha) Subhyphae() []*Hypha {
|
||||
hyphae := []*Hypha{}
|
||||
var hyphae []*Hypha
|
||||
for subh := range YieldExistingHyphae() {
|
||||
if strings.HasPrefix(subh.Name, h.Name+"/") {
|
||||
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_title": "This entry does not exist!",
|
||||
"en.help.entry_not_found": "Entry not found",
|
||||
"en.help.feeds": "Feeds",
|
||||
"en.help.hypha": "Hypha",
|
||||
"en.help.interface": "Interface",
|
||||
"en.help.lock": "Lock",
|
||||
@ -123,6 +124,18 @@ var localizations = map[string]string{
|
||||
"en.ui.about_title": "About {{.name}}",
|
||||
"en.ui.about_usercount": "User count:",
|
||||
"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.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.",
|
||||
@ -205,8 +218,14 @@ var localizations = map[string]string{
|
||||
"en.ui.recent_title+other": "s",
|
||||
"en.ui.register": "Register",
|
||||
"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_noname": "No name given",
|
||||
"en.ui.rename_noname_tip": "No new name is given",
|
||||
"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_to": "New name",
|
||||
"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_title": "Этой страницы не существует!",
|
||||
"ru.help.entry_not_found": "Запись не найдена",
|
||||
"ru.help.feeds": "Ленты",
|
||||
"ru.help.hypha": "Гифа",
|
||||
"ru.help.interface": "Интерфейс",
|
||||
"ru.help.lock": "Блокировка",
|
||||
@ -338,6 +358,18 @@ var localizations = map[string]string{
|
||||
"ru.ui.about_title": "О вики «{{.name}}»",
|
||||
"ru.ui.about_usercount": "Число пользователей:",
|
||||
"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.ask_delete": "Удалить «%s»?",
|
||||
"ru.ui.ask_delete_tip": "В этой версии Микоризы нельзя отменить удаление гифы, но её история останется доступной.",
|
||||
@ -424,8 +456,14 @@ var localizations = map[string]string{
|
||||
"ru.ui.recent_title+one": "недавнее изменение",
|
||||
"ru.ui.register": "Регистрация",
|
||||
"ru.ui.reindex_no_rights": "Вы должны быть администратором, чтобы переиндексировать гифы.",
|
||||
"ru.ui.rename_badname": "Некорректное название",
|
||||
"ru.ui.rename_badname_tip": "Новое название некорректно. Названия не могут содержать символы {{.chars}}.",
|
||||
"ru.ui.rename_link": "Переименовать",
|
||||
"ru.ui.rename_noname": "Нет названия",
|
||||
"ru.ui.rename_noname_tip": "Не задано новое название",
|
||||
"ru.ui.rename_recurse": "Также переименовать подгифы",
|
||||
"ru.ui.rename_taken": "Название занято",
|
||||
"ru.ui.rename_taken_tip": "Гифа {{.name}} уже существует, не могу переименовать",
|
||||
"ru.ui.rename_tip": "Если вы переименуете эту гифу, сломаются все ссылки, ведущие на неё, а также исходящие относительные ссылки. Также вы потеряете всю текущую историю для нового названия. Переименовывайте аккуратно.",
|
||||
"ru.ui.rename_to": "Новое название",
|
||||
"ru.ui.revision_link": "Посмотреть код микоразметки для этой ревизии",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"sibling_hyphae": "Sibling hyphae",
|
||||
"special_pages": "Special pages",
|
||||
"recent_changes": "Recent changes",
|
||||
"feeds": "Feeds",
|
||||
"configuration": "Configuration (for administrators)",
|
||||
"lock": "Lock",
|
||||
"whitelist": "Whitelist",
|
||||
|
@ -43,6 +43,25 @@
|
||||
"rename_to": "New name",
|
||||
"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_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_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": "Гифы-сиблинги",
|
||||
"special_pages": "Специальные страницы",
|
||||
"recent_changes": "Недавние изменения",
|
||||
"feeds": "Ленты",
|
||||
"configuration": "Конфигурация (для администраторов)",
|
||||
"lock": "Блокировка",
|
||||
"whitelist": "Белый список",
|
||||
|
@ -45,6 +45,25 @@
|
||||
"rename_to": "Новое название",
|
||||
"rename_recurse": "Также переименовать подгифы",
|
||||
"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_tip": "В этой версии Микоризы нельзя отменить удаление гифы, но её история останется доступной.",
|
||||
|
1
main.go
1
main.go
@ -1,5 +1,6 @@
|
||||
//go:generate qtc -dir=views
|
||||
//go:generate qtc -dir=tree
|
||||
//go:generate qtc -dir=history
|
||||
//go:generate go-localize -input l18n_src -output l18n
|
||||
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
||||
package main
|
||||
|
@ -4,32 +4,33 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||
"github.com/bouncepaw/mycorrhiza/user"
|
||||
)
|
||||
|
||||
func canFactory(
|
||||
rejectLogger func(*hyphae.Hypha, *user.User, string),
|
||||
action string,
|
||||
dispatcher func(*hyphae.Hypha, *user.User) (string, string),
|
||||
dispatcher func(*hyphae.Hypha, *user.User, *l18n.Localizer) (string, string),
|
||||
noRightsMsg string,
|
||||
notExistsMsg string,
|
||||
careAboutExistence bool,
|
||||
) func(*user.User, *hyphae.Hypha) (string, error) {
|
||||
return func(u *user.User, h *hyphae.Hypha) (string, error) {
|
||||
mustExist bool,
|
||||
) func(*user.User, *hyphae.Hypha, *l18n.Localizer) (string, error) {
|
||||
return func(u *user.User, h *hyphae.Hypha, lc *l18n.Localizer) (string, error) {
|
||||
if !u.CanProceed(action) {
|
||||
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")
|
||||
return "Does not exist", errors.New(notExistsMsg)
|
||||
return lc.Get("ui.act_notexist"), errors.New(lc.Get(notExistsMsg))
|
||||
}
|
||||
|
||||
if dispatcher == nil {
|
||||
return "", nil
|
||||
}
|
||||
errmsg, errtitle := dispatcher(h, u)
|
||||
errmsg, errtitle := dispatcher(h, u, lc)
|
||||
if errtitle == "" {
|
||||
return "", nil
|
||||
}
|
||||
@ -43,8 +44,8 @@ var (
|
||||
rejectDeleteLog,
|
||||
"delete-confirm",
|
||||
nil,
|
||||
"Not enough rights to delete, you must be a moderator",
|
||||
"Cannot delete this hypha because it does not exist",
|
||||
"ui.act_norights_delete",
|
||||
"ui.act_notexist_delete",
|
||||
true,
|
||||
)
|
||||
|
||||
@ -52,24 +53,24 @@ var (
|
||||
rejectRenameLog,
|
||||
"rename-confirm",
|
||||
nil,
|
||||
"Not enough rights to rename, you must be a trusted editor",
|
||||
"Cannot rename this hypha because it does not exist",
|
||||
"ui.act_norights_rename",
|
||||
"ui.act_notexist_rename",
|
||||
true,
|
||||
)
|
||||
|
||||
CanUnattach = canFactory(
|
||||
rejectUnattachLog,
|
||||
"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 == "" {
|
||||
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 "", ""
|
||||
},
|
||||
"Not enough rights to unattach, you must be a trusted editor",
|
||||
"Cannot unattach this hypha because it does not exist",
|
||||
"ui.act_norights_unattach",
|
||||
"ui.act_notexist_unattach",
|
||||
true,
|
||||
)
|
||||
|
||||
@ -77,7 +78,7 @@ var (
|
||||
rejectEditLog,
|
||||
"upload-text",
|
||||
nil,
|
||||
"You must be an editor to edit a hypha",
|
||||
"ui.act_norights_edit",
|
||||
"You cannot edit a hypha that does not exist",
|
||||
false,
|
||||
)
|
||||
@ -86,8 +87,10 @@ var (
|
||||
rejectAttachLog,
|
||||
"upload-binary",
|
||||
nil,
|
||||
"You must be an editor to attach a hypha",
|
||||
"ui.act_norights_attach",
|
||||
"You cannot attach a hypha that does not exist",
|
||||
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/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||
"github.com/bouncepaw/mycorrhiza/user"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
if errtitle, err := CanDelete(u, h); errtitle != "" {
|
||||
if errtitle, err := CanDelete(u, h, lc); errtitle != "" {
|
||||
hop.WithErrAbort(err)
|
||||
return hop, errtitle
|
||||
}
|
||||
|
@ -7,40 +7,41 @@ import (
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/history"
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||
"github.com/bouncepaw/mycorrhiza/user"
|
||||
"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 {
|
||||
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 == "" {
|
||||
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))
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer newHypha.Unlock()
|
||||
hop = history.Operation(history.TypeRenameHypha)
|
||||
|
||||
if errtitle, err := CanRename(u, h); errtitle != "" {
|
||||
if errtitle, err := CanRename(u, h, lc); errtitle != "" {
|
||||
hop.WithErrAbort(err)
|
||||
return hop, errtitle
|
||||
}
|
||||
if errtitle, err := canRenameThisToThat(h, newHypha, u); errtitle != "" {
|
||||
if errtitle, err := canRenameThisToThat(h, newHypha, u, lc); errtitle != "" {
|
||||
hop.WithErrAbort(err)
|
||||
return hop, errtitle
|
||||
}
|
||||
|
@ -5,14 +5,15 @@ import (
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/history"
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||
"github.com/bouncepaw/mycorrhiza/user"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
if errtitle, err := CanUnattach(u, h); errtitle != "" {
|
||||
if errtitle, err := CanUnattach(u, h, lc); errtitle != "" {
|
||||
hop.WithErrAbort(err)
|
||||
return hop, errtitle
|
||||
}
|
||||
|
@ -14,12 +14,13 @@ import (
|
||||
"github.com/bouncepaw/mycorrhiza/files"
|
||||
"github.com/bouncepaw/mycorrhiza/history"
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||
"github.com/bouncepaw/mycorrhiza/mimetype"
|
||||
"github.com/bouncepaw/mycorrhiza/user"
|
||||
)
|
||||
|
||||
// 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)
|
||||
var action string
|
||||
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))
|
||||
}
|
||||
|
||||
if errtitle, err := CanEdit(u, h); err != nil {
|
||||
if errtitle, err := CanEdit(u, h, lc); err != nil {
|
||||
return hop.WithErrAbort(err), errtitle
|
||||
}
|
||||
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.
|
||||
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 (
|
||||
hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload attachment for ‘%s’ with type ‘%s’", h.Name, mime))
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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
|
||||
originalText = "" // for backlink update
|
||||
)
|
||||
// Reject if the path is outside the hyphae dir
|
||||
if !strings.HasPrefix(fullPath, files.HyphaeDir()) {
|
||||
if !isValidPath(fullPath) || !hyphae.IsValidName(h.Name) {
|
||||
err := errors.New("bad path")
|
||||
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(), ""
|
||||
}
|
||||
|
||||
func isValidPath(pathname string) bool {
|
||||
return strings.HasPrefix(pathname, files.HyphaeDir())
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
.non-existent-hypha { }
|
||||
.non-existent-hypha__ways { display: flex; flex-direction: column; width: 100%; margin: 0 0 1rem 0;}
|
||||
.non-existent-hypha__way { border: 1px #999 solid; border-radius: .25rem; padding: .25rem; }
|
||||
.non-existent-hypha__way {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-right: .25rem;
|
||||
}
|
||||
|
||||
.non-existent-hypha__title { margin-bottom: 1rem; }
|
||||
.non-existent-hypha__subtitle { margin: 0; }
|
||||
|
||||
@ -32,9 +35,16 @@ header { width: 100%; margin-bottom: 1rem; }
|
||||
}
|
||||
|
||||
@media screen and (min-width: 500px) {
|
||||
.non-existent-hypha__way { flex: 1; margin-right: .5rem; }
|
||||
.non-existent-hypha__ways { flex-direction: row; }
|
||||
.non-existent-hypha__way:last-child { margin-right: 0; }
|
||||
.non-existent-hypha__way {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.non-existent-hypha__ways::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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__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__message { width: 100%; margin: 0.25em 0; }
|
||||
.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; }
|
||||
}
|
||||
|
||||
/*
|
||||
* Shortcuts
|
||||
*/
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
min-width: 1.5ch;
|
||||
|
@ -1,21 +1,19 @@
|
||||
(function () {
|
||||
window.hyphaChanged = false;
|
||||
let textarea = document.querySelector('.edit-form__textarea');
|
||||
let form = document.querySelector('.edit-form');
|
||||
window.hyphaChanged = false;
|
||||
let textarea = document.querySelector('.edit-form__textarea');
|
||||
let form = document.querySelector('.edit-form');
|
||||
|
||||
let warnBeforeClosing = function (ev) {
|
||||
if (!window.hyphaChanged) return;
|
||||
ev.preventDefault();
|
||||
return ev.returnValue = 'Are you sure you want to exit? You have unsaved changes.';
|
||||
};
|
||||
let warnBeforeClosing = function (ev) {
|
||||
if (!window.hyphaChanged) return;
|
||||
ev.preventDefault();
|
||||
return ev.returnValue = 'Are you sure you want to exit? You have unsaved changes.';
|
||||
};
|
||||
|
||||
textarea.addEventListener('input', function () {
|
||||
window.hyphaChanged = true;
|
||||
});
|
||||
textarea.addEventListener('input', function () {
|
||||
window.hyphaChanged = true;
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function () {
|
||||
window.hyphaChanged = false;
|
||||
});
|
||||
form.addEventListener('submit', function () {
|
||||
window.hyphaChanged = false;
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', warnBeforeClosing);
|
||||
})();
|
||||
window.addEventListener('beforeunload', warnBeforeClosing);
|
||||
|
@ -1,367 +1,367 @@
|
||||
(() => {
|
||||
const $ = document.querySelector.bind(document);
|
||||
const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args));
|
||||
const $ = document.querySelector.bind(document);
|
||||
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) {
|
||||
let elideShift = event.key.toUpperCase() === event.key && event.shiftKey;
|
||||
return (event.ctrlKey ? 'Ctrl+' : '') +
|
||||
(event.altKey ? 'Alt+' : '') +
|
||||
(event.metaKey ? 'Meta+' : '') +
|
||||
(!elideShift && event.shiftKey ? 'Shift+' : '') +
|
||||
(event.key === ',' ? 'Comma' : event.key === ' ' ? 'Space' : event.key);
|
||||
function keyEventToShortcut(event) {
|
||||
let elideShift = event.key.toUpperCase() === event.key && event.shiftKey;
|
||||
return (event.ctrlKey ? 'Ctrl+' : '') +
|
||||
(event.altKey ? 'Alt+' : '') +
|
||||
(event.metaKey ? 'Meta+' : '') +
|
||||
(!elideShift && event.shiftKey ? 'Shift+' : '') +
|
||||
(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 keys = shortcut.split('+');
|
||||
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) {
|
||||
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]) {
|
||||
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;
|
||||
case 'Ctrl': keys[i] = '⌃'; break;
|
||||
case 'Alt': keys[i] = '⌥'; break;
|
||||
case 'Shift': keys[i] = '⇧'; break;
|
||||
case 'Meta': keys[i] = '⌘'; 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) {
|
||||
let name = element.nodeName.toLowerCase();
|
||||
return name === 'textarea' ||
|
||||
name === 'select' ||
|
||||
(name === 'input' && !['submit', 'reset', 'checkbox', 'radio'].includes(element.type)) ||
|
||||
element.isContentEditable;
|
||||
return keys.join(isMac ? '' : ' + ');
|
||||
}
|
||||
|
||||
function isTextField(element) {
|
||||
let name = element.nodeName.toLowerCase();
|
||||
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 = [];
|
||||
let shortcutsGroup = null;
|
||||
add(text, action, description = null, shownInHelp = true) {
|
||||
let shortcuts = text.trim().split(',').map(shortcut => shortcut.trim().split(' '));
|
||||
|
||||
class ShortcutHandler {
|
||||
constructor(element, override, filter = () => true) {
|
||||
this.element = element;
|
||||
this.map = {};
|
||||
this.active = this.map;
|
||||
this.override = override;
|
||||
this.filter = filter;
|
||||
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;
|
||||
|
||||
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 {
|
||||
constructor() {
|
||||
let template = $('#dialog-template');
|
||||
let clonedTemplate = template.content.cloneNode(true);
|
||||
this.backdrop = clonedTemplate.children[0];
|
||||
this.dialog = clonedTemplate.children[1];
|
||||
class ShortcutsHelpDialog {
|
||||
constructor() {
|
||||
let template = $('#dialog-template');
|
||||
let clonedTemplate = template.content.cloneNode(true);
|
||||
this.backdrop = clonedTemplate.children[0];
|
||||
this.dialog = clonedTemplate.children[1];
|
||||
|
||||
this.dialog.classList.add('shortcuts-help');
|
||||
this.dialog.hidden = true;
|
||||
this.backdrop.hidden = true;
|
||||
this.dialog.classList.add('shortcuts-help');
|
||||
this.dialog.hidden = true;
|
||||
this.backdrop.hidden = true;
|
||||
|
||||
document.body.appendChild(this.backdrop);
|
||||
document.body.appendChild(this.dialog);
|
||||
document.body.appendChild(this.backdrop);
|
||||
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__close-button').addEventListener('click', this.close);
|
||||
this.backdrop.addEventListener('click', this.close);
|
||||
this.dialog.querySelector('.dialog__title').textContent = 'List of shortcuts';
|
||||
this.dialog.querySelector('.dialog__close-button').addEventListener('click', this.close);
|
||||
this.backdrop.addEventListener('click', this.close);
|
||||
|
||||
this.shortcuts = new ShortcutHandler(this.dialog, false);
|
||||
this.shortcuts.add('Escape', this.close, null, false);
|
||||
this.shortcuts = new ShortcutHandler(this.dialog, false);
|
||||
this.shortcuts.add('Escape', this.close, null, false);
|
||||
|
||||
let shortcutsGroup;
|
||||
let shortcutsGroupTemplate = document.createElement('div');
|
||||
shortcutsGroupTemplate.className = 'shortcuts-group';
|
||||
let shortcutsGroup;
|
||||
let shortcutsGroupTemplate = document.createElement('div');
|
||||
shortcutsGroupTemplate.className = 'shortcuts-group';
|
||||
|
||||
for (let item of allShortcuts) {
|
||||
if (item.description && !item.shortcut) {
|
||||
shortcutsGroup = shortcutsGroupTemplate.cloneNode();
|
||||
this.dialog.querySelector('.dialog__content').appendChild(shortcutsGroup);
|
||||
for (let item of allShortcuts) {
|
||||
if (item.description && !item.shortcut) {
|
||||
shortcutsGroup = shortcutsGroupTemplate.cloneNode();
|
||||
this.dialog.querySelector('.dialog__content').appendChild(shortcutsGroup);
|
||||
|
||||
let heading = document.createElement('h2');
|
||||
heading.className = 'shortcuts-group-heading';
|
||||
heading.textContent = item.description;
|
||||
shortcutsGroup.appendChild(heading);
|
||||
let heading = document.createElement('h2');
|
||||
heading.className = 'shortcuts-group-heading';
|
||||
heading.textContent = item.description;
|
||||
shortcutsGroup.appendChild(heading);
|
||||
|
||||
} else {
|
||||
let list = document.createElement('ul');
|
||||
list.className = 'shortcuts-list';
|
||||
} else {
|
||||
let list = document.createElement('ul');
|
||||
list.className = 'shortcuts-list';
|
||||
|
||||
for (let shortcut of item) {
|
||||
let listItem = document.createElement('li');
|
||||
listItem.className = 'shortcut-row';
|
||||
list.appendChild(listItem);
|
||||
for (let shortcut of item) {
|
||||
let listItem = document.createElement('li');
|
||||
listItem.className = 'shortcut-row';
|
||||
list.appendChild(listItem);
|
||||
|
||||
let descriptionColumn = document.createElement('div')
|
||||
descriptionColumn.className = 'shortcut-row__description';
|
||||
descriptionColumn.textContent = shortcut.description;
|
||||
listItem.appendChild(descriptionColumn);
|
||||
let descriptionColumn = document.createElement('div')
|
||||
descriptionColumn.className = 'shortcut-row__description';
|
||||
descriptionColumn.textContent = shortcut.description;
|
||||
listItem.appendChild(descriptionColumn);
|
||||
|
||||
let shortcutColumn = document.createElement('div');
|
||||
shortcutColumn.className = 'shortcut-row__keys';
|
||||
shortcutColumn.innerHTML = shortcut.shortcut.split(',')
|
||||
.map(shortcuts => shortcuts.trim().split(' ').map(prettifyShortcut).join(' '))
|
||||
.join(' <span class="kbd-or">or</span> ');
|
||||
listItem.appendChild(shortcutColumn);
|
||||
}
|
||||
let shortcutColumn = document.createElement('div');
|
||||
shortcutColumn.className = 'shortcut-row__keys';
|
||||
shortcutColumn.innerHTML = shortcut.shortcut.split(',')
|
||||
.map(shortcuts => shortcuts.trim().split(' ').map(prettifyShortcut).join(' '))
|
||||
.join(' <span class="kbd-or">or</span> ');
|
||||
listItem.appendChild(shortcutColumn);
|
||||
}
|
||||
|
||||
if (shortcutsGroup) {
|
||||
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', () => {
|
||||
let helpDialog = null;
|
||||
let openHelp = () => {
|
||||
if (!helpDialog) helpDialog = new ShortcutsHelpDialog();
|
||||
helpDialog.open();
|
||||
};
|
||||
open() {
|
||||
this.prevActiveElement = document.activeElement;
|
||||
|
||||
let onEditPage = typeof editTextarea !== 'undefined';
|
||||
document.body.overflow = 'hidden';
|
||||
this.backdrop.hidden = false;
|
||||
this.dialog.hidden = false;
|
||||
this.dialog.focus();
|
||||
}
|
||||
|
||||
// Global shortcuts work everywhere.
|
||||
let globalShortcuts = new ShortcutHandler(document, false);
|
||||
globalShortcuts.add(isMac ? 'Meta+/' : 'Ctrl+/', openHelp);
|
||||
close() {
|
||||
document.body.overflow = '';
|
||||
this.backdrop.hidden = true;
|
||||
this.dialog.hidden = true;
|
||||
|
||||
// Page shortcuts work everywhere except on text fields.
|
||||
let pageShortcuts = new ShortcutHandler(document, false, notTextField);
|
||||
pageShortcuts.add('?', openHelp, null, false);
|
||||
if (this.prevActiveElement) {
|
||||
this.prevActiveElement.focus();
|
||||
this.prevActiveElement = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
window.addEventListener('load', () => {
|
||||
let helpDialog = null;
|
||||
let openHelp = () => {
|
||||
if (!helpDialog) helpDialog = new ShortcutsHelpDialog();
|
||||
helpDialog.open();
|
||||
};
|
||||
|
||||
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) {
|
||||
// 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');
|
||||
} else {
|
||||
// Hypha editor shortcuts. These work only on editor's text area.
|
||||
let editorShortcuts = new ShortcutHandler(editTextarea, true);
|
||||
|
||||
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');
|
||||
});
|
||||
let shortcuts = [
|
||||
// Win+Linux Mac Action Description
|
||||
['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'],
|
||||
];
|
||||
|
||||
} else {
|
||||
// Hypha editor shortcuts. These work only on editor's text area.
|
||||
let editorShortcuts = new ShortcutHandler(editTextarea, true);
|
||||
|
||||
let shortcuts = [
|
||||
// Win+Linux Mac Action Description
|
||||
['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('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 () {
|
||||
this.bindElement(isMac ? 'Meta+Enter' : 'Ctrl+Enter', $('.edit-form__save'), 'Save changes');
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
editorShortcuts.group(function () {
|
||||
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
|
||||
removing = true
|
||||
result = text.substring(cursorPosition, text.length - cursorPosition)
|
||||
} else if ( (prefix == el.value.slice(start-cursorPosition, start)) &&
|
||||
(postfix == el.value.slice(end, end+cursorPosition)) ) {
|
||||
} else if ( (prefix === el.value.slice(start-cursorPosition, start)) &&
|
||||
(postfix === el.value.slice(end, end+cursorPosition)) ) {
|
||||
// selection is surrounded by decorations
|
||||
removing = true
|
||||
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"
|
||||
)
|
||||
|
||||
func findSiblingsAndDescendants(hyphaName string) ([]*sibling, map[string]bool) {
|
||||
hyphaDir := ""
|
||||
func findSiblings(hyphaName string) []*sibling {
|
||||
parentHyphaName := ""
|
||||
if hyphaRawDir := path.Dir(hyphaName); hyphaRawDir != "." {
|
||||
hyphaDir = hyphaRawDir
|
||||
parentHyphaName = hyphaRawDir
|
||||
}
|
||||
var (
|
||||
siblingsMap = make(map[string]bool)
|
||||
siblingCheck = func(h *hyphae.Hypha) hyphae.CheckResult {
|
||||
// I don't like this double comparison, but it is only the way to circumvent some flickups
|
||||
if strings.HasPrefix(h.Name, hyphaDir) && h.Name != hyphaDir && h.Name != hyphaName {
|
||||
switch {
|
||||
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 (
|
||||
rawSubPath = strings.TrimPrefix(h.Name, hyphaDir)[1:]
|
||||
rawSubPath = strings.TrimPrefix(h.Name, parentHyphaName)[1:]
|
||||
slashIdx = strings.IndexRune(rawSubPath, '/')
|
||||
)
|
||||
if slashIdx > -1 {
|
||||
var sibPath = h.Name[:slashIdx+len(hyphaDir)+1]
|
||||
var sibPath = h.Name[:slashIdx+len(parentHyphaName)+1]
|
||||
if _, exists := siblingsMap[sibPath]; !exists {
|
||||
siblingsMap[sibPath] = false
|
||||
}
|
||||
@ -37,20 +42,11 @@ func findSiblingsAndDescendants(hyphaName string) ([]*sibling, map[string]bool)
|
||||
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()
|
||||
)
|
||||
siblingsMap[hyphaName] = true
|
||||
|
||||
i7n.AddCheck(siblingCheck)
|
||||
i7n.AddCheck(descendantCheck)
|
||||
i7n.Ignite()
|
||||
|
||||
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 {
|
||||
return siblings[i].name < siblings[j].name
|
||||
})
|
||||
return siblings, descendantsPool
|
||||
return siblings
|
||||
}
|
||||
|
||||
func countSubhyphae(siblings []*sibling) {
|
||||
@ -90,21 +86,22 @@ func Tree(hyphaName string) (siblingsHTML, childrenHTML, prev, next string) {
|
||||
children := make([]child, 0)
|
||||
I := 0
|
||||
// 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
|
||||
//
|
||||
// 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`
|
||||
siblings, descendantsPool := findSiblingsAndDescendants(hyphaName)
|
||||
var siblings []*sibling
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
siblings = findSiblings(hyphaName)
|
||||
countSubhyphae(siblings)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
children = figureOutChildren(hyphaName, descendantsPool, true).children
|
||||
children = figureOutChildren(hyphaName).children
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
@ -132,32 +129,55 @@ type child struct {
|
||||
children []child
|
||||
}
|
||||
|
||||
func figureOutChildren(hyphaName string, subhyphaePool map[string]bool, exists bool) child {
|
||||
func figureOutChildren(hyphaName string) child {
|
||||
var (
|
||||
nestLevel = strings.Count(hyphaName, "/")
|
||||
adopted = make([]child, 0)
|
||||
descPrefix = hyphaName + "/"
|
||||
child = child{hyphaName, true, make([]child, 0)}
|
||||
)
|
||||
for subhyphaName := range subhyphaePool {
|
||||
subnestLevel := strings.Count(subhyphaName, "/")
|
||||
if subnestLevel-1 == nestLevel && path.Dir(subhyphaName) == hyphaName {
|
||||
delete(subhyphaePool, subhyphaName)
|
||||
adopted = append(adopted, figureOutChildren(subhyphaName, subhyphaePool, true))
|
||||
}
|
||||
}
|
||||
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?
|
||||
|
||||
for desc := range hyphae.YieldExistingHyphae() {
|
||||
var descName = desc.Name
|
||||
if strings.HasPrefix(descName, descPrefix) {
|
||||
var subPath = strings.TrimPrefix(descName, descPrefix)
|
||||
addHyphaToChild(descName, subPath, &child)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -2,9 +2,6 @@
|
||||
{% import "path" %}
|
||||
{% import "github.com/bouncepaw/mycorrhiza/util" %}
|
||||
|
||||
{% func TreeHTML() %}
|
||||
{% endfunc %}
|
||||
|
||||
Subhyphae links are recursive. It may end up looking like that if drawn with
|
||||
pseudographics:
|
||||
╔══════════════╗
|
||||
|
@ -13,53 +13,6 @@ import "path"
|
||||
//line tree/view.qtpl:3
|
||||
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
|
||||
// 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) {
|
||||
//line tree/view.qtpl:16
|
||||
//line tree/view.qtpl:13
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line tree/view.qtpl:18
|
||||
//line tree/view.qtpl:15
|
||||
sort.Slice(c.children, func(i, j int) bool {
|
||||
return c.children[i].name < c.children[j].name
|
||||
})
|
||||
|
||||
//line tree/view.qtpl:21
|
||||
//line tree/view.qtpl:18
|
||||
qw422016.N().S(`
|
||||
<li class="subhyphae__entry">
|
||||
<a class="subhyphae__link `)
|
||||
//line tree/view.qtpl:23
|
||||
//line tree/view.qtpl:20
|
||||
if !c.exists {
|
||||
//line tree/view.qtpl:23
|
||||
//line tree/view.qtpl:20
|
||||
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/`)
|
||||
//line tree/view.qtpl:23
|
||||
//line tree/view.qtpl:20
|
||||
qw422016.E().S(c.name)
|
||||
//line tree/view.qtpl:23
|
||||
//line tree/view.qtpl:20
|
||||
qw422016.N().S(`">
|
||||
`)
|
||||
//line tree/view.qtpl:24
|
||||
//line tree/view.qtpl:21
|
||||
qw422016.E().S(util.BeautifulName(path.Base(c.name)))
|
||||
//line tree/view.qtpl:24
|
||||
//line tree/view.qtpl:21
|
||||
qw422016.N().S(`
|
||||
</a>
|
||||
`)
|
||||
//line tree/view.qtpl:26
|
||||
//line tree/view.qtpl:23
|
||||
if len(c.children) > 0 {
|
||||
//line tree/view.qtpl:26
|
||||
//line tree/view.qtpl:23
|
||||
qw422016.N().S(`
|
||||
<ul>
|
||||
`)
|
||||
//line tree/view.qtpl:28
|
||||
//line tree/view.qtpl:25
|
||||
for _, child := range c.children {
|
||||
//line tree/view.qtpl:28
|
||||
//line tree/view.qtpl:25
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line tree/view.qtpl:29
|
||||
//line tree/view.qtpl:26
|
||||
qw422016.N().S(childHTML(&child))
|
||||
//line tree/view.qtpl:29
|
||||
//line tree/view.qtpl:26
|
||||
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(`
|
||||
</ul>
|
||||
`)
|
||||
//line tree/view.qtpl:32
|
||||
//line tree/view.qtpl:29
|
||||
}
|
||||
//line tree/view.qtpl:32
|
||||
//line tree/view.qtpl:29
|
||||
qw422016.N().S(`
|
||||
</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) {
|
||||
//line tree/view.qtpl:34
|
||||
//line tree/view.qtpl:31
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line tree/view.qtpl:34
|
||||
//line tree/view.qtpl:31
|
||||
streamchildHTML(qw422016, c)
|
||||
//line tree/view.qtpl:34
|
||||
//line tree/view.qtpl:31
|
||||
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 {
|
||||
//line tree/view.qtpl:34
|
||||
//line tree/view.qtpl:31
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line tree/view.qtpl:34
|
||||
//line tree/view.qtpl:31
|
||||
writechildHTML(qb422016, c)
|
||||
//line tree/view.qtpl:34
|
||||
//line tree/view.qtpl:31
|
||||
qs422016 := string(qb422016.B)
|
||||
//line tree/view.qtpl:34
|
||||
//line tree/view.qtpl:31
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line tree/view.qtpl:34
|
||||
//line tree/view.qtpl:31
|
||||
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) {
|
||||
//line tree/view.qtpl:37
|
||||
//line tree/view.qtpl:34
|
||||
qw422016.N().S(`
|
||||
<li class="sibling-hyphae__entry">
|
||||
<a class="sibling-hyphae__link `)
|
||||
//line tree/view.qtpl:39
|
||||
//line tree/view.qtpl:36
|
||||
if !s.exists {
|
||||
//line tree/view.qtpl:39
|
||||
//line tree/view.qtpl:36
|
||||
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/`)
|
||||
//line tree/view.qtpl:39
|
||||
//line tree/view.qtpl:36
|
||||
qw422016.E().S(s.name)
|
||||
//line tree/view.qtpl:39
|
||||
//line tree/view.qtpl:36
|
||||
qw422016.N().S(`">
|
||||
`)
|
||||
//line tree/view.qtpl:40
|
||||
//line tree/view.qtpl:37
|
||||
qw422016.E().S(util.BeautifulName(path.Base(s.name)))
|
||||
//line tree/view.qtpl:40
|
||||
//line tree/view.qtpl:37
|
||||
qw422016.N().S(`
|
||||
<span class="sibling-hyphae__count">
|
||||
`)
|
||||
//line tree/view.qtpl:42
|
||||
//line tree/view.qtpl:39
|
||||
if s.directSubhyphaeCount > 0 {
|
||||
//line tree/view.qtpl:42
|
||||
//line tree/view.qtpl:39
|
||||
qw422016.N().S(`
|
||||
<span class="sibling-hyphae__direct-count">
|
||||
`)
|
||||
//line tree/view.qtpl:44
|
||||
//line tree/view.qtpl:41
|
||||
qw422016.N().D(s.directSubhyphaeCount)
|
||||
//line tree/view.qtpl:44
|
||||
//line tree/view.qtpl:41
|
||||
qw422016.N().S(`
|
||||
</span>
|
||||
`)
|
||||
//line tree/view.qtpl:46
|
||||
//line tree/view.qtpl:43
|
||||
}
|
||||
//line tree/view.qtpl:46
|
||||
//line tree/view.qtpl:43
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line tree/view.qtpl:47
|
||||
//line tree/view.qtpl:44
|
||||
if s.indirectSubhyphaeCount > 0 {
|
||||
//line tree/view.qtpl:47
|
||||
//line tree/view.qtpl:44
|
||||
qw422016.N().S(`
|
||||
<span class="sibling-hyphae__indirect-count">
|
||||
(`)
|
||||
//line tree/view.qtpl:49
|
||||
//line tree/view.qtpl:46
|
||||
qw422016.N().D(s.indirectSubhyphaeCount)
|
||||
//line tree/view.qtpl:49
|
||||
//line tree/view.qtpl:46
|
||||
qw422016.N().S(`)
|
||||
</span>
|
||||
`)
|
||||
//line tree/view.qtpl:51
|
||||
//line tree/view.qtpl:48
|
||||
}
|
||||
//line tree/view.qtpl:51
|
||||
//line tree/view.qtpl:48
|
||||
qw422016.N().S(`
|
||||
</span>
|
||||
</a>
|
||||
</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) {
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
streamsiblingHTML(qw422016, s)
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
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 {
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
writesiblingHTML(qb422016, s)
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
qs422016 := string(qb422016.B)
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
return qs422016
|
||||
//line tree/view.qtpl:55
|
||||
//line tree/view.qtpl:52
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ func SaveUserDatabase() error {
|
||||
}
|
||||
|
||||
func dumpUserCredentials() error {
|
||||
userList := []*User{}
|
||||
var userList []*User
|
||||
|
||||
// TODO: lock the map during saving to prevent corruption
|
||||
for u := range YieldUsers() {
|
||||
@ -119,5 +119,8 @@ func dumpTokens() {
|
||||
log.Println(err)
|
||||
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)
|
||||
|
||||
switch {
|
||||
case !util.IsPossibleUsername(username):
|
||||
case !IsValidUsername(username):
|
||||
return fmt.Errorf("illegal username ‘%s’", username)
|
||||
case !ValidGroup(group):
|
||||
return fmt.Errorf("invalid group ‘%s’", group)
|
||||
|
35
user/user.go
35
user/user.go
@ -2,6 +2,8 @@ package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -9,7 +11,9 @@ import (
|
||||
"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 {
|
||||
// Name is a username. It must follow hypha naming rules.
|
||||
Name string `json:"name"`
|
||||
@ -93,12 +97,10 @@ func (user *User) CanProceed(route string) bool {
|
||||
user.RLock()
|
||||
defer user.RUnlock()
|
||||
|
||||
right, _ := groupRight[user.Group]
|
||||
minimalRight, _ := minimalRights[route]
|
||||
if right >= minimalRight {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
right := groupRight[user.Group]
|
||||
minimalRight := minimalRights[route]
|
||||
|
||||
return right >= minimalRight
|
||||
}
|
||||
|
||||
func (user *User) isCorrectPassword(password string) bool {
|
||||
@ -117,3 +119,22 @@ func (user *User) ShowLockMaybe(w http.ResponseWriter, rq *http.Request) bool {
|
||||
}
|
||||
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.
|
||||
func ListUsersWithGroup(group string) []string {
|
||||
filtered := []string{}
|
||||
var filtered []string
|
||||
for u := range YieldUsers() {
|
||||
if u.Group == group {
|
||||
filtered = append(filtered, u.Name)
|
||||
|
28
util/util.go
28
util/util.go
@ -6,7 +6,6 @@ import (
|
||||
"github.com/bouncepaw/mycorrhiza/files"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/v3/util"
|
||||
@ -65,33 +64,6 @@ func CanonicalName(name string) string {
|
||||
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".
|
||||
func HyphaNameFromRq(rq *http.Request, actions ...string) string {
|
||||
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") %}
|
||||
<ul>
|
||||
<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>
|
||||
</li>
|
||||
<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
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.recent_changes"))
|
||||
//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>
|
||||
</ul>
|
||||
</li>
|
||||
<li>`)
|
||||
//line views/stuff.qtpl:181
|
||||
//line views/stuff.qtpl:182
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.configuration"))
|
||||
//line views/stuff.qtpl:181
|
||||
//line views/stuff.qtpl:182
|
||||
qw422016.N().S(`
|
||||
<ul>
|
||||
<li><a href="/help/`)
|
||||
//line views/stuff.qtpl:183
|
||||
//line views/stuff.qtpl:184
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:183
|
||||
//line views/stuff.qtpl:184
|
||||
qw422016.N().S(`/lock">`)
|
||||
//line views/stuff.qtpl:183
|
||||
//line views/stuff.qtpl:184
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.lock"))
|
||||
//line views/stuff.qtpl:183
|
||||
//line views/stuff.qtpl:184
|
||||
qw422016.N().S(`</a></li>
|
||||
<li><a href="/help/`)
|
||||
//line views/stuff.qtpl:184
|
||||
//line views/stuff.qtpl:185
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:184
|
||||
//line views/stuff.qtpl:185
|
||||
qw422016.N().S(`/whitelist">`)
|
||||
//line views/stuff.qtpl:184
|
||||
//line views/stuff.qtpl:185
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.whitelist"))
|
||||
//line views/stuff.qtpl:184
|
||||
//line views/stuff.qtpl:185
|
||||
qw422016.N().S(`</a></li>
|
||||
<li><a href="/help/`)
|
||||
//line views/stuff.qtpl:185
|
||||
//line views/stuff.qtpl:186
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:185
|
||||
//line views/stuff.qtpl:186
|
||||
qw422016.N().S(`/telegram">`)
|
||||
//line views/stuff.qtpl:185
|
||||
//line views/stuff.qtpl:186
|
||||
qw422016.E().S(lc.GetWithLocale(lang, "help.telegram"))
|
||||
//line views/stuff.qtpl:185
|
||||
//line views/stuff.qtpl:186
|
||||
qw422016.N().S(`</a></li>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
@ -749,91 +758,91 @@ func streamhelpTopicsHTML(qw422016 *qt422016.Writer, lang string, lc *l18n.Local
|
||||
</ul>
|
||||
</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) {
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
streamhelpTopicsHTML(qw422016, lang, lc)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
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 {
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
writehelpTopicsHTML(qb422016, lang, lc)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:191
|
||||
//line views/stuff.qtpl:192
|
||||
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) {
|
||||
//line views/stuff.qtpl:193
|
||||
//line views/stuff.qtpl:194
|
||||
qw422016.N().S(`
|
||||
<a class="help-topic-badge" href="/help/`)
|
||||
//line views/stuff.qtpl:194
|
||||
//line views/stuff.qtpl:195
|
||||
qw422016.E().S(lang)
|
||||
//line views/stuff.qtpl:194
|
||||
//line views/stuff.qtpl:195
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:194
|
||||
//line views/stuff.qtpl:195
|
||||
qw422016.E().S(topic)
|
||||
//line views/stuff.qtpl:194
|
||||
//line views/stuff.qtpl:195
|
||||
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) {
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
streamhelpTopicBadgeHTML(qw422016, lang, topic)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
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 {
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
writehelpTopicBadgeHTML(qb422016, lang, topic)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:195
|
||||
//line views/stuff.qtpl:196
|
||||
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) {
|
||||
//line views/stuff.qtpl:197
|
||||
//line views/stuff.qtpl:198
|
||||
qw422016.N().S(`
|
||||
<div class="layout">
|
||||
<main class="main-width user-list">
|
||||
<h1>`)
|
||||
//line views/stuff.qtpl:200
|
||||
//line views/stuff.qtpl:201
|
||||
qw422016.E().S(lc.Get("ui.users_heading"))
|
||||
//line views/stuff.qtpl:200
|
||||
//line views/stuff.qtpl:201
|
||||
qw422016.N().S(`</h1>
|
||||
`)
|
||||
//line views/stuff.qtpl:202
|
||||
//line views/stuff.qtpl:203
|
||||
var (
|
||||
admins = make([]string, 0)
|
||||
moderators = make([]string, 0)
|
||||
@ -853,149 +862,149 @@ func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||
sort.Strings(moderators)
|
||||
sort.Strings(editors)
|
||||
|
||||
//line views/stuff.qtpl:220
|
||||
//line views/stuff.qtpl:221
|
||||
qw422016.N().S(`
|
||||
<section>
|
||||
<h2>`)
|
||||
//line views/stuff.qtpl:222
|
||||
//line views/stuff.qtpl:223
|
||||
qw422016.E().S(lc.Get("ui.users_admins"))
|
||||
//line views/stuff.qtpl:222
|
||||
//line views/stuff.qtpl:223
|
||||
qw422016.N().S(`</h2>
|
||||
<ol>`)
|
||||
//line views/stuff.qtpl:223
|
||||
//line views/stuff.qtpl:224
|
||||
for _, name := range admins {
|
||||
//line views/stuff.qtpl:223
|
||||
//line views/stuff.qtpl:224
|
||||
qw422016.N().S(`
|
||||
<li><a href="/hypha/`)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:224
|
||||
//line views/stuff.qtpl:225
|
||||
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>
|
||||
</section>
|
||||
<section>
|
||||
<h2>`)
|
||||
//line views/stuff.qtpl:228
|
||||
//line views/stuff.qtpl:229
|
||||
qw422016.E().S(lc.Get("ui.users_moderators"))
|
||||
//line views/stuff.qtpl:228
|
||||
//line views/stuff.qtpl:229
|
||||
qw422016.N().S(`</h2>
|
||||
<ol>`)
|
||||
//line views/stuff.qtpl:229
|
||||
//line views/stuff.qtpl:230
|
||||
for _, name := range moderators {
|
||||
//line views/stuff.qtpl:229
|
||||
//line views/stuff.qtpl:230
|
||||
qw422016.N().S(`
|
||||
<li><a href="/hypha/`)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:230
|
||||
//line views/stuff.qtpl:231
|
||||
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>
|
||||
</section>
|
||||
<section>
|
||||
<h2>`)
|
||||
//line views/stuff.qtpl:234
|
||||
//line views/stuff.qtpl:235
|
||||
qw422016.E().S(lc.Get("ui.users_editors"))
|
||||
//line views/stuff.qtpl:234
|
||||
//line views/stuff.qtpl:235
|
||||
qw422016.N().S(`</h2>
|
||||
<ol>`)
|
||||
//line views/stuff.qtpl:235
|
||||
//line views/stuff.qtpl:236
|
||||
for _, name := range editors {
|
||||
//line views/stuff.qtpl:235
|
||||
//line views/stuff.qtpl:236
|
||||
qw422016.N().S(`
|
||||
<li><a href="/hypha/`)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
qw422016.E().S(name)
|
||||
//line views/stuff.qtpl:236
|
||||
//line views/stuff.qtpl:237
|
||||
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>
|
||||
</section>
|
||||
</main>
|
||||
</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) {
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
StreamUserListHTML(qw422016, lc)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
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 {
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
WriteUserListHTML(qb422016, lc)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:241
|
||||
//line views/stuff.qtpl:242
|
||||
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) {
|
||||
//line views/stuff.qtpl:243
|
||||
//line views/stuff.qtpl:244
|
||||
qw422016.N().S(`
|
||||
<div class="layout">
|
||||
<main class="main-width">
|
||||
<h1>`)
|
||||
//line views/stuff.qtpl:246
|
||||
//line views/stuff.qtpl:247
|
||||
qw422016.E().S(lc.Get("ui.list_heading"))
|
||||
//line views/stuff.qtpl:246
|
||||
//line views/stuff.qtpl:247
|
||||
qw422016.N().S(`</h1>
|
||||
<p>`)
|
||||
//line views/stuff.qtpl:247
|
||||
//line views/stuff.qtpl:248
|
||||
qw422016.E().S(lc.GetPlural("ui.list_desc", hyphae.Count()))
|
||||
//line views/stuff.qtpl:247
|
||||
//line views/stuff.qtpl:248
|
||||
qw422016.N().S(`</p>
|
||||
<ul class="hypha-list">
|
||||
`)
|
||||
//line views/stuff.qtpl:250
|
||||
//line views/stuff.qtpl:251
|
||||
hyphaNames := make(chan string)
|
||||
sortedHypha := hyphae.PathographicSort(hyphaNames)
|
||||
for hypha := range hyphae.YieldExistingHyphae() {
|
||||
@ -1003,252 +1012,252 @@ func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||
}
|
||||
close(hyphaNames)
|
||||
|
||||
//line views/stuff.qtpl:256
|
||||
//line views/stuff.qtpl:257
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line views/stuff.qtpl:257
|
||||
//line views/stuff.qtpl:258
|
||||
for hyphaName := range sortedHypha {
|
||||
//line views/stuff.qtpl:257
|
||||
//line views/stuff.qtpl:258
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line views/stuff.qtpl:258
|
||||
//line views/stuff.qtpl:259
|
||||
hypha := hyphae.ByName(hyphaName)
|
||||
|
||||
//line views/stuff.qtpl:258
|
||||
//line views/stuff.qtpl:259
|
||||
qw422016.N().S(`
|
||||
<li class="hypha-list__entry">
|
||||
<a class="hypha-list__link" href="/hypha/`)
|
||||
//line views/stuff.qtpl:260
|
||||
//line views/stuff.qtpl:261
|
||||
qw422016.E().S(hypha.Name)
|
||||
//line views/stuff.qtpl:260
|
||||
//line views/stuff.qtpl:261
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:260
|
||||
//line views/stuff.qtpl:261
|
||||
qw422016.E().S(util.BeautifulName(hypha.Name))
|
||||
//line views/stuff.qtpl:260
|
||||
//line views/stuff.qtpl:261
|
||||
qw422016.N().S(`</a>
|
||||
`)
|
||||
//line views/stuff.qtpl:261
|
||||
//line views/stuff.qtpl:262
|
||||
if hypha.BinaryPath != "" {
|
||||
//line views/stuff.qtpl:261
|
||||
//line views/stuff.qtpl:262
|
||||
qw422016.N().S(`
|
||||
<span class="hypha-list__amnt-type">`)
|
||||
//line views/stuff.qtpl:262
|
||||
//line views/stuff.qtpl:263
|
||||
qw422016.E().S(filepath.Ext(hypha.BinaryPath)[1:])
|
||||
//line views/stuff.qtpl:262
|
||||
//line views/stuff.qtpl:263
|
||||
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(`
|
||||
</li>
|
||||
`)
|
||||
//line views/stuff.qtpl:265
|
||||
//line views/stuff.qtpl:266
|
||||
}
|
||||
//line views/stuff.qtpl:265
|
||||
//line views/stuff.qtpl:266
|
||||
qw422016.N().S(`
|
||||
</ul>
|
||||
</main>
|
||||
</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) {
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
StreamHyphaListHTML(qw422016, lc)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
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 {
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
WriteHyphaListHTML(qb422016, lc)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:269
|
||||
//line views/stuff.qtpl:270
|
||||
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) {
|
||||
//line views/stuff.qtpl:271
|
||||
//line views/stuff.qtpl:272
|
||||
qw422016.N().S(`
|
||||
<div class="layout">
|
||||
<main class="main-width">
|
||||
<section>
|
||||
<h1>`)
|
||||
//line views/stuff.qtpl:275
|
||||
//line views/stuff.qtpl:276
|
||||
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>
|
||||
<ul>
|
||||
<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>"}))
|
||||
//line views/stuff.qtpl:277
|
||||
qw422016.N().S(`</b> 1.6.0</li>
|
||||
`)
|
||||
//line views/stuff.qtpl:278
|
||||
//line views/stuff.qtpl:279
|
||||
if cfg.UseAuth {
|
||||
//line views/stuff.qtpl:278
|
||||
//line views/stuff.qtpl:279
|
||||
qw422016.N().S(` <li><b>`)
|
||||
//line views/stuff.qtpl:279
|
||||
//line views/stuff.qtpl:280
|
||||
qw422016.E().S(lc.Get("ui.about_usercount"))
|
||||
//line views/stuff.qtpl:279
|
||||
//line views/stuff.qtpl:280
|
||||
qw422016.N().S(`</b> `)
|
||||
//line views/stuff.qtpl:279
|
||||
//line views/stuff.qtpl:280
|
||||
qw422016.N().DUL(user.Count())
|
||||
//line views/stuff.qtpl:279
|
||||
//line views/stuff.qtpl:280
|
||||
qw422016.N().S(`</li>
|
||||
<li><b>`)
|
||||
//line views/stuff.qtpl:280
|
||||
//line views/stuff.qtpl:281
|
||||
qw422016.E().S(lc.Get("ui.about_homepage"))
|
||||
//line views/stuff.qtpl:280
|
||||
//line views/stuff.qtpl:281
|
||||
qw422016.N().S(`</b> <a href="/">`)
|
||||
//line views/stuff.qtpl:280
|
||||
//line views/stuff.qtpl:281
|
||||
qw422016.E().S(cfg.HomeHypha)
|
||||
//line views/stuff.qtpl:280
|
||||
//line views/stuff.qtpl:281
|
||||
qw422016.N().S(`</a></li>
|
||||
<li><b>`)
|
||||
//line views/stuff.qtpl:281
|
||||
//line views/stuff.qtpl:282
|
||||
qw422016.E().S(lc.Get("ui.about_admins"))
|
||||
//line views/stuff.qtpl:281
|
||||
//line views/stuff.qtpl:282
|
||||
qw422016.N().S(`</b>`)
|
||||
//line views/stuff.qtpl:281
|
||||
//line views/stuff.qtpl:282
|
||||
for i, username := range user.ListUsersWithGroup("admin") {
|
||||
//line views/stuff.qtpl:282
|
||||
//line views/stuff.qtpl:283
|
||||
if i > 0 {
|
||||
//line views/stuff.qtpl:282
|
||||
//line views/stuff.qtpl:283
|
||||
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/`)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.E().S(cfg.UserHypha)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.N().S(`/`)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.E().S(username)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.N().S(`">`)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
qw422016.E().S(username)
|
||||
//line views/stuff.qtpl:284
|
||||
//line views/stuff.qtpl:285
|
||||
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>
|
||||
`)
|
||||
//line views/stuff.qtpl:285
|
||||
//line views/stuff.qtpl:286
|
||||
} else {
|
||||
//line views/stuff.qtpl:285
|
||||
//line views/stuff.qtpl:286
|
||||
qw422016.N().S(` <li>`)
|
||||
//line views/stuff.qtpl:286
|
||||
//line views/stuff.qtpl:287
|
||||
qw422016.E().S(lc.Get("ui.about_noauth"))
|
||||
//line views/stuff.qtpl:286
|
||||
//line views/stuff.qtpl:287
|
||||
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>
|
||||
<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>"}))
|
||||
//line views/stuff.qtpl:289
|
||||
//line views/stuff.qtpl:290
|
||||
qw422016.N().S(`</p>
|
||||
</section>
|
||||
</main>
|
||||
</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) {
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
StreamAboutHTML(qw422016, lc)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
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 {
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
WriteAboutHTML(qb422016, lc)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:293
|
||||
//line views/stuff.qtpl:294
|
||||
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) {
|
||||
//line views/stuff.qtpl:295
|
||||
//line views/stuff.qtpl:296
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line views/stuff.qtpl:296
|
||||
//line views/stuff.qtpl:297
|
||||
for _, scriptPath := range cfg.CommonScripts {
|
||||
//line views/stuff.qtpl:296
|
||||
//line views/stuff.qtpl:297
|
||||
qw422016.N().S(`
|
||||
<script src="`)
|
||||
//line views/stuff.qtpl:297
|
||||
//line views/stuff.qtpl:298
|
||||
qw422016.E().S(scriptPath)
|
||||
//line views/stuff.qtpl:297
|
||||
//line views/stuff.qtpl:298
|
||||
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(`
|
||||
`)
|
||||
//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) {
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
StreamCommonScripts(qw422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
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 {
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
WriteCommonScripts(qb422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qs422016 := string(qb422016.B)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line views/stuff.qtpl:299
|
||||
//line views/stuff.qtpl:300
|
||||
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.
|
||||
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func() (string, error), name string, contentType string) {
|
||||
if content, err := f(); err != nil {
|
||||
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func(history.FeedOptions) (string, error), name string, contentType string) {
|
||||
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.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, "An error while generating "+name+": "+err.Error())
|
||||
|
@ -35,7 +35,7 @@ func initMutators(r *mux.Router) {
|
||||
|
||||
func factoryHandlerAsker(
|
||||
actionPath string,
|
||||
asker func(*user.User, *hyphae.Hypha) (string, error),
|
||||
asker func(*user.User, *hyphae.Hypha, *l18n.Localizer) (string, error),
|
||||
succTitleKey string,
|
||||
succPageTemplate func(*http.Request, string, bool) string,
|
||||
) func(http.ResponseWriter, *http.Request) {
|
||||
@ -47,7 +47,7 @@ func factoryHandlerAsker(
|
||||
u = user.FromRequest(rq)
|
||||
lc = l18n.FromRequest(rq)
|
||||
)
|
||||
if errtitle, err := asker(u, h); err != nil {
|
||||
if errtitle, err := asker(u, h, lc); err != nil {
|
||||
httpErr(
|
||||
w,
|
||||
lc,
|
||||
@ -112,15 +112,15 @@ func factoryHandlerConfirmer(
|
||||
|
||||
var handlerUnattachConfirm = factoryHandlerConfirmer(
|
||||
"unattach-confirm",
|
||||
func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.Op, string) {
|
||||
return shroom.UnattachHypha(u, h)
|
||||
func(h *hyphae.Hypha, u *user.User, rq *http.Request) (*history.Op, string) {
|
||||
return shroom.UnattachHypha(u, h, l18n.FromRequest(rq))
|
||||
},
|
||||
)
|
||||
|
||||
var handlerDeleteConfirm = factoryHandlerConfirmer(
|
||||
"delete-confirm",
|
||||
func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.Op, string) {
|
||||
return shroom.DeleteHypha(u, h)
|
||||
func(h *hyphae.Hypha, u *user.User, rq *http.Request) (*history.Op, string) {
|
||||
return shroom.DeleteHypha(u, h, l18n.FromRequest(rq))
|
||||
},
|
||||
)
|
||||
|
||||
@ -136,7 +136,7 @@ func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
|
||||
newHypha = hyphae.ByName(newName)
|
||||
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() {
|
||||
httpErr(w, lc, http.StatusInternalServerError, hyphaName,
|
||||
errtitle,
|
||||
@ -158,7 +158,7 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
|
||||
u = user.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,
|
||||
errtitle,
|
||||
err.Error())
|
||||
@ -201,7 +201,7 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
|
||||
)
|
||||
|
||||
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() {
|
||||
httpErr(w, lc, http.StatusForbidden, hyphaName,
|
||||
errtitle,
|
||||
@ -247,7 +247,7 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
|
||||
lc.Get("ui.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,
|
||||
errtitle,
|
||||
err.Error())
|
||||
@ -264,7 +264,7 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
|
||||
}
|
||||
var (
|
||||
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user