37 Commits

Author SHA1 Message Date
901cb84a1c 3.1.0 changelog 2017-12-10 15:20:27 +13:00
2ae9d52419 bump all versions to 3.1.0 2017-12-10 15:20:22 +13:00
f5d587819f makefile: fix devel target for when build fails 2017-12-10 15:19:33 +13:00
dea0cffc90 statistics page 2017-12-10 15:19:20 +13:00
917b9c1a53 rebuild staticResources.go 2017-12-10 14:21:02 +13:00
758ba9a457 makefile: add "devel" target 2017-12-10 14:19:42 +13:00
d1007bb645 skip redirect when searching (if javascript is available) 2017-12-10 14:19:35 +13:00
35b9973a4a mobile-friendly search results 2017-12-10 14:05:45 +13:00
437e1f60e5 preserve line-selection hash fragment in URL when navigating to a search result 2017-12-10 13:57:32 +13:00
3c8631c0d7 fix search highlighting for previous 2017-12-10 13:57:16 +13:00
3f4d5e2522 preserve consecutive whitespaces in chat messages 2017-12-10 13:53:20 +13:00
a087ad2e7b 'latest' link takes you to the latest for the currently selected hub 2017-12-10 13:49:46 +13:00
a27645772a css: instant on, animate off 2017-12-10 13:44:33 +13:00
86cc1cbb2e add nonce for css 2017-12-10 13:44:26 +13:00
9ebe6ca2fb rebuild staticResources.go 2017-12-10 13:37:38 +13:00
117a5fa51e fix disappearing text in dropdown menu 2017-12-10 13:37:33 +13:00
5f2a1b528d fix spaces in searches turning into plusses 2017-12-10 13:37:22 +13:00
559283566b fix #newpage issues 2017-12-10 13:32:35 +13:00
add69c5c81 pretty up the generated HTML (2) 2017-12-10 13:27:40 +13:00
f261ca0ece pretty up the generated HTML 2017-12-10 13:25:34 +13:00
672f0f606c rebuild staticResources.go 2017-12-10 13:04:00 +13:00
cd1e868daf fix phantom space between dom elements; fix search box being split into two lines in some cases; add more padding in mobile layout 2017-12-10 13:03:47 +13:00
e078383e6d add archiveState.URL() method 2017-12-10 12:50:19 +13:00
8568c4bdc2 replace js state setting, with URL targets in select values ; remove "PCRE" from regex tooltip 2017-12-10 12:50:11 +13:00
7b9aece4d2 use pathEscape / pathUnescape for query/rx URLs (fixes + in regex) 2017-12-10 12:48:24 +13:00
e0ff64cd22 move menucontainer into a single div, prevent text selection on dropdown elements 2017-12-10 12:20:18 +13:00
3f623782b7 use a constant for default font size 2017-12-10 12:19:48 +13:00
92e7064372 inline the toggle_element function 2017-12-10 12:19:35 +13:00
149e226729 move toggleMenu call from html onclick to DOM addEventListener 2017-12-10 12:19:07 +13:00
3c3028b8d1 css: remove unused style block 2017-12-10 12:18:37 +13:00
8a452c0fa6 standardise html-escaping functions 2017-12-10 12:08:22 +13:00
693f541934 rebuild staticResources.go 2017-12-10 12:03:14 +13:00
e6ed43154c replace star png image with inline SVG 2017-12-10 12:03:08 +13:00
533527c890 remove mousetrap.js (APL) in favour of accesskey; add title tags 2017-12-10 11:57:59 +13:00
f28ae17a00 fix legacy index.php still being routable(!) 2017-12-10 11:43:02 +13:00
cd879798e3 bump all versions to 3.0.2 2017-09-06 18:19:57 +12:00
f90a6d1dc6 Added tag release-3.0.1 for changeset 47147713ae1b 2017-09-06 18:19:47 +12:00
12 changed files with 381 additions and 213 deletions

View File

@@ -1 +1,2 @@
718fcb17fc0c119591e837d700c752601c392130 release-3.0.0
47147713ae1b5de60d49faeed6e17f962d5deae0 release-3.0.1

View File

