52 Commits

Author SHA1 Message Date
a9959b81fa doc/README: changelog for v3.1.1 2025-08-20 16:24:27 +12:00
247dbd056a archiveserver: add -git suffix to default server header banner 2025-08-20 16:22:02 +12:00
b8e5f8effe go.mod: declare golang language version 2025-08-20 16:21:50 +12:00
48dca86185 makefile: remove obsolete makefile 2025-08-20 16:21:42 +12:00
219ce85b7f static: replace go-bindata usage with standard go:embed 2025-08-20 16:21:31 +12:00
a1ce0c8841 deps: remove vendoring 2025-08-20 16:21:16 +12:00
5e5d4cb253 doc: remove code.ivysaur.me meta tags in README 2018-12-31 18:48:45 +13:00
3a80de8b9e doc: remove TODO.txt (moved to Gitea issues) 2018-11-07 18:38:38 +13:00
7c2c55cb3c doc: move README to root 2018-11-07 18:35:21 +13:00
3f56aac8c1 convert to go modules 2018-11-06 19:21:20 +13:00
8e7bcbe057 hg2git: convert ignores file 2018-11-06 18:58:15 +13:00
0197b8466f hg-git: remove old .hgtags file 2018-11-06 18:57:10 +13:00
944ae74ba8 doc: add TODO page 2017-12-10 15:34:55 +13:00
d10808c7c1 bump makefile version to 3.1.1 2017-12-10 15:22:00 +13:00
e3ff8095ee Added tag release-3.1.0 for changeset 1f276b596c58 2017-12-10 15:21:41 +13:00
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
21 changed files with 405 additions and 702 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# development
cmd/archive-server/archive-server
cmd/archive-server/config.json
# binaries
_dist/
build/

View File

@@ -1,9 +0,0 @@
mode:regex
# development
^cmd/archive-server/archive-server$
^cmd/archive-server/config.json$
# binaries
^_dist/
^build/

View File

@@ -1 +0,0 @@
718fcb17fc0c119591e837d700c752601c392130 release-3.0.0

View File

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

View File

