mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2024-10-30 11:46:16 +00:00
Merge pull request #106 from pyelias/rss-grouping
Group recent-changes feeds (#61)
This commit is contained in:
commit
80f37420db
39
help/en/feeds.myco
Normal file
39
help/en/feeds.myco
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Help: Feeds
|
||||||
|
Mycorrhiza Wiki has RSS, Atom, and JSON feeds to track the latest changes on the wiki.
|
||||||
|
These feeds are linked on the [[/recent-changes | recent changes page]].
|
||||||
|
|
||||||
|
## Options
|
||||||
|
These feeds have options to combine related changes into groups:
|
||||||
|
* {
|
||||||
|
**period** Can be set to lengths of time like `5m`, `24h`, etc.
|
||||||
|
Edits by the same author that happen within this time of each other can be grouped into one item in the feed.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
**same** Can be set to `author`, `message`, or `none`.
|
||||||
|
Edits will only be grouped together if they have the same author or message. By default, edits need to have the same author and message. If it is `none`, all edits can be grouped.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
**order** Can be set to `old-to-now` (default) or `new-to-old`.
|
||||||
|
This determines what order edits in groups will be shown in in your feed.
|
||||||
|
}
|
||||||
|
|
||||||
|
If none of these options are set, changes will never be grouped.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
URLs for feeds using these options look like this:
|
||||||
|
* {
|
||||||
|
`/recent-changes-rss?period=1h`
|
||||||
|
Changes within one hour of each other with the same author and message will be grouped together.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?period=1h&order=new-to-old`
|
||||||
|
Same as the last one, but the groups will be shown in the opposite order.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?period=1h&same=none`
|
||||||
|
Changes within one hour of each other will be grouped together, even with different authors and messages.
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
`/recent-changes-atom?same=author&same=message`
|
||||||
|
Changes with the same author and message will be grouped together no matter how much time passes between them.
|
||||||
|
}
|
318
history/feed.go
Normal file
318
history/feed.go
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
package history
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
|
||||||
|
"github.com/gorilla/feeds"
|
||||||
|
)
|
||||||
|
|
||||||
|
const changeGroupMaxSize = 30
|
||||||
|
|
||||||
|
func recentChangesFeed(opts FeedOptions) *feeds.Feed {
|
||||||
|
feed := &feeds.Feed{
|
||||||
|
Title: cfg.WikiName + " (recent changes)",
|
||||||
|
Link: &feeds.Link{Href: cfg.URL},
|
||||||
|
Description: fmt.Sprintf("List of %d recent changes on the wiki", changeGroupMaxSize),
|
||||||
|
Updated: time.Now(),
|
||||||
|
}
|
||||||
|
revs := newRecentChangesStream()
|
||||||
|
groups := groupRevisions(revs, opts)
|
||||||
|
for _, grp := range groups {
|
||||||
|
item := grp.feedItem(opts)
|
||||||
|
feed.Add(&item)
|
||||||
|
}
|
||||||
|
return feed
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChangesRSS creates recent changes feed in RSS format.
|
||||||
|
func RecentChangesRSS(opts FeedOptions) (string, error) {
|
||||||
|
return recentChangesFeed(opts).ToRss()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChangesAtom creates recent changes feed in Atom format.
|
||||||
|
func RecentChangesAtom(opts FeedOptions) (string, error) {
|
||||||
|
return recentChangesFeed(opts).ToAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChangesJSON creates recent changes feed in JSON format.
|
||||||
|
func RecentChangesJSON(opts FeedOptions) (string, error) {
|
||||||
|
return recentChangesFeed(opts).ToJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// revisionGroup is a slice of revisions, ordered most recent first.
|
||||||
|
type revisionGroup []Revision
|
||||||
|
|
||||||
|
func newRevisionGroup(rev Revision) revisionGroup {
|
||||||
|
return revisionGroup([]Revision{rev})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp *revisionGroup) addRevision(rev Revision) {
|
||||||
|
*grp = append(*grp, rev)
|
||||||
|
}
|
||||||
|
|
||||||
|
// orderedIndex returns the ith revision in the group following the given order.
|
||||||
|
func (grp *revisionGroup) orderedIndex(i int, order feedGroupOrder) *Revision {
|
||||||
|
switch order {
|
||||||
|
case newToOld:
|
||||||
|
return &(*grp)[i]
|
||||||
|
case oldToNew:
|
||||||
|
return &(*grp)[len(*grp)-1-i]
|
||||||
|
}
|
||||||
|
// unreachable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
|
||||||
|
var (
|
||||||
|
currentYear int
|
||||||
|
currentMonth time.Month
|
||||||
|
)
|
||||||
|
for _, rev := range revs {
|
||||||
|
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
|
||||||
|
currentYear = rev.Time.Year()
|
||||||
|
currentMonth = rev.Time.Month()
|
||||||
|
res = append(res, newRevisionGroup(rev))
|
||||||
|
} else {
|
||||||
|
res[len(res)-1].addRevision(rev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupRevisions groups revisions for a feed.
|
||||||
|
// It returns the first changeGroupMaxSize (30) groups.
|
||||||
|
// The grouping parameter determines when two revisions will be grouped.
|
||||||
|
func groupRevisions(revs recentChangesStream, opts FeedOptions) (res []revisionGroup) {
|
||||||
|
nextRev := revs.iterator()
|
||||||
|
rev, empty := nextRev()
|
||||||
|
if empty {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
currGroup := newRevisionGroup(rev)
|
||||||
|
for rev, done := nextRev(); !done; rev, done = nextRev() {
|
||||||
|
if opts.canGroup(currGroup, rev) {
|
||||||
|
currGroup.addRevision(rev)
|
||||||
|
} else {
|
||||||
|
res = append(res, currGroup)
|
||||||
|
if len(res) == changeGroupMaxSize {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
currGroup = newRevisionGroup(rev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no more revisions, haven't added the last group yet
|
||||||
|
return append(res, currGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
|
||||||
|
title, author := grp.titleAndAuthor(opts.order)
|
||||||
|
return feeds.Item{
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Id: grp[len(grp)-1].Hash,
|
||||||
|
Description: grp.descriptionForFeed(opts.order),
|
||||||
|
Created: grp[len(grp)-1].Time, // earliest revision
|
||||||
|
Updated: grp[0].Time, // latest revision
|
||||||
|
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// titleAndAuthor creates a title and author for a feed item.
|
||||||
|
// If all messages and authors are the same (or there's just one rev), "message by author"
|
||||||
|
// If all authors are the same, "num edits (first message, ...) by author"
|
||||||
|
// Else (even if all messages are the same), "num edits (first message, ...)"
|
||||||
|
func (grp revisionGroup) titleAndAuthor(order feedGroupOrder) (title string, author *feeds.Author) {
|
||||||
|
allMessagesSame := true
|
||||||
|
allAuthorsSame := true
|
||||||
|
for _, rev := range grp[1:] {
|
||||||
|
if rev.Message != grp[0].Message {
|
||||||
|
allMessagesSame = false
|
||||||
|
}
|
||||||
|
if rev.Username != grp[0].Username {
|
||||||
|
allAuthorsSame = false
|
||||||
|
}
|
||||||
|
if !allMessagesSame && !allAuthorsSame {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allMessagesSame && allAuthorsSame {
|
||||||
|
title = grp[0].Message
|
||||||
|
} else {
|
||||||
|
title = fmt.Sprintf("%d edits (%s, ...)", len(grp), grp.orderedIndex(0, order).Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if allAuthorsSame {
|
||||||
|
title += fmt.Sprintf(" by %s", grp[0].Username)
|
||||||
|
author = &feeds.Author{Name: grp[0].Username}
|
||||||
|
} else {
|
||||||
|
author = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return title, author
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grp revisionGroup) descriptionForFeed(order feedGroupOrder) string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
for i := 0; i < len(grp); i++ {
|
||||||
|
desc := grp.orderedIndex(i, order).descriptionForFeed()
|
||||||
|
builder.WriteString(desc)
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type feedOptionParserState struct {
|
||||||
|
isAnythingSet bool
|
||||||
|
conds []groupingCondition
|
||||||
|
order feedGroupOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
// feedGrouping represents a set of conditions that must all be satisfied for revisions to be grouped.
|
||||||
|
// If there are no conditions, revisions will never be grouped.
|
||||||
|
type FeedOptions struct {
|
||||||
|
conds []groupingCondition
|
||||||
|
order feedGroupOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFeedOptions(query url.Values) (FeedOptions, error) {
|
||||||
|
parser := feedOptionParserState{}
|
||||||
|
|
||||||
|
err := parser.parseFeedGroupingPeriod(query)
|
||||||
|
if err != nil {
|
||||||
|
return FeedOptions{}, err
|
||||||
|
}
|
||||||
|
err = parser.parseFeedGroupingSame(query)
|
||||||
|
if err != nil {
|
||||||
|
return FeedOptions{}, err
|
||||||
|
}
|
||||||
|
err = parser.parseFeedGroupingOrder(query)
|
||||||
|
if err != nil {
|
||||||
|
return FeedOptions{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var conds []groupingCondition
|
||||||
|
if parser.isAnythingSet {
|
||||||
|
conds = parser.conds
|
||||||
|
} else {
|
||||||
|
// if no options are applied, do no grouping instead of using the default options
|
||||||
|
conds = nil
|
||||||
|
}
|
||||||
|
return FeedOptions{conds: conds, order: parser.order}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (parser *feedOptionParserState) parseFeedGroupingPeriod(query url.Values) error {
|
||||||
|
if query["period"] != nil {
|
||||||
|
parser.isAnythingSet = true
|
||||||
|
period, err := time.ParseDuration(query.Get("period"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parser.conds = append(parser.conds, periodGroupingCondition{period})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (parser *feedOptionParserState) parseFeedGroupingSame(query url.Values) error {
|
||||||
|
if same := query["same"]; same != nil {
|
||||||
|
parser.isAnythingSet = true
|
||||||
|
if len(same) == 1 && same[0] == "none" {
|
||||||
|
// same=none adds no condition
|
||||||
|
parser.conds = append(parser.conds, sameGroupingCondition{})
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// handle same=author, same=author&same=message, etc.
|
||||||
|
cond := sameGroupingCondition{}
|
||||||
|
for _, sameCond := range same {
|
||||||
|
switch sameCond {
|
||||||
|
case "author":
|
||||||
|
if cond.author {
|
||||||
|
return errors.New("set same=author twice")
|
||||||
|
}
|
||||||
|
cond.author = true
|
||||||
|
case "message":
|
||||||
|
if cond.message {
|
||||||
|
return errors.New("set same=message twice")
|
||||||
|
}
|
||||||
|
cond.message = true
|
||||||
|
default:
|
||||||
|
return errors.New("unknown same option " + sameCond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parser.conds = append(parser.conds, cond)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// same defaults to both author and message
|
||||||
|
// but this won't be applied if no grouping options are set
|
||||||
|
parser.conds = append(parser.conds, sameGroupingCondition{author: true, message: true})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type feedGroupOrder int
|
||||||
|
|
||||||
|
const (
|
||||||
|
newToOld feedGroupOrder = iota
|
||||||
|
oldToNew feedGroupOrder = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func (parser *feedOptionParserState) parseFeedGroupingOrder(query url.Values) error {
|
||||||
|
if order := query["order"]; order != nil {
|
||||||
|
parser.isAnythingSet = true
|
||||||
|
switch query.Get("order") {
|
||||||
|
case "old-to-new":
|
||||||
|
parser.order = oldToNew
|
||||||
|
case "new-to-old":
|
||||||
|
parser.order = newToOld
|
||||||
|
default:
|
||||||
|
return errors.New("unknown order option " + query.Get("order"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parser.order = oldToNew
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// canGroup determines whether a revision can be added to a group.
|
||||||
|
func (opts FeedOptions) canGroup(grp revisionGroup, rev Revision) bool {
|
||||||
|
if len(opts.conds) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cond := range opts.conds {
|
||||||
|
if !cond.canGroup(grp, rev) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupingCondition interface {
|
||||||
|
canGroup(grp revisionGroup, rev Revision) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// periodGroupingCondition will group two revisions if they are within period of each other.
|
||||||
|
type periodGroupingCondition struct {
|
||||||
|
period time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cond periodGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
|
||||||
|
return grp[len(grp)-1].Time.Sub(rev.Time) < cond.period
|
||||||
|
}
|
||||||
|
|
||||||
|
type sameGroupingCondition struct {
|
||||||
|
author bool
|
||||||
|
message bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c sameGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
|
||||||
|
return (!c.author || grp[0].Username == rev.Username) &&
|
||||||
|
(!c.message || grp[0].Message == rev.Message)
|
||||||
|
}
|
@ -4,14 +4,10 @@ package history
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/files"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
@ -54,131 +50,6 @@ func InitGitRepo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
|
|
||||||
type Revision struct {
|
|
||||||
Hash string
|
|
||||||
Username string
|
|
||||||
Time time.Time
|
|
||||||
Message string
|
|
||||||
filesAffectedBuf []string
|
|
||||||
hyphaeAffectedBuf []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// filesAffected tells what files have been affected by the revision.
|
|
||||||
func (rev *Revision) filesAffected() (filenames []string) {
|
|
||||||
if nil != rev.filesAffectedBuf {
|
|
||||||
return rev.filesAffectedBuf
|
|
||||||
}
|
|
||||||
// List of files affected by this revision, one per line.
|
|
||||||
out, err := silentGitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
|
|
||||||
// There's an error? Well, whatever, let's just assign an empty slice, who cares.
|
|
||||||
if err != nil {
|
|
||||||
rev.filesAffectedBuf = []string{}
|
|
||||||
} else {
|
|
||||||
rev.filesAffectedBuf = strings.Split(out.String(), "\n")
|
|
||||||
}
|
|
||||||
return rev.filesAffectedBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine what hyphae were affected by this revision
|
|
||||||
func (rev *Revision) hyphaeAffected() (hyphae []string) {
|
|
||||||
if nil != rev.hyphaeAffectedBuf {
|
|
||||||
return rev.hyphaeAffectedBuf
|
|
||||||
}
|
|
||||||
hyphae = make([]string, 0)
|
|
||||||
var (
|
|
||||||
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
|
|
||||||
set = make(map[string]bool)
|
|
||||||
isNewName = func(hyphaName string) bool {
|
|
||||||
if _, present := set[hyphaName]; present {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
set[hyphaName] = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
filesAffected = rev.filesAffected()
|
|
||||||
)
|
|
||||||
for _, filename := range filesAffected {
|
|
||||||
if strings.IndexRune(filename, '.') >= 0 {
|
|
||||||
dotPos := strings.LastIndexByte(filename, '.')
|
|
||||||
hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
|
|
||||||
if isNewName(hyphaName) {
|
|
||||||
hyphae = append(hyphae, hyphaName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rev.hyphaeAffectedBuf = hyphae
|
|
||||||
return hyphae
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeString returns a human readable time representation.
|
|
||||||
func (rev Revision) TimeString() string {
|
|
||||||
return rev.Time.Format(time.RFC822)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
|
||||||
func (rev Revision) HyphaeLinksHTML() (html string) {
|
|
||||||
hyphae := rev.hyphaeAffected()
|
|
||||||
for i, hyphaName := range hyphae {
|
|
||||||
if i > 0 {
|
|
||||||
html += `<span aria-hidden="true">, </span>`
|
|
||||||
}
|
|
||||||
html += fmt.Sprintf(`<a href="/hypha/%[1]s">%[1]s</a>`, hyphaName)
|
|
||||||
}
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
// descriptionForFeed generates a good enough HTML contents for a web feed.
|
|
||||||
func (rev *Revision) descriptionForFeed() (htmlDesc string) {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<p>%s</p>
|
|
||||||
<p><b>Hyphae affected:</b> %s</p>
|
|
||||||
<pre><code>%s</code></pre>`, rev.Message, rev.HyphaeLinksHTML(), html.EscapeString(rev.textDiff()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// textDiff generates a good enough diff to display in a web feed. It is not html-escaped.
|
|
||||||
func (rev *Revision) textDiff() (diff string) {
|
|
||||||
filenames, ok := rev.mycoFiles()
|
|
||||||
if !ok {
|
|
||||||
return "No text changes"
|
|
||||||
}
|
|
||||||
for _, filename := range filenames {
|
|
||||||
text, err := PrimitiveDiffAtRevision(filename, rev.Hash)
|
|
||||||
if err != nil {
|
|
||||||
diff += "\nAn error has occurred with " + filename + "\n"
|
|
||||||
}
|
|
||||||
diff += text + "\n"
|
|
||||||
}
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// mycoFiles returns filenames of .myco file. It is not ok if there are no myco files.
|
|
||||||
func (rev *Revision) mycoFiles() (filenames []string, ok bool) {
|
|
||||||
filenames = []string{}
|
|
||||||
for _, filename := range rev.filesAffected() {
|
|
||||||
if strings.HasSuffix(filename, ".myco") {
|
|
||||||
filenames = append(filenames, filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filenames, len(filenames) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try and guess what link is the most important by looking at the message.
|
|
||||||
func (rev *Revision) bestLink() string {
|
|
||||||
var (
|
|
||||||
revs = rev.hyphaeAffected()
|
|
||||||
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
|
|
||||||
)
|
|
||||||
switch {
|
|
||||||
case renameRes != nil:
|
|
||||||
return "/hypha/" + renameRes[1]
|
|
||||||
case len(revs) == 0:
|
|
||||||
return ""
|
|
||||||
default:
|
|
||||||
return "/hypha/" + revs[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// I pronounce it as [gɪt͡ʃ].
|
// I pronounce it as [gɪt͡ʃ].
|
||||||
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
||||||
func gitsh(args ...string) (out bytes.Buffer, err error) {
|
func gitsh(args ...string) (out bytes.Buffer, err error) {
|
||||||
@ -204,16 +75,6 @@ func silentGitsh(args ...string) (out bytes.Buffer, err error) {
|
|||||||
return *bytes.NewBuffer(b), err
|
return *bytes.NewBuffer(b), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted.
|
|
||||||
func unixTimestampAsTime(ts string) *time.Time {
|
|
||||||
i, err := strconv.ParseInt(ts, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tm := time.Unix(i, 0)
|
|
||||||
return &tm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename renames from `from` to `to` using `git mv`.
|
// Rename renames from `from` to `to` using `git mv`.
|
||||||
func Rename(from, to string) error {
|
func Rename(from, to string) error {
|
||||||
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
||||||
|
@ -1,199 +0,0 @@
|
|||||||
package history
|
|
||||||
|
|
||||||
// information.go
|
|
||||||
// Things related to gathering existing information.
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/files"
|
|
||||||
|
|
||||||
"github.com/gorilla/feeds"
|
|
||||||
)
|
|
||||||
|
|
||||||
func recentChangesFeed() *feeds.Feed {
|
|
||||||
feed := &feeds.Feed{
|
|
||||||
Title: "Recent changes",
|
|
||||||
Link: &feeds.Link{Href: cfg.URL},
|
|
||||||
Description: "List of 30 recent changes on the wiki",
|
|
||||||
Author: &feeds.Author{Name: "Wikimind", Email: "wikimind@mycorrhiza"},
|
|
||||||
Updated: time.Now(),
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
out, err = silentGitsh(
|
|
||||||
"log", "--oneline", "--no-merges",
|
|
||||||
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
|
|
||||||
"--max-count=30",
|
|
||||||
)
|
|
||||||
revs []Revision
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
for _, line := range strings.Split(out.String(), "\n") {
|
|
||||||
revs = append(revs, parseRevisionLine(line))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("Found %d recent changes", len(revs))
|
|
||||||
for _, rev := range revs {
|
|
||||||
feed.Add(&feeds.Item{
|
|
||||||
Title: rev.Message,
|
|
||||||
Author: &feeds.Author{Name: rev.Username},
|
|
||||||
Id: rev.Hash,
|
|
||||||
Description: rev.descriptionForFeed(),
|
|
||||||
Created: rev.Time,
|
|
||||||
Updated: rev.Time,
|
|
||||||
Link: &feeds.Link{Href: cfg.URL + rev.bestLink()},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return feed
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentChangesRSS creates recent changes feed in RSS format.
|
|
||||||
func RecentChangesRSS() (string, error) {
|
|
||||||
return recentChangesFeed().ToRss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentChangesAtom creates recent changes feed in Atom format.
|
|
||||||
func RecentChangesAtom() (string, error) {
|
|
||||||
return recentChangesFeed().ToAtom()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentChangesJSON creates recent changes feed in JSON format.
|
|
||||||
func RecentChangesJSON() (string, error) {
|
|
||||||
return recentChangesFeed().ToJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentChanges gathers an arbitrary number of latest changes in form of revisions slice.
|
|
||||||
func RecentChanges(n int) []Revision {
|
|
||||||
var (
|
|
||||||
out, err = silentGitsh(
|
|
||||||
"log", "--oneline", "--no-merges",
|
|
||||||
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
|
|
||||||
"--max-count="+strconv.Itoa(n),
|
|
||||||
)
|
|
||||||
revs []Revision
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
for _, line := range strings.Split(out.String(), "\n") {
|
|
||||||
revs = append(revs, parseRevisionLine(line))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("Found %d recent changes", len(revs))
|
|
||||||
return revs
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileChanged tells you if the file has been changed.
|
|
||||||
func FileChanged(path string) bool {
|
|
||||||
_, err := gitsh("diff", "--exit-code", path)
|
|
||||||
return err != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revisions returns slice of revisions for the given hypha name.
|
|
||||||
func Revisions(hyphaName string) ([]Revision, error) {
|
|
||||||
var (
|
|
||||||
out, err = silentGitsh(
|
|
||||||
"log", "--oneline", "--no-merges",
|
|
||||||
// Hash, author email, author time, commit msg separated by tab
|
|
||||||
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
|
|
||||||
"--", hyphaName+".*",
|
|
||||||
)
|
|
||||||
revs []Revision
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
for _, line := range strings.Split(out.String(), "\n") {
|
|
||||||
if line != "" {
|
|
||||||
revs = append(revs, parseRevisionLine(line))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("Found %d revisions for ‘%s’\n", len(revs), hyphaName)
|
|
||||||
return revs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
|
|
||||||
func WithRevisions(hyphaName string, revs []Revision) (html string) {
|
|
||||||
var (
|
|
||||||
currentYear int
|
|
||||||
currentMonth time.Month
|
|
||||||
)
|
|
||||||
for i, rev := range revs {
|
|
||||||
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
|
|
||||||
currentYear = rev.Time.Year()
|
|
||||||
currentMonth = rev.Time.Month()
|
|
||||||
if i != 0 {
|
|
||||||
html += `
|
|
||||||
</ul>
|
|
||||||
</section>`
|
|
||||||
}
|
|
||||||
html += fmt.Sprintf(`
|
|
||||||
<section class="history__month">
|
|
||||||
<a href="#%[1]d-%[2]d" class="history__month-anchor">
|
|
||||||
<h2 id="%[1]d-%[2]d" class="history__month-title">%[3]s</h2>
|
|
||||||
</a>
|
|
||||||
<ul class="history__entries">`,
|
|
||||||
currentYear, currentMonth,
|
|
||||||
strconv.Itoa(currentYear)+" "+rev.Time.Month().String())
|
|
||||||
}
|
|
||||||
html += rev.asHistoryEntry(hyphaName)
|
|
||||||
}
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rev *Revision) asHistoryEntry(hyphaName string) (html string) {
|
|
||||||
author := ""
|
|
||||||
if rev.Username != "anon" {
|
|
||||||
author = fmt.Sprintf(`
|
|
||||||
<span class="history-entry__author">by <a href="/hypha/%[1]s/%[2]s" rel="author">%[2]s</span>`, cfg.UserHypha, rev.Username)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
<li class="history__entry">
|
|
||||||
<a class="history-entry" href="/rev/%[3]s/%[1]s">
|
|
||||||
<time class="history-entry__time">%[2]s</time>
|
|
||||||
<span class="history-entry__hash"><a href="/primitive-diff/%[3]s/%[1]s">%[3]s</a></span>
|
|
||||||
<span class="history-entry__msg">%[4]s</span>
|
|
||||||
</a>%[5]s
|
|
||||||
</li>
|
|
||||||
`, hyphaName, rev.timeToDisplay(), rev.Hash, rev.Message, author)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return time like mm-dd 13:42
|
|
||||||
func (rev *Revision) timeToDisplay() string {
|
|
||||||
D := rev.Time.Day()
|
|
||||||
h, m, _ := rev.Time.Clock()
|
|
||||||
return fmt.Sprintf("%02d — %02d:%02d", D, h, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This regex is wrapped in "". For some reason, these quotes appear at some time and we have to get rid of them.
|
|
||||||
var revisionLinePattern = regexp.MustCompile("\"(.*)\t(.*)@.*\t(.*)\t(.*)\"")
|
|
||||||
|
|
||||||
func parseRevisionLine(line string) Revision {
|
|
||||||
results := revisionLinePattern.FindStringSubmatch(line)
|
|
||||||
return Revision{
|
|
||||||
Hash: results[1],
|
|
||||||
Username: results[2],
|
|
||||||
Time: *unixTimestampAsTime(results[3]),
|
|
||||||
Message: results[4],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileAtRevision shows how the file with the given file path looked at the commit with the hash. It may return an error if git fails.
|
|
||||||
func FileAtRevision(filepath, hash string) (string, error) {
|
|
||||||
out, err := gitsh("show", hash+":"+strings.TrimPrefix(filepath, files.HyphaeDir()+"/"))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return out.String(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrimitiveDiffAtRevision generates a plain-text diff for the given filepath at the commit with the given hash. It may return an error if git fails.
|
|
||||||
func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
|
|
||||||
out, err := silentGitsh("diff", "--unified=1", "--no-color", hash+"~", hash, "--", filepath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return out.String(), err
|
|
||||||
}
|
|
@ -95,9 +95,7 @@ func (hop *Op) WithFilesRenamed(pairs map[string]string) *Op {
|
|||||||
hop.Errs = append(hop.Errs, err)
|
hop.Errs = append(hop.Errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := Rename(from, to); err != nil {
|
hop.gitop("mv", "--force", from, to)
|
||||||
hop.Errs = append(hop.Errs, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hop
|
return hop
|
||||||
|
254
history/revision.go
Normal file
254
history/revision.go
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
package history
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
|
||||||
|
type Revision struct {
|
||||||
|
Hash string
|
||||||
|
Username string
|
||||||
|
Time time.Time
|
||||||
|
Message string
|
||||||
|
filesAffectedBuf []string
|
||||||
|
hyphaeAffectedBuf []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitLog calls `git log` and parses the results.
|
||||||
|
func gitLog(args ...string) ([]Revision, error) {
|
||||||
|
args = append([]string{
|
||||||
|
"log", "--abbrev-commit", "--no-merges",
|
||||||
|
"--pretty=format:%h\t%ae\t%at\t%s",
|
||||||
|
}, args...)
|
||||||
|
out, err := silentGitsh(args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outStr := out.String()
|
||||||
|
if outStr == "" {
|
||||||
|
// if there are no commits to return
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var revs []Revision
|
||||||
|
for _, line := range strings.Split(outStr, "\n") {
|
||||||
|
revs = append(revs, parseRevisionLine(line))
|
||||||
|
}
|
||||||
|
return revs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type recentChangesStream struct {
|
||||||
|
currHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRecentChangesStream() recentChangesStream {
|
||||||
|
// next returns the next n revisions from the stream, ordered most recent first.
|
||||||
|
// If there are less than n revisions remaining, it will return only those.
|
||||||
|
return recentChangesStream{currHash: ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stream *recentChangesStream) next(n int) []Revision {
|
||||||
|
args := []string{"--max-count=" + strconv.Itoa(n)}
|
||||||
|
if stream.currHash == "" {
|
||||||
|
args = append(args, "HEAD")
|
||||||
|
} else {
|
||||||
|
// currHash is the last revision from the last call, so skip it
|
||||||
|
args = append(args, "--skip=1", stream.currHash)
|
||||||
|
}
|
||||||
|
// I don't think this can fail, so ignore the error
|
||||||
|
res, _ := gitLog(args...)
|
||||||
|
if len(res) != 0 {
|
||||||
|
stream.currHash = res[len(res)-1].Hash
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// recentChangesIterator returns a function that returns successive revisions from the stream.
|
||||||
|
// It buffers revisions to avoid calling git every time.
|
||||||
|
func (stream recentChangesStream) iterator() func() (Revision, bool) {
|
||||||
|
var buf []Revision
|
||||||
|
return func() (Revision, bool) {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// no real reason to choose 30, just needs some large number
|
||||||
|
buf = stream.next(30)
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// revs has no revisions left
|
||||||
|
return Revision{}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rev := buf[0]
|
||||||
|
buf = buf[1:]
|
||||||
|
return rev, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentChanges gathers an arbitrary number of latest changes in form of revisions slice, ordered most recent first.
|
||||||
|
func RecentChanges(n int) []Revision {
|
||||||
|
stream := newRecentChangesStream()
|
||||||
|
revs := stream.next(n)
|
||||||
|
log.Printf("Found %d recent changes", len(revs))
|
||||||
|
return revs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revisions returns slice of revisions for the given hypha name, ordered most recent first.
|
||||||
|
func Revisions(hyphaName string) ([]Revision, error) {
|
||||||
|
revs, err := gitLog("--", hyphaName+".*")
|
||||||
|
log.Printf("Found %d revisions for ‘%s’\n", len(revs), hyphaName)
|
||||||
|
return revs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileChanged tells you if the file has been changed since the last commit.
|
||||||
|
func FileChanged(path string) bool {
|
||||||
|
_, err := gitsh("diff", "--exit-code", path)
|
||||||
|
return err != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return time like dd — 13:42
|
||||||
|
func (rev *Revision) timeToDisplay() string {
|
||||||
|
D := rev.Time.Day()
|
||||||
|
h, m, _ := rev.Time.Clock()
|
||||||
|
return fmt.Sprintf("%02d — %02d:%02d", D, h, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var revisionLinePattern = regexp.MustCompile("(.*)\t(.*)@.*\t(.*)\t(.*)")
|
||||||
|
|
||||||
|
// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted.
|
||||||
|
func unixTimestampAsTime(ts string) *time.Time {
|
||||||
|
i, err := strconv.ParseInt(ts, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tm := time.Unix(i, 0)
|
||||||
|
return &tm
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRevisionLine(line string) Revision {
|
||||||
|
results := revisionLinePattern.FindStringSubmatch(line)
|
||||||
|
return Revision{
|
||||||
|
Hash: results[1],
|
||||||
|
Username: results[2],
|
||||||
|
Time: *unixTimestampAsTime(results[3]),
|
||||||
|
Message: results[4],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filesAffected tells what files have been affected by the revision.
|
||||||
|
func (rev *Revision) filesAffected() (filenames []string) {
|
||||||
|
if nil != rev.filesAffectedBuf {
|
||||||
|
return rev.filesAffectedBuf
|
||||||
|
}
|
||||||
|
// List of files affected by this revision, one per line.
|
||||||
|
out, err := silentGitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
|
||||||
|
// There's an error? Well, whatever, let's just assign an empty slice, who cares.
|
||||||
|
if err != nil {
|
||||||
|
rev.filesAffectedBuf = []string{}
|
||||||
|
} else {
|
||||||
|
rev.filesAffectedBuf = strings.Split(out.String(), "\n")
|
||||||
|
}
|
||||||
|
return rev.filesAffectedBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine what hyphae were affected by this revision
|
||||||
|
func (rev *Revision) hyphaeAffected() (hyphae []string) {
|
||||||
|
if nil != rev.hyphaeAffectedBuf {
|
||||||
|
return rev.hyphaeAffectedBuf
|
||||||
|
}
|
||||||
|
hyphae = make([]string, 0)
|
||||||
|
var (
|
||||||
|
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
|
||||||
|
set = make(map[string]bool)
|
||||||
|
isNewName = func(hyphaName string) bool {
|
||||||
|
if _, present := set[hyphaName]; present {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
set[hyphaName] = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
filesAffected = rev.filesAffected()
|
||||||
|
)
|
||||||
|
for _, filename := range filesAffected {
|
||||||
|
if strings.IndexRune(filename, '.') >= 0 {
|
||||||
|
dotPos := strings.LastIndexByte(filename, '.')
|
||||||
|
hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
|
||||||
|
if isNewName(hyphaName) {
|
||||||
|
hyphae = append(hyphae, hyphaName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rev.hyphaeAffectedBuf = hyphae
|
||||||
|
return hyphae
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeString returns a human readable time representation.
|
||||||
|
func (rev Revision) TimeString() string {
|
||||||
|
return rev.Time.Format(time.RFC822)
|
||||||
|
}
|
||||||
|
|
||||||
|
// textDiff generates a good enough diff to display in a web feed. It is not html-escaped.
|
||||||
|
func (rev *Revision) textDiff() (diff string) {
|
||||||
|
filenames, ok := rev.mycoFiles()
|
||||||
|
if !ok {
|
||||||
|
return "No text changes"
|
||||||
|
}
|
||||||
|
for _, filename := range filenames {
|
||||||
|
text, err := PrimitiveDiffAtRevision(filename, rev.Hash)
|
||||||
|
if err != nil {
|
||||||
|
diff += "\nAn error has occurred with " + filename + "\n"
|
||||||
|
}
|
||||||
|
diff += text + "\n"
|
||||||
|
}
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
// mycoFiles returns filenames of .myco file. It is not ok if there are no myco files.
|
||||||
|
func (rev *Revision) mycoFiles() (filenames []string, ok bool) {
|
||||||
|
filenames = []string{}
|
||||||
|
for _, filename := range rev.filesAffected() {
|
||||||
|
if strings.HasSuffix(filename, ".myco") {
|
||||||
|
filenames = append(filenames, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filenames, len(filenames) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try and guess what link is the most important by looking at the message.
|
||||||
|
func (rev *Revision) bestLink() string {
|
||||||
|
var (
|
||||||
|
revs = rev.hyphaeAffected()
|
||||||
|
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case renameRes != nil:
|
||||||
|
return "/hypha/" + renameRes[1]
|
||||||
|
case len(revs) == 0:
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return "/hypha/" + revs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileAtRevision shows how the file with the given file path looked at the commit with the hash. It may return an error if git fails.
|
||||||
|
func FileAtRevision(filepath, hash string) (string, error) {
|
||||||
|
out, err := gitsh("show", hash+":"+strings.TrimPrefix(filepath, files.HyphaeDir()+"/"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrimitiveDiffAtRevision generates a plain-text diff for the given filepath at the commit with the given hash. It may return an error if git fails.
|
||||||
|
func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
|
||||||
|
out, err := silentGitsh("diff", "--unified=1", "--no-color", hash+"~", hash, "--", filepath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out.String(), err
|
||||||
|
}
|
55
history/view.qtpl
Normal file
55
history/view.qtpl
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% import "fmt" %}
|
||||||
|
{% import "github.com/bouncepaw/mycorrhiza/cfg" %}
|
||||||
|
|
||||||
|
HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
||||||
|
{% func (rev Revision) HyphaeLinksHTML() %}
|
||||||
|
{% stripspace %}
|
||||||
|
{% for i, hyphaName := range rev.hyphaeAffected() %}
|
||||||
|
{% if i > 0 %}
|
||||||
|
<span aria-hidden="true">, </span>
|
||||||
|
{% endif %}
|
||||||
|
<a href="/primitive-diff/{%s rev.Hash %}/{%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 %}
|
330
history/view.qtpl.go
Normal file
330
history/view.qtpl.go
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
// 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="/primitive-diff/`)
|
||||||
|
//line history/view.qtpl:11
|
||||||
|
qw422016.E().S(rev.Hash)
|
||||||
|
//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(`">`)
|
||||||
|
//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
|
||||||
|
}
|
@ -102,6 +102,7 @@ var localizations = map[string]string{
|
|||||||
"en.help.empty_error_link": "contributing",
|
"en.help.empty_error_link": "contributing",
|
||||||
"en.help.empty_error_title": "This entry does not exist!",
|
"en.help.empty_error_title": "This entry does not exist!",
|
||||||
"en.help.entry_not_found": "Entry not found",
|
"en.help.entry_not_found": "Entry not found",
|
||||||
|
"en.help.feeds": "Feeds",
|
||||||
"en.help.hypha": "Hypha",
|
"en.help.hypha": "Hypha",
|
||||||
"en.help.interface": "Interface",
|
"en.help.interface": "Interface",
|
||||||
"en.help.lock": "Lock",
|
"en.help.lock": "Lock",
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"sibling_hyphae": "Sibling hyphae",
|
"sibling_hyphae": "Sibling hyphae",
|
||||||
"special_pages": "Special pages",
|
"special_pages": "Special pages",
|
||||||
"recent_changes": "Recent changes",
|
"recent_changes": "Recent changes",
|
||||||
|
"feeds": "Feeds",
|
||||||
"configuration": "Configuration (for administrators)",
|
"configuration": "Configuration (for administrators)",
|
||||||
"lock": "Lock",
|
"lock": "Lock",
|
||||||
"whitelist": "Whitelist",
|
"whitelist": "Whitelist",
|
||||||
|
1
main.go
1
main.go
@ -1,5 +1,6 @@
|
|||||||
//go:generate qtc -dir=views
|
//go:generate qtc -dir=views
|
||||||
//go:generate qtc -dir=tree
|
//go:generate qtc -dir=tree
|
||||||
|
//go:generate qtc -dir=history
|
||||||
//go:generate go-localize -input l18n_src -output l18n
|
//go:generate go-localize -input l18n_src -output l18n
|
||||||
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
||||||
package main
|
package main
|
||||||
|
@ -96,7 +96,7 @@ func Tree(hyphaName string) (siblingsHTML, childrenHTML, prev, next string) {
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
children = figureOutChildren(hyphaName, true).children
|
children = figureOutChildren(hyphaName).children
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@ -124,7 +124,7 @@ type child struct {
|
|||||||
children []child
|
children []child
|
||||||
}
|
}
|
||||||
|
|
||||||
func figureOutChildren(hyphaName string, exists bool) child {
|
func figureOutChildren(hyphaName string) child {
|
||||||
var (
|
var (
|
||||||
descPrefix = hyphaName + "/"
|
descPrefix = hyphaName + "/"
|
||||||
child = child{hyphaName, true, make([]child, 0)}
|
child = child{hyphaName, true, make([]child, 0)}
|
||||||
|
@ -176,6 +176,7 @@ It outputs a poorly formatted JSON, but it works and is valid.
|
|||||||
<li>{%s lc.GetWithLocale(lang, "help.special_pages") %}
|
<li>{%s lc.GetWithLocale(lang, "help.special_pages") %}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/help/{%s lang %}/recent_changes">{%s lc.GetWithLocale(lang, "help.recent_changes") %}</a></li>
|
<li><a href="/help/{%s lang %}/recent_changes">{%s lc.GetWithLocale(lang, "help.recent_changes") %}</a></li>
|
||||||
|
<li><a href="/help/{%s lang %}/feeds">{%s lc.GetWithLocale(lang, "help.feeds") %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>{%s lc.GetWithLocale(lang, "help.configuration") %}
|
<li>{%s lc.GetWithLocale(lang, "help.configuration") %}
|
||||||
|
@ -707,41 +707,50 @@ func streamhelpTopicsHTML(qw422016 *qt422016.Writer, lang string, lc *l18n.Local
|
|||||||
//line views/stuff.qtpl:178
|
//line views/stuff.qtpl:178
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.recent_changes"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.recent_changes"))
|
||||||
//line views/stuff.qtpl:178
|
//line views/stuff.qtpl:178
|
||||||
|
qw422016.N().S(`</a></li>
|
||||||
|
<li><a href="/help/`)
|
||||||
|
//line views/stuff.qtpl:179
|
||||||
|
qw422016.E().S(lang)
|
||||||
|
//line views/stuff.qtpl:179
|
||||||
|
qw422016.N().S(`/feeds">`)
|
||||||
|
//line views/stuff.qtpl:179
|
||||||
|
qw422016.E().S(lc.GetWithLocale(lang, "help.feeds"))
|
||||||
|
//line views/stuff.qtpl:179
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>`)
|
<li>`)
|
||||||
//line views/stuff.qtpl:181
|
//line views/stuff.qtpl:182
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.configuration"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.configuration"))
|
||||||
//line views/stuff.qtpl:181
|
//line views/stuff.qtpl:182
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/help/`)
|
<li><a href="/help/`)
|
||||||
//line views/stuff.qtpl:183
|
//line views/stuff.qtpl:184
|
||||||
qw422016.E().S(lang)
|
qw422016.E().S(lang)
|
||||||
//line views/stuff.qtpl:183
|
//line views/stuff.qtpl:184
|
||||||
qw422016.N().S(`/lock">`)
|
qw422016.N().S(`/lock">`)
|
||||||
//line views/stuff.qtpl:183
|
//line views/stuff.qtpl:184
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.lock"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.lock"))
|
||||||
//line views/stuff.qtpl:183
|
//line views/stuff.qtpl:184
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
<li><a href="/help/`)
|
<li><a href="/help/`)
|
||||||
//line views/stuff.qtpl:184
|
//line views/stuff.qtpl:185
|
||||||
qw422016.E().S(lang)
|
qw422016.E().S(lang)
|
||||||
//line views/stuff.qtpl:184
|
//line views/stuff.qtpl:185
|
||||||
qw422016.N().S(`/whitelist">`)
|
qw422016.N().S(`/whitelist">`)
|
||||||
//line views/stuff.qtpl:184
|
//line views/stuff.qtpl:185
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.whitelist"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.whitelist"))
|
||||||
//line views/stuff.qtpl:184
|
//line views/stuff.qtpl:185
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
<li><a href="/help/`)
|
<li><a href="/help/`)
|
||||||
//line views/stuff.qtpl:185
|
//line views/stuff.qtpl:186
|
||||||
qw422016.E().S(lang)
|
qw422016.E().S(lang)
|
||||||
//line views/stuff.qtpl:185
|
//line views/stuff.qtpl:186
|
||||||
qw422016.N().S(`/telegram">`)
|
qw422016.N().S(`/telegram">`)
|
||||||
//line views/stuff.qtpl:185
|
//line views/stuff.qtpl:186
|
||||||
qw422016.E().S(lc.GetWithLocale(lang, "help.telegram"))
|
qw422016.E().S(lc.GetWithLocale(lang, "help.telegram"))
|
||||||
//line views/stuff.qtpl:185
|
//line views/stuff.qtpl:186
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
<li>...</li>
|
<li>...</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -749,91 +758,91 @@ func streamhelpTopicsHTML(qw422016 *qt422016.Writer, lang string, lc *l18n.Local
|
|||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
func writehelpTopicsHTML(qq422016 qtio422016.Writer, lang string, lc *l18n.Localizer) {
|
func writehelpTopicsHTML(qq422016 qtio422016.Writer, lang string, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
streamhelpTopicsHTML(qw422016, lang, lc)
|
streamhelpTopicsHTML(qw422016, lang, lc)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
func helpTopicsHTML(lang string, lc *l18n.Localizer) string {
|
func helpTopicsHTML(lang string, lc *l18n.Localizer) string {
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
writehelpTopicsHTML(qb422016, lang, lc)
|
writehelpTopicsHTML(qb422016, lang, lc)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:191
|
//line views/stuff.qtpl:192
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:193
|
//line views/stuff.qtpl:194
|
||||||
func streamhelpTopicBadgeHTML(qw422016 *qt422016.Writer, lang, topic string) {
|
func streamhelpTopicBadgeHTML(qw422016 *qt422016.Writer, lang, topic string) {
|
||||||
//line views/stuff.qtpl:193
|
//line views/stuff.qtpl:194
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<a class="help-topic-badge" href="/help/`)
|
<a class="help-topic-badge" href="/help/`)
|
||||||
//line views/stuff.qtpl:194
|
//line views/stuff.qtpl:195
|
||||||
qw422016.E().S(lang)
|
qw422016.E().S(lang)
|
||||||
//line views/stuff.qtpl:194
|
//line views/stuff.qtpl:195
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:194
|
//line views/stuff.qtpl:195
|
||||||
qw422016.E().S(topic)
|
qw422016.E().S(topic)
|
||||||
//line views/stuff.qtpl:194
|
//line views/stuff.qtpl:195
|
||||||
qw422016.N().S(`">?</a>
|
qw422016.N().S(`">?</a>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
func writehelpTopicBadgeHTML(qq422016 qtio422016.Writer, lang, topic string) {
|
func writehelpTopicBadgeHTML(qq422016 qtio422016.Writer, lang, topic string) {
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
streamhelpTopicBadgeHTML(qw422016, lang, topic)
|
streamhelpTopicBadgeHTML(qw422016, lang, topic)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
func helpTopicBadgeHTML(lang, topic string) string {
|
func helpTopicBadgeHTML(lang, topic string) string {
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
writehelpTopicBadgeHTML(qb422016, lang, topic)
|
writehelpTopicBadgeHTML(qb422016, lang, topic)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:195
|
//line views/stuff.qtpl:196
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:197
|
//line views/stuff.qtpl:198
|
||||||
func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:197
|
//line views/stuff.qtpl:198
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<main class="main-width user-list">
|
<main class="main-width user-list">
|
||||||
<h1>`)
|
<h1>`)
|
||||||
//line views/stuff.qtpl:200
|
//line views/stuff.qtpl:201
|
||||||
qw422016.E().S(lc.Get("ui.users_heading"))
|
qw422016.E().S(lc.Get("ui.users_heading"))
|
||||||
//line views/stuff.qtpl:200
|
//line views/stuff.qtpl:201
|
||||||
qw422016.N().S(`</h1>
|
qw422016.N().S(`</h1>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:202
|
//line views/stuff.qtpl:203
|
||||||
var (
|
var (
|
||||||
admins = make([]string, 0)
|
admins = make([]string, 0)
|
||||||
moderators = make([]string, 0)
|
moderators = make([]string, 0)
|
||||||
@ -853,149 +862,149 @@ func StreamUserListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
|||||||
sort.Strings(moderators)
|
sort.Strings(moderators)
|
||||||
sort.Strings(editors)
|
sort.Strings(editors)
|
||||||
|
|
||||||
//line views/stuff.qtpl:220
|
//line views/stuff.qtpl:221
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<section>
|
<section>
|
||||||
<h2>`)
|
<h2>`)
|
||||||
//line views/stuff.qtpl:222
|
//line views/stuff.qtpl:223
|
||||||
qw422016.E().S(lc.Get("ui.users_admins"))
|
qw422016.E().S(lc.Get("ui.users_admins"))
|
||||||
//line views/stuff.qtpl:222
|
//line views/stuff.qtpl:223
|
||||||
qw422016.N().S(`</h2>
|
qw422016.N().S(`</h2>
|
||||||
<ol>`)
|
<ol>`)
|
||||||
//line views/stuff.qtpl:223
|
//line views/stuff.qtpl:224
|
||||||
for _, name := range admins {
|
for _, name := range admins {
|
||||||
//line views/stuff.qtpl:223
|
//line views/stuff.qtpl:224
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li><a href="/hypha/`)
|
<li><a href="/hypha/`)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.E().S(cfg.UserHypha)
|
qw422016.E().S(cfg.UserHypha)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:224
|
//line views/stuff.qtpl:225
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:225
|
//line views/stuff.qtpl:226
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:225
|
//line views/stuff.qtpl:226
|
||||||
qw422016.N().S(`</ol>
|
qw422016.N().S(`</ol>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>`)
|
<h2>`)
|
||||||
//line views/stuff.qtpl:228
|
//line views/stuff.qtpl:229
|
||||||
qw422016.E().S(lc.Get("ui.users_moderators"))
|
qw422016.E().S(lc.Get("ui.users_moderators"))
|
||||||
//line views/stuff.qtpl:228
|
//line views/stuff.qtpl:229
|
||||||
qw422016.N().S(`</h2>
|
qw422016.N().S(`</h2>
|
||||||
<ol>`)
|
<ol>`)
|
||||||
//line views/stuff.qtpl:229
|
//line views/stuff.qtpl:230
|
||||||
for _, name := range moderators {
|
for _, name := range moderators {
|
||||||
//line views/stuff.qtpl:229
|
//line views/stuff.qtpl:230
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li><a href="/hypha/`)
|
<li><a href="/hypha/`)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.E().S(cfg.UserHypha)
|
qw422016.E().S(cfg.UserHypha)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:230
|
//line views/stuff.qtpl:231
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:231
|
//line views/stuff.qtpl:232
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:231
|
//line views/stuff.qtpl:232
|
||||||
qw422016.N().S(`</ol>
|
qw422016.N().S(`</ol>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>`)
|
<h2>`)
|
||||||
//line views/stuff.qtpl:234
|
//line views/stuff.qtpl:235
|
||||||
qw422016.E().S(lc.Get("ui.users_editors"))
|
qw422016.E().S(lc.Get("ui.users_editors"))
|
||||||
//line views/stuff.qtpl:234
|
//line views/stuff.qtpl:235
|
||||||
qw422016.N().S(`</h2>
|
qw422016.N().S(`</h2>
|
||||||
<ol>`)
|
<ol>`)
|
||||||
//line views/stuff.qtpl:235
|
//line views/stuff.qtpl:236
|
||||||
for _, name := range editors {
|
for _, name := range editors {
|
||||||
//line views/stuff.qtpl:235
|
//line views/stuff.qtpl:236
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li><a href="/hypha/`)
|
<li><a href="/hypha/`)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.E().S(cfg.UserHypha)
|
qw422016.E().S(cfg.UserHypha)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.E().S(name)
|
qw422016.E().S(name)
|
||||||
//line views/stuff.qtpl:236
|
//line views/stuff.qtpl:237
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:237
|
//line views/stuff.qtpl:238
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:237
|
//line views/stuff.qtpl:238
|
||||||
qw422016.N().S(`</ol>
|
qw422016.N().S(`</ol>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
func WriteUserListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
func WriteUserListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
StreamUserListHTML(qw422016, lc)
|
StreamUserListHTML(qw422016, lc)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
func UserListHTML(lc *l18n.Localizer) string {
|
func UserListHTML(lc *l18n.Localizer) string {
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
WriteUserListHTML(qb422016, lc)
|
WriteUserListHTML(qb422016, lc)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:241
|
//line views/stuff.qtpl:242
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:243
|
//line views/stuff.qtpl:244
|
||||||
func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:243
|
//line views/stuff.qtpl:244
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<h1>`)
|
<h1>`)
|
||||||
//line views/stuff.qtpl:246
|
//line views/stuff.qtpl:247
|
||||||
qw422016.E().S(lc.Get("ui.list_heading"))
|
qw422016.E().S(lc.Get("ui.list_heading"))
|
||||||
//line views/stuff.qtpl:246
|
//line views/stuff.qtpl:247
|
||||||
qw422016.N().S(`</h1>
|
qw422016.N().S(`</h1>
|
||||||
<p>`)
|
<p>`)
|
||||||
//line views/stuff.qtpl:247
|
//line views/stuff.qtpl:248
|
||||||
qw422016.E().S(lc.GetPlural("ui.list_desc", hyphae.Count()))
|
qw422016.E().S(lc.GetPlural("ui.list_desc", hyphae.Count()))
|
||||||
//line views/stuff.qtpl:247
|
//line views/stuff.qtpl:248
|
||||||
qw422016.N().S(`</p>
|
qw422016.N().S(`</p>
|
||||||
<ul class="hypha-list">
|
<ul class="hypha-list">
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:250
|
//line views/stuff.qtpl:251
|
||||||
hyphaNames := make(chan string)
|
hyphaNames := make(chan string)
|
||||||
sortedHypha := hyphae.PathographicSort(hyphaNames)
|
sortedHypha := hyphae.PathographicSort(hyphaNames)
|
||||||
for hypha := range hyphae.YieldExistingHyphae() {
|
for hypha := range hyphae.YieldExistingHyphae() {
|
||||||
@ -1003,252 +1012,252 @@ func StreamHyphaListHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
|||||||
}
|
}
|
||||||
close(hyphaNames)
|
close(hyphaNames)
|
||||||
|
|
||||||
//line views/stuff.qtpl:256
|
|
||||||
qw422016.N().S(`
|
|
||||||
`)
|
|
||||||
//line views/stuff.qtpl:257
|
|
||||||
for hyphaName := range sortedHypha {
|
|
||||||
//line views/stuff.qtpl:257
|
//line views/stuff.qtpl:257
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:258
|
//line views/stuff.qtpl:258
|
||||||
|
for hyphaName := range sortedHypha {
|
||||||
|
//line views/stuff.qtpl:258
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line views/stuff.qtpl:259
|
||||||
hypha := hyphae.ByName(hyphaName)
|
hypha := hyphae.ByName(hyphaName)
|
||||||
|
|
||||||
//line views/stuff.qtpl:258
|
//line views/stuff.qtpl:259
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<li class="hypha-list__entry">
|
<li class="hypha-list__entry">
|
||||||
<a class="hypha-list__link" href="/hypha/`)
|
<a class="hypha-list__link" href="/hypha/`)
|
||||||
//line views/stuff.qtpl:260
|
//line views/stuff.qtpl:261
|
||||||
qw422016.E().S(hypha.Name)
|
qw422016.E().S(hypha.Name)
|
||||||
//line views/stuff.qtpl:260
|
//line views/stuff.qtpl:261
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:260
|
//line views/stuff.qtpl:261
|
||||||
qw422016.E().S(util.BeautifulName(hypha.Name))
|
qw422016.E().S(util.BeautifulName(hypha.Name))
|
||||||
//line views/stuff.qtpl:260
|
//line views/stuff.qtpl:261
|
||||||
qw422016.N().S(`</a>
|
qw422016.N().S(`</a>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:261
|
//line views/stuff.qtpl:262
|
||||||
if hypha.BinaryPath != "" {
|
if hypha.BinaryPath != "" {
|
||||||
//line views/stuff.qtpl:261
|
//line views/stuff.qtpl:262
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<span class="hypha-list__amnt-type">`)
|
<span class="hypha-list__amnt-type">`)
|
||||||
//line views/stuff.qtpl:262
|
//line views/stuff.qtpl:263
|
||||||
qw422016.E().S(filepath.Ext(hypha.BinaryPath)[1:])
|
qw422016.E().S(filepath.Ext(hypha.BinaryPath)[1:])
|
||||||
//line views/stuff.qtpl:262
|
//line views/stuff.qtpl:263
|
||||||
qw422016.N().S(`</span>
|
qw422016.N().S(`</span>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:263
|
//line views/stuff.qtpl:264
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:263
|
//line views/stuff.qtpl:264
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</li>
|
</li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:265
|
//line views/stuff.qtpl:266
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:265
|
//line views/stuff.qtpl:266
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
func WriteHyphaListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
func WriteHyphaListHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
StreamHyphaListHTML(qw422016, lc)
|
StreamHyphaListHTML(qw422016, lc)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
func HyphaListHTML(lc *l18n.Localizer) string {
|
func HyphaListHTML(lc *l18n.Localizer) string {
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
WriteHyphaListHTML(qb422016, lc)
|
WriteHyphaListHTML(qb422016, lc)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:269
|
//line views/stuff.qtpl:270
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:271
|
//line views/stuff.qtpl:272
|
||||||
func StreamAboutHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
func StreamAboutHTML(qw422016 *qt422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:271
|
//line views/stuff.qtpl:272
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<section>
|
<section>
|
||||||
<h1>`)
|
<h1>`)
|
||||||
//line views/stuff.qtpl:275
|
//line views/stuff.qtpl:276
|
||||||
qw422016.E().S(lc.Get("ui.about_title", &l18n.Replacements{"name": cfg.WikiName}))
|
qw422016.E().S(lc.Get("ui.about_title", &l18n.Replacements{"name": cfg.WikiName}))
|
||||||
//line views/stuff.qtpl:275
|
//line views/stuff.qtpl:276
|
||||||
qw422016.N().S(`</h1>
|
qw422016.N().S(`</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>`)
|
<li><b>`)
|
||||||
//line views/stuff.qtpl:277
|
//line views/stuff.qtpl:278
|
||||||
qw422016.N().S(lc.Get("ui.about_version", &l18n.Replacements{"pre": "<a href=\"https://mycorrhiza.wiki\">", "post": "</a>"}))
|
qw422016.N().S(lc.Get("ui.about_version", &l18n.Replacements{"pre": "<a href=\"https://mycorrhiza.wiki\">", "post": "</a>"}))
|
||||||
//line views/stuff.qtpl:277
|
//line views/stuff.qtpl:278
|
||||||
qw422016.N().S(`</b> 1.5.0</li>
|
qw422016.N().S(`</b> 1.5.0</li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:278
|
//line views/stuff.qtpl:279
|
||||||
if cfg.UseAuth {
|
if cfg.UseAuth {
|
||||||
//line views/stuff.qtpl:278
|
//line views/stuff.qtpl:279
|
||||||
qw422016.N().S(` <li><b>`)
|
qw422016.N().S(` <li><b>`)
|
||||||
//line views/stuff.qtpl:279
|
//line views/stuff.qtpl:280
|
||||||
qw422016.E().S(lc.Get("ui.about_usercount"))
|
qw422016.E().S(lc.Get("ui.about_usercount"))
|
||||||
//line views/stuff.qtpl:279
|
//line views/stuff.qtpl:280
|
||||||
qw422016.N().S(`</b> `)
|
qw422016.N().S(`</b> `)
|
||||||
//line views/stuff.qtpl:279
|
//line views/stuff.qtpl:280
|
||||||
qw422016.N().DUL(user.Count())
|
qw422016.N().DUL(user.Count())
|
||||||
//line views/stuff.qtpl:279
|
//line views/stuff.qtpl:280
|
||||||
qw422016.N().S(`</li>
|
qw422016.N().S(`</li>
|
||||||
<li><b>`)
|
<li><b>`)
|
||||||
//line views/stuff.qtpl:280
|
//line views/stuff.qtpl:281
|
||||||
qw422016.E().S(lc.Get("ui.about_homepage"))
|
qw422016.E().S(lc.Get("ui.about_homepage"))
|
||||||
//line views/stuff.qtpl:280
|
//line views/stuff.qtpl:281
|
||||||
qw422016.N().S(`</b> <a href="/">`)
|
qw422016.N().S(`</b> <a href="/">`)
|
||||||
//line views/stuff.qtpl:280
|
//line views/stuff.qtpl:281
|
||||||
qw422016.E().S(cfg.HomeHypha)
|
qw422016.E().S(cfg.HomeHypha)
|
||||||
//line views/stuff.qtpl:280
|
//line views/stuff.qtpl:281
|
||||||
qw422016.N().S(`</a></li>
|
qw422016.N().S(`</a></li>
|
||||||
<li><b>`)
|
<li><b>`)
|
||||||
//line views/stuff.qtpl:281
|
//line views/stuff.qtpl:282
|
||||||
qw422016.E().S(lc.Get("ui.about_admins"))
|
qw422016.E().S(lc.Get("ui.about_admins"))
|
||||||
//line views/stuff.qtpl:281
|
//line views/stuff.qtpl:282
|
||||||
qw422016.N().S(`</b>`)
|
qw422016.N().S(`</b>`)
|
||||||
//line views/stuff.qtpl:281
|
//line views/stuff.qtpl:282
|
||||||
for i, username := range user.ListUsersWithGroup("admin") {
|
for i, username := range user.ListUsersWithGroup("admin") {
|
||||||
//line views/stuff.qtpl:282
|
//line views/stuff.qtpl:283
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
//line views/stuff.qtpl:282
|
//line views/stuff.qtpl:283
|
||||||
qw422016.N().S(`<span aria-hidden="true">, </span>
|
qw422016.N().S(`<span aria-hidden="true">, </span>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:283
|
//line views/stuff.qtpl:284
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:283
|
//line views/stuff.qtpl:284
|
||||||
qw422016.N().S(` <a href="/hypha/`)
|
qw422016.N().S(` <a href="/hypha/`)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.E().S(cfg.UserHypha)
|
qw422016.E().S(cfg.UserHypha)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.N().S(`/`)
|
qw422016.N().S(`/`)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.E().S(username)
|
qw422016.E().S(username)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.N().S(`">`)
|
qw422016.N().S(`">`)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.E().S(username)
|
qw422016.E().S(username)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.N().S(`</a>`)
|
qw422016.N().S(`</a>`)
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:284
|
//line views/stuff.qtpl:285
|
||||||
qw422016.N().S(`</li>
|
qw422016.N().S(`</li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:285
|
//line views/stuff.qtpl:286
|
||||||
} else {
|
} else {
|
||||||
//line views/stuff.qtpl:285
|
//line views/stuff.qtpl:286
|
||||||
qw422016.N().S(` <li>`)
|
qw422016.N().S(` <li>`)
|
||||||
//line views/stuff.qtpl:286
|
//line views/stuff.qtpl:287
|
||||||
qw422016.E().S(lc.Get("ui.about_noauth"))
|
qw422016.E().S(lc.Get("ui.about_noauth"))
|
||||||
//line views/stuff.qtpl:286
|
//line views/stuff.qtpl:287
|
||||||
qw422016.N().S(`</li>
|
qw422016.N().S(`</li>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:287
|
//line views/stuff.qtpl:288
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:287
|
//line views/stuff.qtpl:288
|
||||||
qw422016.N().S(` </ul>
|
qw422016.N().S(` </ul>
|
||||||
<p>`)
|
<p>`)
|
||||||
//line views/stuff.qtpl:289
|
//line views/stuff.qtpl:290
|
||||||
qw422016.N().S(lc.Get("ui.about_hyphae", &l18n.Replacements{"link": "<a href=\"/list\">/list</a>"}))
|
qw422016.N().S(lc.Get("ui.about_hyphae", &l18n.Replacements{"link": "<a href=\"/list\">/list</a>"}))
|
||||||
//line views/stuff.qtpl:289
|
//line views/stuff.qtpl:290
|
||||||
qw422016.N().S(`</p>
|
qw422016.N().S(`</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
func WriteAboutHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
func WriteAboutHTML(qq422016 qtio422016.Writer, lc *l18n.Localizer) {
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
StreamAboutHTML(qw422016, lc)
|
StreamAboutHTML(qw422016, lc)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
func AboutHTML(lc *l18n.Localizer) string {
|
func AboutHTML(lc *l18n.Localizer) string {
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
WriteAboutHTML(qb422016, lc)
|
WriteAboutHTML(qb422016, lc)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:293
|
//line views/stuff.qtpl:294
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:295
|
//line views/stuff.qtpl:296
|
||||||
func StreamCommonScripts(qw422016 *qt422016.Writer) {
|
func StreamCommonScripts(qw422016 *qt422016.Writer) {
|
||||||
//line views/stuff.qtpl:295
|
//line views/stuff.qtpl:296
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:296
|
//line views/stuff.qtpl:297
|
||||||
for _, scriptPath := range cfg.CommonScripts {
|
for _, scriptPath := range cfg.CommonScripts {
|
||||||
//line views/stuff.qtpl:296
|
//line views/stuff.qtpl:297
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<script src="`)
|
<script src="`)
|
||||||
//line views/stuff.qtpl:297
|
//line views/stuff.qtpl:298
|
||||||
qw422016.E().S(scriptPath)
|
qw422016.E().S(scriptPath)
|
||||||
//line views/stuff.qtpl:297
|
//line views/stuff.qtpl:298
|
||||||
qw422016.N().S(`"></script>
|
qw422016.N().S(`"></script>
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:298
|
//line views/stuff.qtpl:299
|
||||||
}
|
}
|
||||||
//line views/stuff.qtpl:298
|
//line views/stuff.qtpl:299
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
`)
|
`)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
func WriteCommonScripts(qq422016 qtio422016.Writer) {
|
func WriteCommonScripts(qq422016 qtio422016.Writer) {
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
StreamCommonScripts(qw422016)
|
StreamCommonScripts(qw422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
func CommonScripts() string {
|
func CommonScripts() string {
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
WriteCommonScripts(qb422016)
|
WriteCommonScripts(qb422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/stuff.qtpl:299
|
//line views/stuff.qtpl:300
|
||||||
}
|
}
|
||||||
|
@ -61,8 +61,14 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// genericHandlerOfFeeds is a helper function for the web feed handlers.
|
// genericHandlerOfFeeds is a helper function for the web feed handlers.
|
||||||
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func() (string, error), name string, contentType string) {
|
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func(history.FeedOptions) (string, error), name string, contentType string) {
|
||||||
if content, err := f(); err != nil {
|
opts, err := history.ParseFeedOptions(rq.URL.Query())
|
||||||
|
var content string
|
||||||
|
if err == nil {
|
||||||
|
content, err = f(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
w.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
fmt.Fprint(w, "An error while generating "+name+": "+err.Error())
|
fmt.Fprint(w, "An error while generating "+name+": "+err.Error())
|
||||||
|
Loading…
Reference in New Issue
Block a user