diff --git a/CHANGELOG.md b/CHANGELOG.md index d2526d2..af799a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ #### Changed - Styles are applied sorted by specificity #5 +#### Fixed +- Fixed nested style references + [Commits](https://github.com/yonaskolb/XcodeGen/compare/0.1.0...0.2.0) ## 0.1.0 diff --git a/README.md b/README.md index b28711b..9de736f 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ styles: - [Hot Reloading](#hot-reloading) - [🎨 Theme](#theme) - [Style Selectors](#style-selectors) - - [Included Styles](#included-styles) + - [Styles References](#style-references) - [View hierarchy styles](#view-hierarchy-styles) - [Style Context](#style-context) - [🖌 Style Properties](#style-properties) @@ -186,9 +186,9 @@ styles: Styles will be applied in order of specificity, so the more specific a style is (more selectors), the later it will be applied. -### Included Styles +### Style references -Each style may also have a `styles` array that is an array of other inherited styles, who's properties will also be applied. +Each style may also have a `styles` array that is an array of other inherited styles, who's properties will also be applied without overwriting anything. ```yml styles: diff --git a/Stylist/Style.swift b/Stylist/Style.swift index 6c94927..65f6533 100644 --- a/Stylist/Style.swift +++ b/Stylist/Style.swift @@ -17,6 +17,24 @@ public class Style: Equatable { self.subStyles = subStyles } + init(dictionary: [String: Any]) throws { + var properties: [StylePropertyValue] = [] + var subStyles: [String: Style] = [:] + + for (propertyName, value) in dictionary { + + if let subDictionary = value as? [String: Any] { + let style = try Style(dictionary: subDictionary) + subStyles[propertyName] = style + continue + } + + properties.append(try StylePropertyValue(string: propertyName, value: value)) + } + self.properties = properties.sorted { $0.name < $1.name } + self.subStyles = subStyles + } + public static func == (lhs: Style, rhs: Style) -> Bool { return lhs.properties == rhs.properties && lhs.subStyles == rhs.subStyles diff --git a/Stylist/Theme.swift b/Stylist/Theme.swift index 20dfe12..d581981 100644 --- a/Stylist/Theme.swift +++ b/Stylist/Theme.swift @@ -52,69 +52,59 @@ extension Theme { public init(dictionary: [String: Any]) throws { var styles: [StyleSelector] = [] var variables: [String: Any] = dictionary["variables"] as? [String: Any] ?? [:] - let stylesDictionary = (dictionary["styles"] as? [String: Any]) ?? [:] - - for (key, value) in stylesDictionary { - if var styleDictionary = value as? [String: Any] { - if let styles = styleDictionary["styles"] as? [String] { - for style in styles { - if let sharedStyle = stylesDictionary[style] as? [String: Any] { - for (styleKey, styleValue) in sharedStyle { - if styleDictionary[styleKey] == nil { - styleDictionary[styleKey] = styleValue - } - } - } else { - throw ThemeError.invalidStyleReference(style: key, reference: style) - } - } - styleDictionary["styles"] = nil - } - - func parseStyle(dictionary: [String: Any]) throws -> Style { + var stylesDictionary = (dictionary["styles"] as? [String: Any]) ?? [:] - var properties: [StylePropertyValue] = [] - var subStyles: [String: Style] = [:] + var visitedStyles: Set = [] - for (propertyName, value) in dictionary { + func getResolvedStyle(_ style: String, from parentStyle: String) throws -> [String: Any] { + guard !visitedStyles.contains(style) else { + throw ThemeError.styleReferenceCycle(references: visitedStyles) + } + visitedStyles.insert(style) - if let subDictionary = value as? [String: Any] { - let style = try parseStyle(dictionary: subDictionary) - subStyles[propertyName] = style - continue + guard var styleDictionary = stylesDictionary[style] as? [String: Any] else { + throw ThemeError.invalidStyleReference(style: parentStyle, reference: style) + } + if let styles = styleDictionary["styles"] as? [String] { + for subStyleName in styles { + let subStyle = try getResolvedStyle(subStyleName, from: style) + for (styleKey, styleValue) in subStyle { + if styleDictionary[styleKey] == nil { + styleDictionary[styleKey] = styleValue } + } + } + } + styleDictionary["styles"] = nil - func resolveVariable(_ value: Any) throws -> Any { - var propertyValue = value - if let string = propertyValue as? String, string.hasPrefix("$") { - var variableName = string.trimmingCharacters(in: CharacterSet(charactersIn: "$")) - let parts = variableName.components(separatedBy: ":") - if parts.count > 1 { - variableName = parts[0] - } - guard let variable = variables[variableName] else { - throw ThemeError.invalidVariable(name: propertyName, variable: variableName) - } - propertyValue = variable - if parts.count > 1 { - propertyValue = "\(propertyValue):" + Array(parts.dropFirst()).joined(separator: ":") - } - } - return propertyValue - } + for (key, value) in styleDictionary { - let propertyValue = try resolveVariable(value) - properties.append(try StylePropertyValue(string: propertyName, value: propertyValue)) + if let string = value as? String, string.hasPrefix("$") { + var variableName = string.trimmingCharacters(in: CharacterSet(charactersIn: "$")) + let parts = variableName.components(separatedBy: ":") + if parts.count > 1 { + variableName = parts[0] + } + guard let variable = variables[variableName] else { + throw ThemeError.invalidVariable(name: key, variable: variableName) + } + var variableValue = variable + if parts.count > 1 { + variableValue = "\(variableValue):" + Array(parts.dropFirst()).joined(separator: ":") } - - return try Style(properties: properties, subStyles: subStyles) + + styleDictionary[key] = variableValue } - let style = try parseStyle(dictionary: styleDictionary) - let styleSelector = try StyleSelector(selector: key, style: style) - styles.append(styleSelector) } + return styleDictionary } - self.styles = styles.sorted() + self.variables = variables + self.styles = try stylesDictionary.keys.map { selector in + visitedStyles = [] + let resolvedStyleDictionary = try getResolvedStyle(selector, from: "") + let style = try Style(dictionary: resolvedStyleDictionary) + return try StyleSelector(selector: selector, style: style) + }.sorted() } } diff --git a/Stylist/ThemeError.swift b/Stylist/ThemeError.swift index f50be28..72234fe 100644 --- a/Stylist/ThemeError.swift +++ b/Stylist/ThemeError.swift @@ -13,6 +13,7 @@ public enum ThemeError: Error, Equatable { case decodingError case invalidVariable(name: String, variable: String) case invalidStyleReference(style: String, reference: String) + case styleReferenceCycle(references: Set) case invalidPropertyState(name: String, state: String) case invalidDevice(name: String, device: String) case invalidSizeClass(name: String, sizeClass: String) diff --git a/Tests/ThemeDecodingTests.swift b/Tests/ThemeDecodingTests.swift index f24b911..e6edee9 100644 --- a/Tests/ThemeDecodingTests.swift +++ b/Tests/ThemeDecodingTests.swift @@ -43,10 +43,13 @@ class ThemeDecodingTests: XCTestCase { func testStyleInheriting() throws { let string = """ styles: + main: + styles: [primary] + color: green primary: tintColor: red header: - styles: [primary] + styles: [main] textColor: blue """ @@ -55,9 +58,14 @@ class ThemeDecodingTests: XCTestCase { let expectedTheme = Theme( styles: [ try StyleSelector(selector: "header", style: Style(properties: [ + StylePropertyValue(name: "color", value: "green"), StylePropertyValue(name: "textColor", value: "blue"), StylePropertyValue(name: "tintColor", value: "red"), ])), + try StyleSelector(selector: "main", style: Style(properties: [ + StylePropertyValue(name: "color", value: "green"), + StylePropertyValue(name: "tintColor", value: "red"), + ])), try StyleSelector(selector: "primary", style: Style(properties: [ StylePropertyValue(name: "tintColor", value: "red"), ])), @@ -207,8 +215,8 @@ class ThemeDecodingTests: XCTestCase { func testThemeDecodingErrors() throws { - func themeString(style: String = "testStyle", property: String? = nil) throws { - var theme = "" + func parseTheme(theme: String = "", style: String = "testStyle", property: String? = nil) throws { + var theme = theme if let property = property { theme += "\nstyles:\n \(style):\n \(property)" } @@ -224,39 +232,49 @@ class ThemeDecodingTests: XCTestCase { } expectError(ThemeError.invalidVariable(name: "prop", variable: "variable")) { - try themeString(property: "prop: $variable") + try parseTheme(property: "prop: $variable") } expectError(ThemeError.invalidStyleReference(style: "testStyle", reference: "invalid")) { - try themeString(property: "styles: [invalid]") + try parseTheme(property: "styles: [invalid]") } expectError(ThemeError.invalidPropertyState(name: "color", state: "invalid")) { - try themeString(property: "color:invalid: red") + try parseTheme(property: "color:invalid: red") } expectError(ThemeError.invalidDevice(name: "color", device: "invalid")) { - try themeString(property: "color(device:invalid): red") + try parseTheme(property: "color(device:invalid): red") } expectError(ThemeError.invalidStyleContext("color(invalid)")) { - try themeString(property: "color(invalid): red") + try parseTheme(property: "color(invalid): red") } expectError(ThemeError.invalidStyleContext("color(invalid:ipad)")) { - try themeString(property: "color(invalid:ipad): red") + try parseTheme(property: "color(invalid:ipad): red") } expectError(ThemeError.invalidStyleSelector("InvalidClass")) { - try themeString(style: "InvalidClass", property: "color: red") + try parseTheme(style: "InvalidClass", property: "color: red") } expectError(ThemeError.invalidStyleSelector("Module.class.style.invalid")) { - try themeString(style: "Module.class.style.invalid", property: "color: red") + try parseTheme(style: "Module.class.style.invalid", property: "color: red") } expectError(ThemeError.invalidStyleSelector("Module.Invalid")) { - try themeString(style: "Module.Invalid", property: "color: red") + try parseTheme(style: "Module.Invalid", property: "color: red") + } + + expectError(ThemeError.styleReferenceCycle(references: ["one", "two"])) { + try parseTheme(theme: """ + styles: + one: + styles: [two] + two: + styles: [one] + """) } }