package theme import ( "fmt" "regexp" "sort" "strconv" "strings" ) var fontSizeOverrideKeys = map[string]bool{ "h1": true, "h2": true, "h3": true, "h4": true, "h5": true, "h6": true, "post-title": true, "post-lede": true, "post-body": true, "post-h2": true, "post-h3": true, "post-meta": true, "page-title": true, "page-lede": true, "index-card-title": true, "hero-title": true, "hero-subtitle": true, "button": true, "nav-link": true, } var fontSizePattern = regexp.MustCompile(`^(\d+(?:\.\d+)?|\.\d+)(rem|px)$`) // FontSizeOverrideKeys returns the canonical override keys, sorted. func FontSizeOverrideKeys() []string { keys := make([]string, 0, len(fontSizeOverrideKeys)) for k := range fontSizeOverrideKeys { keys = append(keys, k) } sort.Strings(keys) return keys } // ValidateFontSize accepts unit-suffixed CSS lengths within sane bounds. // Values are emitted into a stylesheet, so this is a security gate. func ValidateFontSize(v string) error { m := fontSizePattern.FindStringSubmatch(v) if m == nil { return fmt.Errorf("font size %q must be a number with rem or px unit", v) } n, err := strconv.ParseFloat(m[1], 64) if err != nil { return fmt.Errorf("font size %q is not a number", v) } switch m[2] { case "rem": if n < 0.25 || n > 10 { return fmt.Errorf("font size %q out of range (0.25rem to 10rem)", v) } case "px": if n < 4 || n > 160 { return fmt.Errorf("font size %q out of range (4px to 160px)", v) } } return nil } // ParseFontSizeBase normalizes a stored base font size and validates // unit-suffixed values. func ParseFontSizeBase(v string) (string, error) { v = strings.TrimSpace(v) if v == "" { return "", nil } // Legacy bare numbers were never consumed by any stylesheet, so they are // normalized to the 1rem default rather than resurrected as a real size. if _, err := strconv.ParseFloat(v, 64); err == nil { v = "1rem" } if err := ValidateFontSize(v); err != nil { return "", err } return v, nil } // NormalizeFontSizeBase returns "" for anything invalid so bad stored data // degrades to the CSS fallback instead of breaking the sheet. func NormalizeFontSizeBase(v string) string { out, _ := ParseFontSizeBase(v) return out } // SanitizeFontSizeOverrides drops empty values, rejects unknown keys and // invalid values, and returns a fresh map safe to persist and emit. func SanitizeFontSizeOverrides(m map[string]string) (map[string]string, error) { if len(m) == 0 { return nil, nil } out := make(map[string]string, len(m)) for k, v := range m { if v == "" { continue } if !fontSizeOverrideKeys[k] { return nil, fmt.Errorf("unknown font size element %q", k) } if err := ValidateFontSize(v); err != nil { return nil, err } out[k] = v } if len(out) == 0 { return nil, nil } return out, nil }