1
0
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:
Timur Ismagilov 2021-11-06 01:57:18 +05:00 committed by GitHub
commit dbb45b2ded
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 2002 additions and 1179 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
mycorrhiza
config.mk

View File

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

View File

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

View File

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

@ -0,0 +1 @@
WIKIPATH=~/src/example-wiki

View File

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

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

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

View File

@ -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
View 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.
}

View File

@ -1,6 +1,6 @@
# Справка
Это документация к **вики-движку Микориза** версии **1.5** ([[https://mycorrhiza.wiki | официальная вики]]).
Это документация к **вики-движку Микориза** версии **1.6** ([[https://mycorrhiza.wiki | официальная вики]]).
**Выберите тему из списка.**

41
help/ru/feeds.myco Normal file
View 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
View 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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "Посмотреть код микоразметки для этой ревизии",

View File

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

View File

@ -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.",

View File

@ -18,6 +18,7 @@
"sibling_hyphae": "Гифы-сиблинги",
"special_pages": "Специальные страницы",
"recent_changes": "Недавние изменения",
"feeds": "Ленты",
"configuration": "Конфигурация (для администраторов)",
"lock": "Блокировка",
"whitelist": "Белый список",

View File

@ -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": "В этой версии Микоризы нельзя отменить удаление гифы, но её история останется доступной.",

View File

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

View File

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

View File

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

View File

@ -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>^?!:#@&gt;&lt;*|\"\\'&amp;%</code>")
return lc.Get("ui.rename_badname"), errors.New(lc.Get("ui.rename_badname_tip", &l18n.Replacements{"chars": "<code>^?!:#@&gt;&lt;*|\"\\'&amp;%</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
}

View File

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

View File

@ -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())
}

View File

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

View File

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

View File

@ -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 profiles 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 profiles hypha');
});
if (!onEditPage) {
// Hypha shortcuts
pageShortcuts.group('Hypha', function () {
this.bindCollection('', 'article .wikilink', 'First 9 hyphas 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 hyphas 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 hyphas 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 hyphas 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');
});
}
});

View File

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

@ -0,0 +1,9 @@
//go:build tools
// +build tools
package tools
import (
_ "github.com/chekoopa/go-localize"
_ "github.com/valyala/quicktemplate/qtc"
)

View File

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

View File

@ -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:
╔══════════════╗

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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") %}

View File

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

View File

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

View File

@ -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() {