diff --git a/flag.go b/flag.go index 8a9857a..2bd83ff 100644 --- a/flag.go +++ b/flag.go @@ -10,8 +10,9 @@ import ( ) func init() { - flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at") - flag.StringVar(&util.HomePage, "home", "home", "The home page") + flag.StringVar(&util.URL, "url", "http://0.0.0.0:$port", "URL at which your wiki can be found. Used to generate feeds") + flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at using HTTP") + flag.StringVar(&util.HomePage, "home", "home", "The home page name") flag.StringVar(&util.SiteTitle, "title", "🍄", "How to call your wiki in the navititle") flag.StringVar(&util.UserTree, "user-tree", "u", "Hypha which is a superhypha of all user pages") flag.StringVar(&util.AuthMethod, "auth-method", "none", "What auth method to use. Variants: \"none\", \"fixed\"") @@ -34,6 +35,10 @@ func parseCliArgs() { log.Fatal(err) } + if util.URL == "http://0.0.0.0:$port" { + util.URL = "http://0.0.0.0:" + util.ServerPort + } + if !isCanonicalName(util.HomePage) { log.Fatal("Error: you must use a proper name for the homepage") } diff --git a/go.mod b/go.mod index 995478b..b5d6fc3 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.14 require ( github.com/adrg/xdg v0.2.2 + github.com/gorilla/feeds v1.1.1 github.com/valyala/quicktemplate v1.6.3 ) diff --git a/go.sum b/go.sum index dbbc51b..284389c 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo= github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= +github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/history/history.go b/history/history.go index 247b4c5..dcd6268 100644 --- a/history/history.go +++ b/history/history.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os/exec" + "regexp" "strconv" "strings" "time" @@ -13,6 +14,8 @@ import ( "github.com/bouncepaw/mycorrhiza/util" ) +var renameMsgPattern = regexp.MustCompile(`^Rename ‘(.*)’ to ‘.*’`) + // Start initializes git credentials. func Start(wikiDir string) { _, err := gitsh("config", "user.name", "wikimind") @@ -27,10 +30,46 @@ func Start(wikiDir string) { // 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 + Hash string + Username string + Time time.Time + Message string + hyphaeAffectedBuf []string +} + +// 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 ( + // List of files affected by this revision, one per line. + out, err = gitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash) + // 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 + } + ) + if err != nil { + return hyphae + } + for _, filename := range strings.Split(out.String(), "\n") { + 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. @@ -40,42 +79,38 @@ func (rev Revision) TimeString() string { // HyphaeLinks returns a comma-separated list of hyphae that were affected by this revision as HTML string. func (rev Revision) HyphaeLinks() (html string) { - // diff-tree --no-commit-id --name-only -r - var ( - // List of files affected by this revision, one per line. - out, err = gitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash) - // set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most). - set = make(map[string]bool) - isNewName = func(hyphaName string) bool { - if _, present := set[hyphaName]; present { - return false - } else { - set[hyphaName] = true - return true - } - } - ) - if err != nil { - return "" - } - for _, filename := range strings.Split(out.String(), "\n") { - // If filename has an ampersand: - if strings.IndexRune(filename, '.') >= 0 { - // Remove ampersanded suffix from filename: - ampersandPos := strings.LastIndexByte(filename, '.') - hyphaName := string([]byte(filename)[0:ampersandPos]) // is it safe? - if isNewName(hyphaName) { - // Entries are separated by commas - if len(set) > 1 { - html += `` - } - html += fmt.Sprintf(`%[1]s`, hyphaName) - } + hyphae := rev.hyphaeAffected() + for i, hyphaName := range hyphae { + if i > 0 { + html += `` } + html += fmt.Sprintf(`%[1]s`, hyphaName) } return html } +func (rev *Revision) descriptionForFeed() (html string) { + return fmt.Sprintf( + `

%s

+

Hyphae affected: %s

`, rev.Message, rev.HyphaeLinks()) +} + +// 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 "/page/" + renameRes[1] + case len(revs) == 0: + return "" + default: + return "/page/" + revs[0] + } +} + func (rev Revision) RecentChangesEntry() (html string) { if user.AuthUsed && rev.Username != "anon" { return fmt.Sprintf(` diff --git a/history/information.go b/history/information.go index 80d5b5d..82c0eb3 100644 --- a/history/information.go +++ b/history/information.go @@ -11,8 +11,56 @@ import ( "github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/util" + "github.com/gorilla/feeds" ) +func recentChangesFeed() *feeds.Feed { + feed := &feeds.Feed{ + Title: "Recent changes", + Link: &feeds.Link{Href: util.URL}, + Description: "List of 30 recent changes on the wiki", + Author: &feeds.Author{Name: "Wikimind", Email: "wikimind@mycorrhiza"}, + Updated: time.Now(), + } + var ( + out, err = gitsh( + "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)) + } + } + 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: util.URL + rev.bestLink()}, + }) + } + return feed +} + +func RecentChangesRSS() (string, error) { + return recentChangesFeed().ToRss() +} + +func RecentChangesAtom() (string, error) { + return recentChangesFeed().ToAtom() +} + +func RecentChangesJSON() (string, error) { + return recentChangesFeed().ToJSON() +} + func RecentChanges(n int) string { var ( out, err = gitsh( diff --git a/http_history.go b/http_history.go index 8bec1ae..de010d5 100644 --- a/http_history.go +++ b/http_history.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log" "net/http" "strconv" @@ -14,6 +15,9 @@ import ( func init() { http.HandleFunc("/history/", handlerHistory) http.HandleFunc("/recent-changes/", handlerRecentChanges) + http.HandleFunc("/recent-changes-rss", handlerRecentChangesRSS) + http.HandleFunc("/recent-changes-atom", handlerRecentChangesAtom) + http.HandleFunc("/recent-changes-json", handlerRecentChangesJSON) } // handlerHistory lists all revisions of a hypha @@ -46,3 +50,28 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) { http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther) } } + +func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func() (string, error), name string) { + log.Println(rq.URL) + if content, err := f(); 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()) + } else { + w.Header().Set("Content-Type", "application/rss+xml") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, content) + } +} + +func handlerRecentChangesRSS(w http.ResponseWriter, rq *http.Request) { + genericHandlerOfFeeds(w, rq, history.RecentChangesRSS, "RSS") +} + +func handlerRecentChangesAtom(w http.ResponseWriter, rq *http.Request) { + genericHandlerOfFeeds(w, rq, history.RecentChangesAtom, "Atom") +} + +func handlerRecentChangesJSON(w http.ResponseWriter, rq *http.Request) { + genericHandlerOfFeeds(w, rq, history.RecentChangesJSON, "JSON feed") +} diff --git a/templates/asset.qtpl.go b/templates/asset.qtpl.go index 16def29..a90d0ab 100644 --- a/templates/asset.qtpl.go +++ b/templates/asset.qtpl.go @@ -39,6 +39,7 @@ textarea {font-size:15px;} .edit {height:100%;} .edit-form {height:90%;} .edit-form textarea {width:100%;height:90%;} +.icon {margin-right: .25rem; vertical-align: bottom; } main h1:not(.navi-title) {font-size:1.7rem;} blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;} diff --git a/templates/default.css b/templates/default.css index 5d5c6e2..a7c0f07 100644 --- a/templates/default.css +++ b/templates/default.css @@ -14,6 +14,7 @@ textarea {font-size:15px;} .edit {height:100%;} .edit-form {height:90%;} .edit-form textarea {width:100%;height:90%;} +.icon {margin-right: .25rem; vertical-align: bottom; } main h1:not(.navi-title) {font-size:1.7rem;} blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;} diff --git a/templates/recent_changes.qtpl b/templates/recent_changes.qtpl index 738f39a..2980fd4 100644 --- a/templates/recent_changes.qtpl +++ b/templates/recent_changes.qtpl @@ -18,6 +18,8 @@ recent changes +

Subscribe via RSS, Atom or JSON feed.

+ {% comment %} Here I am, willing to add some accesibility using ARIA. Turns out, role="feed" is not supported in any screen reader as of September diff --git a/templates/recent_changes.qtpl.go b/templates/recent_changes.qtpl.go index 8268ef1..329ccc5 100644 --- a/templates/recent_changes.qtpl.go +++ b/templates/recent_changes.qtpl.go @@ -77,81 +77,83 @@ func StreamRecentChangesHTML(qw422016 *qt422016.Writer, changes []string, n int) recent changes +

Subscribe via RSS, Atom or JSON feed.

+ `) -//line templates/recent_changes.qtpl:26 +//line templates/recent_changes.qtpl:28 qw422016.N().S(`
`) -//line templates/recent_changes.qtpl:29 +//line templates/recent_changes.qtpl:31 if len(changes) == 0 { -//line templates/recent_changes.qtpl:29 +//line templates/recent_changes.qtpl:31 qw422016.N().S(`

Could not find any recent changes.

`) -//line templates/recent_changes.qtpl:31 +//line templates/recent_changes.qtpl:33 } else { -//line templates/recent_changes.qtpl:31 +//line templates/recent_changes.qtpl:33 qw422016.N().S(` `) -//line templates/recent_changes.qtpl:32 +//line templates/recent_changes.qtpl:34 for i, entry := range changes { -//line templates/recent_changes.qtpl:32 +//line templates/recent_changes.qtpl:34 qw422016.N().S(` `) -//line templates/recent_changes.qtpl:37 +//line templates/recent_changes.qtpl:39 } -//line templates/recent_changes.qtpl:37 +//line templates/recent_changes.qtpl:39 qw422016.N().S(` `) -//line templates/recent_changes.qtpl:38 +//line templates/recent_changes.qtpl:40 } -//line templates/recent_changes.qtpl:38 +//line templates/recent_changes.qtpl:40 qw422016.N().S(`
`) -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 } -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 func WriteRecentChangesHTML(qq422016 qtio422016.Writer, changes []string, n int) { -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 StreamRecentChangesHTML(qw422016, changes, n) -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 qt422016.ReleaseWriter(qw422016) -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 } -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 func RecentChangesHTML(changes []string, n int) string { -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 qb422016 := qt422016.AcquireByteBuffer() -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 WriteRecentChangesHTML(qb422016, changes, n) -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 qs422016 := string(qb422016.B) -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 qt422016.ReleaseByteBuffer(qb422016) -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 return qs422016 -//line templates/recent_changes.qtpl:41 +//line templates/recent_changes.qtpl:43 } diff --git a/util/util.go b/util/util.go index 745bf96..032e9f7 100644 --- a/util/util.go +++ b/util/util.go @@ -8,6 +8,7 @@ import ( ) var ( + URL string ServerPort string HomePage string SiteTitle string