@@ -2,14 +2,18 @@ package archive
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"html/template" "html"
"io/ioutil" "io/ioutil"
"math" "math"
"net/http" "net/http"
"net/url"
"os" "os"
"regexp" "regexp"
"sort"
"strings" "strings"
"time"
) )
const ( const (
@@ -22,6 +26,7 @@ type ArchiveState struct {
logBestSlug string logBestSlug string
query string query string
queryIsRegex bool queryIsRegex bool
isStats bool
ym YearMonth ym YearMonth
page int page int
highestPage 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) { func (this *ArchiveState) selectSource(log *LogSource, slug string) {
this.log = log this.log = log
this.logBestSlug = slug this.logBestSlug = slug
@@ -57,6 +86,11 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
lines := strings.Split(string(fc), "\n") 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 this.highestPage = int(math.Ceil(float64(len(lines))/float64(this.svr.cfg.LinesPerPage))) - 1
if this.page == pageNotSet || this.page > this.highestPage { if this.page == pageNotSet || this.page > this.highestPage {
this.page = this.highestPage this.page = this.highestPage
@@ -73,12 +107,136 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
output := "" output := ""
for i := startLine; i < endLine; i += 1 { 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)) 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. // renderSearch renders the search results.
// - Mandatory: log, query, queryIsRegex // - Mandatory: log, query, queryIsRegex
func (this *ArchiveState) renderSearch(w http.ResponseWriter) { func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
@@ -101,7 +259,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
this.renderTemplateHead(w) this.renderTemplateHead(w)
totalResults := 0 totalResults := 0
w.Write([]byte(`<ul>`)) w.Write([]byte(`<ul class="search-results">`))
limit := this.log.LatestDate().Next() // one off the end limit := this.log.LatestDate().Next() // one off the end
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() { 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 lineNo := i % this.svr.cfg.LinesPerPage
url := fmt.Sprintf(`/%s/%d/%d/page-%d#line-%d`, this.logBestSlug, ympair.Year, ympair.Month, page, lineNo) 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>`)) w.Write([]byte(`</ul>`))
if totalResults == 0 { 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 { } else {
w.Write([]byte(`<br><em>Found ` + fmt.Sprintf("%d", totalResults) + ` total result(s).</em><br><br>`)) 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. // renderError renders a plain text string, escaping it for HTML use.
func (this *ArchiveState) renderError(w http.ResponseWriter, msg string) { 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) { func (this *ArchiveState) renderTemplate(w http.ResponseWriter, body []byte) {
@@ -169,55 +327,70 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
title = this.log.Description + ` Archives` 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> w.Write([]byte(`<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>` + template.HTMLEscapeString(title) + `</title> <title>` + html.EscapeString(title) + `</title>
<link rel="stylesheet" type="text/css" href="/style.css"> <link rel="stylesheet" type="text/css" href="/style.css?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `">
</head> </head>
<body> <body>
<div class="layout-top nav"> <div class="layout-top nav">
<div id="tr1" style="display:none;"></div> <div id="menu-container" class="noselect">
<div id="tr2" style="display:none;"></div> <div id="tr1"></div>
<div class="ddmenu" id="spm" style="display:none;"> <div id="tr2"></div>
<a href="/">Latest</a> <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 increase</a>
<a onclick="fontSize(-1);">Font decrease</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> <a href="/download" onclick="return confirm('Are you sure you want to download a backup?');">Download backup</a>
</div> </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"> <span class="area-nav">
<form method="GET" id="frmHub"> <select onchange="window.location.pathname = this.value;">
<select name="h" id="selHub"> `))
`))
for i, h := range this.svr.cfg.Logs { for i, h := range this.svr.cfg.Logs {
slug, _ := this.svr.bestSlugFor(&this.svr.cfg.Logs[i]) slug, _ := this.svr.bestSlugFor(&this.svr.cfg.Logs[i])
current := (this.log == &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(` w.Write([]byte(`
</select> </select>
</form>
`)) `))
if showPageURLs { if this.showPageURLs() {
w.Write([]byte(` w.Write([]byte(`
<select onchange="window.location.pathname = this.value;">
<form method="GET"> `))
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `">
<select id="seldate" onchange="setYM(this);">
`))
// Generate month dropdown options // 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() { for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
if ympair.Year != lastY { if ympair.Year != lastY {
if lastY != -1 { 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 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 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(` 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> <a class="btn" href="` + pageBase + `/page-0">&laquo;</a>
<input type="hidden" name="y" id="f_y" value=""> <a class="btn" accesskey="h" title="Previous page (Alt+H)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">&lsaquo;</a>
<input type="hidden" name="m" id="f_m" value=""> <span class="current-page">` + fmt.Sprintf("%d", this.page) + `</span>
<input type="hidden" name="p" value="0" > <a class="btn" accesskey="l" title="Next page (Alt+L)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">&rsaquo;</a>
</form> <a class="btn" href="` + pageBase + `">&raquo;</a>
<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>
`)) `))
} }
w.Write([]byte(` w.Write([]byte(`
<div class="pad"></div>
</span> </span>
<span class="area-search"> <span class="area-search">
<form method="GET" id="search-form">
<form method="GET"> <input type="hidden" name="h" value="` + html.EscapeString(this.logBestSlug) + `">
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `"> <input type="text" id="searchbox" name="q" value="` + html.EscapeString(this.query) + `" placeholder="Search..." accesskey="m" title="Search (Alt+M)">
<input type="text" id="searchbox" name="q" value="` + template.HTMLEscapeString(this.query) + `" placeholder="Search...">
<input type="submit" value="&raquo;"> <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> </form>
</span> </span>
</div> </div>
<div class="layout-body" id="chatarea"> <div class="layout-body" id="chatarea">`,
`)) ))
// Header ends // Header ends
} }
func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) { func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) {
w.Write([]byte(` w.Write([]byte(`</div>
</div>
<script type="text/javascript" src="/archive.js?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `"></script> <script type="text/javascript" src="/archive.js?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `"></script>
</body> </body>
</html> </html>

View File

@@ -1,79 +0,0 @@
#
# Makefile for archive
#
VERSION:=3.0.1
SOURCES:=Makefile \
static \
cmd $(wildcard cmd/archive-server/*.go) \
$(wildcard *.go)
GOFLAGS := -ldflags='-s -w -X code.ivysaur.me/archive.SERVER_VERSION=archive/${VERSION}' -gcflags='-trimpath=$(GOPATH)' -asmflags='-trimpath=$(GOPATH)'
#
# Phony targets
#
.PHONY: all dist clean
all: build/linux64/archive-server build/win32/archive-server.exe
dist: \
_dist/archive-$(VERSION)-linux64.tar.gz \
_dist/archive-$(VERSION)-win32.7z \
_dist/archive-$(VERSION)-src.zip
clean:
if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi
if [ -d ./build ] ; then rm -r ./build ; fi
if [ -f ./archive ] ; then rm ./archive ; fi
#
# Generated files
#
staticResources.go: static/ static/*
go-bindata -o staticResources.go -prefix static -pkg archive static
#
# Release artefacts
#
build/linux64/archive-server: $(SOURCES) staticResources.go
mkdir -p build/linux64
(cd cmd/archive-server ; \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
go build $(GOFLAGS) -o ../../build/linux64/archive-server \
)
build/win32/archive-server.exe: $(SOURCES) staticResources.go
mkdir -p build/win32
(cd cmd/archive-server ; \
PATH=/usr/lib/mxe/usr/bin:$(PATH) CC=i686-w64-mingw32.static-gcc \
CGO_ENABLED=1 GOOS=windows GOARCH=386 \
go build $(GOFLAGS) -o ../../build/win32/archive-server.exe \
)
build/linux64/config.json.SAMPLE: cmd/archive-server/config.json.SAMPLE
cp cmd/archive-server/config.json.SAMPLE build/linux64/config.json.SAMPLE
build/win32/config.json.SAMPLE: cmd/archive-server/config.json.SAMPLE
cp cmd/archive-server/config.json.SAMPLE build/win32/config.json.SAMPLE
_dist/archive-$(VERSION)-linux64.tar.gz: build/linux64/archive-server build/linux64/config.json.SAMPLE
mkdir -p _dist
tar caf _dist/archive-$(VERSION)-linux64.tar.gz -C build/linux64 archive-server config.json.SAMPLE --owner=0 --group=0
_dist/archive-$(VERSION)-win32.7z: build/win32/archive-server.exe build/win32/config.json.SAMPLE
mkdir -p _dist
( cd build/win32 ; \
if [ -f dist.7z ] ; then rm dist.7z ; fi ; \
7z a dist.7z archive-server.exe config.json.SAMPLE ; \
mv dist.7z ../../_dist/archive-$(VERSION)-win32.7z \
)
_dist/archive-$(VERSION)-src.zip: $(SOURCES)
hg archive --type=zip _dist/archive-$(VERSION)-src.zip

View File

@@ -1,10 +1,36 @@
# archive
![](https://img.shields.io/badge/written%20in-Go-blue.svg)
A web interface for browsing chat logs. A web interface for browsing chat logs.
As of the 3.0 release, `archive` is available as a standalone binary for Linux and Windows. As of the 3.0 release, `archive` is available as a standalone binary for Linux and Windows.
Written in Golang, PHP ## Changelog
=CHANGELOG= 2025-08-20: 3.1.1
- Update dependencies, replace go-bindata with `go:embed`
2018-11-06: (no release tag)
- Convert from hg to git
- Convert to Go Modules
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 2017-09-06: 3.0.1
- Breaking: Revert date formatting in filenames back to strftime-compatible - Breaking: Revert date formatting in filenames back to strftime-compatible

View File

@@ -2,6 +2,7 @@ package archive
import ( import (
"bytes" "bytes"
"embed"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -10,6 +11,11 @@ import (
"time" "time"
) )
// Static assets
//go:embed static/*
var staticAssets embed.FS
func (this *ArchiveServer) lookupSourceByNumericString(slug string) *LogSource { func (this *ArchiveServer) lookupSourceByNumericString(slug string) *LogSource {
intval, err := strconv.Atoi(slug) intval, err := strconv.Atoi(slug)
if err != nil { if err != nil {
@@ -71,7 +77,7 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handle static assets // Handle static assets
static, err := Asset(r.URL.Path[1:]) static, err := staticAssets.ReadFile(`static/` + r.URL.Path[1:])
if err == nil { if err == nil {
http.ServeContent(w, r, r.URL.Path[1:], this.startup, bytes.NewReader(static)) http.ServeContent(w, r, r.URL.Path[1:], this.startup, bytes.NewReader(static))
return return
@@ -121,7 +127,7 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if matches := this.rxSearch.FindStringSubmatch(r.URL.Path); len(matches) > 0 { } else if matches := this.rxSearch.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil { if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1]) arc.selectSource(ls, matches[1])
arc.query, _ = url.QueryUnescape(matches[2]) arc.query, _ = url.PathUnescape(matches[2])
arc.queryIsRegex = false arc.queryIsRegex = false
arc.renderSearch(w) arc.renderSearch(w)
@@ -132,13 +138,24 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if matches := this.rxSearchRx.FindStringSubmatch(r.URL.Path); len(matches) > 0 { } else if matches := this.rxSearchRx.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil { if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1]) arc.selectSource(ls, matches[1])
arc.query, _ = url.QueryUnescape(matches[2]) arc.query, _ = url.PathUnescape(matches[2])
arc.queryIsRegex = true arc.queryIsRegex = true
arc.renderSearch(w) arc.renderSearch(w)
} else { } else {
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1])) 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 { } else {
arc.renderError(w, "Unknown route.") arc.renderError(w, "Unknown route.")
@@ -157,9 +174,9 @@ func (this *ArchiveServer) legacyRoute(w http.ResponseWriter, r *http.Request) {
if u.hasGet("q") { if u.hasGet("q") {
if u.hasGet("rx") { 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 { } 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") { } 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 // Index returns a single int that can be used to compare this YearMonth with
// other YearMonth objects. // other YearMonth objects.
func (ym YearMonth) Index() int { func (ym YearMonth) Index() int {

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module code.ivysaur.me/archive
go 1.24.4
require github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4=
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

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 */ /* archive.js */
function i(s) { var alreadyLoaded = false;
return document.getElementById(s);
}
function t(e) { var $chatArea = document.getElementById("chatarea");
e.style.display = (e.style.display == 'none') ? 'block' : 'none'; var DEFAULT_FONT_SIZE = 12;
}
function urldesc(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
}
function cookie_set(key, value) { function cookie_set(key, value) {
document.cookie = (key+"="+value+"; expires=Sat, 20 Sep 2059 09:05:12; path=/"); document.cookie = (key+"="+value+"; expires=Sat, 20 Sep 2059 09:05:12; path=/");
@@ -42,6 +24,10 @@ function cookie_get(key) {
} }
function highlight(str) { function highlight(str) {
var urldesc = function(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
};
return str 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(/(\[\d\d\d\d-\d\d-\d\d\s\d\d\:\d\d\:\d\d\] )(\*.+)/g, "$1<span class=\"sys\">$2</span>")
.replace( .replace(
@@ -62,7 +48,7 @@ function highlight(str) {
function fontSize(change) { function fontSize(change) {
var curSize = cookie_get("fontsize"); var curSize = cookie_get("fontsize");
if (curSize === null) { if (curSize === null) {
curSize = 12; curSize = DEFAULT_FONT_SIZE;
} else { } else {
curSize = + curSize; curSize = + curSize;
} }
@@ -71,25 +57,22 @@ function fontSize(change) {
cookie_set("fontsize", curSize); cookie_set("fontsize", curSize);
i("chatarea").style["fontSize"] = ""+curSize+"px"; $chatArea.style["fontSize"] = ""+curSize+"px";
} }
function toggleMenu() { function toggleMenu() {
t(i("tr1")); var $container = document.getElementById("menu-container");
t(i("tr2")); $container.style.display = ($container.style.display == 'block') ? 'none' : 'block';
t(i("spm"));
} }
function highlightLine(no) { 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>'; 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() { function onLoad() {
if (alreadyLoaded) { 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-' document.location.hash.substr(0, 6) === '#line-'
) { ) {
highlightLine( parseInt(document.location.hash.substr(6), 10) ); highlightLine( parseInt(document.location.hash.substr(6), 10) );
document.location.hash = '';
} }
// //
@@ -117,60 +103,23 @@ function onLoad() {
// //
i('selHub').onchange = function() { var $form = document.getElementById("search-form");
if ( /\/search\//.test(window.location.pathname) ) { $form.addEventListener("submit", function(ev) {
window.location.pathname = i('selHub').value + "/search/" + encodeURIComponent( i('searchbox').value ); var query = $form.elements["q"].value;
if (query.length === 0) {
} else if ( /\/rx\//.test(window.location.pathname) ) { alert("No search text entered");
window.location.pathname = i('selHub').value + "/rx/" + encodeURIComponent( i('searchbox').value ) ev.preventDefault();
return false;
} else {
i('frmHub').submit();
} }
};
// 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(); }); ev.preventDefault();
Mousetrap.bind('ctrl+alt+l', function() { i("pgnext").click(); }); return false;
Mousetrap.bind('ctrl+alt+j', function() {
YMgoto( YMmod(i("seldate").value, 1) )
});
Mousetrap.bind('ctrl+alt+k', function() {
YMgoto( YMmod(i("seldate").value, -1) )
}); });
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); 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 { #chatarea {
word-break:break-word; word-break:break-word;
white-space: pre; /* Safari 1/2, IE 6/7 */
white-space: pre-wrap; /* Chrome, Firefox, IE8++ */
} }
.timestamp { .timestamp {
@@ -77,14 +79,6 @@ select {
color: darkgreen; color: darkgreen;
} }
.logo {
width: 102px;
height: 37px;
display: block;
border:0;
padding-bottom: 1.0em;
}
.gt { .gt {
color:#0A0; color:#0A0;
font-weight:bold; font-weight:bold;
@@ -104,11 +98,18 @@ select {
.nav { .nav {
background: #DDD; background: #DDD;
box-shadow: 0px 4px 24px #CCC; box-shadow: 0px 4px 24px #CCC;
font-size:0; /* remove phantom spaces between elements */
} }
.nav form { .nav form {
display: inline; display: inline;
} }
.nav a, .nav .current-page, .nav select {
font-size: 12px;
line-height: 12px;
}
.nav .btn { .nav .btn {
background: white; background: white;
color:black; color:black;
@@ -117,8 +118,14 @@ select {
text-decoration:none; text-decoration:none;
} }
.nav a {
transition:all 0.1s linear;
}
.nav a:hover { .nav a:hover {
border-color: grey black black grey; border-color: grey black black grey;
transition:all 0s linear;
} }
.area-search { .area-search {
@@ -130,16 +137,33 @@ select {
} }
#logo { #logo {
cursor:pointer;
display:inline-block; display:inline-block;
width:16px; width:16px;
height: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-left:2px;
margin-right:4px; 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 */ /* Dropdown */
#menu-container {
display:none; /* default */
}
.ddmenu { .ddmenu {
display:block; display:block;
position:absolute; position:absolute;
@@ -165,8 +189,6 @@ select {
.ddmenu a:hover { .ddmenu a:hover {
background:#FFF; background:#FFF;
-moz-transition:all 0.1s linear;
-webkit-transition:all 0.1s linear;
} }
#tr1 { #tr1 {
@@ -202,7 +224,7 @@ select {
text-align:center; text-align:center;
} }
.nav, .nav select { .nav a, .nav .current-page, .nav select {
font-size: 16px; font-size: 16px;
line-height:16px; line-height:16px;
} }
@@ -227,12 +249,29 @@ select {
} }
.area-search { .area-search {
display:block;
float:none; float:none;
margin-top: 4px;
} }
#searchbox { #searchbox {
width:auto; 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) { @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 { 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 { func (this *URLHelper) hasGet(sz string) bool {

View File

@@ -1,19 +0,0 @@
Copyright (c) 2012 Jehiah Czebotar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,4 +0,0 @@
go-strftime
===========
go implementation of strftime

View File

@@ -1,72 +0,0 @@
// go implementation of strftime
package strftime
import (
"strings"
"time"
)
// taken from time/format.go
var conversion = map[rune]string{
/*stdLongMonth */ 'B': "January",
/*stdMonth */ 'b': "Jan",
// stdNumMonth */ 'm': "1",
/*stdZeroMonth */ 'm': "01",
/*stdLongWeekDay */ 'A': "Monday",
/*stdWeekDay */ 'a': "Mon",
// stdDay */ 'd': "2",
// stdUnderDay */ 'd': "_2",
/*stdZeroDay */ 'd': "02",
/*stdHour */ 'H': "15",
// stdHour12 */ 'I': "3",
/*stdZeroHour12 */ 'I': "03",
// stdMinute */ 'M': "4",
/*stdZeroMinute */ 'M': "04",
// stdSecond */ 'S': "5",
/*stdZeroSecond */ 'S': "05",
/*stdLongYear */ 'Y': "2006",
/*stdYear */ 'y': "06",
/*stdPM */ 'p': "PM",
// stdpm */ 'p': "pm",
/*stdTZ */ 'Z': "MST",
// stdISO8601TZ */ 'z': "Z0700", // prints Z for UTC
// stdISO8601ColonTZ */ 'z': "Z07:00", // prints Z for UTC
/*stdNumTZ */ 'z': "-0700", // always numeric
// stdNumShortTZ */ 'b': "-07", // always numeric
// stdNumColonTZ */ 'b': "-07:00", // always numeric
/* nonStdMilli */ 'L': ".000",
}
// This is an alternative to time.Format because no one knows
// what date 040305 is supposed to create when used as a 'layout' string
// this takes standard strftime format options. For a complete list
// of format options see http://strftime.org/
func Format(format string, t time.Time) string {
retval := make([]byte, 0, len(format))
for i, ni := 0, 0; i < len(format); i = ni + 2 {
ni = strings.IndexByte(format[i:], '%')
if ni < 0 {
ni = len(format)
} else {
ni += i
}
retval = append(retval, []byte(format[i:ni])...)
if ni+1 < len(format) {
c := format[ni+1]
if c == '%' {
retval = append(retval, '%')
} else {
if layoutCmd, ok := conversion[rune(c)]; ok {
retval = append(retval, []byte(t.Format(layoutCmd))...)
} else {
retval = append(retval, '%', c)
}
}
} else {
if ni < len(format) {
retval = append(retval, '%')
}
}
}
return string(retval)
}

View File

@@ -1,49 +0,0 @@
package strftime
import (
"fmt"
"testing"
"time"
)
func ExampleFormat() {
t := time.Unix(1340244776, 0)
utc, _ := time.LoadLocation("UTC")
t = t.In(utc)
fmt.Println(Format("%Y-%m-%d %H:%M:%S", t))
// Output:
// 2012-06-21 02:12:56
}
func TestNoLeadingPercentSign(t *testing.T) {
tm := time.Unix(1340244776, 0)
utc, _ := time.LoadLocation("UTC")
tm = tm.In(utc)
result := Format("aaabbb0123456789%Y", tm)
if result != "aaabbb01234567892012" {
t.Logf("%s != %s", result, "aaabbb01234567892012")
t.Fail()
}
}
func TestUnsupported(t *testing.T) {
tm := time.Unix(1340244776, 0)
utc, _ := time.LoadLocation("UTC")
tm = tm.In(utc)
result := Format("%0%1%%%2", tm)
if result != "%0%1%%2" {
t.Logf("%s != %s", result, "%0%1%%2")
t.Fail()
}
}
func TestRubyStrftime(t *testing.T) {
tm := time.Unix(1340244776, 0)
utc, _ := time.LoadLocation("UTC")
tm = tm.In(utc)
result := Format("%H:%M:%S%L", tm)
if result != "02:12:56.000" {
t.Logf("%s != %s", result, "02:12:56.000")
t.Fail()
}
}