Skip to content
quarry Artifact [5460fdfb18]
Login

Artifact [5460fdfb18]

Artifact 5460fdfb18958d76d6890eac3c27191663ffe9de6cfbecbe7f46d1e78d1b8445:


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)
	})
}