From 004f4d56675d7fdb0e1d58eb1d9873ef46856af6 Mon Sep 17 00:00:00 2001 From: Timur Shafigullin Date: Fri, 28 Jul 2023 17:55:55 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=93=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=82=D0=B5=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/FigmaGen/Commands/TokensCommand.swift | 29 ++++ Sources/FigmaGen/Dependencies.swift | 21 ++- .../Tokens/DefaultTokensGenerator.swift | 10 +- ...ltTokensGenerationParametersResolver.swift | 8 +- .../DefaultBoxShadowTokensGenerator.swift | 46 +----- .../Color/DefaultColorTokensGenerator.swift | 129 +-------------- .../Theme/DefaultThemeTokensGenerator.swift | 38 +++++ .../Theme/ThemeTokensGenerator.swift | 8 + .../BoxShadowTokensContextProvider.swift | 8 + ...BoxShadowTokensContextProviderError.swift} | 2 +- ...efaultBoxShadowTokensContextProvider.swift | 48 ++++++ .../ColorTokensContextProvider.swift | 8 + .../DefaultColorTokensContextProvider.swift | 148 ++++++++++++++++++ .../Tokens/TokensTemplateConfiguration.swift | 1 + .../TokensGenerationParameters.swift | 1 + .../Hex/StencilFullHexModificator.swift | 17 +- Templates/ColorTokens.stencil | 4 +- 17 files changed, 347 insertions(+), 179 deletions(-) create mode 100644 Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift create mode 100644 Sources/FigmaGen/Generators/Tokens/Generators/Theme/ThemeTokensGenerator.swift create mode 100644 Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProvider.swift rename Sources/FigmaGen/Generators/Tokens/{Generators/BoxShadow/BoxShadowTokensGeneratorError.swift => Providers/BoxShadowTokensContext/BoxShadowTokensContextProviderError.swift} (85%) create mode 100644 Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/DefaultBoxShadowTokensContextProvider.swift create mode 100644 Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift create mode 100644 Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift diff --git a/Sources/FigmaGen/Commands/TokensCommand.swift b/Sources/FigmaGen/Commands/TokensCommand.swift index 09fec33..c03ad45 100644 --- a/Sources/FigmaGen/Commands/TokensCommand.swift +++ b/Sources/FigmaGen/Commands/TokensCommand.swift @@ -153,6 +153,30 @@ final class TokensCommand: AsyncExecutableCommand { """ ) + let themeTemplate = Key( + "--theme-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + let themeTemplateOptions = VariadicKey( + "--theme-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 themeDestination = Key( + "--theme-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) + // MARK: - Initializers init(generator: TokensGenerator) { @@ -204,6 +228,11 @@ extension TokensCommand { template: boxShadowsTemplate.value, templateOptions: resolveTemplateOptions(boxShadowsTemplateOptions.value), destination: boxShadowsDestination.value + ), + theme: TokensTemplateConfiguration.Template( + template: themeTemplate.value, + templateOptions: resolveTemplateOptions(themeTemplateOptions.value), + destination: themeDestination.value ) ) ) diff --git a/Sources/FigmaGen/Dependencies.swift b/Sources/FigmaGen/Dependencies.swift index 397f3d2..91b58ae 100644 --- a/Sources/FigmaGen/Dependencies.swift +++ b/Sources/FigmaGen/Dependencies.swift @@ -67,6 +67,12 @@ enum Dependencies { static let tokensGenerationParametersResolver: TokensGenerationParametersResolver = DefaultTokensGenerationParametersResolver() + static let colorTokensContextProvider: ColorTokensContextProvider = DefaultColorTokensContextProvider( + tokensResolver: tokensResolver + ) + + static let boxShadowTokensContextProvider: BoxShadowTokensContextProvider = DefaultBoxShadowTokensContextProvider() + // MARK: - static let templateContextCoder: TemplateContextCoder = DefaultTemplateContextCoder() @@ -120,8 +126,8 @@ enum Dependencies { ) static let colorTokensGenerator: ColorTokensGenerator = DefaultColorTokensGenerator( - tokensResolver: tokensResolver, - templateRenderer: templateRenderer + templateRenderer: templateRenderer, + colorTokensContextProvider: colorTokensContextProvider ) static let baseColorTokensGenerator: BaseColorTokensGenerator = DefaultBaseColorTokensGenerator( @@ -140,7 +146,13 @@ enum Dependencies { ) static let boxShadowTokensGenerator: BoxShadowTokensGenerator = DefaultBoxShadowTokensGenerator( - tokensResolver: tokensResolver, + boxShadowTokensContextProvider: boxShadowTokensContextProvider, + templateRenderer: templateRenderer + ) + + static let themeTokensGenerator: ThemeTokensGenerator = DefaultThemeTokensGenerator( + colorTokensContextProvider: colorTokensContextProvider, + boxShadowsContextProvider: boxShadowTokensContextProvider, templateRenderer: templateRenderer ) @@ -151,7 +163,8 @@ enum Dependencies { baseColorTokensGenerator: baseColorTokensGenerator, fontFamilyTokensGenerator: fontFamilyTokensGenerator, typographyTokensGenerator: typographyTokensGenerator, - boxShadowTokensGenerator: boxShadowTokensGenerator + boxShadowTokensGenerator: boxShadowTokensGenerator, + themeTokensGenerator: themeTokensGenerator ) static let libraryGenerator: LibraryGenerator = DefaultLibraryGenerator( diff --git a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift index 284535d..69ecf39 100644 --- a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift @@ -13,6 +13,7 @@ final class DefaultTokensGenerator: TokensGenerator { let fontFamilyTokensGenerator: FontFamilyTokensGenerator let typographyTokensGenerator: TypographyTokensGenerator let boxShadowTokensGenerator: BoxShadowTokensGenerator + let themeTokensGenerator: ThemeTokensGenerator // MARK: - Initializers @@ -23,7 +24,8 @@ final class DefaultTokensGenerator: TokensGenerator { baseColorTokensGenerator: BaseColorTokensGenerator, fontFamilyTokensGenerator: FontFamilyTokensGenerator, typographyTokensGenerator: TypographyTokensGenerator, - boxShadowTokensGenerator: BoxShadowTokensGenerator + boxShadowTokensGenerator: BoxShadowTokensGenerator, + themeTokensGenerator: ThemeTokensGenerator ) { self.tokensProvider = tokensProvider self.tokensGenerationParametersResolver = tokensGenerationParametersResolver @@ -32,6 +34,7 @@ final class DefaultTokensGenerator: TokensGenerator { self.fontFamilyTokensGenerator = fontFamilyTokensGenerator self.typographyTokensGenerator = typographyTokensGenerator self.boxShadowTokensGenerator = boxShadowTokensGenerator + self.themeTokensGenerator = themeTokensGenerator } // MARK: - Instance Methods @@ -63,6 +66,11 @@ final class DefaultTokensGenerator: TokensGenerator { renderParameters: parameters.tokens.boxShadowRender, tokenValues: tokenValues ) + + try themeTokensGenerator.generate( + renderParameters: parameters.tokens.themeRender, + tokenValues: tokenValues + ) } // MARK: - diff --git a/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift index bd13a32..6a47224 100644 --- a/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift +++ b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift @@ -98,6 +98,11 @@ final class DefaultTokensGenerationParametersResolver: TokensGenerationParameter nativeTemplateName: "BoxShadowTokens" ) + let themeRender = resolveRenderParameters( + template: configuration.templates?.theme, + nativeTemplateName: "Theme" + ) + return TokensGenerationParameters( file: file, tokens: TokensGenerationParameters.TokensParameters( @@ -105,7 +110,8 @@ final class DefaultTokensGenerationParametersResolver: TokensGenerationParameter baseColorRender: baseColorRender, fontFamilyRender: fontFamilyRender, typographyRender: typographyRender, - boxShadowRender: boxShadowRender + boxShadowRender: boxShadowRender, + themeRender: themeRender ) ) } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/DefaultBoxShadowTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/DefaultBoxShadowTokensGenerator.swift index 67e8c51..14847a6 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/DefaultBoxShadowTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/DefaultBoxShadowTokensGenerator.swift @@ -4,58 +4,20 @@ final class DefaultBoxShadowTokensGenerator: BoxShadowTokensGenerator { // MARK: - Instance Properties - let tokensResolver: TokensResolver + let boxShadowTokensContextProvider: BoxShadowTokensContextProvider let templateRenderer: TemplateRenderer // MARK: - Initializers - init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { - self.tokensResolver = tokensResolver + init(boxShadowTokensContextProvider: BoxShadowTokensContextProvider, templateRenderer: TemplateRenderer) { + self.boxShadowTokensContextProvider = boxShadowTokensContextProvider self.templateRenderer = templateRenderer } // MARK: - Instance Methods - private func makeTheme(value: TokenBoxShadowValue) -> BoxShadowToken.Theme { - BoxShadowToken.Theme( - color: value.color, - type: value.type, - x: value.x, - y: value.y, - blur: value.blur, - spread: value.spread - ) - } - - private func makeBoxShadowToken( - from dayTokenValue: TokenValue, - tokenValues: TokenValues - ) throws -> BoxShadowToken? { - guard case let .boxShadow(dayValue) = dayTokenValue.type else { - return nil - } - - guard let nightTokenValue = tokenValues.night.first(where: { $0.name == dayTokenValue.name }) else { - throw BoxShadowTokensGeneratorError(code: .nightValueNotFound(tokenName: dayTokenValue.name)) - } - - guard case let .boxShadow(nightValue) = nightTokenValue.type else { - throw BoxShadowTokensGeneratorError(code: .nightValueNotFound(tokenName: dayTokenValue.name)) - } - - return BoxShadowToken( - path: dayTokenValue.name.components(separatedBy: "."), - dayTheme: makeTheme(value: dayValue), - nightTheme: makeTheme(value: nightValue) - ) - } - - // MARK: - - func generate(renderParameters: RenderParameters, tokenValues: TokenValues) throws { - let boxShadows = try tokenValues.day - .compactMap { try makeBoxShadowToken(from: $0, tokenValues: tokenValues) } - .sorted { $0.path.joined() < $1.path.joined() } + let boxShadows = try boxShadowTokensContextProvider.fetchBoxShadowTokensContext(from: tokenValues) try templateRenderer.renderTemplate( renderParameters.template, diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift index a3e975b..88014fb 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift @@ -4,141 +4,28 @@ final class DefaultColorTokensGenerator: ColorTokensGenerator { // MARK: - Instance Properties - let tokensResolver: TokensResolver let templateRenderer: TemplateRenderer + let colorTokensContextProvider: ColorTokensContextProvider // MARK: - Initializers - init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { - self.tokensResolver = tokensResolver + init( + templateRenderer: TemplateRenderer, + colorTokensContextProvider: ColorTokensContextProvider + ) { self.templateRenderer = templateRenderer + self.colorTokensContextProvider = colorTokensContextProvider } // MARK: - Instance Methods - private func resolveNightValue( - tokenName: String, - fallbackValue: String, - tokenValues: TokenValues - ) throws -> String { - guard let nightToken = tokenValues.night.first(where: { $0.name == tokenName }) else { - return fallbackValue - } - - guard case .color(let nightValue) = nightToken.type else { - return fallbackValue - } - - return try tokensResolver.resolveHexColorValue(nightValue, tokenValues: tokenValues) - } - - private func resolveNightReference( - tokenName: String, - fallbackRefence: String, - tokenValues: TokenValues - ) -> String { - guard let nightToken = tokenValues.night.first(where: { $0.name == tokenName }) else { - return fallbackRefence - } - - guard case .color(let nightValue) = nightToken.type else { - return fallbackRefence - } - - return nightValue - } - - private func structure(tokenColors: [ColorToken], atNamePath namePath: [String] = []) -> [String: Any] { - var structuredColors: [String: Any] = [:] - - if let name = namePath.last { - structuredColors["name"] = name - } - - 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 - } - - private func makeColorToken( - dayValue: String, - tokenName: String, - tokenValues: TokenValues, - path: [String] - ) throws -> ColorToken { - let dayHexColorValue = try tokensResolver.resolveHexColorValue( - dayValue, - 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 - ) - ), - name: tokenName, - path: path - ) - } - - // MARK: - - func generate(renderParameters: RenderParameters, tokenValues: TokenValues) throws { - let colors: [ColorToken] = try tokenValues.day.compactMap { (token: TokenValue) in - guard case .color(let dayValue) = token.type else { - return nil - } - - let path = token.name.components(separatedBy: ".") - - guard path[0] != "gradient" else { - return nil - } - - return try makeColorToken( - dayValue: dayValue, - tokenName: token.name, - tokenValues: tokenValues, - path: path - ) - } - - let structuredColors = structure(tokenColors: colors) + let context = try colorTokensContextProvider.fetchColorTokensContext(from: tokenValues) try templateRenderer.renderTemplate( renderParameters.template, to: renderParameters.destination, - context: ["colorTokens": structuredColors] + context: ["colors": context] ) } } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift new file mode 100644 index 0000000..5795c5f --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift @@ -0,0 +1,38 @@ +import Foundation + +final class DefaultThemeTokensGenerator: ThemeTokensGenerator { + + // MARK: - Instance Properties + + let colorTokensContextProvider: ColorTokensContextProvider + let boxShadowsContextProvider: BoxShadowTokensContextProvider + let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init( + colorTokensContextProvider: ColorTokensContextProvider, + boxShadowsContextProvider: BoxShadowTokensContextProvider, + templateRenderer: TemplateRenderer + ) { + self.colorTokensContextProvider = colorTokensContextProvider + self.boxShadowsContextProvider = boxShadowsContextProvider + self.templateRenderer = templateRenderer + } + + // MARK: - Instance Methods + + func generate(renderParameters: RenderParameters, tokenValues: TokenValues) throws { + let colorsContext = try colorTokensContextProvider.fetchColorTokensContext(from: tokenValues) + let boxShadowsContext = try boxShadowsContextProvider.fetchBoxShadowTokensContext(from: tokenValues) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "colors": colorsContext, + "boxShadows": boxShadowsContext + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Theme/ThemeTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/ThemeTokensGenerator.swift new file mode 100644 index 0000000..ceb9b2d --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/ThemeTokensGenerator.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol ThemeTokensGenerator { + + // MARK: - Instance Methods + + func generate(renderParameters: RenderParameters, tokenValues: TokenValues) throws +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProvider.swift new file mode 100644 index 0000000..a4a9597 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProvider.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol BoxShadowTokensContextProvider { + + // MARK: - Instance Methods + + func fetchBoxShadowTokensContext(from tokenValues: TokenValues) throws -> [BoxShadowToken] +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/BoxShadowTokensGeneratorError.swift b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProviderError.swift similarity index 85% rename from Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/BoxShadowTokensGeneratorError.swift rename to Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProviderError.swift index 36f4f2f..d9f638b 100644 --- a/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/BoxShadowTokensGeneratorError.swift +++ b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProviderError.swift @@ -1,6 +1,6 @@ import Foundation -struct BoxShadowTokensGeneratorError: Error, CustomStringConvertible { +struct BoxShadowTokensContextProviderError: Error, CustomStringConvertible { // MARK: - Nested Types diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/DefaultBoxShadowTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/DefaultBoxShadowTokensContextProvider.swift new file mode 100644 index 0000000..eb8a6a9 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/DefaultBoxShadowTokensContextProvider.swift @@ -0,0 +1,48 @@ +import Foundation + +final class DefaultBoxShadowTokensContextProvider: BoxShadowTokensContextProvider { + + // MARK: - Instance Methods + + private func makeTheme(value: TokenBoxShadowValue) -> BoxShadowToken.Theme { + BoxShadowToken.Theme( + color: value.color, + type: value.type, + x: value.x, + y: value.y, + blur: value.blur, + spread: value.spread + ) + } + + private func makeBoxShadowToken( + from dayTokenValue: TokenValue, + tokenValues: TokenValues + ) throws -> BoxShadowToken? { + guard case let .boxShadow(dayValue) = dayTokenValue.type else { + return nil + } + + guard let nightTokenValue = tokenValues.night.first(where: { $0.name == dayTokenValue.name }) else { + throw BoxShadowTokensContextProviderError(code: .nightValueNotFound(tokenName: dayTokenValue.name)) + } + + guard case let .boxShadow(nightValue) = nightTokenValue.type else { + throw BoxShadowTokensContextProviderError(code: .nightValueNotFound(tokenName: dayTokenValue.name)) + } + + return BoxShadowToken( + path: dayTokenValue.name.components(separatedBy: "."), + dayTheme: makeTheme(value: dayValue), + nightTheme: makeTheme(value: nightValue) + ) + } + + // MARK: - + + func fetchBoxShadowTokensContext(from tokenValues: TokenValues) throws -> [BoxShadowToken] { + try tokenValues.day + .compactMap { try makeBoxShadowToken(from: $0, tokenValues: tokenValues) } + .sorted { $0.path.joined() < $1.path.joined() } + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift new file mode 100644 index 0000000..cbdfbce --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol ColorTokensContextProvider { + + // MARK: - Instance Methods + + func fetchColorTokensContext(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 new file mode 100644 index 0000000..890e962 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/DefaultColorTokensContextProvider.swift @@ -0,0 +1,148 @@ +import Foundation + +final class DefaultColorTokensContextProvider: ColorTokensContextProvider { + + // MARK: - Instance Properties + + let tokensResolver: TokensResolver + + // MARK: - Initializers + + init(tokensResolver: TokensResolver) { + self.tokensResolver = tokensResolver + } + + // MARK: - Instance Methods + + private func fallbackWarning(tokenName: String) { + print("[⚠️] Night value for token '\(tokenName)' not found, using day value.") + } + + private func resolveNightValue( + tokenName: String, + fallbackValue: String, + tokenValues: TokenValues + ) throws -> String { + guard let nightToken = tokenValues.night.first(where: { $0.name == tokenName }) else { + fallbackWarning(tokenName: tokenName) + return fallbackValue + } + + guard case .color(let nightValue) = nightToken.type else { + fallbackWarning(tokenName: tokenName) + return fallbackValue + } + + return try tokensResolver.resolveHexColorValue(nightValue, tokenValues: tokenValues) + } + + private func resolveNightReference( + tokenName: String, + fallbackRefence: String, + tokenValues: TokenValues + ) -> String { + guard let nightToken = tokenValues.night.first(where: { $0.name == tokenName }) else { + fallbackWarning(tokenName: tokenName) + return fallbackRefence + } + + guard case .color(let nightValue) = nightToken.type else { + fallbackWarning(tokenName: tokenName) + return fallbackRefence + } + + return nightValue + } + + private func makeColorToken( + dayValue: String, + tokenName: String, + tokenValues: TokenValues, + path: [String] + ) throws -> ColorToken { + let dayHexColorValue = try tokensResolver.resolveHexColorValue( + dayValue, + 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 + ) + ), + name: tokenName, + path: path + ) + } + + 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] { + let colors: [ColorToken] = try tokenValues.day.compactMap { (token: TokenValue) in + guard case .color(let dayValue) = token.type else { + return nil + } + + let path = token.name.components(separatedBy: ".") + + guard path[0] != "gradient" else { + return nil + } + + return try makeColorToken( + dayValue: dayValue, + tokenName: token.name, + tokenValues: tokenValues, + path: path + ) + } + + return structure(tokenColors: colors) + } +} diff --git a/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift b/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift index ed23deb..1587e61 100644 --- a/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift +++ b/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift @@ -33,6 +33,7 @@ struct TokensTemplateConfiguration: Decodable { let fontFamilies: Template? let typographies: Template? let boxShadows: Template? + let theme: Template? } extension TokensTemplateConfiguration.Template { diff --git a/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift b/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift index 8e60193..82429bf 100644 --- a/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift +++ b/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift @@ -13,6 +13,7 @@ struct TokensGenerationParameters { let fontFamilyRender: RenderParameters let typographyRender: RenderParameters let boxShadowRender: RenderParameters + let themeRender: RenderParameters } // MARK: - Instance Properties diff --git a/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilFullHexModificator.swift b/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilFullHexModificator.swift index 5ce4fe8..a917892 100644 --- a/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilFullHexModificator.swift +++ b/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilFullHexModificator.swift @@ -28,24 +28,27 @@ final class StencilFullHexModificator: StencilModificator { throw StencilFilterError(code: .invalidValue(input), filter: name) } - let hex = input.uppercased() + let hex = String(input.uppercased().dropFirst()) + let updatedHex: String switch hex.count { case .rgb: - return Array(hex) + updatedHex = Array(hex) .map { "\($0)\($0)" } .joined() .appendingHexAlpha(to: hexPosition) case .rrggbb: - return hex.appendingHexAlpha(to: hexPosition) + updatedHex = hex.appendingHexAlpha(to: hexPosition) case .rrggbbaa: - return hex + updatedHex = hex default: throw StencilFilterError(code: .invalidValue(input), filter: name) } + + return updatedHex.prepending("#") } } @@ -53,9 +56,9 @@ extension Int { // MARK: - Type Properties - fileprivate static let rgb = 4 - fileprivate static let rrggbb = 7 - fileprivate static let rrggbbaa = 9 + fileprivate static let rgb = 3 + fileprivate static let rrggbb = 6 + fileprivate static let rrggbbaa = 8 } extension String { diff --git a/Templates/ColorTokens.stencil b/Templates/ColorTokens.stencil index 0693972..eb9359d 100644 --- a/Templates/ColorTokens.stencil +++ b/Templates/ColorTokens.stencil @@ -1,5 +1,5 @@ {% include "FileHeader.stencil" %} -{% if colorTokens %} +{% if colors %} {% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} {% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} {% set tokenTypeName %}{{ options.tokenTypeName|default:"ColorTokens" }}{% endset %} @@ -31,7 +31,7 @@ import AppKit #endif {{ accessModifier }} struct {{ tokenTypeName }} { - {% call recursiveBlock colorTokens %} + {% call recursiveBlock colors %} } {% else %} // No color tokens found From 44ec06b619fc7d79a120b6ccb21a849fc67707ae Mon Sep 17 00:00:00 2001 From: Timur Shafigullin Date: Fri, 28 Jul 2023 19:22:27 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=93=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=82=D0=B5=D0=BC=D1=8B=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=BC=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Demo/.figmagen.yml | 5 + Demo/FigmaGenDemo.xcodeproj/project.pbxproj | 4 + Demo/FigmaGenDemo/Generated/Theme.swift | 131 ++++++++++++++++++++ Templates/Theme.stencil | 118 ++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 Demo/FigmaGenDemo/Generated/Theme.swift create mode 100644 Templates/Theme.stencil diff --git a/Demo/.figmagen.yml b/Demo/.figmagen.yml index 14f2675..66d8b76 100644 --- a/Demo/.figmagen.yml +++ b/Demo/.figmagen.yml @@ -56,3 +56,8 @@ tokens: templateOptions: publicAccess: true shadowTypeName: ShadowToken + theme: + destination: FigmaGenDemo/Generated/Theme.swift + templateOptions: + publicAccess: true + shadowTypeName: ShadowToken diff --git a/Demo/FigmaGenDemo.xcodeproj/project.pbxproj b/Demo/FigmaGenDemo.xcodeproj/project.pbxproj index d8dccca..d914ed4 100644 --- a/Demo/FigmaGenDemo.xcodeproj/project.pbxproj +++ b/Demo/FigmaGenDemo.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 8E5334162A420D9B006D6569 /* SF-Pro-Display-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8E5334062A420D9B006D6569 /* SF-Pro-Display-Semibold.otf */; }; 8E5334172A420D9B006D6569 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5334072A420D9B006D6569 /* AppDelegate.swift */; }; 8E53341C2A420DA4006D6569 /* FigmaGenDemoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E53341A2A420DA4006D6569 /* FigmaGenDemoTests.swift */; }; + 8E97DB762A741FD600E8AAC1 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E97DB752A741FD600E8AAC1 /* Theme.swift */; }; 8EAE283E2A716D4A007C477F /* BoxShadowTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EAE283D2A716D4A007C477F /* BoxShadowTokens.swift */; }; 8EFF17F52A70096E00C47577 /* FontFamilyTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EFF17F32A70096D00C47577 /* FontFamilyTokens.swift */; }; 8EFF17F62A70096E00C47577 /* TypographyTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EFF17F42A70096D00C47577 /* TypographyTokens.swift */; }; @@ -82,6 +83,7 @@ 8E5334082A420D9B006D6569 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8E53341A2A420DA4006D6569 /* FigmaGenDemoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaGenDemoTests.swift; sourceTree = ""; }; 8E53341B2A420DA4006D6569 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8E97DB752A741FD600E8AAC1 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 8EAE283D2A716D4A007C477F /* BoxShadowTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxShadowTokens.swift; sourceTree = ""; }; 8EFF17F32A70096D00C47577 /* FontFamilyTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontFamilyTokens.swift; sourceTree = ""; }; 8EFF17F42A70096D00C47577 /* TypographyTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypographyTokens.swift; sourceTree = ""; }; @@ -152,6 +154,7 @@ 8E5333F62A420D9B006D6569 /* Images.swift */, 8E5333F42A420D9B006D6569 /* ShadowStyle.swift */, 8E5333F52A420D9B006D6569 /* TextStyle.swift */, + 8E97DB752A741FD600E8AAC1 /* Theme.swift */, 8EFF17F42A70096D00C47577 /* TypographyTokens.swift */, ); path = Generated; @@ -403,6 +406,7 @@ 8E53340A2A420D9B006D6569 /* TextStyle.swift in Sources */, 8E53340B2A420D9B006D6569 /* Images.swift in Sources */, 8E5334092A420D9B006D6569 /* ShadowStyle.swift in Sources */, + 8E97DB762A741FD600E8AAC1 /* Theme.swift in Sources */, 8E53340C2A420D9B006D6569 /* ColorStyle.swift in Sources */, 8E44BFD52A67ECB300EE5D7E /* ColorTokens.swift in Sources */, ); diff --git a/Demo/FigmaGenDemo/Generated/Theme.swift b/Demo/FigmaGenDemo/Generated/Theme.swift new file mode 100644 index 0000000..b5b9966 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/Theme.swift @@ -0,0 +1,131 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct Theme { + + public let colors: ColorTokens + public let shadows: BoxShadowTokens + public let typographies: TypographyTokens + + init( + colors: ColorTokens, + shadows: BoxShadowTokens, + typographies: TypographyTokens = TypographyTokens() + ) { + self.colors = colors + self.shadows = shadows + self.typographies = typographies + } +} + +extension Theme { + + public static let defaultLight = Self( + colors: ColorTokens( + accent: ColorTokens.Accent( + bg: UIColor(hex: 0xC3DAFEFF), + default: UIColor(hex: 0x7F9CF5FF), + onAccent: UIColor(hex: 0xFFFFFFFF) + ), + bg: ColorTokens.Bg( + default: UIColor(hex: 0xFFFFFFFF), + muted: UIColor(hex: 0xF7FAFCFF), + subtle: UIColor(hex: 0xEDF2F7FF) + ), + fg: ColorTokens.Fg( + default: UIColor(hex: 0x000000FF), + muted: UIColor(hex: 0x4A5568FF), + subtle: UIColor(hex: 0xA0AEC0FF) + ), + shadows: ColorTokens.Shadows( + default: UIColor(hex: 0x1A202CFF) + ) + ), + shadows: BoxShadowTokens( + level1: ShadowToken( + offset: CGSize(width: 0, height: 4), + radius: 12, + color: UIColor(hex: 0x7090B029), + opacity: 1.0 + ), + level2: ShadowToken( + offset: CGSize(width: 0, height: 8), + radius: 16, + color: UIColor(hex: 0x7090B03D), + opacity: 1.0 + ), + level3: ShadowToken( + offset: CGSize(width: 0, height: 12), + radius: 24, + color: UIColor(hex: 0x7090B052), + opacity: 1.0 + ) + ) + ) + + public static let defaultDark = Self( + colors: ColorTokens( + accent: ColorTokens.Accent( + bg: UIColor(hex: 0x434190FF), + default: UIColor(hex: 0x5A67D8FF), + onAccent: UIColor(hex: 0xFFFFFFFF) + ), + bg: ColorTokens.Bg( + default: UIColor(hex: 0x1A202CFF), + muted: UIColor(hex: 0x4A5568FF), + subtle: UIColor(hex: 0x718096FF) + ), + fg: ColorTokens.Fg( + default: UIColor(hex: 0xFFFFFFFF), + muted: UIColor(hex: 0xE2E8F0FF), + subtle: UIColor(hex: 0xA0AEC0FF) + ), + shadows: ColorTokens.Shadows( + default: UIColor(hex: 0x00000000) + ) + ), + shadows: BoxShadowTokens( + level1: ShadowToken( + offset: CGSize(width: 0, height: 4), + radius: 12, + color: UIColor(hex: 0x7090B029), + opacity: 1.0 + ), + level2: ShadowToken( + offset: CGSize(width: 0, height: 8), + radius: 16, + color: UIColor(hex: 0x7090B03D), + opacity: 1.0 + ), + level3: ShadowToken( + offset: CGSize(width: 0, height: 12), + radius: 24, + color: UIColor(hex: 0x7090B052), + opacity: 1.0 + ) + ) + ) +} + +private extension UIColor { + + convenience init(hex: UInt32) { + let red = UInt8((hex >> 24) & 0xFF) + let green = UInt8((hex >> 16) & 0xFF) + let blue = UInt8((hex >> 8) & 0xFF) + let alpha = UInt8(hex & 0xFF) + + self.init( + red: CGFloat(red) / 255.0, + green: CGFloat(green) / 255.0, + blue: CGFloat(blue) / 255.0, + alpha: CGFloat(alpha) / 255.0 + ) + } +} diff --git a/Templates/Theme.stencil b/Templates/Theme.stencil new file mode 100644 index 0000000..5362449 --- /dev/null +++ b/Templates/Theme.stencil @@ -0,0 +1,118 @@ +{% include "FileHeader.stencil" %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set themeTypeName %}{{ options.styleTypeName|default:"Theme" }}{% endset %} +{% set colorTokensTypeName %}{{ options.tokenTypeName|default:"ColorTokens" }}{% endset %} +{% set boxShadowTokensTypeName %}{{ options.tokenTypeName|default:"BoxShadowTokens" }}{% endset %} +{% set typographyTokensTypeName %}{{ options.tokenTypeName|default:"TypographyTokens" }}{% endset %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% set shadowTypeName %}{{ options.shadowTypeName|default:"Shadow" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord }}{% endmacro %} +{% macro typeName name %}{{ name|swiftIdentifier:"pretty"|upperFirstLetter|escapeReservedKeywords }}{% endmacro %} +{% macro hexColor color %}{{ color|fullHex|uppercase|replace:"#","0x" }}{% endmacro %} +{% macro argumentsBlock item theme parentTypeName %} +{% for color in item.colors %} +{% call propertyName color.path.last %}: {{ colorTypeName }}(hex: {% call hexColor color[theme].value %}){% if not forloop.last %},{% endif %} +{% endfor %} +{% for child in item.children %} +{% set childTypeName %}{% if parentTypeName %}{{ parentTypeName }}.{% endif %}{% call typeName child.name %}{% endset %} +{% call propertyName child.name %}: {{ childTypeName }}( + {% filter indent:4 %} + {% call argumentsBlock child theme childTypeName %} + {% endfilter %} +){% if not forloop.last %},{% endif %} +{% endfor %} +{% endmacro %} +{% macro colorsArgumentsBlock colors theme %} +{% if colors %} +colors: {{ colorTokensTypeName }}( + {% filter indent:4 %} + {% call argumentsBlock colors theme colorTokensTypeName %} + {% endfilter %} +), +{% endif %} +{% endmacro %} +{% macro shadowsArgumentsBlock boxShadows theme %} +{% if boxShadows %} +shadows: {{ boxShadowTokensTypeName }}( +{% for boxShadow in boxShadows %} + {% call propertyName boxShadow.path.last %}: {{ shadowTypeName }}( + offset: CGSize(width: {{ boxShadow[theme].x }}, height: {{ boxShadow[theme].y }}), + radius: {{ boxShadow[theme].blur }}, + color: {{ colorTypeName }}(hex: {% call hexColor boxShadow[theme].color %}), + opacity: 1.0 + ){% if not forloop.last %},{% endif %} +{% endfor %} +) +{% endif %} +{% endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ themeTypeName }} { + + {% if colors %} + {{ accessModifier }} let colors: {{ colorTokensTypeName }} + {% endif %} + {% if boxShadows %} + {{ accessModifier }} let shadows: {{ boxShadowTokensTypeName }} + {% endif %} + {{ accessModifier }} let typographies: {{ typographyTokensTypeName }} + + init( + {% if colors %} + colors: {{ colorTokensTypeName }}, + {% endif %} + {% if boxShadows %} + shadows: {{ boxShadowTokensTypeName }}, + {% endif %} + typographies: {{ typographyTokensTypeName }} = {{ typographyTokensTypeName }}() + ) { + {% if colors %} + self.colors = colors + {% endif %} + {% if boxShadows %} + self.shadows = shadows + {% endif %} + self.typographies = typographies + } +} + +extension {{ themeTypeName }} { + + {{ accessModifier }} static let defaultLight = Self( + {% filter indent:8 %} + {% call colorsArgumentsBlock colors "dayTheme" %} + {% call shadowsArgumentsBlock boxShadows "dayTheme" %} + {% endfilter %} + ) + + {{ accessModifier }} static let defaultDark = Self( + {% filter indent:8 %} + {% call colorsArgumentsBlock colors "nightTheme" %} + {% call shadowsArgumentsBlock boxShadows "nightTheme" %} + {% endfilter %} + ) +} + +{% if colors or boxShadows %} +private extension {{ colorTypeName }} { + + convenience init(hex: UInt32) { + let red = UInt8((hex >> 24) & 0xFF) + let green = UInt8((hex >> 16) & 0xFF) + let blue = UInt8((hex >> 8) & 0xFF) + let alpha = UInt8(hex & 0xFF) + + self.init( + red: CGFloat(red) / 255.0, + green: CGFloat(green) / 255.0, + blue: CGFloat(blue) / 255.0, + alpha: CGFloat(alpha) / 255.0 + ) + } +} +{% endif %}