Skip to content
This repository has been archived by the owner on Jan 30, 2024. It is now read-only.

Support Zeplin's new Spacing feature #17

Merged
merged 3 commits into from Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions Examples/Android/dimens.xml.prism
@@ -0,0 +1,7 @@
<!-- This file was generated using Prism -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
{{% FOR spacing %}}
<dimen name="{{% spacing.identity.snakecase %}}">{{% spacing.value %}}dp</dimen>
{{% END spacing %}}
</resources>
8 changes: 8 additions & 0 deletions Examples/iOS/SwiftUI/Spacing.swift.prism
@@ -0,0 +1,8 @@
// This file was generated using Prism
import Foundation

enum Spacing {
{{% FOR spacing %}}
static let {{% spacing.identity.camelcase %}} = CGFloat({{% spacing.value %}})
{{% END spacing %}}
}
8 changes: 8 additions & 0 deletions Examples/iOS/UIKit/Spacing.swift.prism
@@ -0,0 +1,8 @@
// This file was generated using Prism
import Foundation

enum Spacing {
{{% FOR spacing %}}
static let {{% spacing.identity.camelcase %}} = {{% spacing.value %}}
{{% END spacing %}}
}
8 changes: 4 additions & 4 deletions Examples/iOS/UIKit/TextStyles.swift.prism
Expand Up @@ -10,10 +10,10 @@ extension TextStyle {
// UI element in your app. See `TextStyle.swift` for some
// more inspiration around this.
{{% FOR textStyle %}}
static let {{%textStyle.identity.camelcase%}} = TextStyle(
fontName: "{{%textStyle.fontName%}}",
fontSize: {{%textStyle.fontSize%}},
color: .{{%textStyle.color.identity.camelcase%}}{{% IF textStyle.alignment %}},{{% ENDIF %}}
static let {{% textStyle.identity.camelcase %}} = TextStyle(
fontName: "{{% textStyle.fontName %}}",
fontSize: {{% textStyle.fontSize %}},
color: .{{% textStyle.color.identity.camelcase %}}{{% IF textStyle.alignment %}},{{% ENDIF %}}
{{% IF textStyle.alignment %}}alignment: .{{%textStyle.alignment|replace(justify,justified)%}}{{% ENDIF %}}
)

Expand Down
13 changes: 9 additions & 4 deletions Sources/PrismCore/Models/AssetIdentity.swift
Expand Up @@ -18,7 +18,7 @@ public extension AssetIdentifiable {
/// A synthesized identity for iOS and Android styles based
/// on the provided Asset name.
var identity: Project.AssetIdentity {
return .init(name: name)
.init(name: name)
}
}

Expand All @@ -27,16 +27,20 @@ public extension Project {
struct AssetIdentity {
/// A snake-cased version of the name
private var snakecased: String {
return words
words
.map { $0.lowercased() }
.joined(separator: "_")
}

/// A camelCased version of the name
private var camelcased: String {
/// A set of terms that sould have uppercased presentattion
/// Usually those would be units of size
let uppercaseTerms = ["xxs", "xs", "x", "m", "l", "xl", "xxl", "xxxl"]

return (words.first?.lowercased() ?? "") +
words.dropFirst()
.map { $0.capitalized }
.map { uppercaseTerms.contains($0) ? $0.uppercased() : $0.capitalized }
.joined()
}

Expand Down Expand Up @@ -84,7 +88,7 @@ public extension Project {

extension Project.AssetIdentity: CustomStringConvertible {
public var description: String {
return "AssetIdentity(name: \(name), \(Style.allCases.map { "\($0.rawValue): \($0.identifier(for: self))" }.joined(separator: ", ")))"
"AssetIdentity(name: \(name), \(Style.allCases.map { "\($0.rawValue): \($0.identifier(for: self))" }.joined(separator: ", ")))"
}
}

Expand Down Expand Up @@ -116,3 +120,4 @@ public extension Project.AssetIdentity {
// MARK: - Zeplin Model Conformances
extension Color: AssetIdentifiable {}
extension TextStyle: AssetIdentifiable {}
extension Spacing: AssetIdentifiable {}
5 changes: 4 additions & 1 deletion Sources/PrismCore/Models/ProjectAssets.swift
Expand Up @@ -19,12 +19,15 @@ public struct ProjectAssets: Codable, Equatable {

/// Project's Text Styles.
public let textStyles: [TextStyle]

/// Project's Spacing tokens.
public let spacing: [Spacing]
}

extension ProjectAssets: CustomStringConvertible {
/// A short description for the project.
public var description: String {
return "Zeplin Project \(id) has \(colors.count) colors and \(textStyles.count) text styles"
"Zeplin Project \(id) has \(colors.count) colors and \(textStyles.count) text styles"
}
}

Expand Down
29 changes: 27 additions & 2 deletions Sources/PrismCore/Prism.swift
Expand Up @@ -41,6 +41,7 @@ public class Prism {
let group = DispatchGroup()
var colors = [Color]()
var textStyles = [TextStyle]()
var spacings = [Spacing]()
var errors = [ZeplinAPI.Error]()

/// Get linked style guides and their colors and
Expand All @@ -52,7 +53,7 @@ public class Prism {

switch result {
case .success(let styleguides):
// Get text styles and colors separately
// Get text styles, colors and spacing separately
// for each styleguide
for styleguide in styleguides {
group.enter()
Expand All @@ -76,6 +77,17 @@ public class Prism {
group.leave()
}
)

group.enter()
api.getPagedItems(
work: { page, api, completion in
api.getStyleguideSpacings(for: styleguide.id, page: page, completion: completion)
},
completion: { result in
result.appendValuesOrErrors(values: &spacings, errors: &errors)
group.leave()
}
)
}
case .failure(let error):
errors.append(error)
Expand Down Expand Up @@ -106,6 +118,18 @@ public class Prism {
}
)

// Get project spacing
group.enter()
api.getPagedItems(
work: { page, api, completion in
api.getProjectSpacings(for: projectId, page: page, completion: completion)
},
completion: { result in
result.appendValuesOrErrors(values: &spacings, errors: &errors)
group.leave()
}
)

/// It's required to wait and block here when running in CLI.
/// Otherwise, Prism terminates without waiting for the result to
/// come back.
Expand All @@ -116,7 +140,8 @@ public class Prism {
} else {
completion(.success(ProjectAssets(id: projectId,
colors: colors.sorted { $0.name < $1.name },
textStyles: textStyles.sorted { $0.name < $1.name })))
textStyles: textStyles.sorted { $0.name < $1.name },
spacing: spacings.sorted(by: { $0.value < $1.value }))))
}
}
}
Expand Down
74 changes: 59 additions & 15 deletions Sources/PrismCore/TemplateParser/TemplateParser+Token.swift
Expand Up @@ -15,30 +15,35 @@ extension TemplateParser {
///
/// Tokens use the {{%tokenName%}} structure, for example {{%textStyle.fontSize%}}.
enum Token {
/// Color
// Color
case colorRed(Int)
case colorGreen(Int)
case colorBlue(Int)
case colorAlpha(Double)
case colorAlpha(Float)
case colorARGB(String)
case colorRGB(String)
case colorIdentity(identity: Project.AssetIdentity,
style: Project.AssetIdentity.Style)

/// Text Style
// Text Style
case textStyleFontName(String)
case textStyleFontSize(Float)
case textStyleIdentity(identity: Project.AssetIdentity,
style: Project.AssetIdentity.Style)
case textStyleAlignment(String?)
case textStyleLineHeight(Float?)
case textStyleLetterSpacing(Float?)

// Spacing
case spacingIdentity(identity: Project.AssetIdentity,
style: Project.AssetIdentity.Style)
case spacingValue(Float)

/// Parse a raw color token, such as "color.r", into its
/// appropriate Token case (e.g. `.colorRed(value)` in this case).
///
/// - parameter rawColorToken: A raw color token, e.g. `color.*`
/// - parameter colorr: A project color with an asset identity
/// - parameter color: A project color with an asset identity
init(rawColorToken: String, color: Color) throws {
let cleanToken = rawColorToken.lowercased()
.trimmingCharacters(in: .whitespaces)
Expand Down Expand Up @@ -76,7 +81,8 @@ extension TemplateParser {
/// appropriate Token case (e.g. `.textStyleFontName(value)` in this case).
///
/// - parameter rawTextStyleToken: A raw text style token, e.g. `textStyle.*`
/// - parameter colorr: A project color with an asset identity
/// - parameter textStyle: A project text style with an asset identity
/// - parameter color: A project color with an asset identity
init(rawTextStyleToken: String, textStyle: TextStyle, colors: [Color]) throws {
let cleanToken = rawTextStyleToken.lowercased()
.trimmingCharacters(in: .whitespaces)
Expand Down Expand Up @@ -119,6 +125,34 @@ extension TemplateParser {
}
}

/// Parse a raw spacing token, such as "spacing.value", into its
/// appropriate Token case (e.g. `.spacingValue(value)` in this case).
///
/// - parameter rawSpacingToken: A raw text style token, e.g. `spacing.*`
/// - parameter spacing: A spacing entity
init(rawSpacingToken: String, spacing: Spacing) throws {
let cleanToken = rawSpacingToken.lowercased()
.trimmingCharacters(in: .whitespaces)
guard cleanToken.hasPrefix("spacing.") else {
throw Error.unknownToken(token: rawSpacingToken)
}

let spacingToken = String(cleanToken.dropFirst(8))

switch spacingToken {
case "identity":
self = .spacingIdentity(identity: spacing.identity, style: .raw)
case "identity.camelcase":
self = .spacingIdentity(identity: spacing.identity, style: .camelcase)
case "identity.snakecase":
self = .spacingIdentity(identity: spacing.identity, style: .snakecase)
case "value":
self = .spacingValue(spacing.value)
default:
throw Error.unknownToken(token: rawSpacingToken)
}
}

/// Process the current token, while applying the provided
/// set of transformations.
///
Expand All @@ -128,18 +162,19 @@ extension TemplateParser {
func stringValue(transformations: [Transformation]) -> String? {
var baseString: String?
switch self {
case .colorAlpha(let a):
baseString = String(format: "%.2f", a)
case let .colorIdentity(identity, style),
let .textStyleIdentity(identity, style),
let .spacingIdentity(identity, style):
baseString = style.identifier(for: identity)
case .colorAlpha(let alpha):
baseString = String(format: "%.2f", alpha)
case .colorRed(let c),
.colorGreen(let c),
.colorBlue(let c):
baseString = "\(c)"
case .colorARGB(let hex),
.colorRGB(let hex):
baseString = hex
case let .colorIdentity(identity, style),
let .textStyleIdentity(identity, style):
baseString = style.identifier(for: identity)
case .textStyleFontName(let name):
baseString = name
case .textStyleFontSize(let size):
Expand All @@ -148,14 +183,16 @@ extension TemplateParser {
baseString = alignment
case .textStyleLetterSpacing(let spacing):
if let spacing = spacing {
baseString = "\(spacing.roundToNearest())"
baseString = spacing.roundedToNearest()
}
case .textStyleLineHeight(let height):
if let height = height {
baseString = "\(height.roundToNearest())"
baseString = height.roundedToNearest()
}
case .spacingValue(let value):
baseString = value.roundedToNearest()
}

guard let output = baseString else { return nil }
return transformations.reduce(into: output) { $0 = $1.apply(to: $0) }
}
Expand All @@ -165,7 +202,14 @@ extension TemplateParser {
// MARK: - Private Helpers
private extension Float {
/// Round the Float to the nearest 2 floating points
func roundToNearest() -> Float {
(self * 100).rounded(.toNearestOrEven) / 100
func roundedToNearest() -> String {
let value = (self * 100).rounded(.toNearestOrEven) / 100

let int = Int(value)
if Float(int) == value {
return "\(int)"
} else {
return "\(value)"
}
}
}
Expand Up @@ -41,7 +41,8 @@ extension TemplateParser {
} else {
params = nsValue.substring(with: match.range(at: 3))
.components(separatedBy: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
.map { $0.trimmingCharacters(in: .whitespaces)
.stripQuotes() }
}

switch (action, params.count) {
Expand Down Expand Up @@ -73,3 +74,15 @@ extension TemplateParser {
}
}
}

private extension String {
func stripQuotes() -> String {
guard count >= 2,
first == Character("\""),
first == last else {
return self
}

return String(self[index(after: startIndex)..<index(before: endIndex)])
}
}