aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/theme.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/theme.go')
-rw-r--r--internal/web/theme.go68
1 files changed, 52 insertions, 16 deletions
diff --git a/internal/web/theme.go b/internal/web/theme.go
index c0c0729..26181c2 100644
--- a/internal/web/theme.go
+++ b/internal/web/theme.go
@@ -28,6 +28,11 @@ type Theme struct {
Font string `json:"font"` // sans | serif | mono
HeadingFont string `json:"heading_font"` // same | sans | serif | mono
FontSize string `json:"font_size"` // small | normal | large
+ // Uploaded fonts (file uuids or ""): when set, the blog is typeset in the
+ // file and Font/HeadingFont are the stack readers see until it loads, or
+ // instead of it when their browser cannot use it.
+ CustomFont string `json:"custom_font"`
+ CustomHeadingFont string `json:"custom_heading_font"`
HeaderImage string `json:"header_image"`
HeaderImageHeight string `json:"header_image_height"` // short | medium | tall
@@ -142,7 +147,7 @@ func color(v, def string) string {
// optColor is for colours that may be unset ("" = inherit).
func optColor(v string) string { return color(v, "") }
-func imageID(v string) string {
+func fileID(v string) string {
if _, err := uuid.Parse(v); err != nil {
return ""
}
@@ -153,7 +158,7 @@ func imageID(v string) string {
func (t *Theme) normalize() {
d := DefaultTheme()
t.BgColor = color(t.BgColor, d.BgColor)
- t.BgImage = imageID(t.BgImage)
+ t.BgImage = fileID(t.BgImage)
t.BgMode = pick(t.BgMode, "cover", "tile", "fixed")
t.ContentBg = color(t.ContentBg, d.ContentBg)
t.TextColor = color(t.TextColor, d.TextColor)
@@ -165,7 +170,9 @@ func (t *Theme) normalize() {
t.Font = pick(t.Font, "sans", "serif", "mono")
t.HeadingFont = pick(t.HeadingFont, "same", "sans", "serif", "mono")
t.FontSize = pick(t.FontSize, "normal", "small", "large")
- t.HeaderImage = imageID(t.HeaderImage)
+ t.CustomFont = fileID(t.CustomFont)
+ t.CustomHeadingFont = fileID(t.CustomHeadingFont)
+ t.HeaderImage = fileID(t.HeaderImage)
t.HeaderImageHeight = pick(t.HeaderImageHeight, "medium", "short", "tall")
t.HeaderAlign = pick(t.HeaderAlign, "left", "center")
t.HeaderBg = color(t.HeaderBg, d.HeaderBg)
@@ -184,9 +191,9 @@ func (t *Theme) normalize() {
t.FooterBg = color(t.FooterBg, d.FooterBg)
t.FooterColor = color(t.FooterColor, d.FooterColor)
t.FooterAlign = pick(t.FooterAlign, "center", "left")
- t.Logo = imageID(t.Logo)
+ t.Logo = fileID(t.Logo)
t.LogoSize = pick(t.LogoSize, "medium", "small", "large")
- t.Favicon = imageID(t.Favicon)
+ t.Favicon = fileID(t.Favicon)
t.LeftWidth = clampInt(t.LeftWidth, minColumnWidth, maxColumnWidth)
t.RightWidth = clampInt(t.RightWidth, minColumnWidth, maxColumnWidth)
if t.LeftWidth+t.RightWidth > maxColumnsSum { // the right column gives way
@@ -250,10 +257,12 @@ func ThemeFromForm(cur Theme, f url.Values) Theme {
t.FooterColor = get("footer_color")
t.FooterAlign = get("footer_align")
t.LogoSize = get("logo_size")
- t.BgImage = pickImage(t.BgImage, get("bg_image"))
- t.HeaderImage = pickImage(t.HeaderImage, get("header_image"))
- t.Favicon = pickImage(t.Favicon, get("favicon"))
- t.Logo = pickImage(t.Logo, get("logo"))
+ t.BgImage = pickFile(t.BgImage, get("bg_image"))
+ t.HeaderImage = pickFile(t.HeaderImage, get("header_image"))
+ t.Favicon = pickFile(t.Favicon, get("favicon"))
+ t.Logo = pickFile(t.Logo, get("logo"))
+ t.CustomFont = pickFile(t.CustomFont, get("custom_font"))
+ t.CustomHeadingFont = pickFile(t.CustomHeadingFont, get("custom_heading_font"))
t.HeaderOn, t.LeftOn, t.RightOn, t.FooterOn = on("header_on"), on("left_on"), on("right_on"), on("footer_on")
t.LeftWidth, t.RightWidth = num("left_width"), num("right_width")
t.KeepColumns = on("keep_columns")
@@ -280,9 +289,9 @@ func (t Theme) AreaOn(area string) bool {
// MainWidth is what the main column gets when both side columns are shown.
func (t Theme) MainWidth() int { return 100 - t.LeftWidth - t.RightWidth }
-// pickImage applies an imagepick radio: "none" clears, an id selects, and an
-// empty value (no radio in the form) keeps what was there.
-func pickImage(cur, v string) string {
+// pickFile applies an imagepick or fontpick select: "none" clears, an id
+// selects, and an empty value (no select in the form) keeps what was there.
+func pickFile(cur, v string) string {
switch v {
case "":
return cur
@@ -364,10 +373,31 @@ func fontStack(name string) string {
return `"Helvetica Neue", Helvetica, Arial, sans-serif`
}
-// Helpers used by the CSS template.
-func (t Theme) FontFamily() string { return fontStack(t.Font) }
+// Helpers used by the CSS template. The @font-face families are named for
+// their role, so a stack is the uploaded font first and the built-in choice as
+// its fallback.
+func (t Theme) FontFamily() string {
+ if t.CustomFont != "" {
+ return `"BlogFont", ` + fontStack(t.Font)
+ }
+ return fontStack(t.Font)
+}
+
+// HeadingFontFamily builds on the text's stack when the heading font is
+// "same", so a custom heading font falls back to the custom text font.
+func (t Theme) HeadingFontFamily() string {
+ stack := t.FontFamily()
+ if t.HeadingFont != "same" {
+ stack = fontStack(t.HeadingFont)
+ }
+ if t.CustomHeadingFont != "" {
+ return `"BlogHeadingFont", ` + stack
+ }
+ return stack
+}
-func (t Theme) HeadingFontFamily() string { return fontStack(t.HeadingFont) }
+// HeadingFontSet says whether headings need a font-family rule of their own.
+func (t Theme) HeadingFontSet() bool { return t.HeadingFont != "same" || t.CustomHeadingFont != "" }
func (t Theme) FontSizePx() string {
switch t.FontSize {
@@ -436,13 +466,19 @@ func (t Theme) HeaderImageMaxHeight() string {
}
var themeCSS = template.Must(template.New("theme").Funcs(template.FuncMap{"sub": func(a, b int) int { return a - b }}).Parse(`
+{{- if .CustomFont}}
+@font-face { font-family:"BlogFont"; src:url(/media/{{.CustomFont}}); font-display:swap; }
+{{- end}}
+{{- if .CustomHeadingFont}}
+@font-face { font-family:"BlogHeadingFont"; src:url(/media/{{.CustomHeadingFont}}); font-display:swap; }
+{{- end}}
body { margin:0; background-color:{{.BgColor}}; color:{{.TextColor}}; font-family:{{.FontFamily}}; font-size:{{.FontSizePx}}; line-height:1.55;
{{- if .BgImage}} background-image:url(/media/{{.BgImage}});
{{- if eq .BgMode "cover"}} background-size:cover; background-position:center top; background-repeat:no-repeat;
{{- else if eq .BgMode "fixed"}} background-size:cover; background-position:center; background-attachment:fixed; background-repeat:no-repeat;
{{- else}} background-repeat:repeat;{{end}}
{{- end}} }
-{{- if ne .HeadingFont "same"}}
+{{- if .HeadingFontSet}}
h1, h2, h3, h4, h5, h6 { font-family:{{.HeadingFontFamily}}; }
{{- end}}
a { color:{{.LinkColor}};{{if eq .LinkUnderline "always"}} text-decoration:underline;{{else}} text-decoration:none;{{end}} }