Diff
diff --git a/element/expander.go b/element/expander.go
index 56af9e3..92a9e1c 100644
--- a/element/expander.go
+++ b/element/expander.go
@@ -4 +3,0 @@ import (
- "grim/color"
@@ -9,12 +7,0 @@ import (
-var backgroundProps = []string{
- "background-image",
- "background-position-x",
- "background-position-y",
- "background-size",
- "background-repeat",
- "background-attachment",
- "background-origin",
- "background-clip",
- "background-color",
-}
-
@@ -23,10 +10,5 @@ func Expander(styles map[string]string) map[string]string {
-
- for _, v := range backgroundProps {
- for _, bg := range parsed {
- if bg[v] != "" {
- if styles[v] != "" {
- styles[v] += "," + bg[v]
- } else {
- styles[v] = bg[v]
- }
- }
+ delete(styles, "background")
+ // Print result
+ for key, value := range parsed {
+ if value != "" {
+ styles[key] = value
@@ -35 +16,0 @@ func Expander(styles map[string]string) map[string]string {
- delete(styles, "background")
@@ -98,5 +79,4 @@ func convertMarginToIndividualProperties(margin string) (string, string, string,
-// parseBackground parses CSS background shorthand into its component properties
-func parseBackground(background string) []map[string]string {
- // Split into layers
- layers := splitLayers(background)
- result := make([]map[string]string, len(layers))
+// ParseBackground takes a CSS background shorthand and returns a map of its component parts.
+func parseBackground(background string) map[string]string {
+ parts := splitBackground(background)
+ result := make(map[string]string)
@@ -104,3 +84,9 @@ func parseBackground(background string) []map[string]string {
- for i, layer := range layers {
- result[i] = parseLayer(layer)
- }
+ // Default component properties
+ result["background-color"] = ""
+ result["background-image"] = "none"
+ result["background-repeat"] = "repeat"
+ result["background-position"] = "0% 0%"
+ result["background-size"] = "auto"
+ result["background-attachment"] = "scroll"
+ result["background-origin"] = "padding-box"
+ result["background-clip"] = "border-box"
@@ -108,74 +94 @@ func parseBackground(background string) []map[string]string {
- return result
-}
-
-// splitLayers splits a background string into individual layers
-func splitLayers(background string) []string {
- var layers []string
- var currentLayer strings.Builder
- parenDepth := 0
-
- for _, char := range background {
- if char == '(' {
- parenDepth++
- currentLayer.WriteRune(char)
- } else if char == ')' {
- parenDepth--
- currentLayer.WriteRune(char)
- } else if char == ',' && parenDepth == 0 {
- layers = append(layers, strings.TrimSpace(currentLayer.String()))
- currentLayer.Reset()
- } else {
- currentLayer.WriteRune(char)
- }
- }
-
- if currentLayer.Len() > 0 {
- layers = append(layers, strings.TrimSpace(currentLayer.String()))
- }
-
- return layers
-}
-
-// parseLayer parses a single background layer
-func parseLayer(layer string) map[string]string {
- // Initialize with default values
- result := map[string]string{
- "background-color": "",
- "background-image": "none",
- "background-repeat": "repeat",
- "background-position": "0% 0%",
- "background-position-x": "0%",
- "background-position-y": "0%",
- "background-size": "auto",
- "background-attachment": "scroll",
- "background-origin": "padding-box",
- "background-clip": "border-box",
- }
-
- // Extract url() part first
- urlMatch := extractURL(layer)
- if urlMatch != "" {
- result["background-image"] = urlMatch
- layer = strings.Replace(layer, urlMatch, "", 1)
- }
-
- // Look for position/size pattern (with /)
- if strings.Contains(layer, "/") {
- posAndSize := processPositionAndSize(layer)
- if posAndSize["position"] != "" {
- result["background-position"] = posAndSize["position"]
- // Parse position into x and y components
- parsePositionXY(posAndSize["position"], result)
- }
- if posAndSize["size"] != "" {
- result["background-size"] = posAndSize["size"]
- }
-
- // Remove the position/size part
- if posAndSize["original"] != "" {
- layer = strings.Replace(layer, posAndSize["original"], "", 1)
- }
- }
- // Process remaining tokens
- tokens := splitTokens(layer)
- for _, token := range tokens {
+ for _, part := range parts {
@@ -183,23 +96,3 @@ func parseLayer(layer string) map[string]string {
- case isColorValue(token):
- result["background-color"] = token
- case isRepeatValue(token):
- result["background-repeat"] = token
- case isAttachmentValue(token):
- result["background-attachment"] = token
- case isBoxValue(token):
- if result["background-origin"] == "padding-box" {
- result["background-origin"] = token
- } else {
- result["background-clip"] = token
- }
- case isPositionValue(token):
- // Only set if not already set by position/size pattern
- if result["background-position"] == "0% 0%" {
- result["background-position"] = token
- parsePositionXY(token, result)
- } else {
- result["background-position"] += " " + token
- parsePositionXY(result["background-position"], result)
- }
- }
- }
+ // Handle background-image (assuming url format)
+ case strings.HasPrefix(part, "url("):
+ result["background-image"] = part
@@ -207,2 +100,3 @@ func parseLayer(layer string) map[string]string {
- return result
-}
+ // Handle background-repeat (no-repeat, repeat-x, repeat-y)
+ case part == "no-repeat" || part == "repeat" || part == "repeat-x" || part == "repeat-y":
+ result["background-repeat"] = part
@@ -210,38 +104,3 @@ func parseLayer(layer string) map[string]string {
-// splitTokens splits a string into tokens, preserving content within parentheses
-func splitTokens(s string) []string {
- var tokens []string
- var currentToken strings.Builder
- parenDepth := 0
- inSpace := true // Track if we're in whitespace
-
- for _, char := range s {
- if char == '(' {
- parenDepth++
- currentToken.WriteRune(char)
- inSpace = false
- } else if char == ')' {
- parenDepth--
- currentToken.WriteRune(char)
- inSpace = false
- } else if char == ' ' && parenDepth == 0 {
- // If we're at the top level (not inside parentheses) and encounter whitespace
- if !inSpace && currentToken.Len() > 0 {
- // We've reached the end of a token
- tokens = append(tokens, currentToken.String())
- currentToken.Reset()
- }
- inSpace = true
- } else {
- // Add the character to the current token
- currentToken.WriteRune(char)
- inSpace = false
- }
- }
-
- // Add the final token if any
- if currentToken.Len() > 0 {
- tokens = append(tokens, currentToken.String())
- }
-
- return tokens
-}
+ // Handle background-attachment (scroll or fixed)
+ case part == "scroll" || part == "fixed":
+ result["background-attachment"] = part
@@ -249,8 +108,3 @@ func splitTokens(s string) []string {
-// parsePositionXY extracts X and Y components from a position string
-func parsePositionXY(position string, result map[string]string) {
- // Handle specific patterns explicitly
- if position == "right 3rem top 1rem" {
- result["background-position-x"] = "right 3rem"
- result["background-position-y"] = "top 1rem"
- return
- }
+ // Handle background-position (percentage or predefined values)
+ case strings.Contains(part, "%") || isPosition(part):
+ result["background-position"] = part
@@ -258,5 +112,3 @@ func parsePositionXY(position string, result map[string]string) {
- if position == "center" {
- result["background-position-x"] = "center"
- result["background-position-y"] = "center"
- return
- }
+ // Handle background-size (contain, cover, or specific size)
+ case part == "contain" || part == "cover" || strings.Contains(part, "px") || strings.Contains(part, "%"):
+ result["background-size"] = part
@@ -264,2 +116,4 @@ func parsePositionXY(position string, result map[string]string) {
- // More general pattern handling
- parts := strings.Fields(position)
+ // Handle background-origin (border-box, padding-box, content-box)
+ case part == "border-box" || part == "padding-box" || part == "content-box":
+ result["background-origin"] = part
+ result["background-clip"] = part // background-clip defaults to the same as background-origin
@@ -267,8 +121,3 @@ func parsePositionXY(position string, result map[string]string) {
- // Look for patterns like "right 10px" or "top 20px"
- for i := 0; i < len(parts)-1; i++ {
- if (parts[i] == "right" || parts[i] == "left") && isLengthOrPercentage(parts[i+1]) {
- result["background-position-x"] = parts[i] + " " + parts[i+1]
- } else if (parts[i] == "top" || parts[i] == "bottom") && isLengthOrPercentage(parts[i+1]) {
- result["background-position-y"] = parts[i] + " " + parts[i+1]
- }
- }
+ // Handle background-color (rgb, rgba, hsl, hsla)
+ case isColorFunction(part):
+ result["background-color"] = part
@@ -276,33 +125,3 @@ func parsePositionXY(position string, result map[string]string) {
- // If we haven't set both x and y yet, handle simpler cases
- if result["background-position-x"] == "0%" || result["background-position-y"] == "0%" {
- if len(parts) == 1 {
- switch parts[0] {
- case "left", "right":
- result["background-position-x"] = parts[0]
- result["background-position-y"] = "center"
- case "top", "bottom":
- result["background-position-x"] = "center"
- result["background-position-y"] = parts[0]
- default:
- // Single length/percentage applies to x
- if isLengthOrPercentage(parts[0]) {
- result["background-position-x"] = parts[0]
- result["background-position-y"] = "center"
- }
- }
- } else if len(parts) == 2 {
- // Two-value case
- if isPositionKeyword(parts[0]) && isPositionKeyword(parts[1]) {
- // Two keywords
- if isHorizontalKeyword(parts[0]) {
- result["background-position-x"] = parts[0]
- result["background-position-y"] = parts[1]
- } else {
- result["background-position-x"] = parts[1]
- result["background-position-y"] = parts[0]
- }
- } else if isLengthOrPercentage(parts[0]) && isLengthOrPercentage(parts[1]) {
- // Two lengths/percentages
- result["background-position-x"] = parts[0]
- result["background-position-y"] = parts[1]
- }
+ // Handle background-color for basic colors or unknown values
+ default:
+ result["background-color"] = part
@@ -311,7 +129,0 @@ func parsePositionXY(position string, result map[string]string) {
-}
-
-// Helper function to check for position keywords
-func isPositionKeyword(value string) bool {
- return value == "left" || value == "center" || value == "right" ||
- value == "top" || value == "bottom"
-}
@@ -319,8 +131 @@ func isPositionKeyword(value string) bool {
-// Helper function to check for horizontal keywords
-func isHorizontalKeyword(value string) bool {
- return value == "left" || value == "center" || value == "right"
-}
-
-func isColorValue(value string) bool {
- _, e := color.ParseRGBA(value)
- return e == nil
+ return result
@@ -329,7 +134,4 @@ func isColorValue(value string) bool {
-// extractURL finds and extracts a url() function
-func extractURL(s string) string {
- urlStart := strings.Index(s, "url(")
- if urlStart < 0 {
- return ""
- }
-
+// splitBackground splits background properties while preserving functions like rgb(), rgba(), hsl(), etc.
+func splitBackground(background string) []string {
+ var result []string
+ var current strings.Builder
@@ -337 +139 @@ func extractURL(s string) string {
- urlEnd := -1
+ inWord := false
@@ -339,2 +141,3 @@ func extractURL(s string) string {
- for i := urlStart; i < len(s); i++ {
- if s[i] == '(' {
+ for _, char := range background {
+ // Track parentheses depth
+ if char == '(' {
@@ -342 +145,5 @@ func extractURL(s string) string {
- } else if s[i] == ')' {
+ current.WriteRune(char)
+ inWord = true
+ continue
+ }
+ if char == ')' {
@@ -344,3 +151,15 @@ func extractURL(s string) string {
- if parenDepth == 0 {
- urlEnd = i + 1
- break
+ current.WriteRune(char)
+ inWord = true
+ continue
+ }
+
+ // Handle spaces - they're separators only when not inside parentheses
+ if char == ' ' || char == '\t' {
+ if parenDepth > 0 {
+ // Inside parentheses, preserve the space
+ current.WriteRune(char)
+ } else if inWord {
+ // End of a word, add to results
+ result = append(result, current.String())
+ current.Reset()
+ inWord = false
@@ -347,0 +167 @@ func extractURL(s string) string {
+ continue
@@ -349,8 +168,0 @@ func extractURL(s string) string {
- }
-
- if urlEnd > 0 {
- return s[urlStart:urlEnd]
- }
-
- return ""
-}
@@ -358,6 +170,3 @@ func extractURL(s string) string {
-// processPositionAndSize extracts position and size from a string containing '/'
-func processPositionAndSize(s string) map[string]string {
- result := map[string]string{
- "position": "",
- "size": "",
- "original": "",
+ // Any other character
+ current.WriteRune(char)
+ inWord = true
@@ -366,41 +175,3 @@ func processPositionAndSize(s string) map[string]string {
- parts := strings.Split(s, "/")
- if len(parts) < 2 {
- return result
- }
-
- beforeSlash := strings.TrimSpace(parts[0])
- afterSlash := strings.TrimSpace(parts[1])
-
- // Find position tokens (from the end of beforeSlash)
- posTokens := []string{}
- beforeSlashParts := strings.Fields(beforeSlash)
- for i := len(beforeSlashParts) - 1; i >= 0; i-- {
- if isPositionValue(beforeSlashParts[i]) {
- posTokens = append([]string{beforeSlashParts[i]}, posTokens...)
- } else {
- break
- }
- }
-
- // Find size tokens (from the beginning of afterSlash)
- sizeTokens := []string{}
- afterSlashParts := strings.Fields(afterSlash)
- for _, part := range afterSlashParts {
- if isSizeValue(part) {
- sizeTokens = append(sizeTokens, part)
- } else {
- break
- }
- }
-
- if len(posTokens) > 0 {
- result["position"] = strings.Join(posTokens, " ")
- }
-
- if len(sizeTokens) > 0 {
- result["size"] = strings.Join(sizeTokens, " ")
- }
-
- // Build the original string
- if result["position"] != "" && result["size"] != "" {
- result["original"] = result["position"] + " / " + result["size"]
+ // Add the last part if there is one
+ if current.Len() > 0 {
+ result = append(result, current.String())
@@ -412,37 +183,7 @@ func processPositionAndSize(s string) map[string]string {
-// Helper functions for property type detection
-func isRepeatValue(value string) bool {
- repeatValues := []string{"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round"}
- for _, val := range repeatValues {
- if value == val {
- return true
- }
- }
- return false
-}
-
-func isAttachmentValue(value string) bool {
- return value == "scroll" || value == "fixed" || value == "local"
-}
-
-func isBoxValue(value string) bool {
- return value == "border-box" || value == "padding-box" || value == "content-box"
-}
-
-func isPositionValue(value string) bool {
- positionKeywords := []string{"left", "center", "right", "top", "bottom"}
- for _, keyword := range positionKeywords {
- if value == keyword {
- return true
- }
- }
- return isLengthOrPercentage(value)
-}
-
-func isSizeValue(value string) bool {
- sizeKeywords := []string{"auto", "cover", "contain"}
- for _, keyword := range sizeKeywords {
- if value == keyword {
- return true
- }
- }
- return isLengthOrPercentage(value)
+// Helper to check if a string is a valid CSS color function (e.g., rgb(), rgba(), hsl(), hsla())
+func isColorFunction(value string) bool {
+ // Check for rgb(), rgba(), hsl(), or hsla() functions
+ return strings.HasPrefix(value, "rgb(") ||
+ strings.HasPrefix(value, "rgba(") ||
+ strings.HasPrefix(value, "hsl(") ||
+ strings.HasPrefix(value, "hsla(")
@@ -451,5 +192,5 @@ func isSizeValue(value string) bool {
-func isLengthOrPercentage(value string) bool {
- units := []string{"px", "em", "rem", "vh", "vw", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm"}
-
- for _, unit := range units {
- if strings.HasSuffix(value, unit) {
+// Helper to check if a string is a valid background position
+func isPosition(value string) bool {
+ positions := []string{"left", "right", "top", "bottom", "center"}
+ for _, pos := range positions {
+ if value == pos {
@@ -459,4 +200 @@ func isLengthOrPercentage(value string) bool {
-
- // Check if it's a number
- _, err := strconv.ParseFloat(value, 64)
- return err == nil
+ return false