diff --git a/Sources/FigmaGen/Commands/TokensCommand.swift b/Sources/FigmaGen/Commands/TokensCommand.swift index 28ad27c..aae5b32 100644 --- a/Sources/FigmaGen/Commands/TokensCommand.swift +++ b/Sources/FigmaGen/Commands/TokensCommand.swift @@ -260,6 +260,30 @@ final class TokensCommand: AsyncExecutableCommand { """ ) + let gradientTemplate = Key( + "--gradient-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + let gradientTemplateOptions = VariadicKey( + "--gradient-options", + description: """ + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """ + ) + + let gradientDestination = Key( + "--gradient-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) + // MARK: - Initializers init(generator: TokensGenerator) { @@ -345,6 +369,13 @@ extension TokensCommand { templateOptions: resolveTemplateOptions(bordersTemplateOptions.value), destination: bordersDestination.value ) + ], + gradient: [ + TemplateConfiguration( + template: gradientTemplate.value, + templateOptions: resolveTemplateOptions(gradientTemplateOptions.value), + destination: gradientDestination.value + ) ] ) ) diff --git a/Sources/FigmaGen/Dependencies.swift b/Sources/FigmaGen/Dependencies.swift index 25658fc..8201736 100644 --- a/Sources/FigmaGen/Dependencies.swift +++ b/Sources/FigmaGen/Dependencies.swift @@ -83,6 +83,10 @@ enum Dependencies { static let boxShadowTokensContextProvider: BoxShadowTokensContextProvider = DefaultBoxShadowTokensContextProvider() + static let gradientTokensContextProvider: ColorTokensContextProvider = DefaultGradientTokensContextProvider( + tokensResolver: tokensResolver + ) + // MARK: - static let templateContextCoder: TemplateContextCoder = DefaultTemplateContextCoder() @@ -170,6 +174,7 @@ enum Dependencies { static let themeTokensGenerator: ThemeTokensGenerator = DefaultThemeTokensGenerator( colorTokensContextProvider: colorTokensContextProvider, + gradientTokensContextProvider: gradientTokensContextProvider, boxShadowsContextProvider: boxShadowTokensContextProvider, templateRenderer: templateRenderer ) @@ -184,6 +189,11 @@ enum Dependencies { templateRenderer: templateRenderer ) + static let gradientTokensGenerator: GradientTokensGenerator = DefaultGradientTokensGenerator( + templateRenderer: templateRenderer, + gradientProvider: gradientTokensContextProvider + ) + static let tokensGenerator: TokensGenerator = DefaultTokensGenerator( tokensProvider: tokensProvider, tokensGenerationParametersResolver: tokensGenerationParametersResolver, @@ -194,7 +204,8 @@ enum Dependencies { boxShadowTokensGenerator: boxShadowTokensGenerator, themeTokensGenerator: themeTokensGenerator, spacingTokensGenerator: spacingTokensGenerator, - bordersTokensGenerator: borderTokensGenerator + bordersTokensGenerator: borderTokensGenerator, + gradientTokensGenerator: gradientTokensGenerator ) static let libraryGenerator: LibraryGenerator = DefaultLibraryGenerator( diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/ColorToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/ColorToken.swift index 2a6e788..639ff65 100644 --- a/Sources/FigmaGen/Generators/Tokens/Contexts/ColorToken.swift +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/ColorToken.swift @@ -1,6 +1,6 @@ import Foundation -struct ColorToken: Encodable { +struct ColorToken: TokenProtocol, Encodable { // MARK: - Nested Types diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/LinearGradientToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/LinearGradientToken.swift new file mode 100644 index 0000000..36e0023 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/LinearGradientToken.swift @@ -0,0 +1,28 @@ +import Foundation + +struct LinearGradientToken: TokenProtocol, Encodable { + + struct ColorStop: Encodable { + let color: String + let percentage: CGFloat + } + + // Нужен, т.к CGPoint нельзя использовать корректно в stencil шаблоне + struct Point: Encodable { + let x: CGFloat + let y: CGFloat + } + + struct GradientThemeValue: Encodable { + let stops: [ColorStop] + let startPoint: Point + let endPoint: Point + } + + let path: [String] + let name: String + + let dayTheme: GradientThemeValue + let nightTheme: GradientThemeValue + let zpDayTheme: GradientThemeValue +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/TokenProtocol.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/TokenProtocol.swift new file mode 100644 index 0000000..d9198f3 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/TokenProtocol.swift @@ -0,0 +1,12 @@ +import Foundation + +protocol TokenProtocol { + var name: String { get } + var path: [String] { get } +} + +extension TokenProtocol { + var name: String { + path.joined(separator: ".") + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift index 5cce8aa..135cb61 100644 --- a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift @@ -16,7 +16,7 @@ final class DefaultTokensGenerator: TokensGenerator { let themeTokensGenerator: ThemeTokensGenerator let spacingTokensGenerator: SpacingTokensGenerator let bordersTokensGenerator: BorderTokensGenerator - + let gradientTokensGenerator: GradientTokensGenerator // MARK: - Initializers @@ -30,7 +30,8 @@ final class DefaultTokensGenerator: TokensGenerator { boxShadowTokensGenerator: BoxShadowTokensGenerator, themeTokensGenerator: ThemeTokensGenerator, spacingTokensGenerator: SpacingTokensGenerator, - bordersTokensGenerator: BorderTokensGenerator + bordersTokensGenerator: BorderTokensGenerator, + gradientTokensGenerator: GradientTokensGenerator ) { self.tokensProvider = tokensProvider self.tokensGenerationParametersResolver = tokensGenerationParametersResolver @@ -42,6 +43,7 @@ final class DefaultTokensGenerator: TokensGenerator { self.themeTokensGenerator = themeTokensGenerator self.spacingTokensGenerator = spacingTokensGenerator self.bordersTokensGenerator = bordersTokensGenerator + self.gradientTokensGenerator = gradientTokensGenerator } // MARK: - Instance Methods @@ -57,6 +59,7 @@ final class DefaultTokensGenerator: TokensGenerator { try generateThemeTokens(parameters: parameters, tokenValues: tokenValues) try generateSpacingTokens(parameters: parameters, tokenValues: tokenValues) try generateBorderTokens(parameters: parameters, tokenValues: tokenValues) + try generateGradientTokens(parameters: parameters, tokenValues: tokenValues) } private func fetchTokens(from parameters: TokensGenerationParameters) async throws -> TokenValues { @@ -133,6 +136,14 @@ final class DefaultTokensGenerator: TokensGenerator { ) } + private func generateGradientTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + gradientTokensGenerator, + renderParameters: parameters.tokens.gradientRenderParameters, + tokenValues: tokenValues + ) + } + private func generateTokens( _ generator: BaseTokenGenerator, renderParameters: [RenderParameters]?, diff --git a/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift index 8add0c3..0c68907 100644 --- a/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift +++ b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift @@ -94,6 +94,11 @@ final class DefaultTokensGenerationParametersResolver: TokensGenerationParameter defaultTemplateType: .native(name: "BorderTokens") ) + let gradientRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.gradient, + defaultTemplateType: .native(name: "GradientTokens") + ) + return TokensGenerationParameters( file: file, remoteFile: remoteFile, @@ -105,7 +110,8 @@ final class DefaultTokensGenerationParametersResolver: TokensGenerationParameter boxShadowRenderParameters: boxShadowRenderParameters, themeRenderParameters: themeRenderParameters, spacingRenderParameters: spacingRenderParameters, - bordersRenderParameters: borderRenderParameters + bordersRenderParameters: borderRenderParameters, + gradientRenderParameters: gradientRenderParameters ) ) } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift index 88014fb..8e93fd6 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift @@ -20,7 +20,7 @@ final class DefaultColorTokensGenerator: ColorTokensGenerator { // MARK: - Instance Methods func generate(renderParameters: RenderParameters, tokenValues: TokenValues) throws { - let context = try colorTokensContextProvider.fetchColorTokensContext(from: tokenValues) + let context = try colorTokensContextProvider.extractTokenContext(from: tokenValues) try templateRenderer.renderTemplate( renderParameters.template, diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/DefaultGradientTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/DefaultGradientTokensGenerator.swift new file mode 100644 index 0000000..c347a5b --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/DefaultGradientTokensGenerator.swift @@ -0,0 +1,29 @@ +import Foundation + +struct DefaultGradientTokensGenerator: GradientTokensGenerator { + + // MARK: - Instance properties + + private let templateRenderer: TemplateRenderer + private let provider: ColorTokensContextProvider + + init( + templateRenderer: TemplateRenderer, + gradientProvider: ColorTokensContextProvider + ) { + self.templateRenderer = templateRenderer + self.provider = gradientProvider + } + + // MARK: - GradientTokensGenerator + + func generate(renderParameters: RenderParameters, tokenValues: TokenValues) throws { + let gradientContext = try provider.extractTokenContext(from: tokenValues) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: ["gradients": gradientContext] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/GradientTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/GradientTokensGenerator.swift new file mode 100644 index 0000000..0cb5b79 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/GradientTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol GradientTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift index 5795c5f..782f478 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift @@ -5,6 +5,7 @@ final class DefaultThemeTokensGenerator: ThemeTokensGenerator { // MARK: - Instance Properties let colorTokensContextProvider: ColorTokensContextProvider + let gradientTokensContextProvider: ColorTokensContextProvider let boxShadowsContextProvider: BoxShadowTokensContextProvider let templateRenderer: TemplateRenderer @@ -12,10 +13,12 @@ final class DefaultThemeTokensGenerator: ThemeTokensGenerator { init( colorTokensContextProvider: ColorTokensContextProvider, + gradientTokensContextProvider: ColorTokensContextProvider, boxShadowsContextProvider: BoxShadowTokensContextProvider, templateRenderer: TemplateRenderer ) { self.colorTokensContextProvider = colorTokensContextProvider + self.gradientTokensContextProvider = gradientTokensContextProvider self.boxShadowsContextProvider = boxShadowsContextProvider self.templateRenderer = templateRenderer } @@ -23,15 +26,17 @@ final class DefaultThemeTokensGenerator: ThemeTokensGenerator { // MARK: - Instance Methods func generate(renderParameters: RenderParameters, tokenValues: TokenValues) throws { - let colorsContext = try colorTokensContextProvider.fetchColorTokensContext(from: tokenValues) + let colorsContext = try colorTokensContextProvider.extractTokenContext(from: tokenValues) let boxShadowsContext = try boxShadowsContextProvider.fetchBoxShadowTokensContext(from: tokenValues) + let gradientsContext = try gradientTokensContextProvider.extractTokenContext(from: tokenValues) try templateRenderer.renderTemplate( renderParameters.template, to: renderParameters.destination, context: [ "colors": colorsContext, - "boxShadows": boxShadowsContext + "boxShadows": boxShadowsContext, + "gradients": gradientsContext ] ) } diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider+Extensions.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider+Extensions.swift new file mode 100644 index 0000000..55de38d --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider+Extensions.swift @@ -0,0 +1,42 @@ +import Foundation + +extension ColorTokensContextProvider { + + func structure( + tokens: [T], + atNamePath namePath: [String] = [], + contextName: String = "tokens" + ) -> [String: Any] { + var structuredTokens: [String: Any] = [:] + + if let name = namePath.last { + structuredTokens["name"] = name + } + + if !namePath.isEmpty { + structuredTokens["path"] = namePath + } + + let filteredTokens = tokens + .filter { $0.path.count == namePath.count + 1 } + .sorted { $0.name.lowercased() < $1.name.lowercased() } + + if !filteredTokens.isEmpty { + structuredTokens[contextName] = filteredTokens + } + + let childTokens = tokens.filter { $0.path.count > namePath.count + 1 } + + let children = Dictionary(grouping: childTokens) { $0.path[namePath.count] } + .sorted { $0.key < $1.key } + .map { name, tokens in + structure(tokens: tokens, atNamePath: namePath + [name], contextName: contextName) + } + + if !children.isEmpty { + structuredTokens["children"] = children + } + + return structuredTokens + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift index cbdfbce..785b0b8 100644 --- a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift @@ -4,5 +4,5 @@ protocol ColorTokensContextProvider { // MARK: - Instance Methods - func fetchColorTokensContext(from tokenValues: TokenValues) throws -> [String: Any] + func extractTokenContext(from tokenValues: TokenValues) throws -> [String: Any] } diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift index 8973278..10c25b9 100644 --- a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift @@ -101,43 +101,9 @@ final class DefaultColorTokensContextProvider: ColorTokensContextProvider { ) } - private func structure(tokenColors: [ColorToken], atNamePath namePath: [String] = []) -> [String: Any] { - var structuredColors: [String: Any] = [:] - - if let name = namePath.last { - structuredColors["name"] = name - } - - if !namePath.isEmpty { - structuredColors["path"] = namePath - } - - let colors = tokenColors - .filter { $0.path.count == namePath.count + 1 } - .sorted { $0.name.lowercased() < $1.name.lowercased() } - - if !colors.isEmpty { - structuredColors["colors"] = colors - } - - let childTokenColors = tokenColors.filter { $0.path.count > namePath.count + 1 } - - let children = Dictionary(grouping: childTokenColors) { $0.path[namePath.count] } - .sorted { $0.key < $1.key } - .map { name, colors in - structure(tokenColors: colors, atNamePath: namePath + [name]) - } - - if !children.isEmpty { - structuredColors["children"] = children - } - - return structuredColors - } - // MARK: - - func fetchColorTokensContext(from tokenValues: TokenValues) throws -> [String: Any] { + func extractTokenContext(from tokenValues: TokenValues) throws -> [String: Any] { let colors: [ColorToken] = try tokenValues.hhDay.compactMap { (token: TokenValue) in guard case .color(let dayValue) = token.type else { return nil @@ -157,6 +123,6 @@ final class DefaultColorTokensContextProvider: ColorTokensContextProvider { ) } - return structure(tokenColors: colors) + return structure(tokens: colors, contextName: "colors") } } diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultGradientTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultGradientTokensContextProvider.swift new file mode 100644 index 0000000..6d8df5d --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultGradientTokensContextProvider.swift @@ -0,0 +1,179 @@ +import Foundation + +struct DefaultGradientTokensContextProvider: ColorTokensContextProvider { + + let tokensResolver: TokensResolver + + init(tokensResolver: TokensResolver) { + self.tokensResolver = tokensResolver + } + + // MARK: - Private methods + + private func resolvePoints(angle: CGFloat) -> (start: CGPoint, end: CGPoint)? { + let start = 3.0 * .pi / 2 + let u = start + angle + + let ucos = cos(u) + let usin = sin(u) + + let xedge = ucos > 0 ? 1.0 : 0 + let yedge = usin > 0 ? 1.0 : 0 + + let tx = ucos == 0 ? nil : (xedge - 0.5) / ucos + let ty = usin == 0 ? nil : (yedge - 0.5) / usin + + let t = [tx, ty] + .compactMap(\.self) + .filter { $0 > 0} + .min() + + guard let t else { + // Невозможно вычислить пересечение с границей + return nil + } + + let endPoint = CGPoint(x: 0.5 + t * ucos, y: 0.5 + t * usin) + let startPoint = CGPoint(x: 0.5 - t * ucos, y: 0.5 - t * usin) + + return (start: startPoint, end: endPoint) + } + + private func resolveThemeValue( + from gradient: LinearGradient, + tokenName: String + ) -> LinearGradientToken.GradientThemeValue? { + guard let points = resolvePoints(angle: gradient.radians) else { + return nil + } + + return LinearGradientToken.GradientThemeValue( + stops: gradient.colorStopList.compactMap { stop in + let percentage = stop.percentage.replacingOccurrences(of: "%", with: "") + guard let percentage = Double(percentage) else { + return nil + } + + return .init( + color: stop.color.hexString, + percentage: percentage / 100 + ) + }, + startPoint: LinearGradientToken.Point( + x: points.start.x, + y: points.start.y + ), + endPoint: LinearGradientToken.Point( + x: points.end.x, + y: points.end.y + ) + ) + } + + private func resolveGradientToken( + tokenName: String, + fallbackColorToken: LinearGradientToken.GradientThemeValue, + tokenValues: TokenValues, + theme: Theme + ) throws -> LinearGradientToken.GradientThemeValue { + let themeData: (tokenValues: [TokenValue], warningPrefix: String) + switch theme { + case .night: + themeData = (tokenValues.hhNight, "Night") + + case .zpDay: + themeData = (tokenValues.zpDay, "ZpDay") + + case .day, .undefined: + themeData = ([], "") + } + + guard let themeToken = themeData.tokenValues.first(where: { $0.name == tokenName }) else { + return fallbackColorToken + } + + guard case .color(let themeValue) = themeToken.type else { + return fallbackColorToken + } + + let gradient = try tokensResolver.resolveLinearGradientValue( + themeValue, + tokenValues: tokenValues, + theme: theme + ) + + return resolveThemeValue(from: gradient, tokenName: tokenName) ?? fallbackColorToken + } + + private func createGradientToken( + _ gradientValue: String, + tokenName: String, + path: [String], + tokenValues: TokenValues + ) throws -> LinearGradientToken? { + let dayGradient = try tokensResolver.resolveLinearGradientValue( + gradientValue, + tokenValues: tokenValues, + theme: .day + ) + + guard let dayToken = resolveThemeValue(from: dayGradient, tokenName: tokenName) else { + return nil + } + + let nightToken = try resolveGradientToken( + tokenName: tokenName, + fallbackColorToken: dayToken, + tokenValues: tokenValues, + theme: .night + ) + + let zpDayToken = try resolveGradientToken( + tokenName: tokenName, + fallbackColorToken: dayToken, + tokenValues: tokenValues, + theme: .zpDay + ) + + return LinearGradientToken( + path: path, + name: tokenName, + dayTheme: dayToken, + nightTheme: nightToken, + zpDayTheme: zpDayToken + ) + } + + // TODO: @mi.fedorov поддержать мульти-темы + private func extractGradientToken( + from token: TokenValue, + tokenValues: TokenValues + ) throws -> LinearGradientToken? { + let path = token.name.components(separatedBy: ".") + + guard + case .color(let dayValue) = token.type, + dayValue.contains("gradient"), + path[0] != "color" + else { + return nil + } + + return try createGradientToken( + dayValue, + tokenName: token.name, + path: path, + tokenValues: tokenValues + ) + } + + // MARK: - GradientTokensContextProvider + + func extractTokenContext(from tokenValues: TokenValues) throws -> [String : Any] { + let gradient = try tokenValues.hhDay.compactMap { + try extractGradientToken(from: $0, tokenValues: tokenValues) + } + + return structure(tokens: gradient, contextName: "gradients") + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift b/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift index c4bd3f2..3d0266c 100644 --- a/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift +++ b/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift @@ -68,6 +68,45 @@ final class DefaultTokensResolver: TokensResolver { return try makeColor(hex: value, alpha: 1.0) } + private func resolveRGBAWithHex( + hex: String, + alphaPercent: String, + value: String + ) throws -> Color { + let alpha = alphaPercent.replacingOccurrences(of: "%", with: "") + guard let alpha = Double(alpha) else { + throw TokensGeneratorError(code: .invalidAlphaComponent(alpha: alphaPercent + value)) + } + + return try makeColor(hex: hex, alpha: alpha / 100.0) + } + + private func resolveRGBA( + red: String, + green: String, + blue: String, + alpha: String, + rgbaValue: String + ) throws -> Color { + guard + let red = Double(red), + let green = Double(green), + let blue = Double(blue), + let alpha = Double(alpha) + else { + throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: rgbaValue)) + } + + let normalizationFactor: CGFloat = 255.0 + + return Color( + red: CGFloat(red) / normalizationFactor, + green: CGFloat(green) / normalizationFactor, + blue: CGFloat(blue) / normalizationFactor, + alpha: alpha + ) + } + // MARK: - TokensResolver func resolveValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> String { @@ -127,18 +166,28 @@ final class DefaultTokensResolver: TokensResolver { .slice(from: "(", to: ")", includingBounds: false)? .components(separatedBy: ", ") - guard let components, components.count == 2 else { + guard let components 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)) + switch components.count { + case .rgbaWithHex: + return try resolveRGBAWithHex( + hex: components[0], + alphaPercent: components[1], + value: value + ) + case .rgba: + return try resolveRGBA( + red: components[0], + green: components[1], + blue: components[2], + alpha: components[3], + rgbaValue: value + ) + default: + throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: value)) } - - return try makeColor(hex: hex, alpha: alpha / 100.0) } func resolveHexColorValue(_ value: String, tokenValues: TokenValues, theme: Theme) throws -> String { @@ -193,4 +242,6 @@ extension Int { fileprivate static let rgb = 3 fileprivate static let rrggbb = 6 + fileprivate static let rgbaWithHex = 2 + fileprivate static let rgba = 4 } diff --git a/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift b/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift index 81f7eb0..9cf1aba 100644 --- a/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift +++ b/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift @@ -34,6 +34,7 @@ protocol TokensResolver { /// /// Supported formats: /// - `rgba(hex_color, alpha-value-percentage)` + /// – `rgba(red, green, blue, 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) diff --git a/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift b/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift index bf165db..532368c 100644 --- a/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift +++ b/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift @@ -13,6 +13,7 @@ struct TokensTemplateConfiguration { let theme: [TemplateConfiguration]? let spacing: [TemplateConfiguration]? let borders: [TemplateConfiguration]? + let gradient: [TemplateConfiguration]? } // MARK: - Decodable @@ -30,6 +31,7 @@ extension TokensTemplateConfiguration: Decodable { case theme case spacing case borders + case gradient } // MARK: - Initializers @@ -51,5 +53,6 @@ extension TokensTemplateConfiguration: Decodable { theme = try container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .theme)?.templates spacing = try container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .spacing)?.templates borders = try? container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .borders)?.templates + gradient = try? container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .gradient)?.templates } } diff --git a/Sources/FigmaGen/Models/LinearGradient.swift b/Sources/FigmaGen/Models/LinearGradient.swift index 92a0120..c97b708 100644 --- a/Sources/FigmaGen/Models/LinearGradient.swift +++ b/Sources/FigmaGen/Models/LinearGradient.swift @@ -17,3 +17,18 @@ struct LinearGradient: Codable, Hashable { let angle: String let colorStopList: [LinearColorStop] } + +extension LinearGradient { + + var radians: Double { + if angle.hasSuffix("deg"), let deg = Double(angle.replacingOccurrences(of: "deg", with: "")) { + return deg * .pi / 180 + } + + if angle.hasSuffix("rad"), let rad = Double(angle.replacingOccurrences(of: "rad", with: "")) { + return rad + } + + return Double(angle) ?? .zero + } +} diff --git a/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift b/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift index 6c4a7b0..b500391 100644 --- a/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift +++ b/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift @@ -16,6 +16,7 @@ struct TokensGenerationParameters { let themeRenderParameters: [RenderParameters]? let spacingRenderParameters: [RenderParameters]? let bordersRenderParameters: [RenderParameters]? + let gradientRenderParameters: [RenderParameters]? } // MARK: - Instance Properties diff --git a/Templates/GradientTokens.stencil b/Templates/GradientTokens.stencil new file mode 100644 index 0000000..8f1a445 --- /dev/null +++ b/Templates/GradientTokens.stencil @@ -0,0 +1,43 @@ +{% include "FileHeader.stencil" %} +{% if gradients %} +{% set gradientTypeName %}{{ options.colorTypeName|default:"LinearGradient" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro typeName name %}{{ name|swiftIdentifier:"pretty"|upperFirstLetter|escapeReservedKeywords }}{% endmacro %} +{% macro recursiveBlock item %} + {% for gradient in item.gradients %} + + /// {{ gradient.name }} + /// + /// Day Theme: + {% for stop in gradient.dayTheme.stops %} + /// - {{ stop.color }} {{ stop.percentage }}% + {% endfor %} + /// Night Theme: + {% for stop in gradient.nightTheme.stops %} + /// - {{ stop.color }} {{ stop.percentage }}% + {% endfor %} + public let {% call propertyName gradient.path.last %}: {{ gradientTypeName }} + {% endfor %} + {% for child in item.children %} + {% if child.name == "gradient" %} + {% call recursiveBlock child %} + {% else %} + public struct {% call typeName child.name %} { + {% filter indent:4 %} + {% call recursiveBlock child %} + {% endfilter %} + } + + public let {% call propertyName child.name %}: {% call typeName child.name %} + {% endif %} + {% endfor %} +{% endmacro %} + +import SwiftUI + +public struct Gradients { + {% call recursiveBlock gradients %} +} +{% else %} +// No color tokens found +{% endif %}