@@ -18,7 +18,7 @@ type ArchiveServer struct {
cfg *Config
startup time.Time
rxViewRoot, rxViewPage, rxSearch, rxSearchRx *regexp.Regexp
rxViewRoot, rxViewPage, rxSearch, rxSearchRx, rxStats *regexp.Regexp
}
func NewArchiveServer(cfg *Config) (*ArchiveServer, error) {
@@ -49,6 +49,7 @@ func NewArchiveServer(cfg *Config) (*ArchiveServer, error) {
rxViewPage: regexp.MustCompile(`^/([^/]+)/(\d+)/(\d+)/(?:page-)?(\d+)$`),
rxSearch: regexp.MustCompile(`^/([^/]+)/search/(.*)$`),
rxSearchRx: regexp.MustCompile(`^/([^/]+)/rx/(.*)$`),
rxStats: regexp.MustCompile((`^/([^/]+)/stats/?$`)),
}, nil
}

View File

@@ -2,14 +2,18 @@ package archive
import (
"bufio"
"errors"
"fmt"
"html/template"
"html"
"io/ioutil"
"math"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"strings"
"time"
)
const (
@@ -22,6 +26,7 @@ type ArchiveState struct {
logBestSlug string
query string
queryIsRegex bool
isStats bool
ym YearMonth
page int
highestPage int
@@ -34,6 +39,30 @@ func NewArchiveState(svr *ArchiveServer) *ArchiveState {
}
}
func (this *ArchiveState) showPageURLs() bool {
return this.log != nil && len(this.query) == 0 && !this.isStats
}
func (this *ArchiveState) URL() string {
if this.isStats {
return fmt.Sprintf(`/%s/stats`, this.logBestSlug)
} else if len(this.query) > 0 {
if this.queryIsRegex {
return fmt.Sprintf(`/%s/rx/%s`, this.logBestSlug, url.QueryEscape(this.query))
} else {
return fmt.Sprintf(`/%s/search/%s`, this.logBestSlug, url.QueryEscape(this.query))
}
} else {
if this.page == pageNotSet {
return fmt.Sprintf(`/%s/%s/%s`, this.logBestSlug, this.ym.Year, this.ym.Month)
} else {
return fmt.Sprintf(`/%s/%s/%s/page-%d`, this.logBestSlug, this.ym.Year, this.ym.Month, this.page)
}
}
}
func (this *ArchiveState) selectSource(log *LogSource, slug string) {
this.log = log
this.logBestSlug = slug
@@ -57,6 +86,11 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
lines := strings.Split(string(fc), "\n")
// Work around #newpage: if the last line is blank, skip
if len(lines) > 0 && len(lines[len(lines)-1]) == 0 {
lines = lines[:len(lines)-1]
}
this.highestPage = int(math.Ceil(float64(len(lines))/float64(this.svr.cfg.LinesPerPage))) - 1
if this.page == pageNotSet || this.page > this.highestPage {
this.page = this.highestPage
@@ -73,12 +107,136 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
output := ""
for i := startLine; i < endLine; i += 1 {
output += template.HTMLEscapeString(lines[i]) + "<br>\n"
output += html.EscapeString(lines[i]) + "\n"
}
this.renderTemplate(w, []byte(output))
}
// parseStatsFor loads user post statistics for a single yearMonth in a single log source.
func (this *ArchiveState) parseStatsFor(ls *LogSource, ym YearMonth, into map[string]int) error {
fname, err := this.svr.LogFile(ls, ym)
if err != nil {
return err
}
fc, err := ioutil.ReadFile(fname)
if err != nil {
return err
}
rxUser := regexp.MustCompile(`(?ms)^[^<\r\n]*<([^>\r\n]+)>.+?$`)
matches := rxUser.FindAllSubmatch(fc, -1)
if matches == nil || len(matches) == 0 {
return errors.New("No matches")
}
for _, match := range matches {
username := string(match[1])
if ct, ok := into[username]; ok {
into[username] = ct + 1
} else {
into[username] = 1
}
}
return nil
}
func (this *ArchiveState) renderStats(w http.ResponseWriter) {
// Lines per year
// Users / posts/year
startTime := time.Now()
yearsToUsersToPostCount := make(map[int]map[string]int, 0)
totalErrors := 0
var lastError error = nil
ym := this.log.EarliestDate()
orderedYears := make([]int, 0)
for {
usersToPostCount, ok := yearsToUsersToPostCount[ym.Year]
if !ok {
usersToPostCount = make(map[string]int)
orderedYears = append(orderedYears, ym.Year)
}
err := this.parseStatsFor(this.log, ym, usersToPostCount)
if err != nil {
//log.Printf("Stats(%s): %s", this.logBestSlug, err.Error())
totalErrors += 1
lastError = err
}
//fmt.Printf("%#v\n", usersToPostCount)
yearsToUsersToPostCount[ym.Year] = usersToPostCount
if ym.Equals(this.log.LatestDate()) {
break
}
ym = ym.Next()
}
ret := make([]byte, 0)
if lastError != nil {
ret = append(ret, []byte(fmt.Sprintf("<em>Got %d errors, including: '%s'</em>\n\n", totalErrors, lastError.Error()))...)
}
//
allUsersExistence := make(map[string]struct{})
for _, usersMap := range yearsToUsersToPostCount {
for username, _ := range usersMap {
allUsersExistence[username] = struct{}{}
}
}
allUsernames := make([]string, 0, len(allUsersExistence))
for username, _ := range allUsersExistence {
allUsernames = append(allUsernames, username)
}
sort.Strings(allUsernames)
//
ret = append(ret, []byte(`<table><thead><tr><th>&nbsp;</th>`)...)
for _, year := range orderedYears {
ret = append(ret, []byte(fmt.Sprintf(`<th>%d</th>`, year))...)
}
ret = append(ret, []byte("</tr><tbody>\n")...)
for _, username := range allUsernames {
ret = append(ret, []byte(fmt.Sprintf(`<tr><td>%s</td>`, html.EscapeString(username)))...)
for _, year := range orderedYears {
usersMap := yearsToUsersToPostCount[year]
posts, _ /*ok*/ := usersMap[username]
ret = append(ret, []byte(fmt.Sprintf(`<td>%d</td>`, posts))...)
}
ret = append(ret, []byte("</tr>\n")...)
}
ret = append(ret, []byte(`</tbody><tfoot><th style="text-align:right;">TOTAL:</th>`)...)
for _, year := range orderedYears {
postsTotalForYear := 0
for _, userPostCount := range yearsToUsersToPostCount[year] {
postsTotalForYear += userPostCount
}
ret = append(ret, []byte(fmt.Sprintf(`<th>%d</th>`, postsTotalForYear))...)
}
duration := time.Now().Sub(startTime)
ret = append(ret, []byte(fmt.Sprintf("</tfoot></table>\n\n<em>%d total users\nStatistics generated in %s</em>", len(allUsernames), duration.String()))...)
this.renderTemplate(w, ret)
}
// renderSearch renders the search results.
// - Mandatory: log, query, queryIsRegex
func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
@@ -101,7 +259,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
this.renderTemplateHead(w)
totalResults := 0
w.Write([]byte(`<ul>`))
w.Write([]byte(`<ul class="search-results">`))
limit := this.log.LatestDate().Next() // one off the end
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
@@ -131,7 +289,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
lineNo := i % this.svr.cfg.LinesPerPage
url := fmt.Sprintf(`/%s/%d/%d/page-%d#line-%d`, this.logBestSlug, ympair.Year, ympair.Month, page, lineNo)
w.Write([]byte(`<li><a href="` + template.HTMLEscapeString(url) + `">&raquo;</a> ` + template.HTMLEscapeString(scanner.Text()) + `</li>`))
w.Write([]byte(`<li><a href="` + html.EscapeString(url) + `">&raquo;</a> ` + html.EscapeString(scanner.Text()) + `</li>`))
}
}()
@@ -140,7 +298,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
w.Write([]byte(`</ul>`))
if totalResults == 0 {
w.Write([]byte(`No search results for &quot;<em>` + template.HTMLEscapeString(this.query) + `</em>&quot;`))
w.Write([]byte(`No search results for &quot;<em>` + html.EscapeString(this.query) + `</em>&quot;`))
} else {
w.Write([]byte(`<br><em>Found ` + fmt.Sprintf("%d", totalResults) + ` total result(s).</em><br><br>`))
}
@@ -150,7 +308,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
// renderError renders a plain text string, escaping it for HTML use.
func (this *ArchiveState) renderError(w http.ResponseWriter, msg string) {
this.renderTemplate(w, []byte(template.HTMLEscapeString(msg)))
this.renderTemplate(w, []byte(html.EscapeString(msg)))
}
func (this *ArchiveState) renderTemplate(w http.ResponseWriter, body []byte) {
@@ -169,54 +327,69 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
title = this.log.Description + ` Archives`
}
showPageURLs := (this.log != nil && len(this.query) == 0)
latestUrl := `/`
if this.log != nil {
latestUrl = fmt.Sprintf(`/%s/%d/%d`, url.PathEscape(this.logBestSlug), this.log.LatestDate().Year, this.log.LatestDate().Month)
}
statsUrl := fmt.Sprintf(`/%s/stats`, url.PathEscape(this.logBestSlug))
w.Write([]byte(`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>` + template.HTMLEscapeString(title) + `</title>
<link rel="stylesheet" type="text/css" href="/style.css">
<title>` + html.EscapeString(title) + `</title>
<link rel="stylesheet" type="text/css" href="/style.css?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `">
</head>
<body>
<div class="layout-top nav">
<div id="tr1" style="display:none;"></div>
<div id="tr2" style="display:none;"></div>
<div class="ddmenu" id="spm" style="display:none;">
<a href="/">Latest</a>
<div id="menu-container" class="noselect">
<div id="tr1"></div>
<div id="tr2"></div>
<div class="ddmenu">
<a href="` + html.EscapeString(latestUrl) + `">Latest</a>
<a href="` + html.EscapeString(statsUrl) + `">Statistics</a>
<a onclick="fontSize(1);">Font increase</a>
<a onclick="fontSize(-1);">Font decrease</a>
<a href="/download" onclick="return confirm('Are you sure you want to download a backup?');">Download backup</a>
</div>
</div>
<a onclick="toggleMenu();"><div id="logo" class="layout-pushdown"></div></a>
<div id="logo" class="layout-pushdown"><svg viewBox="0 0 24 24">
<path fill="#000000" d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z" />
</svg></div>
<span class="area-nav">
<form method="GET" id="frmHub">
<select name="h" id="selHub">
<select onchange="window.location.pathname = this.value;">
`))
for i, h := range this.svr.cfg.Logs {
slug, _ := this.svr.bestSlugFor(&this.svr.cfg.Logs[i])
current := (this.log == &this.svr.cfg.Logs[i])
w.Write([]byte(`<option value="` + template.HTMLEscapeString(slug) + `" ` + attr(current, "selected") + `>` + template.HTMLEscapeString(h.Description) + `</option>`))
targetUri := `/` + url.PathEscape(slug) + `/`
if len(this.query) > 0 {
if this.queryIsRegex {
targetUri += `rx/` + url.PathEscape(this.query)
} else {
targetUri += `search/` + url.PathEscape(this.query)
}
} else {
targetUri += fmt.Sprintf(`%d/%d`, h.LatestDate().Year, h.LatestDate().Month)
}
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<option value=\"%s\"%s>%s</option>\n", html.EscapeString(targetUri), attr(current, " selected"), html.EscapeString(h.Description))))
}
w.Write([]byte(`
</select>
</form>
`))
if showPageURLs {
if this.showPageURLs() {
w.Write([]byte(`
<form method="GET">
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `">
<select id="seldate" onchange="setYM(this);">
<select onchange="window.location.pathname = this.value;">
`))
// Generate month dropdown options
@@ -226,14 +399,24 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
if ympair.Year != lastY {
if lastY != -1 {
w.Write([]byte(`</optgroup>`))
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
}
w.Write([]byte(`<optgroup label="` + fmt.Sprintf("%d", ympair.Year) + `">`))
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<optgroup label=\"%d\">\n", ympair.Year)))
lastY = ympair.Year
}
w.Write([]byte(fmt.Sprintf(`<option value="%d-%d" %s>%s</option>`, ympair.Year, ympair.Month, attr(ympair.Equals(this.ym), "selected"), template.HTMLEscapeString(ympair.Month.String()))))
targetUri := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, ympair.Year, ympair.Month)
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t\t<option value=\"%s\"%s>%s</option>\n", html.EscapeString(targetUri), attr(ympair.Equals(this.ym), " selected"), html.EscapeString(ympair.Month.String()))))
}
if lastY != -1 {
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
}
w.Write([]byte(`
</select>
<div class="mini-separator layout-pushdown"></div>
`))
//
@@ -249,51 +432,46 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
nextPage = this.highestPage
}
prevMonth := this.ym.Prev() // FIXME check bounds
prevMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, prevMonth.Year, prevMonth.Month)
nextMonth := this.ym.Next() // FIXME check bounds
nextMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, nextMonth.Year, nextMonth.Month)
w.Write([]byte(`
</optgroup>
<a style="display:none;" id="monthprev" accesskey="j" href="` + html.EscapeString(prevMonthLink) + `">
<a style="display:none;" id="monthnext" accesskey="k" href="` + html.EscapeString(nextMonthLink) + `">
</select>
<input type="hidden" name="y" id="f_y" value="">
<input type="hidden" name="m" id="f_m" value="">
<input type="hidden" name="p" value="0" >
</form>
<div class="mini-separator layout-pushdown"></div>
<a class="btn" href="` + pageBase + `/page-0">&laquo;</a><a
class="btn" id="pgprev" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">&lsaquo;</a>
` + fmt.Sprintf("%d", this.page) + `
<a class="btn" id="pgnext" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">&rsaquo;</a><a
class="btn" href="` + pageBase + `">&raquo;</a>
<a class="btn" href="` + pageBase + `/page-0">&laquo;</a>
<a class="btn" accesskey="h" title="Previous page (Alt+H)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">&lsaquo;</a>
<span class="current-page">` + fmt.Sprintf("%d", this.page) + `</span>
<a class="btn" accesskey="l" title="Next page (Alt+L)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">&rsaquo;</a>
<a class="btn" href="` + pageBase + `">&raquo;</a>
`))
}
w.Write([]byte(`
<div class="pad"></div>
</span>
<span class="area-search">
<form method="GET">
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `">
<input type="text" id="searchbox" name="q" value="` + template.HTMLEscapeString(this.query) + `" placeholder="Search...">
<form method="GET" id="search-form">
<input type="hidden" name="h" value="` + html.EscapeString(this.logBestSlug) + `">
<input type="text" id="searchbox" name="q" value="` + html.EscapeString(this.query) + `" placeholder="Search..." accesskey="m" title="Search (Alt+M)">
<input type="submit" value="&raquo;">
<input type="checkbox" class="layout-pushdown" name="rx" value="1" title="PCRE Regular Expression" ` + attr(this.queryIsRegex, "checked") + `>
<input type="checkbox" class="layout-pushdown" name="rx" value="1" title="Regular Expression" ` + attr(this.queryIsRegex, "checked") + `>
</form>
</span>
</div>
<div class="layout-body" id="chatarea">
`))
<div class="layout-body" id="chatarea">`,
))
// Header ends
}
func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) {
w.Write([]byte(`
</div>
w.Write([]byte(`</div>
<script type="text/javascript" src="/archive.js?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `"></script>
</body>
</html>

View File

@@ -2,7 +2,7 @@
# Makefile for archive
#
VERSION:=3.0.1
VERSION:=3.1.0
SOURCES:=Makefile \
static \
@@ -15,7 +15,7 @@ GOFLAGS := -ldflags='-s -w -X code.ivysaur.me/archive.SERVER_VERSION=archive/${V
# Phony targets
#
.PHONY: all dist clean
.PHONY: all dist clean devel
all: build/linux64/archive-server build/win32/archive-server.exe
@@ -29,6 +29,9 @@ clean:
if [ -d ./build ] ; then rm -r ./build ; fi
if [ -f ./archive ] ; then rm ./archive ; fi
devel: staticResources.go
( cd cmd/archive-server && go build && ./archive-server -listen "127.0.0.1:8000" )
#
# Generated files
#

View File

@@ -121,7 +121,7 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if matches := this.rxSearch.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1])
arc.query, _ = url.QueryUnescape(matches[2])
arc.query, _ = url.PathUnescape(matches[2])
arc.queryIsRegex = false
arc.renderSearch(w)
@@ -132,13 +132,24 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if matches := this.rxSearchRx.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1])
arc.query, _ = url.QueryUnescape(matches[2])
arc.query, _ = url.PathUnescape(matches[2])
arc.queryIsRegex = true
arc.renderSearch(w)
} else {
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1]))
}
} else if matches := this.rxStats.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1])
arc.isStats = true
arc.renderStats(w)
} else {
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1]))
}
} else {
arc.renderError(w, "Unknown route.")
@@ -157,9 +168,9 @@ func (this *ArchiveServer) legacyRoute(w http.ResponseWriter, r *http.Request) {
if u.hasGet("q") {
if u.hasGet("rx") {
u.redirectf(`/%d/rx/%s`, hubid, url.QueryEscape(u.get("q")))
u.redirectf(`/%d/rx/%s`, hubid, url.PathEscape(u.get("q")))
} else {
u.redirectf(`/%d/search/%s`, hubid, url.QueryEscape(u.get("q")))
u.redirectf(`/%d/search/%s`, hubid, url.PathEscape(u.get("q")))
}
} else if u.hasGet("y") && u.hasGet("m") {

View File

@@ -27,6 +27,14 @@ func (ym YearMonth) Next() YearMonth {
}
}
func (ym YearMonth) Prev() YearMonth {
if ym.Month == time.January {
return YearMonth{Year: ym.Year - 1, Month: time.December}
} else {
return YearMonth{Year: ym.Year, Month: ym.Month - 1}
}
}
// Index returns a single int that can be used to compare this YearMonth with
// other YearMonth objects.
func (ym YearMonth) Index() int {

View File

@@ -6,6 +6,23 @@ Written in Golang, PHP
=CHANGELOG=
2017-12-10: 3.1.0
- Feature: Statistics page
- Enhancement: Upgrade corner menu image to support high DPI
- Enhancement: Mobile-friendly buttons in search results
- Enhancement: Explain keyboard shortcuts when hovering over form elements
- Enhancement: Improve performance when changing sources and when searching, by removing a network roundtrip
- Enhancement: "Latest" link takes you to the latest for the current data source
- Change ctrl+alt+[H/J/K/L/M]... keyboard shortcuts to just alt+[...], remove dependency on APL `mousetrap.js`
- Fix an issue with accessing the legacy php controller after the Go port
- Fix an issue with using plus characters in regular expression searches
- Fix an issue with blank pages appearing if there was a divisible number of log entries
- Fix an issue with browsers using stale stylesheets
- Fix a cosmetic issue with text selection when using dropdown menu items
- Fix a cosmetic issue with PCRE text in regular expression hover message (it's no longer PCRE)
- Fix a cosmetic issue with search box layout at some screen sizes
- Fix a cosmetic issue with animation responsiveness
2017-09-06: 3.0.1
- Breaking: Revert date formatting in filenames back to strftime-compatible

View File

@@ -1,27 +1,9 @@
/* mousetrap v1.4.6 craig.is/killing/mice */
(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;g<l[a].length;++g)if(k=
l[a][g],!(!c&&k.seq&&n[k.seq]!=k.level||h!=k.action||("keypress"!=h||d.metaKey||d.ctrlKey)&&b.sort().join(",")!==k.modifiers.sort().join(","))){var m=c&&k.seq==c&&k.level==v;(!c&&k.combo==e||m)&&l[a].splice(g,1);f.push(k)}return f}function K(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function x(a,b,d,c){m.stopCallback(b,b.target||b.srcElement,d,c)||!1!==a(b,d)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?
b.stopPropagation():b.cancelBubble=!0)}function y(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=A(a);b&&("keyup"==a.type&&z===b?z=!1:m.handleKey(b,K(a),a))}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||"meta"==a}function L(a,b,d,c){function e(b){return function(){u=b;++n[a];clearTimeout(D);D=setTimeout(t,1E3)}}function v(b){x(d,b,a);"keyup"!==c&&(z=A(b));setTimeout(t,10)}for(var g=n[a]=0;g<b.length;++g){var f=g+1===b.length?v:e(c||E(b[g+1]).action);F(b[g],f,c,a,g)}}function E(a,b){var d,
c,e,f=[];d="+"===a?["+"]:a.split("+");for(e=0;e<d.length;++e)c=d[e],G[c]&&(c=G[c]),b&&"keypress"!=b&&H[c]&&(c=H[c],f.push("shift")),w(c)&&f.push(c);d=c;e=b;if(!e){if(!p){p={};for(var g in h)95<g&&112>g||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1<f.length?L(a,f,b,d):(d=E(a,d),l[d.key]=l[d.key]||[],C(d.key,d.modifiers,{type:d.action},
c,a,e),l[d.key][c?"unshift":"push"]({callback:b,modifiers:d.modifiers,action:d.action,seq:c,level:e,combo:a}))}var h={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},B={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},H={"~":"`","!":"1",
"@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c<a.length;++c)F(a[c],b,d);return this},
unbind:function(a,b){return m.bind(a,function(){},b)},trigger:function(a,b){if(q[a+":"+b])q[a+":"+b]({},a);return this},reset:function(){l={};q={};return this},stopCallback:function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable},handleKey:function(a,b,d){var c=C(a,b,d),e;b={};var f=0,g=!1;for(e=0;e<c.length;++e)c[e].seq&&(f=Math.max(f,c[e].level));for(e=0;e<c.length;++e)c[e].seq?c[e].level==f&&(g=!0,
b[c[e].seq]=1,x(c[e].callback,d,c[e].combo,c[e].seq)):g||x(c[e].callback,d,c[e].combo);c="keypress"==d.type&&I;d.type!=u||w(a)||c||t(b);I=g&&"keydown"==d.type}};J.Mousetrap=m;"function"===typeof define&&define.amd&&define(m)})(window,document);
/* archive.js */
function i(s) {
return document.getElementById(s);
}
var alreadyLoaded = false;
function t(e) {
e.style.display = (e.style.display == 'none') ? 'block' : 'none';
}
function urldesc(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
}
var $chatArea = document.getElementById("chatarea");
var DEFAULT_FONT_SIZE = 12;
function cookie_set(key, value) {
document.cookie = (key+"="+value+"; expires=Sat, 20 Sep 2059 09:05:12; path=/");
@@ -42,6 +24,10 @@ function cookie_get(key) {
}
function highlight(str) {
var urldesc = function(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
};
return str
.replace(/(\[\d\d\d\d-\d\d-\d\d\s\d\d\:\d\d\:\d\d\] )(\*.+)/g, "$1<span class=\"sys\">$2</span>")
.replace(
@@ -62,7 +48,7 @@ function highlight(str) {
function fontSize(change) {
var curSize = cookie_get("fontsize");
if (curSize === null) {
curSize = 12;
curSize = DEFAULT_FONT_SIZE;
} else {
curSize = + curSize;
}
@@ -71,25 +57,22 @@ function fontSize(change) {
cookie_set("fontsize", curSize);
i("chatarea").style["fontSize"] = ""+curSize+"px";
$chatArea.style["fontSize"] = ""+curSize+"px";
}
function toggleMenu() {
t(i("tr1"));
t(i("tr2"));
t(i("spm"));
var $container = document.getElementById("menu-container");
$container.style.display = ($container.style.display == 'block') ? 'none' : 'block';
}
function highlightLine(no) {
var lines = i('chatarea').innerHTML.split('<br>');
var lines = $chatArea.innerHTML.split("\n");
lines[no] = '<span class="line-highlighted">' + lines[no] + '</span>';
i('chatarea').innerHTML = lines.join('<br>');
$chatArea.innerHTML = lines.join("\n");
}
var alreadyLoaded = false;
function onLoad() {
if (alreadyLoaded) {
@@ -99,7 +82,11 @@ function onLoad() {
//
i('chatarea').innerHTML = highlight(i('chatarea').innerHTML);
$chatArea.innerHTML = highlight( $chatArea.innerHTML );
//
document.getElementById("logo").addEventListener("click", toggleMenu);
//
@@ -108,7 +95,6 @@ function onLoad() {
document.location.hash.substr(0, 6) === '#line-'
) {
highlightLine( parseInt(document.location.hash.substr(6), 10) );
document.location.hash = '';
}
//
@@ -117,60 +103,23 @@ function onLoad() {
//
i('selHub').onchange = function() {
if ( /\/search\//.test(window.location.pathname) ) {
window.location.pathname = i('selHub').value + "/search/" + encodeURIComponent( i('searchbox').value );
} else if ( /\/rx\//.test(window.location.pathname) ) {
window.location.pathname = i('selHub').value + "/rx/" + encodeURIComponent( i('searchbox').value )
} else {
i('frmHub').submit();
var $form = document.getElementById("search-form");
$form.addEventListener("submit", function(ev) {
var query = $form.elements["q"].value;
if (query.length === 0) {
alert("No search text entered");
ev.preventDefault();
return false;
}
};
//
var prefix = ($form.elements["rx"].checked ? "/rx/" : "/search/");
window.location.hash = "";
window.location.pathname = "/" + $form.elements["h"].value + prefix + encodeURIComponent(query);
Mousetrap.bind('ctrl+alt+h', function() { i("pgprev").click(); });
Mousetrap.bind('ctrl+alt+l', function() { i("pgnext").click(); });
Mousetrap.bind('ctrl+alt+j', function() {
YMgoto( YMmod(i("seldate").value, 1) )
});
Mousetrap.bind('ctrl+alt+k', function() {
YMgoto( YMmod(i("seldate").value, -1) )
ev.preventDefault();
return false;
});
Mousetrap.bind('ctrl+alt+m', function() {
i("searchbox").focus();
});
//
}
function YMmod(str, change) {
var t = str.split('-').map(function(x) { return +x; });
t[1] += change;
if (t[1] == 13) {
t[0] += 1;
t[1] = 1;
}
if (t[1] == 0) {
t[0] -= 1;
t[1] = 12;
}
return t.join('-');
}
function YMgoto(str) {
var t = str.split("-");
i("f_y").value = t[0];
i("f_m").value = t[1];
i("seldate").form.submit();
}
function setYM(el) {
YMgoto(el.value);
}
window.addEventListener('load', onLoad);

View File

@@ -1,16 +0,0 @@
<?php
/*
Chat Archives
`````````````
Requires PHP 5.4 (short-array syntax and ENT_SUBSTITUTE) with short_open_tag
URL rewriting for nginx;
location / {
try_files $uri /index.php?$args;
}
*/
require __DIR__.'/../includes/bootstrap.php';
URLRouter::routeRequest();

View File

@@ -61,6 +61,8 @@ select {
#chatarea {
word-break:break-word;
white-space: pre; /* Safari 1/2, IE 6/7 */
white-space: pre-wrap; /* Chrome, Firefox, IE8++ */
}
.timestamp {
@@ -77,14 +79,6 @@ select {
color: darkgreen;
}
.logo {
width: 102px;
height: 37px;
display: block;
border:0;
padding-bottom: 1.0em;
}
.gt {
color:#0A0;
font-weight:bold;
@@ -104,11 +98,18 @@ select {
.nav {
background: #DDD;
box-shadow: 0px 4px 24px #CCC;
font-size:0; /* remove phantom spaces between elements */
}
.nav form {
display: inline;
}
.nav a, .nav .current-page, .nav select {
font-size: 12px;
line-height: 12px;
}
.nav .btn {
background: white;
color:black;
@@ -117,8 +118,14 @@ select {
text-decoration:none;
}
.nav a {
transition:all 0.1s linear;
}
.nav a:hover {
border-color: grey black black grey;
transition:all 0s linear;
}
.area-search {
@@ -130,16 +137,33 @@ select {
}
#logo {
cursor:pointer;
display:inline-block;
width:16px;
height:16px;
background:transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbtJREFUOMt9kb1qAlEQhacJ6QJBTJEiTSDpEqw1jYWIpQ9gIYIWghamELGysRE0Eo0GRRQrCyslIKIi/hUawVIEfYW8wGTOjbssyZqFbznMOeeyO5cOhwOdotls+sB/mZNGvV73tdttBtAnD9jv96aUy+XNZDJhAH0qR7vd7g+VSuWuVCrxZrNRQGNmlqXtdkvFYtFeKBR8uVzuTWgIH/j01WqlgMbs6L0hiw66JAN7NpvlfD7P1WoV/8uyOB4OhzydThXQmMFDBll00KVMJvOYSqV4Pp/zbDbjfr//L8ggiw66tF6vKZlMvkejUe71emppnU7HFHjIIIsOurRcLhWJROI5HA7v8KmtVssUeMggq/VosVjoiGEPBoNcq9VMgYeMsUOyJJ1QKPQai8VYNqyurtvtKqAxg4eMsUPj8VjH6/V+xuNxbjQaamHpdFoBjRk8ZIwdGo1GOg6H40u7Lo/Hg/AEQGvXi4yxQ4PBQBGJRC5sNhv7/X52u93bQCAQ1TxozOAhg6zm/byIboVHi8XidLlcr6KdgucXTjnkxWq1Pol+QEd15TQdec4Ei3AlXAv3R26Os0vh3Nj5Bg36USLeWI8KAAAAAElFTkSuQmCC') no-repeat 0 0;
margin-left:2px;
margin-right:4px;
}
/* Utility class */
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
/* Dropdown */
#menu-container {
display:none; /* default */
}
.ddmenu {
display:block;
position:absolute;
@@ -165,8 +189,6 @@ select {
.ddmenu a:hover {
background:#FFF;
-moz-transition:all 0.1s linear;
-webkit-transition:all 0.1s linear;
}
#tr1 {
@@ -202,7 +224,7 @@ select {
text-align:center;
}
.nav, .nav select {
.nav a, .nav .current-page, .nav select {
font-size: 16px;
line-height:16px;
}
@@ -227,12 +249,29 @@ select {
}
.area-search {
display:block;
float:none;
margin-top: 4px;
}
#searchbox {
width:auto;
}
ul.search-results {
list-style-type: none;
padding-left: 2px;
}
.search-results li a:first-child {
/* convert mini link to touchable */
display:inline-block;
padding: 2px 6px;
margin: 2px;
border: 1px solid grey;
background: lightgrey;
}
}
@media (max-width: 400px) {

File diff suppressed because one or more lines are too long

View File

@@ -25,7 +25,7 @@ func (this *URLHelper) intval(sz string) int {
}
func (this *URLHelper) get(sz string) string {
return this.r.URL.Query().Get(sz)
return this.r.URL.Query().Get(sz) // n.b. automatically unescaped
}
func (this *URLHelper) hasGet(sz string) bool {