diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/DefaultBaseColorTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/DefaultBaseColorTokensGenerator.swift index 1ff067e..099fb13 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/DefaultBaseColorTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/DefaultBaseColorTokensGenerator.swift @@ -23,7 +23,7 @@ final class DefaultBaseColorTokensGenerator: BaseColorTokensGenerator { return BaseColorToken( path: token.name.components(separatedBy: "."), - value: try tokensResolver.resolveHexColorValue(value, tokenValues: tokenValues) + value: try tokensResolver.resolveHexColorValue(value, tokenValues: tokenValues, theme: .undefined) ) } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/DefaultFontFamilyTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/DefaultFontFamilyTokensGenerator.swift index 6b62899..6d4d997 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/DefaultFontFamilyTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/DefaultFontFamilyTokensGenerator.swift @@ -24,7 +24,7 @@ final class DefaultFontFamilyTokensGenerator: FontFamilyTokensGenerator { return FontFamilyToken( path: tokenValue.name.components(separatedBy: "."), - value: try tokensResolver.resolveValue(value, tokenValues: tokenValues) + value: try tokensResolver.resolveValue(value, tokenValues: tokenValues, theme: .undefined) ) } } @@ -37,7 +37,7 @@ final class DefaultFontFamilyTokensGenerator: FontFamilyTokensGenerator { return FontWeightToken( path: tokenValue.name.components(separatedBy: "."), - value: try tokensResolver.resolveValue(value, tokenValues: tokenValues) + value: try tokensResolver.resolveValue(value, tokenValues: tokenValues, theme: .undefined) ) } } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/DefaultSpacingTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/DefaultSpacingTokensGenerator.swift index 5a981f4..13f4c2d 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/DefaultSpacingTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/DefaultSpacingTokensGenerator.swift @@ -23,7 +23,7 @@ final class DefaultSpacingTokensGenerator: SpacingTokensGenerator { return SpacingToken( path: token.name.components(separatedBy: "."), - value: try tokensResolver.resolveValue(value, tokenValues: tokenValues) + value: try tokensResolver.resolveValue(value, tokenValues: tokenValues, theme: .undefined) ) } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Typography/DefaultTypographyTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Typography/DefaultTypographyTokensGenerator.swift index 42d9c23..ecd8a12 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/Typography/DefaultTypographyTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Typography/DefaultTypographyTokensGenerator.swift @@ -21,7 +21,11 @@ final class DefaultTypographyTokensGenerator: TypographyTokensGenerator { tokenValues: TokenValues ) throws -> TypographyToken.LineHeightToken { let lineHeightValue = value.lineHeight - let lineHeightResolvedValue = try tokensResolver.resolveValue(lineHeightValue, tokenValues: tokenValues) + let lineHeightResolvedValue = try tokensResolver.resolveValue( + lineHeightValue, + tokenValues: tokenValues, + theme: .undefined + ) guard lineHeightResolvedValue.hasSuffix("%") else { return TypographyToken.LineHeightToken( @@ -30,7 +34,9 @@ final class DefaultTypographyTokensGenerator: TypographyTokensGenerator { ) } - let fontSize = Double(try tokensResolver.resolveValue(value.fontSize, tokenValues: tokenValues)) + let fontSize = Double( + try tokensResolver.resolveValue(value.fontSize, tokenValues: tokenValues, theme: .undefined) + ) let lineHeight = Double(lineHeightResolvedValue.dropLast()).map { $0 / 100.0 } guard let fontSize else { @@ -59,12 +65,12 @@ final class DefaultTypographyTokensGenerator: TypographyTokensGenerator { let letterSpacing = Double( try tokensResolver - .resolveValue(letterSpacingValue, tokenValues: tokenValues) + .resolveValue(letterSpacingValue, tokenValues: tokenValues, theme: .undefined) .removingFirst("%") ).map { $0 / 100.0 } let fontSize = Double( - try tokensResolver.resolveValue(value.fontSize, tokenValues: tokenValues) + try tokensResolver.resolveValue(value.fontSize, tokenValues: tokenValues, theme: .undefined) ) guard let fontSize else { @@ -86,7 +92,7 @@ final class DefaultTypographyTokensGenerator: TypographyTokensGenerator { private func makeContextToken(value: String, tokenValues: TokenValues) throws -> ContextToken { ContextToken( path: value.components(separatedBy: "."), - value: try tokensResolver.resolveValue(value, tokenValues: tokenValues) + value: try tokensResolver.resolveValue(value, tokenValues: tokenValues, theme: .undefined) ) } diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift index 7339e15..0957b6c 100644 --- a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift @@ -33,14 +33,18 @@ final class DefaultColorTokensContextProvider: ColorTokensContextProvider { return fallbackValue } - return try tokensResolver.resolveHexColorValue(nightValue, tokenValues: tokenValues) + return try tokensResolver.resolveHexColorValue( + nightValue, + tokenValues: tokenValues, + theme: .night + ) } private func resolveNightReference( tokenName: String, fallbackRefence: String, tokenValues: TokenValues - ) -> String { + ) throws -> String { guard let nightToken = tokenValues.night.first(where: { $0.name == tokenName }) else { fallbackWarning(tokenName: tokenName) return fallbackRefence @@ -51,7 +55,7 @@ final class DefaultColorTokensContextProvider: ColorTokensContextProvider { return fallbackRefence } - return nightValue + return try tokensResolver.resolveBaseReference(nightValue, tokenValues: tokenValues.night) } private func makeColorToken( @@ -62,26 +66,30 @@ final class DefaultColorTokensContextProvider: ColorTokensContextProvider { ) throws -> ColorToken { let dayHexColorValue = try tokensResolver.resolveHexColorValue( dayValue, + tokenValues: tokenValues, + theme: .day + ) + + let dayReference = try tokensResolver.resolveBaseReference( + dayValue, + tokenValues: tokenValues.day + ) + + let nightReference = try resolveNightReference( + tokenName: tokenName, + fallbackRefence: dayValue, + tokenValues: tokenValues + ) + + let nightHexColorValue = try resolveNightValue( + tokenName: tokenName, + fallbackValue: dayHexColorValue, tokenValues: tokenValues ) return ColorToken( - dayTheme: ColorToken.Theme( - value: dayHexColorValue, - reference: dayValue - ), - nightTheme: ColorToken.Theme( - value: try resolveNightValue( - tokenName: tokenName, - fallbackValue: dayHexColorValue, - tokenValues: tokenValues - ), - reference: resolveNightReference( - tokenName: tokenName, - fallbackRefence: dayValue, - tokenValues: tokenValues - ) - ), + dayTheme: ColorToken.Theme(value: dayHexColorValue, reference: dayReference), + nightTheme: ColorToken.Theme(value: nightHexColorValue, reference: nightReference), name: tokenName, path: path ) @@ -131,7 +139,7 @@ final class DefaultColorTokensContextProvider: ColorTokensContextProvider { let path = token.name.components(separatedBy: ".") - guard path[0] != "gradient" else { + guard path[0] != "gradient" && !dayValue.contains("gradient") else { return nil } diff --git a/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift b/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift index 9ad3303..05f428c 100644 --- a/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift +++ b/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift @@ -60,9 +60,9 @@ final class DefaultTokensResolver: TokensResolver { } } - private func resolveColorValue(_ value: String, tokenValues: TokenValues) throws -> Color { + private func resolveColorValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> Color { if value.hasPrefix("rgba") { - return try resolveRGBAColorValue(value, tokenValues: tokenValues) + return try resolveRGBAColorValue(value, tokenValues: tokenValues, theme: theme) } return try makeColor(hex: value, alpha: 1.0) @@ -70,8 +70,8 @@ final class DefaultTokensResolver: TokensResolver { // MARK: - TokensResolver - func resolveValue(_ value: String, tokenValues: TokenValues) throws -> String { - let allTokens = tokenValues.all + func resolveValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> String { + let themeTokens = tokenValues.getThemeTokenValues(theme: theme) let resolvedValue = try value.replacingOccurrences(matchingPattern: #"\{.*?\}"#) { referenceName in let referenceName = String( @@ -80,7 +80,7 @@ final class DefaultTokensResolver: TokensResolver { .dropLast() ) - guard let token = allTokens.first(where: { $0.name == referenceName }) else { + guard let token = themeTokens.first(where: { $0.name == referenceName }) else { throw TokensGeneratorError(code: .referenceNotFound(name: referenceName)) } @@ -88,14 +88,42 @@ final class DefaultTokensResolver: TokensResolver { throw TokensGeneratorError(code: .unexpectedTokenValueType(name: referenceName)) } - return try resolveValue(value, tokenValues: tokenValues) + return try resolveValue(value, tokenValues: tokenValues, theme: theme) } return evaluteValue(resolvedValue) } - func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues) throws -> Color { - let components = try resolveValue(value, tokenValues: tokenValues) + func resolveBaseReference(_ reference: String, tokenValues: [TokenValue]) throws -> String { + try reference.replacingOccurrences(matchingPattern: #"\{.*?\}"#) { referenceName in + if referenceName.contains("color.base") { + return referenceName + } + + guard referenceName.contains("color.") else { + return referenceName + } + + let referenceName = String( + referenceName + .dropFirst() + .dropLast() + ) + + guard let token = tokenValues.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 resolveBaseReference(value, tokenValues: tokenValues) + } + } + + func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> Color { + let components = try resolveValue(value, tokenValues: tokenValues, theme: theme) .slice(from: "(", to: ")", includingBounds: false)? .components(separatedBy: ", ") @@ -113,18 +141,17 @@ final class DefaultTokensResolver: TokensResolver { return try makeColor(hex: hex, alpha: alpha / 100.0) } - func resolveHexColorValue(_ value: String, tokenValues: TokenValues) throws -> String { - let resolvedValue = try resolveValue(value, tokenValues: tokenValues) + func resolveHexColorValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> String { + let resolvedValue = try resolveValue(value, tokenValues: tokenValues, theme: theme) if resolvedValue.hasPrefix("#") { return resolvedValue } - - return try resolveColorValue(resolvedValue, tokenValues: tokenValues).hexString + return try resolveColorValue(resolvedValue, tokenValues: tokenValues, theme: theme).hexString } - func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues) throws -> LinearGradient { - let value = try resolveValue(value, tokenValues: tokenValues) + func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> LinearGradient { + let value = try resolveValue(value, tokenValues: tokenValues, theme: theme) guard let startFunctionIndex = value.firstIndex(of: "("), let endFunctionIndex = value.lastIndex(of: ")") else { throw TokensGeneratorError(code: .failedToExtractLinearGradientParams(linearGradient: value)) @@ -148,7 +175,7 @@ final class DefaultTokensResolver: TokensResolver { let percentage = String(rawColorStop[separatorRange.upperBound...]) let rawColor = String(rawColorStop[...separatorRange.lowerBound]) - let color = try resolveColorValue(rawColor, tokenValues: tokenValues) + let color = try resolveColorValue(rawColor, tokenValues: tokenValues, theme: theme) return LinearGradient.LinearColorStop(color: color, percentage: percentage) } diff --git a/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift b/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift index 73f0282..81f7eb0 100644 --- a/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift +++ b/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift @@ -13,8 +13,21 @@ protocol TokensResolver { /// - Parameters: /// - value: String value to resolve /// - tokenValues: All token values + /// - theme: Theme /// - Returns: Resolved value. - func resolveValue(_ value: String, tokenValues: TokenValues) throws -> String + func resolveValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> String + + /// Resolving `reference` from `tokenValues`. + /// + /// Example: If reference `{color.background.primary}` and `tokenValues` has token with name + /// `color.background.primary` and reference `{color.base.black}`, + /// the function will return `{color.base.black}` else `{color.background.primary}`. + /// + /// - Parameters: + /// - reference: String reference to resolve + /// - tokenValues: Tokens to search reference. Use theme specific tokens. + /// - Returns: Resolved value. + func resolveBaseReference(_ reference: String, tokenValues: [TokenValue]) throws -> String /// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)`` /// and convert `rgba()` to ``Color`` object @@ -34,8 +47,9 @@ protocol TokensResolver { /// ) /// ``` /// - tokenValues: All token values + /// - theme: Theme /// - Returns: ``Color`` object with values resolved from `rgba()` - func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues) throws -> Color + func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> Color /// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)`` /// and convert `rgba()` to hex value @@ -52,8 +66,9 @@ protocol TokensResolver { /// ``` /// Or simple reference to another color: `{color.base.white}` /// - tokenValues: All token values + /// - theme: Theme /// - Returns: Hex value of the color - func resolveHexColorValue(_ value: String, tokenValues: TokenValues) throws -> String + func resolveHexColorValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> String /// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)`` /// and convert `linear-gradient()` to ``LinearGradient`` object @@ -73,6 +88,7 @@ protocol TokensResolver { /// ) /// ``` /// - tokenValues: All token values + /// - theme: Theme /// - Returns: ``LinearGradient`` object with values resolved from `linear-gradient()` - func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues) throws -> LinearGradient + func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> LinearGradient } diff --git a/Sources/FigmaGen/Models/Token/Theme.swift b/Sources/FigmaGen/Models/Token/Theme.swift new file mode 100644 index 0000000..23e26ff --- /dev/null +++ b/Sources/FigmaGen/Models/Token/Theme.swift @@ -0,0 +1,7 @@ +import Foundation + +enum Theme: Codable { + case day + case night + case undefined +} diff --git a/Sources/FigmaGen/Models/Token/TokenValues.swift b/Sources/FigmaGen/Models/Token/TokenValues.swift index a12bf10..1b60d71 100644 --- a/Sources/FigmaGen/Models/Token/TokenValues.swift +++ b/Sources/FigmaGen/Models/Token/TokenValues.swift @@ -13,7 +13,18 @@ struct TokenValues: Codable, Hashable { // MARK: - Instance Properties - var all: [TokenValue] { - [core, semantic, colors, typography, day, night].flatMap { $0 } + /// Возвращает набор токенов для определенной темы. + /// Для undefined возвращается полный набор токенов. Нужен для Spacer, Font и других независимых от темы параметров. + func getThemeTokenValues(theme: Theme) -> [TokenValue] { + switch theme { + case .day: + return [day, core, semantic, colors, typography].flatMap { $0 } + + case .night: + return [night, core, semantic, colors, typography].flatMap { $0 } + + case .undefined: + return [core, semantic, colors, typography, day, night].flatMap { $0 } + } } } diff --git a/Tests/FigmaGenTests/TokensResolverTests.swift b/Tests/FigmaGenTests/TokensResolverTests.swift index b168804..7b2903f 100644 --- a/Tests/FigmaGenTests/TokensResolverTests.swift +++ b/Tests/FigmaGenTests/TokensResolverTests.swift @@ -27,7 +27,7 @@ final class TokensResolverTests: XCTestCase { let value = "{core.space.1-x} + {core.space.1-x} / 2" let expectedValue = "6" - let actualValue = try tokensResolver.resolveValue(value, tokenValues: tokenValues) + let actualValue = try tokensResolver.resolveValue(value, tokenValues: tokenValues, theme: .day) XCTAssertEqual(actualValue, expectedValue) } @@ -39,11 +39,11 @@ final class TokensResolverTests: XCTestCase { 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) + let actualNumberValue = try tokensResolver.resolveValue(numberValue, tokenValues: .empty, theme: .day) + let actualPercentValue = try tokensResolver.resolveValue(percentValue, tokenValues: .empty, theme: .day) + let actualTextValue = try tokensResolver.resolveValue(textValue, tokenValues: .empty, theme: .day) + let actualPixelValue = try tokensResolver.resolveValue(pixelValue, tokenValues: .empty, theme: .day) + let actualColorValue = try tokensResolver.resolveValue(colorValue, tokenValues: .empty, theme: .day) XCTAssertEqual(actualNumberValue, numberValue) XCTAssertEqual(actualPercentValue, percentValue) @@ -71,7 +71,7 @@ final class TokensResolverTests: XCTestCase { 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) + let actualColor = try tokensResolver.resolveRGBAColorValue(value, tokenValues: tokenValues, theme: .day) XCTAssertEqual(actualColor, expectedColor) } @@ -80,7 +80,7 @@ final class TokensResolverTests: XCTestCase { 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) + let actualColor = try tokensResolver.resolveRGBAColorValue(value, tokenValues: .empty, theme: .day) XCTAssertEqual(actualColor, expectedColor) } @@ -131,7 +131,8 @@ final class TokensResolverTests: XCTestCase { let actualLinearGradient = try tokensResolver.resolveLinearGradientValue( value, - tokenValues: tokenValues + tokenValues: tokenValues, + theme: .day ) XCTAssertEqual(actualLinearGradient, expectedLinearGradient) @@ -166,7 +167,8 @@ final class TokensResolverTests: XCTestCase { let actualLinearGradient = try tokensResolver.resolveLinearGradientValue( value, - tokenValues: .empty + tokenValues: .empty, + theme: .day ) XCTAssertEqual(actualLinearGradient, expectedLinearGradient) @@ -195,8 +197,8 @@ final class TokensResolverTests: XCTestCase { let value2 = "rgba( {color.base.gray.5} , {semantic.opacity.disabled})" let expectedHexColor2 = "#1111117A" - let actualHexColor1 = try tokensResolver.resolveHexColorValue(value1, tokenValues: tokenValues) - let actualHexColor2 = try tokensResolver.resolveHexColorValue(value2, tokenValues: tokenValues) + let actualHexColor1 = try tokensResolver.resolveHexColorValue(value1, tokenValues: tokenValues, theme: .day) + let actualHexColor2 = try tokensResolver.resolveHexColorValue(value2, tokenValues: tokenValues, theme: .day) XCTAssertEqual(actualHexColor1, expectedHexColor1) XCTAssertEqual(actualHexColor2, expectedHexColor2) @@ -206,10 +208,60 @@ final class TokensResolverTests: XCTestCase { let value = "rgba(#FFFFFF, 48%)" let expectedHexColor = "#FFFFFF7A" - let actualHexColor = try tokensResolver.resolveHexColorValue(value, tokenValues: .empty) + let actualHexColor = try tokensResolver.resolveHexColorValue(value, tokenValues: .empty, theme: .day) XCTAssertEqual(actualHexColor, expectedHexColor) } + + func testResolveBaseReference() throws { + let tokenValues = TokenValues( + core: [], + semantic: [], + colors: [ + TokenValue(type: .color(value: "#000000"), name: "color.base.black") + ], + typography: [], + day: [], + night: [ + TokenValue(type: .color(value: "{color.base.black}"), name: "color.background.primary"), + TokenValue(type: .color(value: "{color.background.primary}"), name: "color.background.primary.nested") + ] + ) + + let value = "{color.background.primary.nested}" + let expectedBaseReference = "{color.base.black}" + + let actualBaseReference = try tokensResolver.resolveBaseReference(value, tokenValues: tokenValues.night) + + XCTAssertEqual(actualBaseReference, expectedBaseReference) + } + + func testResolveBaseReferenceWithOpacity() 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: .color(value: "#000000"), name: "color.base.black") + ], + typography: [], + day: [], + night: [ + TokenValue(type: .color(value: "{color.base.black}"), name: "color.background.primary"), + TokenValue(type: .color(value: "{color.background.primary}"), name: "color.background.primary.nested") + ] + ) + + let value = "rgba( {color.background.primary.nested}, {semantic.opacity.disabled})" + let expectedBaseReference = "rgba( {color.base.black}, {semantic.opacity.disabled})" + + let actualBaseReference = try tokensResolver.resolveBaseReference(value, tokenValues: tokenValues.night) + + XCTAssertEqual(actualBaseReference, expectedBaseReference) + } } extension TokenValues {