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..712dd0a 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 +} 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 new file mode 100644 index 0000000..afed373 --- /dev/null +++ b/Tests/FigmaGenTests/TokensResolverTests.swift @@ -0,0 +1,189 @@ +#if canImport(FigmaGen) +import XCTest +@testable import FigmaGen + +final class TokensResolverTests: XCTestCase { + + // MARK: - Instance Properties + + private let tokensResolver = DefaultTokensResolver() + + // MARK: - Instance Methods + + func testResolveValuesWithReferences() 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) + } + + 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