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

176 lines
4.4 KiB
Go

package theme
import (
"fmt"
"math"
"strconv"
"strings"
)
// HSLToHex converts an HSL string like "220 14% 96%" to a hex color like "#f4f5f7".
// This is needed for email templates since email clients don't support CSS variables.
func HSLToHex(hsl string) string {
h, s, l, err := parseHSL(hsl)
if err != nil {
// Return a fallback color on parse error
return "#000000"
}
r, g, b := hslToRGB(h, s, l)
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}
// parseHSL parses an HSL string like "220 14% 96%" into h (0-360), s (0-1), l (0-1).
func parseHSL(hsl string) (h, s, l float64, err error) {
// Trim and normalize spaces
hsl = strings.TrimSpace(hsl)
parts := strings.Fields(hsl)
if len(parts) != 3 {
return 0, 0, 0, fmt.Errorf("invalid HSL format: expected 3 parts, got %d", len(parts))
}
// Parse hue (0-360 degrees)
h, err = strconv.ParseFloat(parts[0], 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid hue value: %s", parts[0])
}
// Parse saturation (0-100%)
satStr := strings.TrimSuffix(parts[1], "%")
s, err = strconv.ParseFloat(satStr, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid saturation value: %s", parts[1])
}
s = s / 100.0 // Convert to 0-1 range
// Parse lightness (0-100%)
lightStr := strings.TrimSuffix(parts[2], "%")
l, err = strconv.ParseFloat(lightStr, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid lightness value: %s", parts[2])
}
l = l / 100.0 // Convert to 0-1 range
return h, s, l, nil
}
// hslToRGB converts HSL values to RGB.
// h: 0-360, s: 0-1, l: 0-1
// Returns r, g, b in 0-255 range.
func hslToRGB(h, s, l float64) (r, g, b uint8) {
// Normalize hue to 0-1 range
h = h / 360.0
var rFloat, gFloat, bFloat float64
if s == 0 {
// Achromatic (gray)
rFloat = l
gFloat = l
bFloat = l
} else {
var q float64
if l < 0.5 {
q = l * (1 + s)
} else {
q = l + s - l*s
}
p := 2*l - q
rFloat = hueToRGB(p, q, h+1.0/3.0)
gFloat = hueToRGB(p, q, h)
bFloat = hueToRGB(p, q, h-1.0/3.0)
}
r = uint8(math.Round(rFloat * 255))
g = uint8(math.Round(gFloat * 255))
b = uint8(math.Round(bFloat * 255))
return r, g, b
}
// hueToRGB is a helper function for HSL to RGB conversion.
func hueToRGB(p, q, t float64) float64 {
if t < 0 {
t += 1
}
if t > 1 {
t -= 1
}
if t < 1.0/6.0 {
return p + (q-p)*6*t
}
if t < 1.0/2.0 {
return q
}
if t < 2.0/3.0 {
return p + (q-p)*(2.0/3.0-t)*6
}
return p
}
// ThemeColorsToEmail converts theme color settings (stored as HSL) to EmailColors (hex).
// This function takes the color map from theme settings and returns hex colors for email inlining.
func ThemeColorsToEmail(colors map[string]string) map[string]string {
result := make(map[string]string)
colorKeys := []string{
"primary", "primary-foreground",
"secondary", "secondary-foreground",
"background", "foreground",
"muted", "muted-foreground",
"border",
"card", "card-foreground",
"accent", "accent-foreground",
"destructive", "destructive-foreground",
}
for _, key := range colorKeys {
if hsl, ok := colors[key]; ok && hsl != "" {
result[key] = HSLToHex(hsl)
}
}
return result
}
// ColorSchemeToEmailColors converts a theme ColorScheme (HSL strings) to hex colors for email inlining.
// Email clients don't support CSS variables, so colors must be inlined as hex.
func ColorSchemeToEmailColors(cs *ColorScheme) EmailColors {
if cs == nil {
return EmailColors{}
}
return EmailColors{
Primary: HSLToHex(cs.Primary),
PrimaryForeground: HSLToHex(cs.PrimaryForeground),
Secondary: HSLToHex(cs.Secondary),
SecondaryForeground: HSLToHex(cs.SecondaryForeground),
Background: HSLToHex(cs.Background),
Foreground: HSLToHex(cs.Foreground),
Muted: HSLToHex(cs.Muted),
MutedForeground: HSLToHex(cs.MutedForeground),
Border: HSLToHex(cs.Border),
Card: HSLToHex(cs.Card),
CardForeground: HSLToHex(cs.CardForeground),
}
}
// EmailColors contains theme colors converted to hex for email inlining.
// Mirrors templates.EmailColors structure but lives in theme package for conversion.
type EmailColors struct {
Primary string
PrimaryForeground string
Secondary string
SecondaryForeground string
Background string
Foreground string
Muted string
MutedForeground string
Border string
Card string
CardForeground string
}