diff options
| author | grm <grm@eyesin.space> | 2026-09-17 21:29:44 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-17 21:29:44 +0300 |
| commit | e96bbec551c0388e972894b349413fa6ef607352 (patch) | |
| tree | c63b50d515fc0a0f754f970b8d316dd37f73ac80 /internal/web/web_test.go | |
| parent | 69912c4ac04ada3286c09400ce523d5d3ab4cfb4 (diff) | |
| download | blogspace-e96bbec551c0388e972894b349413fa6ef607352.tar.gz blogspace-e96bbec551c0388e972894b349413fa6ef607352.tar.bz2 blogspace-e96bbec551c0388e972894b349413fa6ef607352.zip | |
Add custom fonts: upload WOFF2/WOFF/TTF/OTF files and pick them on the Design tab
Fonts become a file kind of their own (sniffed from the bytes like images,
served inline from /media), and the theme gets custom_font and
custom_heading_font: a chosen file is declared as @font-face and put first
in the stack, the built-in Font / Heading font choice behind it as the
fallback, so there is no "custom" enum value and no invalid state. The
pickers are plain selects with an upload input, no script. Fonts uploaded
before this are reclassified by the migration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/web_test.go')
| -rw-r--r-- | internal/web/web_test.go | 35 |
1 files changed, 33 insertions, 2 deletions
diff --git a/internal/web/web_test.go b/internal/web/web_test.go index 914c991..42d29e4 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -82,8 +82,8 @@ func TestRootRoutePrecedence(t *testing.T) { } func TestThemeNormalizeAndCSS(t *testing.T) { - th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","logo":"../etc","link_hover":"blue","heading_font":"wingdings","favicon":"x","logo_size":"huge","left_width":5,"right_width":90,"header_rule":"neon\"><script>"}`)) - if th.BgColor != DefaultTheme().BgColor || th.Font != "sans" || th.BgImage != "" || th.Logo != "" || th.LinkHover != "" || th.HeadingFont != "same" || th.Favicon != "" || th.LogoSize != "medium" || th.HeaderRule != "none" || th.RuleClass() != "" { + th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","logo":"../etc","link_hover":"blue","heading_font":"wingdings","favicon":"x","logo_size":"huge","left_width":5,"right_width":90,"header_rule":"neon\"><script>","custom_font":"x.woff2);}body{display:none"}`)) + if th.BgColor != DefaultTheme().BgColor || th.Font != "sans" || th.BgImage != "" || th.Logo != "" || th.LinkHover != "" || th.HeadingFont != "same" || th.Favicon != "" || th.LogoSize != "medium" || th.HeaderRule != "none" || th.RuleClass() != "" || th.CustomFont != "" { t.Errorf("normalize: %+v", th) } if th.LeftWidth != 15 || th.RightWidth != 40 { @@ -128,6 +128,37 @@ func TestThemeNormalizeAndCSS(t *testing.T) { if !strings.Contains(css, ".site-nav a { color:#222222; font-weight:normal; text-decoration:underline; }") { t.Errorf("plain nav css: %s", css) } + if css := DefaultTheme().CSS(); strings.Contains(css, "@font-face") || strings.Contains(css, "h1, h2") { + t.Errorf("default css should declare no font faces or heading rule: %s", css) + } +} + +// An uploaded font is declared as a face and put first in the stack, the +// built-in choice behind it; the heading font falls back to the text's stack. +func TestCustomFontCSS(t *testing.T) { + body, head := "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "6ba7b811-9dad-11d1-80b4-00c04fd430c8" + th := ThemeFromForm(DefaultTheme(), url.Values{"custom_font": {body}, "font": {"serif"}}) + css := th.CSS() + for _, want := range []string{`@font-face { font-family:"BlogFont"; src:url(/media/` + body + `); font-display:swap; }`, `font-family:"BlogFont", Georgia`} { + if !strings.Contains(css, want) { + t.Errorf("css missing %q:\n%s", want, css) + } + } + if strings.Contains(css, "BlogHeadingFont") || strings.Contains(css, "h1, h2") { + t.Errorf("no heading font was chosen: %s", css) + } + // A heading font with "same as text" still gets its own rule, falling back to the text font. + th = ThemeFromForm(th, url.Values{"custom_font": {body}, "custom_heading_font": {head}, "font": {"serif"}, "heading_font": {"same"}}) + css = th.CSS() + for _, want := range []string{`font-family:"BlogHeadingFont"; src:url(/media/` + head + `)`, `h1, h2, h3, h4, h5, h6 { font-family:"BlogHeadingFont", "BlogFont", Georgia`} { + if !strings.Contains(css, want) { + t.Errorf("css missing %q:\n%s", want, css) + } + } + th = ThemeFromForm(th, url.Values{"custom_font": {"none"}, "custom_heading_font": {head}, "heading_font": {"mono"}}) + if css = th.CSS(); strings.Contains(css, "BlogFont\"") || !strings.Contains(css, `h1, h2, h3, h4, h5, h6 { font-family:"BlogHeadingFont", "Courier New"`) { + t.Errorf("heading stack after clearing the text font: %s", css) + } } // The header rule is drawn beside the title and logo blocks — after them, and |
