Skip to content

Commit

Permalink
refactor style parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
yonaskolb committed Jun 6, 2018
1 parent e4c75f8 commit 4a34777
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 68 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions Stylist/Style.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
96 changes: 43 additions & 53 deletions Stylist/Theme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = []

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()
}
}
1 change: 1 addition & 0 deletions Stylist/ThemeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>)
case invalidPropertyState(name: String, state: String)
case invalidDevice(name: String, device: String)
case invalidSizeClass(name: String, sizeClass: String)
Expand Down
42 changes: 30 additions & 12 deletions Tests/ThemeDecodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""

Expand All @@ -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"),
])),
Expand Down Expand Up @@ -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)"
}
Expand All @@ -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]
""")
}
}

Expand Down

0 comments on commit 4a34777

Please sign in to comment.