check-safety/internal/theme/google_fonts.go
Alex Dunmow cd88c808b0 initial: standalone check-safety module hoisted from CMS
Static safety/lint runner for the BlockNinja codebase. ~25 invariant
checks across Go and frontend sources. Was at git.dev.alexdunmow.com:block/ninja
in backend/cmd/check-safety/ until the 2026-06-06 consolidation moved
the BlockNinja repos under a shared ~/src/blockninja/ parent.

This repo is the standalone extraction:
- Own go.mod (git.dev.alexdunmow.com/block/check-safety, go 1.26.4)
- Vendored internal/{helpers,theme} from CMS (Go's internal/ rule
  blocks cross-module imports; vendoring is the workaround)
- CLI contract unchanged: `check-safety <target-dir> [--flags]`
- CMS Makefile shells into ../check-safety for safety-check /
  install-safety-checker targets

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 13:04:02 +08:00

135 lines
6.6 KiB
Go

package theme
import "strings"
// GoogleFont represents a curated Google Font
type GoogleFont struct {
Family string `json:"family"`
Category string `json:"category"`
Variants []string `json:"variants"`
}
// CuratedGoogleFonts returns the list of popular Google Fonts
// These are embedded in the binary - no API key needed
func CuratedGoogleFonts() []GoogleFont {
return []GoogleFont{
// Sans-serif
{Family: "Inter", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Roboto", Category: "sans-serif", Variants: []string{"400", "500", "700", "400i", "700i"}},
{Family: "Open Sans", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Lato", Category: "sans-serif", Variants: []string{"400", "700", "400i", "700i"}},
{Family: "Montserrat", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Poppins", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Source Sans 3", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Nunito", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Raleway", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Work Sans", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "DM Sans", Category: "sans-serif", Variants: []string{"400", "500", "700", "400i", "700i"}},
{Family: "Manrope", Category: "sans-serif", Variants: []string{"400", "500", "600", "700"}},
{Family: "Plus Jakarta Sans", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Outfit", Category: "sans-serif", Variants: []string{"400", "500", "600", "700"}},
{Family: "Lexend", Category: "sans-serif", Variants: []string{"400", "500", "600", "700"}},
{Family: "Figtree", Category: "sans-serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Geist", Category: "sans-serif", Variants: []string{"400", "500", "600", "700"}},
// Serif
{Family: "Playfair Display", Category: "serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Merriweather", Category: "serif", Variants: []string{"400", "700", "400i", "700i"}},
{Family: "Lora", Category: "serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "PT Serif", Category: "serif", Variants: []string{"400", "700", "400i", "700i"}},
{Family: "Source Serif 4", Category: "serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Libre Baskerville", Category: "serif", Variants: []string{"400", "700", "400i"}},
{Family: "Crimson Text", Category: "serif", Variants: []string{"400", "600", "700", "400i", "600i", "700i"}},
{Family: "EB Garamond", Category: "serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Bitter", Category: "serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Cormorant", Category: "serif", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
// Monospace
{Family: "JetBrains Mono", Category: "monospace", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Fira Code", Category: "monospace", Variants: []string{"400", "500", "600", "700"}},
{Family: "Source Code Pro", Category: "monospace", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "IBM Plex Mono", Category: "monospace", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Roboto Mono", Category: "monospace", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Space Mono", Category: "monospace", Variants: []string{"400", "700", "400i", "700i"}},
{Family: "Inconsolata", Category: "monospace", Variants: []string{"400", "500", "600", "700"}},
// Display
{Family: "Oswald", Category: "display", Variants: []string{"400", "500", "600", "700"}},
{Family: "Bebas Neue", Category: "display", Variants: []string{"400"}},
{Family: "Anton", Category: "display", Variants: []string{"400"}},
{Family: "Archivo Black", Category: "display", Variants: []string{"400"}},
{Family: "Righteous", Category: "display", Variants: []string{"400"}},
{Family: "Rubik", Category: "display", Variants: []string{"400", "500", "600", "700", "400i", "700i"}},
{Family: "Space Grotesk", Category: "display", Variants: []string{"400", "500", "600", "700"}},
{Family: "Sora", Category: "display", Variants: []string{"400", "500", "600", "700"}},
// Handwriting
{Family: "Dancing Script", Category: "handwriting", Variants: []string{"400", "500", "600", "700"}},
{Family: "Pacifico", Category: "handwriting", Variants: []string{"400"}},
{Family: "Caveat", Category: "handwriting", Variants: []string{"400", "500", "600", "700"}},
{Family: "Satisfy", Category: "handwriting", Variants: []string{"400"}},
}
}
// BuildGoogleFontURL generates a Google Fonts CSS URL
func BuildGoogleFontURL(family string, variants []string) string {
// Format: https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap
// For italic: family=Inter:ital,wght@0,400;0,700;1,400;1,700
hasItalic := false
weights := []string{}
for _, v := range variants {
if len(v) > 0 && v[len(v)-1] == 'i' {
hasItalic = true
weights = append(weights, v[:len(v)-1])
} else {
weights = append(weights, v)
}
}
// Deduplicate weights
seen := make(map[string]bool)
uniqueWeights := []string{}
for _, w := range weights {
if !seen[w] {
seen[w] = true
uniqueWeights = append(uniqueWeights, w)
}
}
familyEncoded := family
// Replace spaces with +
for i := 0; i < len(familyEncoded); i++ {
if familyEncoded[i] == ' ' {
familyEncoded = familyEncoded[:i] + "+" + familyEncoded[i+1:]
}
}
if hasItalic {
// Use ital,wght axis
var specs []string
for _, w := range uniqueWeights {
specs = append(specs, "0,"+w) // normal
}
for _, w := range uniqueWeights {
specs = append(specs, "1,"+w) // italic
}
return "https://fonts.googleapis.com/css2?family=" + familyEncoded + ":ital,wght@" + joinStrings(specs, ";") + "&display=swap"
}
return "https://fonts.googleapis.com/css2?family=" + familyEncoded + ":wght@" + joinStrings(uniqueWeights, ";") + "&display=swap"
}
func joinStrings(strs []string, sep string) string {
if len(strs) == 0 {
return ""
}
var result strings.Builder
result.WriteString(strs[0])
for i := 1; i < len(strs); i++ {
result.WriteString(sep + strs[i])
}
return result.String()
}