From 5e76e6b5c2fdaa6f8ae36b429be8f178f3d73c01 Mon Sep 17 00:00:00 2001 From: Timur Shafigullin Date: Mon, 10 Jul 2023 12:35:09 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=A0=D0=B5=D0=B7=D0=BE=D0=BB=D0=B2=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/FigmaGen/Dependencies.swift | 7 +- .../Tokens/DefaultTokensGenerator.swift | 130 ++---------------- .../Resolver/DefaultTokensResolver.swift | 125 +++++++++++++++++ .../Tokens/Resolver/TokensResolver.swift | 60 ++++++++ 4 files changed, 203 insertions(+), 119 deletions(-) create mode 100644 Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift create mode 100644 Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift diff --git a/Sources/FigmaGen/Dependencies.swift b/Sources/FigmaGen/Dependencies.swift index 44b4ebd..2510b30 100644 --- a/Sources/FigmaGen/Dependencies.swift +++ b/Sources/FigmaGen/Dependencies.swift @@ -62,6 +62,10 @@ enum Dependencies { // MARK: - + static let tokensResolver: TokensResolver = DefaultTokensResolver() + + // MARK: - + static let templateContextCoder: TemplateContextCoder = DefaultTemplateContextCoder() static let stencilExtensions: [StencilExtension] = [ @@ -116,6 +120,7 @@ enum Dependencies { ) static let tokensGenerator: TokensGenerator = DefaultTokensGenerator( - tokensProvider: tokensProvider + tokensProvider: tokensProvider, + tokensResolver: tokensResolver ) } diff --git a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift index 8c99d58..4c49e27 100644 --- a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift @@ -7,132 +7,20 @@ final class DefaultTokensGenerator: TokensGenerator, GenerationParametersResolvi // MARK: - Instance Properties let tokensProvider: TokensProvider + let tokensResolver: TokensResolver let defaultTemplateType = RenderTemplateType.native(name: "Tokens") let defaultDestination = RenderDestination.console // MARK: - Initializers - init(tokensProvider: TokensProvider) { + init(tokensProvider: TokensProvider, tokensResolver: TokensResolver) { self.tokensProvider = tokensProvider + self.tokensResolver = tokensResolver } // MARK: - Instance Methods - private func evaluteValue(_ value: String) -> String { - let expression = AnyExpression(value) - - do { - return try expression.evaluate() - } catch { - return value - } - } - - private func resolveValue(_ value: String, tokenValues: TokenValues) throws -> String { - let allTokens = tokenValues.all - - let resolvedValue = try value.replacingOccurrences(matchingPattern: #"\{.*?\}"#) { referenceName in - let referenceName = referenceName - .removingFirst() - .removingLast() - - guard let token = allTokens.first(where: { $0.name == referenceName }) else { - throw TokensGeneratorError(code: .referenceNotFound(name: referenceName)) - } - - guard let value = token.type.stringValue else { - throw TokensGeneratorError(code: .unexpectedTokenValueType(name: referenceName)) - } - - return try resolveValue(value, tokenValues: tokenValues) - } - - return evaluteValue(resolvedValue) - } - - private func makeColor(hex: String, alpha: CGFloat) throws -> Color { - let hex = hex - .trimmingCharacters(in: .whitespacesAndNewlines) - .uppercased() - .filter { $0 != "#" } - - guard hex.count == 6 else { - throw TokensGeneratorError(code: .invalidHEXComponent(hex: hex)) - } - - var rgbValue: UInt64 = 0 - Scanner(string: hex).scanHexInt64(&rgbValue) - - return Color( - red: Double((rgbValue & 0xFF0000) >> 16) / 255.0, - green: Double((rgbValue & 0x00FF00) >> 8) / 255.0, - blue: Double(rgbValue & 0x0000FF) / 255.0, - alpha: alpha - ) - } - - private func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues) throws -> Color { - let components = value - .slice(from: "(", to: ")", includingBounds: false)? - .components(separatedBy: ", ") - - guard let components, components.count == 2 else { - throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: value)) - } - - let hex = components[0] - let alphaPercent = components[1] - - guard let alpha = Double(alphaPercent.dropLast()) else { - throw TokensGeneratorError(code: .invalidAlphaComponent(alpha: alphaPercent)) - } - - return try makeColor(hex: hex, alpha: alpha / 100.0) - } - - private func resolveColorValue(_ value: String, tokenValues: TokenValues) throws -> Color { - if value.hasPrefix("rgba") { - return try resolveRGBAColorValue(value, tokenValues: tokenValues) - } - - return try makeColor(hex: value, alpha: 1.0) - } - - private func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues) throws -> LinearGradient { - guard let startFunctionIndex = value.firstIndex(of: "("), let endFunctionIndex = value.lastIndex(of: ")") else { - throw TokensGeneratorError(code: .failedToExtractLinearGradientParams(linearGradient: value)) - } - - let rawParams = value[value.index(after: startFunctionIndex).. String { + let expression = AnyExpression(value) + + do { + return try expression.evaluate() + } catch { + return value + } + } + + private func makeColor(hex: String, alpha: CGFloat) throws -> Color { + let hex = hex + .trimmingCharacters(in: .whitespacesAndNewlines) + .uppercased() + .filter { $0 != "#" } + + guard hex.count == 6 else { + throw TokensGeneratorError(code: .invalidHEXComponent(hex: hex)) + } + + var rgbValue: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&rgbValue) + + return Color( + red: Double((rgbValue & 0xFF0000) >> 16) / 255.0, + green: Double((rgbValue & 0x00FF00) >> 8) / 255.0, + blue: Double(rgbValue & 0x0000FF) / 255.0, + alpha: alpha + ) + } + + private func resolveColorValue(_ value: String, tokenValues: TokenValues) throws -> Color { + if value.hasPrefix("rgba") { + return try resolveRGBAColorValue(value, tokenValues: tokenValues) + } + + return try makeColor(hex: value, alpha: 1.0) + } + + // MARK: - TokensResolver + + func resolveValue(_ value: String, tokenValues: TokenValues) throws -> String { + let allTokens = tokenValues.all + + let resolvedValue = try value.replacingOccurrences(matchingPattern: #"\{.*?\}"#) { referenceName in + let referenceName = referenceName + .removingFirst() + .removingLast() + + guard let token = allTokens.first(where: { $0.name == referenceName }) else { + throw TokensGeneratorError(code: .referenceNotFound(name: referenceName)) + } + + guard let value = token.type.stringValue else { + throw TokensGeneratorError(code: .unexpectedTokenValueType(name: referenceName)) + } + + return try resolveValue(value, tokenValues: tokenValues) + } + + return evaluteValue(resolvedValue) + } + + func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues) throws -> Color { + let components = try resolveValue(value, tokenValues: tokenValues) + .slice(from: "(", to: ")", includingBounds: false)? + .components(separatedBy: ", ") + + guard let components, components.count == 2 else { + throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: value)) + } + + let hex = components[0] + let alphaPercent = components[1] + + guard let alpha = Double(alphaPercent.dropLast()) else { + throw TokensGeneratorError(code: .invalidAlphaComponent(alpha: alphaPercent)) + } + + return try makeColor(hex: hex, alpha: alpha / 100.0) + } + + func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues) throws -> LinearGradient { + let value = try resolveValue(value, tokenValues: tokenValues) + + guard let startFunctionIndex = value.firstIndex(of: "("), let endFunctionIndex = value.lastIndex(of: ")") else { + throw TokensGeneratorError(code: .failedToExtractLinearGradientParams(linearGradient: value)) + } + + let rawParams = value[value.index(after: startFunctionIndex).. String + + /// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)`` + /// and convert `rgba()` to ``Color`` object + /// + /// Supported formats: + /// - `rgba(hex_color, alpha-value-percentage)` + /// - TO DO: Support more formats + /// + /// [Color tokens examples and should be supported later](https://docs.tokens.studio/available-tokens/color-tokens#solid-colors) + /// + /// - Parameters: + /// - value: Raw `rgba()` with references, e.g.: + /// ``` + /// rgba( + /// {color.base.white}, + /// {semantic.opacity.disabled} + /// ) + /// ``` + /// - tokenValues: All token values + /// - Returns: ``Color`` object with values resolved from `rgba()` + func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues) throws -> Color + + /// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)`` + /// and convert `linear-gradient()` to ``LinearGradient`` object + /// + /// Supported formats: + /// - `linear-gradient(angle, [hex_color||rgba() length-percentage])` + /// + /// [Gradients tokens examples](https://docs.tokens.studio/available-tokens/color-tokens#gradients) + /// + /// - Parameters: + /// - value: Raw `linear-gradient()` with references, e.g: + /// ``` + /// linear-gradient( + /// 0deg, + /// rgba({color.base.red.50}, {semantic.opacity.transparent}) 0%, + /// {color.base.red.50} {semantic.opacity.visible} + /// ) + /// ``` + /// - tokenValues: All token values + /// - Returns: ``LinearGradient`` object with values resolved from `linear-gradient()` + func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues) throws -> LinearGradient +} From f54960a65f28ba769ee1d12171fa2be869c1c427 Mon Sep 17 00:00:00 2001 From: Timur Shafigullin Date: Mon, 10 Jul 2023 13:08:19 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/FigmaGenTests/TokensResolverTests.swift | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Tests/FigmaGenTests/TokensResolverTests.swift diff --git a/Tests/FigmaGenTests/TokensResolverTests.swift b/Tests/FigmaGenTests/TokensResolverTests.swift new file mode 100644 index 0000000..ce798e4 --- /dev/null +++ b/Tests/FigmaGenTests/TokensResolverTests.swift @@ -0,0 +1,33 @@ +import XCTest +@testable import FigmaGen + +final class TokensResolverTests: XCTestCase { + + // MARK: - Instance Properties + + private let tokensResolver = DefaultTokensResolver() + + // MARK: - Instance Methods + + func testResolveValues() throws { + let tokenValues = TokenValues( + core: [ + TokenValue(type: .core(value: "2"), name: "core.x-base"), // 2 + TokenValue(type: .core(value: "{core.x-base} * 2"), name: "core.2-x-base"), // 4 + TokenValue(type: .spacing(value: "{core.2-x-base} * 1"), name: "core.space.1-x") // 4 + ], + semantic: [], + colors: [], + typography: [], + day: [], + night: [] + ) + + let value = "{core.space.1-x} + {core.space.1-x} / 2" + let expectedValue = "6" + + let actualValue = try tokensResolver.resolveValue(value, tokenValues: tokenValues) + + XCTAssertEqual(actualValue, expectedValue) + } +} From f1452accd7380941dd48c69b63009b929cb6cc9c Mon Sep 17 00:00:00 2001 From: Timur Shafigullin Date: Mon, 10 Jul 2023 15:45:22 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tokens/DefaultTokensGenerator.swift | 14 +- Sources/FigmaGen/Models/Color.swift | 38 +++++ Tests/FigmaGenTests/TokensResolverTests.swift | 158 +++++++++++++++++- 3 files changed, 201 insertions(+), 9 deletions(-) diff --git a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift index 4c49e27..712dd0a 100644 --- a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift @@ -34,15 +34,13 @@ final class DefaultTokensGenerator: TokensGenerator, GenerationParametersResolvi } .forEach { tokenValue, value in if value.hasPrefix("rgba") { - print( - "[\(tokenValue.name)] " + - "\(try tokensResolver.resolveRGBAColorValue(value, tokenValues: tokenValues))" - ) + let color = try tokensResolver.resolveRGBAColorValue(value, tokenValues: tokenValues) + + print("[\(tokenValue.name)] \(color)") } else if value.hasPrefix("linear-gradient") { - print( - "[\(tokenValue.name)] " + - "\(try tokensResolver.resolveLinearGradientValue(value, tokenValues: tokenValues))" - ) + let gradient = try tokensResolver.resolveLinearGradientValue(value, tokenValues: tokenValues) + + print("[\(tokenValue.name)] \(gradient)") } else { print("[\(tokenValue.name)] \(value)") } diff --git a/Sources/FigmaGen/Models/Color.swift b/Sources/FigmaGen/Models/Color.swift index 7aee576..4a113b9 100644 --- a/Sources/FigmaGen/Models/Color.swift +++ b/Sources/FigmaGen/Models/Color.swift @@ -9,3 +9,41 @@ struct Color: Codable, Hashable { let blue: Double let alpha: Double } + +extension Color { + + // MARK: - Type Methods + + static func == (lhs: Color, rhs: Color) -> Bool { + lhs.hexString == rhs.hexString + } + + // MARK: - Instance Properties + + var hexString: String { + let multiplier = CGFloat(255.0) + + if alpha == 1.0 { + return String( + format: "#%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier) + ) + } + + return String( + format: "#%02lX%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier), + Int(alpha * multiplier) + ) + } + + // MARK: - Instance Methods + + func hash(into hasher: inout Hasher) { + hasher.combine(hexString) + } +} diff --git a/Tests/FigmaGenTests/TokensResolverTests.swift b/Tests/FigmaGenTests/TokensResolverTests.swift index ce798e4..91e69c3 100644 --- a/Tests/FigmaGenTests/TokensResolverTests.swift +++ b/Tests/FigmaGenTests/TokensResolverTests.swift @@ -1,3 +1,4 @@ +#if canImport(FigmaGen) import XCTest @testable import FigmaGen @@ -9,7 +10,7 @@ final class TokensResolverTests: XCTestCase { // MARK: - Instance Methods - func testResolveValues() throws { + func testResolveValuesWithReferences() throws { let tokenValues = TokenValues( core: [ TokenValue(type: .core(value: "2"), name: "core.x-base"), // 2 @@ -30,4 +31,159 @@ final class TokensResolverTests: XCTestCase { XCTAssertEqual(actualValue, expectedValue) } + + func testResolveValuesWithoutReferences() throws { + let numberValue = "10" + let percentValue = "-2.50%" + let textValue = "none" + let pixelValue = "0px" + let colorValue = "#ffffff" + + let actualNumberValue = try tokensResolver.resolveValue(numberValue, tokenValues: .empty) + let actualPercentValue = try tokensResolver.resolveValue(percentValue, tokenValues: .empty) + let actualTextValue = try tokensResolver.resolveValue(textValue, tokenValues: .empty) + let actualPixelValue = try tokensResolver.resolveValue(pixelValue, tokenValues: .empty) + let actualColorValue = try tokensResolver.resolveValue(colorValue, tokenValues: .empty) + + XCTAssertEqual(actualNumberValue, numberValue) + XCTAssertEqual(actualPercentValue, percentValue) + XCTAssertEqual(actualTextValue, textValue) + XCTAssertEqual(actualPixelValue, pixelValue) + XCTAssertEqual(actualColorValue, colorValue) + } + + func testResolveRGBAColorWithReferences() throws { + let tokenValues = TokenValues( + core: [ + TokenValue(type: .opacity(value: "48%"), name: "core.opacity.48") + ], + semantic: [ + TokenValue(type: .opacity(value: "{core.opacity.48}"), name: "semantic.opacity.disabled") + ], + colors: [ + TokenValue(type: .core(value: "#ffffff"), name: "color.base.white") + ], + typography: [], + day: [], + night: [] + ) + + let value = "rgba({color.base.white}, {semantic.opacity.disabled})" + let expectedColor = Color(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.48) + + let actualColor = try tokensResolver.resolveRGBAColorValue(value, tokenValues: tokenValues) + + XCTAssertEqual(actualColor, expectedColor) + } + + func testResolveRGBAColorWithoutReferences() throws { + let value = "rgba(#FFFFFF, 48%)" + let expectedColor = Color(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.48) + + let actualColor = try tokensResolver.resolveRGBAColorValue(value, tokenValues: .empty) + + XCTAssertEqual(actualColor, expectedColor) + } + + func testResolveLinearGradientWithReferences() throws { + let tokenValues = TokenValues( + core: [ + TokenValue(type: .opacity(value: "0%"), name: "core.opacity.0") + ], + semantic: [ + TokenValue(type: .opacity(value: "{core.opacity.0}"), name: "semantic.opacity.transparent") + ], + colors: [ + TokenValue(type: .color(value: "#d64030"), name: "color.base.red.50") + ], + typography: [], + day: [], + night: [] + ) + + let firstColor = "rgba({color.base.red.50}, {semantic.opacity.transparent})" + let secondColor = "{color.base.red.50}" + let value = "linear-gradient(0deg, \(firstColor) 0%, \(secondColor) 100%" + + let expectedLinearGradient = LinearGradient( + angle: "0deg", + colorStopList: [ + LinearGradient.LinearColorStop( + color: Color( + red: 0.8392156862745098, + green: 0.25098039215686274, + blue: 0.18823529411764706, + alpha: 0.0 + ), + percentage: "0%" + ), + LinearGradient.LinearColorStop( + color: Color( + red: 0.8392156862745098, + green: 0.25098039215686274, + blue: 0.18823529411764706, + alpha: 1.0 + ), + percentage: "100%" + ) + ] + ) + + let actualLinearGradient = try tokensResolver.resolveLinearGradientValue( + value, + tokenValues: tokenValues + ) + + XCTAssertEqual(actualLinearGradient, expectedLinearGradient) + } + + func testResolveLinearGradientWithoutReferences() throws { + let value = "linear-gradient(0deg, rgba(#d64030, 0%) 0%, #d64030 100%)" + + let expectedLinearGradient = LinearGradient( + angle: "0deg", + colorStopList: [ + LinearGradient.LinearColorStop( + color: Color( + red: 0.8392156862745098, + green: 0.25098039215686274, + blue: 0.18823529411764706, + alpha: 0.0 + ), + percentage: "0%" + ), + LinearGradient.LinearColorStop( + color: Color( + red: 0.8392156862745098, + green: 0.25098039215686274, + blue: 0.18823529411764706, + alpha: 1.0 + ), + percentage: "100%" + ) + ] + ) + + let actualLinearGradient = try tokensResolver.resolveLinearGradientValue( + value, + tokenValues: .empty + ) + + XCTAssertEqual(actualLinearGradient, expectedLinearGradient) + } +} + +extension TokenValues { + + // MARK: - Type Properties + + static let empty = Self( + core: [], + semantic: [], + colors: [], + typography: [], + day: [], + night: [] + ) } +#endif From a36d74c83bebf35bbfc4d027c91e538c3150e1e9 Mon Sep 17 00:00:00 2001 From: Timur Shafigullin Date: Mon, 10 Jul 2023 16:24:53 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/FigmaGenTests/TokensResolverTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FigmaGenTests/TokensResolverTests.swift b/Tests/FigmaGenTests/TokensResolverTests.swift index 91e69c3..afed373 100644 --- a/Tests/FigmaGenTests/TokensResolverTests.swift +++ b/Tests/FigmaGenTests/TokensResolverTests.swift @@ -103,7 +103,7 @@ final class TokensResolverTests: XCTestCase { let firstColor = "rgba({color.base.red.50}, {semantic.opacity.transparent})" let secondColor = "{color.base.red.50}" - let value = "linear-gradient(0deg, \(firstColor) 0%, \(secondColor) 100%" + let value = "linear-gradient(0deg, \(firstColor) 0%, \(secondColor) 100%)" let expectedLinearGradient = LinearGradient( angle: "0deg",