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>
135 lines
6.6 KiB
Go
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()
|
|
}
|