diff options
Diffstat (limited to 'internal/web/theme.go')
| -rw-r--r-- | internal/web/theme.go | 68 |
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}} } |
