package web
import (
"html/template"
"net/http"
"strings"
"time"
"craftdesign.group/quarry/internal/highlight"
"craftdesign.group/quarry/internal/theme"
)
// A reader's site theme and code theme (internal/theme): two cookies,
// like the light/dark choice, so they hold for anyone, signed in or
// not, and reach the page before it is drawn. Each holds a family, a
// light theme and a dark one; the page draws the side that matches the
// light/dark choice. An empty or unknown value is no choice: quarry's
// own palette, and code the same as the site.
const (
siteThemeCookie = "site-theme"
codeThemeCookie = "code-theme"
)
// themeChoice reads one theme cookie, or nothing for no choice or an id
// this build does not know (a theme dropped since it was chosen). A
// single theme's id, saved before themes came in pairs, finds its family.
func themeChoice(req *http.Request, name string) (theme.Family, bool) {
c, err := req.Cookie(name)
if err != nil {
return theme.Family{}, false
}
return theme.FamilyByID(c.Value)
}
// readAppearance sets a page's theme sheets from the reader's cookies.
func readAppearance(req *http.Request, data *pageData) {
if f, ok := themeChoice(req, siteThemeCookie); ok {
data.SiteThemeURL = theme.FamilySiteURL(f)
}
if f, ok := themeChoice(req, codeThemeCookie); ok {
data.CodeThemeURL = theme.FamilyCodeURL(f)
}
}
// appearView is /me's Appearance section: both lists of families, each
// with the current choice marked, and a sample to watch while choosing.
type appearView struct {
Site, Code []themeOption
SiteID string
CodeID string
SampleCode template.HTML
SampleDiff []sampleRow
}
type themeOption struct {
ID, Name, URL string
}
type sampleRow struct {
Class string
Old, New string
HTML template.HTML
}
// The sample: a few lines that show every syntax colour, and a diff
// that shows both rows and a word mark.
const sampleGo = `// Lease is one hold on a name.
type Lease struct {
Name string
Until time.Time
}
func (l *Lease) Renew(d time.Duration) error {
if d <= 0 {
return fmt.Errorf("renew %q: %d is not a duration", l.Name, d)
}
l.Until = time.Now().Add(d)
return nil
}`
var sampleDiffOld = []string{
"func (l *Lease) Lost() bool {",
"\treturn time.Now().After(l.Until)",
"}",
}
var sampleDiffNew = []string{
"func (l *Lease) Lost() bool {",
"\treturn l.ctx.Err() != nil || time.Now().After(l.Until)",
"}",
}
func appearanceOf(req *http.Request) *appearView {
v := &appearView{}
if f, ok := themeChoice(req, siteThemeCookie); ok {
v.SiteID = f.ID
}
if f, ok := themeChoice(req, codeThemeCookie); ok {
v.CodeID = f.ID
}
for _, f := range theme.Families {
v.Site = append(v.Site, themeOption{ID: f.ID, Name: f.Name, URL: theme.FamilySiteURL(f)})
v.Code = append(v.Code, themeOption{ID: f.ID, Name: f.Name, URL: theme.FamilyCodeURL(f)})
}
if html, ok := highlight.Code([]byte(sampleGo), "go", ""); ok {
v.SampleCode = template.HTML(html)
}
oldL, _, _ := highlight.Lines([]byte(strings.Join(sampleDiffOld, "\n")), "go", "", 0, len(sampleDiffOld))
newL, _, _ := highlight.Lines([]byte(strings.Join(sampleDiffNew, "\n")), "go", "", 0, len(sampleDiffNew))
at := func(l []string, i int) template.HTML {
if i < len(l) {
return template.HTML(l[i])
}
return ""
}
v.SampleDiff = []sampleRow{
{Class: "d-context", Old: "41", New: "41", HTML: at(newL, 0)},
{Class: "d-deletion", Old: "42", HTML: at(oldL, 1)},
{Class: "d-addition", New: "42", HTML: at(newL, 1)},
{Class: "d-context", Old: "43", New: "43", HTML: at(newL, 2)},
}
return v
}
// registerAppearance serves every theme's sheets and takes /me's form.
func registerAppearance(mux *http.ServeMux) {
for url, css := range theme.Assets() {
mux.HandleFunc("GET "+url, func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.Write([]byte(css))
})
}
mux.HandleFunc("POST /me/appearance", func(w http.ResponseWriter, req *http.Request) {
for _, f := range []struct{ field, cookie string }{{"site", siteThemeCookie}, {"code", codeThemeCookie}} {
id := req.FormValue(f.field)
if id != "" {
fam, ok := theme.FamilyByID(id)
if !ok {
http.Error(w, "unknown theme", http.StatusBadRequest)
return
}
id = fam.ID
}
c := &http.Cookie{Name: f.cookie, Value: id, Path: "/",
HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 400 * 24 * 60 * 60}
if id == "" {
// Quarry's own palette is the absence of a choice, as
// "follow the system" is for light and dark.
c.MaxAge, c.Expires = -1, time.Unix(0, 0)
}
http.SetCookie(w, c)
}
http.Redirect(w, req, "/me#appearance", http.StatusSeeOther)
})
}