Skip to content
quarry Artifact [7b5ca57670]
Login

Artifact [7b5ca57670]

Artifact 7b5ca57670ca312753f0bf37272633f67222522d6f0eafd404f98c457a1e4abc:


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. 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).
func themeChoice(req *http.Request, name string) (theme.Theme, bool) {
	c, err := req.Cookie(name)
	if err != nil {
		return theme.Theme{}, false
	}
	return theme.ByID(c.Value)
}

// readAppearance sets a page's theme sheets from the reader's cookies.
func readAppearance(req *http.Request, data *pageData) {
	if t, ok := themeChoice(req, siteThemeCookie); ok {
		data.SiteTheme, data.SiteThemeURL = t.Name, theme.SiteURL(t)
	}
	if t, ok := themeChoice(req, codeThemeCookie); ok {
		data.CodeThemeURL = theme.CodeURL(t)
	}
}

// appearView is /me's Appearance section: both lists, each with the
// current choice marked, and a sample to watch while choosing.
type appearView struct {
	Site, Code []themeGroup
	SiteID     string
	CodeID     string
	SampleCode template.HTML
	SampleDiff []sampleRow
}

type themeGroup struct {
	Label  string
	Themes []themeOption
}

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 t, ok := themeChoice(req, siteThemeCookie); ok {
		v.SiteID = t.ID
	}
	if t, ok := themeChoice(req, codeThemeCookie); ok {
		v.CodeID = t.ID
	}
	dark, light := themeGroup{Label: "Dark"}, themeGroup{Label: "Light"}
	code := themeGroup{Label: "Dark"}
	codeLight := themeGroup{Label: "Light"}
	for _, t := range theme.All {
		s := themeOption{ID: t.ID, Name: t.Name, URL: theme.SiteURL(t)}
		c := themeOption{ID: t.ID, Name: t.Name, URL: theme.CodeURL(t)}
		if t.Dark {
			dark.Themes, code.Themes = append(dark.Themes, s), append(code.Themes, c)
		} else {
			light.Themes, codeLight.Themes = append(light.Themes, s), append(codeLight.Themes, c)
		}
	}
	v.Site = []themeGroup{dark, light}
	v.Code = []themeGroup{code, codeLight}
	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 _, ok := theme.ByID(id); id != "" && !ok {
				http.Error(w, "unknown theme", http.StatusBadRequest)
				return
			}
			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)
	})
}