diff --git a/Docs/Choose_and_Edit_Scheme.png b/Docs/Choose_and_Edit_Scheme.png new file mode 100644 index 0000000..57e515a Binary files /dev/null and b/Docs/Choose_and_Edit_Scheme.png differ diff --git a/Docs/Generate_from_config.png b/Docs/Generate_from_config.png new file mode 100644 index 0000000..e2d4149 Binary files /dev/null and b/Docs/Generate_from_config.png differ diff --git a/Docs/Generate_tokens_from_Figma.png b/Docs/Generate_tokens_from_Figma.png new file mode 100644 index 0000000..db55ed9 Binary files /dev/null and b/Docs/Generate_tokens_from_Figma.png differ diff --git a/Docs/Generate_tokens_from_GitHub_file.png b/Docs/Generate_tokens_from_GitHub_file.png new file mode 100644 index 0000000..7f3fa60 Binary files /dev/null and b/Docs/Generate_tokens_from_GitHub_file.png differ diff --git a/Package.resolved b/Package.resolved index 9149f58..0d278ad 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,6 +18,15 @@ "version" : "0.13.7" } }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, { "identity" : "komondor", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 19ce1fe..21da2cd 100755 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,8 @@ let package = Package( .package(url: "https://github.com/SwiftGen/StencilSwiftKit.git", from: "2.7.2"), .package(url: "https://github.com/mxcl/PromiseKit.git", from: "8.0.0"), .package(url: "https://github.com/almazrafi/DictionaryCoder.git", from: "1.0.0"), - .package(url: "https://github.com/nicklockwood/Expression.git", from: "0.13.0") + .package(url: "https://github.com/nicklockwood/Expression.git", from: "0.13.0"), + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2") ], targets: [ .executableTarget( @@ -34,7 +35,12 @@ let package = Package( "PromiseKit", "DictionaryCoder", "FigmaGenTools", - "Expression" + "Expression", + .product( + name: "KeychainAccess", + package: "KeychainAccess", + condition: .when(platforms: [.macOS]) + ) ], path: "Sources/FigmaGen" ), @@ -43,7 +49,12 @@ let package = Package( dependencies: [ "SwiftCLI", "PathKit", - "PromiseKit" + "PromiseKit", + .product( + name: "KeychainAccess", + package: "KeychainAccess", + condition: .when(platforms: [.macOS]) + ) ], path: "Sources/FigmaGenTools" ), diff --git a/README.md b/README.md index 7f0ea89..e1906b4 100755 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ Currently, FigmaGen supports the following entities: - [Text styles](#text-styles) - [Shadow styles](#shadow-styles) - [Images](#images) +- [Tokens](#tokens) +- [Working on FigmaGen](#Working-on-FigmaGen) - [Communication](#communication) - [License](#license) @@ -491,6 +493,113 @@ Key | Type | Default value | Description `imageTypeName` | String | UIImage | Image type name. If the target platform is macOS, specify `NSImage`. `publicAccess` | Boolean | false | Adds `public` access modifier to the declarations in the generated file. +## Tokens + +Configuration sections for generationg Tokens: +- `accessToken`: an access token string that is needed to execute Figma API requests (see [Figma access token](#figma-access-token)). +- `file`: URL of a Figma file, which data will be used to generate code (see [Figma file](#figma-file)). +- `remoteRepoConfig`: parameters that will be used to generate tokens from remote file (see [Remote repository parameters](#remote-repository-parameters)) +- `templates`: parameters of styles for generation (see [Parameters of styles](#parameters-of-styles-for-generation)) + +### Token parameters +Sample configuration: +```yaml +tokens: + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo + remoteRepoConfig: { [remote repository parameters](#remote-repository-parameters) } + templates: + colors: + { standard configuration set} + baseColors: + { standard configuration set } + fontFamilies: + { standard configuration set } + typographies: + { standard configuration set } + boxShadows: + { standard configuration set } + spacing: + { standard configuration set } + theme: + { standard configuration set } +``` + +### Parameters of styles for generation + +Sample configuration for color styles: +```yaml + colors: + destination: ./Generated/ColorTokens.swift + templateOptions: + publicAccess: true +``` + +### Remote repository parameters + +Remote repository section: +- `owner`: owner of the remote repository +- `repo`: name of the remote repository +- `branch`: name of the branch in remote repository +- `filePath`: file name or path to the file in remote repository +- `accessToken`: an access token string or object that is needed to execute GitHub API requests + +Remote repository parameters structure: +```yaml + remoteRepoConfig: + owner: { your repository owner } + repo: { your repository name } + branch: { your repository branch } + filePath: { your repository file path } + accessToken: { your GitHub access token } + # OR + accessToken: + env: { your repository environment } + keychain: + service: { service name } + key: { key name } +``` + +Sample configuration for remote repository parameters with string `accessToken`: +```yaml + remoteRepoConfig: + owner: hhru + repo: FigmaGen + branch: master + filePath: tokens.json + accessToken: ghp_259614ac9fbc93bd84c43bbe2bt53m0TaM4A +``` + +Sample configuration for remote repository parameters with object `accessToken`: +```yaml + remoteRepoConfig: + owner: hhru + repo: FigmaGen + branch: master + filePath: tokens.json + accessToken: + env: GITHUB_API + keychain: + service: GitHub Token + key: hh +``` + +## Working on FigmaGen + +To work on FigmaGen you need to open the `Package.swift` file, select the `figmagen` scheme and edit the scheme + +#### +![](Docs/Choose_and_Edit_Scheme.png) + +#### Arguments for generating tokens with FigmaGen from file configurations +![](Docs/Generate_from_config.png) + +#### Arguments for generating tokens with FigmaGen using a Figma file +![](Docs/Generate_tokens_from_Figma.png) + +#### Arguments for generating tokens with FigmaGen using a Github file +![](Docs/Generate_tokens_from_GitHub_file.png) + --- ## Communication diff --git a/Sources/FigmaGen/Commands/GenerationConfigurableCommand.swift b/Sources/FigmaGen/Commands/GenerationConfigurableCommand.swift index ba0eb43..e4f3930 100644 --- a/Sources/FigmaGen/Commands/GenerationConfigurableCommand.swift +++ b/Sources/FigmaGen/Commands/GenerationConfigurableCommand.swift @@ -59,7 +59,7 @@ extension GenerationConfigurableCommand { return nil } - return .value(accessToken) + return AccessTokenConfiguration(value: accessToken) } private func resolveTemplateOptions() -> [String: Any] { diff --git a/Sources/FigmaGen/Commands/TokensCommand.swift b/Sources/FigmaGen/Commands/TokensCommand.swift index 72fd130..d2ba276 100644 --- a/Sources/FigmaGen/Commands/TokensCommand.swift +++ b/Sources/FigmaGen/Commands/TokensCommand.swift @@ -25,6 +25,41 @@ final class TokensCommand: AsyncExecutableCommand { """ ) + let remoteFileOwnerKey = Key( + "--remoteFileOwner", + description: """ + Remote Repo owner key to generate text styles from. + """ + ) + + let remoteFileRepoKey = Key( + "--remoteFileRepo", + description: """ + Remote Repo key to generate text styles from. + """ + ) + + let remoteFileBranchKey = Key( + "--remoteFileBranch", + description: """ + Remote Repo branch to generate text styles from. + """ + ) + + let remoteFilePathKey = Key( + "--remoteFilePath", + description: """ + Remote Repo file key to generate text styles from. + """ + ) + + let remoteRepoAccessTokenKey = Key( + "--remoteRepoAccessToken", + description: """ + Remote Repo personal access token to make requests to the GitHub. + """ + ) + let accessToken = Key( "--accessToken", description: """ @@ -228,6 +263,7 @@ extension TokensCommand { var configuration: TokensConfiguration { TokensConfiguration( file: resolveFileConfiguration(), + remoteRepoConfig: resolveRemoteRepoConfiguration(), accessToken: resolveAccessTokenConfiguration(), templates: TokensTemplateConfiguration( colors: [ @@ -298,12 +334,32 @@ extension TokensCommand { ) } + private func resolveRemoteRepoConfiguration() -> RemoteRepoConfiguration? { + guard + let fileOwner = remoteFileOwnerKey.value, + let fileRepo = remoteFileRepoKey.value, + let fileBranch = remoteFileBranchKey.value, + let filePath = remoteFilePathKey.value, + let remoteRepoAccessToken = remoteRepoAccessTokenKey.value + else { + return nil + } + + return RemoteRepoConfiguration( + owner: fileOwner, + repo: fileRepo, + branch: fileBranch, + filePath: filePath, + accessToken: AccessTokenConfiguration(value: remoteRepoAccessToken) + ) + } + private func resolveAccessTokenConfiguration() -> AccessTokenConfiguration? { guard let accessToken = accessToken.value else { return nil } - return .value(accessToken) + return AccessTokenConfiguration(value: accessToken) } private func resolveTemplateOptions(_ templateOptionsValues: [String]) -> [String: Any] { diff --git a/Sources/FigmaGen/Dependencies.swift b/Sources/FigmaGen/Dependencies.swift index b75800f..502aea2 100644 --- a/Sources/FigmaGen/Dependencies.swift +++ b/Sources/FigmaGen/Dependencies.swift @@ -7,6 +7,9 @@ enum Dependencies { static let dataProvider: DataProvider = DefaultDataProvider() + static let gitHubHTTPService: GitHubHTTPService = HTTPService() + static let gitHubAPIProvider: RemoteRepoProvider = GitHubAPIProvider(httpService: gitHubHTTPService) + static let figmaHTTPService: FigmaHTTPService = HTTPService() static let figmaAPIProvider: FigmaAPIProvider = DefaultFigmaAPIProvider(httpService: figmaHTTPService) @@ -58,7 +61,8 @@ enum Dependencies { ) static let tokensProvider: TokensProvider = DefaultTokensProvider( - apiProvider: figmaAPIProvider + figmaApiProvider: figmaAPIProvider, + gitHubApiProvider: gitHubAPIProvider ) // MARK: - diff --git a/Sources/FigmaGen/Generators/GenerationParametersError.swift b/Sources/FigmaGen/Generators/GenerationParametersError.swift index 43bcb17..5c4eb44 100644 --- a/Sources/FigmaGen/Generators/GenerationParametersError.swift +++ b/Sources/FigmaGen/Generators/GenerationParametersError.swift @@ -6,6 +6,7 @@ enum GenerationParametersError: Error, CustomStringConvertible { case invalidFileConfiguration case invalidAccessToken + case invalidGitHubAccessToken // MARK: - Instance Properties @@ -16,6 +17,9 @@ enum GenerationParametersError: Error, CustomStringConvertible { case .invalidAccessToken: return "Figma access token cannot be empty or nil" + + case .invalidGitHubAccessToken: + return "GitHiub access token cannot be empty or nil" } } } diff --git a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift index 6e43976..6f6f45c 100644 --- a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift +++ b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift @@ -43,7 +43,7 @@ final class DefaultTokensGenerator: TokensGenerator { // MARK: - Instance Methods private func generate(parameters: TokensGenerationParameters) async throws { - let tokenValues = try await tokensProvider.fetchTokens(from: parameters.file) + let tokenValues = try await fetchTokens(from: parameters) try generateColorsTokens(parameters: parameters, tokenValues: tokenValues) try generateBaseColorsTokens(parameters: parameters, tokenValues: tokenValues) @@ -54,6 +54,16 @@ final class DefaultTokensGenerator: TokensGenerator { try generateSpacingTokens(parameters: parameters, tokenValues: tokenValues) } + private func fetchTokens(from parameters: TokensGenerationParameters) async throws -> TokenValues { + if let file = parameters.file { + return try await tokensProvider.fetchTokens(from: file) + } else if let remoteFile = parameters.remoteFile { + return try await tokensProvider.fetchTokens(from: remoteFile) + } else { + throw GenerationParametersError.invalidFileConfiguration + } + } + private func generateSpacingTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { try generateTokens( spacingTokensGenerator, diff --git a/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift index 9646f74..5f4c646 100644 --- a/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift +++ b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift @@ -21,19 +21,38 @@ final class DefaultTokensGenerationParametersResolver: TokensGenerationParameter // swiftlint:disable:next function_body_length func resolveGenerationParameters(from configuration: TokensConfiguration) throws -> TokensGenerationParameters { - guard let fileConfiguration = configuration.file else { - throw GenerationParametersError.invalidFileConfiguration + + let file = try configuration.file.map { fileConfiguration in + guard let accessToken = accessTokenResolver.resolveAccessToken(from: configuration.accessToken) else { + throw GenerationParametersError.invalidAccessToken + } + + return FileParameters( + key: fileConfiguration.key, + version: fileConfiguration.version, + accessToken: accessToken + ) } - guard let accessToken = accessTokenResolver.resolveAccessToken(from: configuration.accessToken) else { - throw GenerationParametersError.invalidAccessToken + let remoteFile = try configuration.remoteRepoConfig.map { remoteFileConfiguration in + guard + let accessToken = accessTokenResolver.resolveAccessToken(from: remoteFileConfiguration.accessToken) + else { + throw GenerationParametersError.invalidGitHubAccessToken + } + + return RemoteFileParameters( + owner: remoteFileConfiguration.owner, + repo: remoteFileConfiguration.repo, + branch: remoteFileConfiguration.branch, + filePath: remoteFileConfiguration.filePath, + accessToken: accessToken + ) } - let file = FileParameters( - key: fileConfiguration.key, - version: fileConfiguration.version, - accessToken: accessToken - ) + if file.isNil && remoteFile.isNil { + throw GenerationParametersError.invalidFileConfiguration + } let colorRenderParameters = renderParametersResolver.resolveRenderParameters( templates: configuration.templates?.colors, @@ -72,6 +91,7 @@ final class DefaultTokensGenerationParametersResolver: TokensGenerationParameter return TokensGenerationParameters( file: file, + remoteFile: remoteFile, tokens: TokensGenerationParameters.TokensParameters( colorRenderParameters: colorRenderParameters, baseColorRenderParameters: baseColorRenderParameters, diff --git a/Sources/FigmaGen/Models/Configuration/AccessTokenConfiguration.swift b/Sources/FigmaGen/Models/Configuration/AccessTokenConfiguration.swift index b3681c0..ae1e8f8 100644 --- a/Sources/FigmaGen/Models/Configuration/AccessTokenConfiguration.swift +++ b/Sources/FigmaGen/Models/Configuration/AccessTokenConfiguration.swift @@ -1,25 +1,51 @@ import Foundation -enum AccessTokenConfiguration: Decodable { +struct AccessTokenConfiguration: Decodable { // MARK: - Nested Types private enum CodingKeys: String, CodingKey { case environmentVariable = "env" + case keychain + } + + // MARK: - + + struct KeychainParameters: Decodable, Equatable { + + // MARK: - Instance Properties + + let service: String + let key: String } // MARK: - Enumeration Cases - case environmentVariable(String) - case value(String) + let value: String? + let environmentVariable: String? + let keychainParameters: KeychainParameters? // MARK: - Initializers init(from decoder: Decoder) throws { if let container = try? decoder.container(keyedBy: CodingKeys.self) { - self = .environmentVariable(try container.decode(String.self, forKey: .environmentVariable)) + self.value = nil + self.environmentVariable = try container.decodeIfPresent(forKey: .environmentVariable) + self.keychainParameters = try container.decodeIfPresent(forKey: .keychain) } else { - self = .value(try String(from: decoder)) + self.value = try String(from: decoder) + self.environmentVariable = nil + self.keychainParameters = nil } } + + init( + value: String? = nil, + environmentVariable: String? = nil, + keychainParameters: KeychainParameters? = nil + ) { + self.value = value + self.environmentVariable = environmentVariable + self.keychainParameters = keychainParameters + } } diff --git a/Sources/FigmaGen/Models/Configuration/AccessTokenError.swift b/Sources/FigmaGen/Models/Configuration/AccessTokenError.swift new file mode 100644 index 0000000..f80f9bf --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/AccessTokenError.swift @@ -0,0 +1,17 @@ +import Foundation + +enum AccessTokenError: Error, CustomStringConvertible { + + // MARK: - Instance Properties + + case failedCreateAccessToken + + // MARK: - CustomStringConvertible + + var description: String { + switch self { + case .failedCreateAccessToken: + return "Failed to create access token" + } + } +} diff --git a/Sources/FigmaGen/Models/Configuration/RemoteRepoConfiguration.swift b/Sources/FigmaGen/Models/Configuration/RemoteRepoConfiguration.swift new file mode 100644 index 0000000..fbf6887 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/RemoteRepoConfiguration.swift @@ -0,0 +1,10 @@ +import Foundation + +struct RemoteRepoConfiguration: Decodable { + + let owner: String + let repo: String + let branch: String + let filePath: String + let accessToken: AccessTokenConfiguration? +} diff --git a/Sources/FigmaGen/Models/Configuration/Tokens/TokensConfiguration.swift b/Sources/FigmaGen/Models/Configuration/Tokens/TokensConfiguration.swift index aefe0ac..d8c64a6 100644 --- a/Sources/FigmaGen/Models/Configuration/Tokens/TokensConfiguration.swift +++ b/Sources/FigmaGen/Models/Configuration/Tokens/TokensConfiguration.swift @@ -6,11 +6,13 @@ struct TokensConfiguration: Decodable { private enum CodingKeys: String, CodingKey { case templates + case remoteRepoConfig } // MARK: - Instance Properties let file: FileConfiguration? + let remoteRepoConfig: RemoteRepoConfiguration? let accessToken: AccessTokenConfiguration? let templates: TokensTemplateConfiguration? @@ -24,15 +26,18 @@ struct TokensConfiguration: Decodable { let container = try decoder.container(keyedBy: CodingKeys.self) + self.remoteRepoConfig = try container.decodeIfPresent(forKey: .remoteRepoConfig) self.templates = try container.decodeIfPresent(forKey: .templates) } init( file: FileConfiguration?, + remoteRepoConfig: RemoteRepoConfiguration?, accessToken: AccessTokenConfiguration?, templates: TokensTemplateConfiguration? ) { self.file = file + self.remoteRepoConfig = remoteRepoConfig self.accessToken = accessToken self.templates = templates } @@ -46,6 +51,7 @@ struct TokensConfiguration: Decodable { return Self( file: file ?? base.file, + remoteRepoConfig: remoteRepoConfig, accessToken: accessToken ?? base.accessToken, templates: templates ) diff --git a/Sources/FigmaGen/Models/Parameters/RemoteFileParameters.swift b/Sources/FigmaGen/Models/Parameters/RemoteFileParameters.swift new file mode 100644 index 0000000..f9872ab --- /dev/null +++ b/Sources/FigmaGen/Models/Parameters/RemoteFileParameters.swift @@ -0,0 +1,23 @@ +import Foundation + +struct RemoteFileParameters { + let owner: String + let repo: String + let branch: String + let filePath: String + let accessToken: String + + init( + owner: String, + repo: String, + branch: String, + filePath: String, + accessToken: String + ) { + self.owner = owner + self.repo = repo + self.branch = branch + self.filePath = filePath + self.accessToken = accessToken + } +} diff --git a/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift b/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift index 0ea77e6..224cef9 100644 --- a/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift +++ b/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift @@ -19,6 +19,7 @@ struct TokensGenerationParameters { // MARK: - Instance Properties - let file: FileParameters + let file: FileParameters? + let remoteFile: RemoteFileParameters? let tokens: TokensParameters } diff --git a/Sources/FigmaGen/Models/Token/TokenBorderValue.swift b/Sources/FigmaGen/Models/Token/TokenBorderValue.swift new file mode 100644 index 0000000..f689c33 --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenBorderValue.swift @@ -0,0 +1,9 @@ +import Foundation + +struct TokenBorderValue: Codable, Hashable { + + // MARK: - Instance Properties + + let width: String + let style: String +} diff --git a/Sources/FigmaGen/Models/Token/TokenValue.swift b/Sources/FigmaGen/Models/Token/TokenValue.swift index a2914a4..4d0985a 100644 --- a/Sources/FigmaGen/Models/Token/TokenValue.swift +++ b/Sources/FigmaGen/Models/Token/TokenValue.swift @@ -17,7 +17,9 @@ extension TokenValue: Decodable { private enum RawType: String, Decodable { case a11yScales case animation + case border case borderRadius + case borderWidth case boxShadow case color case core @@ -35,6 +37,7 @@ extension TokenValue: Decodable { case textCase case textDecoration case typography + case unknown } @@ -62,9 +65,15 @@ extension TokenValue: Decodable { case .animation: self.type = .animation(value: try container.decode(forKey: .value)) + case .border: + self.type = .border(value: try container.decode(forKey: .value)) + case .borderRadius: self.type = .borderRadius(value: try container.decode(forKey: .value)) + case .borderWidth: + self.type = .borderWidth(value: try container.decode(forKey: .value)) + case .boxShadow: self.type = .boxShadow(value: try container.decode(forKey: .value)) @@ -141,9 +150,15 @@ extension TokenValue: Encodable { case let .animation(value): try container.encode(value, forKey: .value) + case let .border(value): + try container.encode(value, forKey: .value) + case let .borderRadius(value): try container.encode(value, forKey: .value) + case let .borderWidth(value): + try container.encode(value, forKey: .value) + case let .boxShadow(value): try container.encode(value, forKey: .value) diff --git a/Sources/FigmaGen/Models/Token/TokenValueType.swift b/Sources/FigmaGen/Models/Token/TokenValueType.swift index 13329e5..b7f5322 100644 --- a/Sources/FigmaGen/Models/Token/TokenValueType.swift +++ b/Sources/FigmaGen/Models/Token/TokenValueType.swift @@ -6,7 +6,9 @@ enum TokenValueType: Hashable { case a11yScales(value: String) case animation(value: TokenAnimationValue) + case border(value: TokenBorderValue) case borderRadius(value: String) + case borderWidth(value: String) case boxShadow(value: TokenBoxShadowValue) case color(value: String) case core(value: String) @@ -24,6 +26,7 @@ enum TokenValueType: Hashable { case textCase(value: String) case textDecoration(value: String) case typography(value: TokenTypographyValue) + case unknown // MARK: - Instance Properties @@ -31,6 +34,7 @@ enum TokenValueType: Hashable { var stringValue: String? { switch self { case let .a11yScales(value), + let .borderWidth(value), let .borderRadius(value), let .color(value), let .core(value), @@ -49,7 +53,7 @@ enum TokenValueType: Hashable { let .textDecoration(value): return value - case .animation, .boxShadow, .typography, .unknown: + case .animation, .boxShadow, .typography, .unknown, .border: return nil } } diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyCodable+extension.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyCodable+extension.swift new file mode 100644 index 0000000..b020aa7 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyCodable+extension.swift @@ -0,0 +1,42 @@ +import Foundation +import FigmaGenTools + +extension AnyCodable { + + func getAllGitHubTokenValues() -> [GitHubTokenValue] { + var gitHubTokenValues: [GitHubTokenValue] = [] + + guard let dictionary = self.value as? [String: Any] else { + return gitHubTokenValues + } + + guard let data = try? JSONEncoder().encode(self) else { + return [] + } + + if let value = try? JSONDecoder().decode(GitHubTokenValue.self, from: data) { + return [value] + } else if let values = try? JSONDecoder().decode([String: GitHubTokenValue].self, from: data) { + let valuesResult = values.compactMap { key, value in + let name = value.name ?? key + return value.copyWith(name: name) + } + gitHubTokenValues.append(contentsOf: valuesResult) + } else { + let results = dictionary.map { key, value in + let tokenValues = AnyCodable(value).getAllGitHubTokenValues() + return tokenValues.map { value in + var name = key + if let valueName = value.name { + name += ".\(valueName)" + } + return value.copyWith(name: name) + } + } + + gitHubTokenValues.append(contentsOf: results.flatMap { $0 }) + } + + return gitHubTokenValues + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyKey.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyKey.swift new file mode 100644 index 0000000..0c67543 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyKey.swift @@ -0,0 +1,28 @@ +import Foundation + +struct AnyKey: CodingKey { + + enum Errors: Error { + case invalidKeyName + } + + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = Int(stringValue) + } + + init?(intValue: Int) { + self.intValue = intValue + stringValue = "\(intValue)" + } + + static func key(named name: String) throws -> Self { + guard let key = Self(stringValue: name) else { + throw Errors.invalidKeyName + } + return key + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubFile.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubFile.swift new file mode 100644 index 0000000..bcfb623 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubFile.swift @@ -0,0 +1,46 @@ +import Foundation +import FigmaGenTools + +struct GitHubFile: Codable, Hashable { + + let tokenValues: [String: [GitHubTokenValue]] + + enum CodingKeys: String, CodingKey { + case results + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: AnyKey.self) + + tokenValues = container.allKeys + .map { key -> (key: AnyKey, values: [GitHubTokenValue]?) in + let parametersForKey = try? container.decodeIfPresent([String: AnyCodable].self, forKey: key) + let tokenValues = parametersForKey? + .map { key, value in + let result: [GitHubTokenValue] = value + .getAllGitHubTokenValues() + .map { githubValue in + var name = key + if let valueName = githubValue.name { + name += ".\(valueName)" + } + return githubValue.copyWith(name: name) + } + .sorted(by: { $0.name ?? "" < $1.name ?? "" }) + return result + } + .flatMap { $0 } + + return (key: key, values: tokenValues) + } + .reduce(into: [String: [GitHubTokenValue]]()) { partialResult, mapResult in + partialResult[mapResult.key.stringValue] = mapResult.values + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(tokenValues) + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubTokenValue.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubTokenValue.swift new file mode 100644 index 0000000..003a769 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubTokenValue.swift @@ -0,0 +1,228 @@ +import Foundation + +struct GitHubTokenValue: Hashable { + + // MARK: - Instance Properties + + let name: String? + let type: TokenValueType + let typeName: String + + func copyWith(name: String? = nil, type: TokenValueType? = nil, typeName: String? = nil) -> Self { + Self( + name: name ?? self.name, + type: type ?? self.type, + typeName: typeName ?? self.typeName + ) + } +} + +// MARK: - Decodable + +extension GitHubTokenValue: Decodable { + + // MARK: - Nested Types + + private enum RawType: String, Decodable { + + case a11yScales + case animation + case border + case borderRadius + case borderWidth + case boxShadow + case color + case core + case dimension + case fontFamilies + case fontSizes + case fontWeights + case letterSpacing + case lineHeights + case opacity + case paragraphSpacing + case scaling + case sizing + case spacing + case textCase + case textDecoration + case typography + + case unknown + } + + fileprivate enum CodingKeys: String, CodingKey { + case type + case name + case value + } + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + // swiftlint:disable:previous function_body_length + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.name = try container.decodeIfPresent(forKey: .name) + + self.typeName = try container.decode(String.self, forKey: .type) + let rawType = RawType(rawValue: typeName) ?? .unknown + + switch rawType { + case .a11yScales: + self.type = .a11yScales(value: try container.decode(forKey: .value)) + + case .animation: + self.type = .animation(value: try container.decode(forKey: .value)) + + case .borderRadius: + self.type = .borderRadius(value: try container.decode(forKey: .value)) + + case .boxShadow: + self.type = .boxShadow(value: try container.decode(forKey: .value)) + + case .color: + self.type = .color(value: try container.decode(forKey: .value)) + + case .core: + self.type = .core(value: try container.decode(forKey: .value)) + + case .dimension: + self.type = .dimension(value: try container.decode(forKey: .value)) + + case .fontFamilies: + self.type = .fontFamilies(value: try container.decode(forKey: .value)) + + case .fontSizes: + self.type = .fontSizes(value: try container.decode(forKey: .value)) + + case .fontWeights: + self.type = .fontWeights(value: try container.decode(forKey: .value)) + + case .letterSpacing: + self.type = .letterSpacing(value: try container.decode(forKey: .value)) + + case .lineHeights: + self.type = .lineHeights(value: try container.decode(forKey: .value)) + + case .opacity: + self.type = .opacity(value: try container.decode(forKey: .value)) + + case .paragraphSpacing: + self.type = .paragraphSpacing(value: try container.decode(forKey: .value)) + + case .scaling: + self.type = .scaling(value: try container.decode(forKey: .value)) + + case .sizing: + self.type = .sizing(value: try container.decode(forKey: .value)) + + case .spacing: + self.type = .spacing(value: try container.decode(forKey: .value)) + + case .textCase: + self.type = .textCase(value: try container.decode(forKey: .value)) + + case .textDecoration: + self.type = .textDecoration(value: try container.decode(forKey: .value)) + + case .typography: + self.type = .typography(value: try container.decode(forKey: .value)) + + case .borderWidth: + self.type = .borderWidth(value: try container.decode(forKey: .value)) + + case .border: + self.type = .border(value: try container.decode(forKey: .value)) + + case .unknown: + self.type = .unknown + } + } +} + +// MARK: - Encodable + +extension GitHubTokenValue: Encodable { + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { + // swiftlint:disable:previous function_body_length + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(name, forKey: .name) + try container.encode(typeName, forKey: .type) + + switch type { + case let .a11yScales(value): + try container.encode(value, forKey: .value) + + case let .animation(value): + try container.encode(value, forKey: .value) + + case let .borderRadius(value): + try container.encode(value, forKey: .value) + + case let .boxShadow(value): + try container.encode(value, forKey: .value) + + case let .color(value): + try container.encode(value, forKey: .value) + + case let .core(value): + try container.encode(value, forKey: .value) + + case let .dimension(value): + try container.encode(value, forKey: .value) + + case let .fontFamilies(value): + try container.encode(value, forKey: .value) + + case let .fontSizes(value): + try container.encode(value, forKey: .value) + + case let .fontWeights(value): + try container.encode(value, forKey: .value) + + case let .letterSpacing(value): + try container.encode(value, forKey: .value) + + case let .lineHeights(value): + try container.encode(value, forKey: .value) + + case let .opacity(value): + try container.encode(value, forKey: .value) + + case let .paragraphSpacing(value): + try container.encode(value, forKey: .value) + + case let .scaling(value): + try container.encode(value, forKey: .value) + + case let .sizing(value): + try container.encode(value, forKey: .value) + + case let .spacing(value): + try container.encode(value, forKey: .value) + + case let .textCase(value): + try container.encode(value, forKey: .value) + + case let .textDecoration(value): + try container.encode(value, forKey: .value) + + case let .typography(value): + try container.encode(value, forKey: .value) + + case let .borderWidth(value): + try container.encode(value, forKey: .value) + + case let .border(value): + try container.encode(value, forKey: .value) + + case .unknown: + try container.encodeNil(forKey: .value) + } + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIEmptyResponse.swift b/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIEmptyResponse.swift new file mode 100644 index 0000000..aefca8f --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIEmptyResponse.swift @@ -0,0 +1,3 @@ +import Foundation + +struct GitHubAPIEmptyResponse: Decodable { } diff --git a/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIProvider.swift b/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIProvider.swift new file mode 100644 index 0000000..9147cdc --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIProvider.swift @@ -0,0 +1,142 @@ +import Foundation +import PromiseKit +import FigmaGenTools + +final class GitHubAPIProvider: RemoteRepoProvider { + + // MARK: - Instance Properties + + private let queryEncoder: HTTPQueryEncoder + private let bodyEncoder: HTTPBodyEncoder + private let responseDecoder: HTTPResponseDecoder + private let baseURL = URL(string: "https://raw.githubusercontent.com")! + + // MARK: - + + let httpService: GitHubHTTPService + + init(httpService: GitHubHTTPService) { + self.httpService = httpService + + let urlEncoder = URLEncoder(boolEncodingStrategy: .literal) + let jsonEncoder = JSONEncoder() + let jsonDecoder = JSONDecoder() + + urlEncoder.dateEncodingStrategy = .formatted(.gitHubAPI(withMilliseconds: true)) + jsonEncoder.dateEncodingStrategy = .formatted(.gitHubAPI(withMilliseconds: true)) + + jsonDecoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + + if let date = DateFormatter.gitHubAPI(withMilliseconds: true).date(from: dateString) { + return date + } + + if let date = DateFormatter.gitHubAPI(withMilliseconds: false).date(from: dateString) { + return date + } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Date string does not match format expected by formatter" + ) + } + + self.queryEncoder = HTTPQueryURLEncoder(urlEncoder: urlEncoder) + self.bodyEncoder = HTTPBodyJSONEncoder(jsonEncoder: jsonEncoder) + self.responseDecoder = jsonDecoder + } + + private func makeHTTPRoute(for route: Route) -> HTTPRoute { + let url = baseURL + .appendingPathComponent(route.urlPath) + + let headers = route.accessToken.map { [HTTPHeader.gitHubAccessToken($0)] } ?? [] + + return HTTPRoute( + method: route.httpMethod, + url: url, + headers: headers + ) + } + + private func handleHTTPError(_ error: HTTPError) -> Error { + guard let errorData = error.data, error.reason is HTTPStatusCode else { + return error + } + + guard let apiError = try? responseDecoder.decode(FigmaError.self, from: errorData) else { + return error + } + + return apiError + } +} + +// MARK: - RemoteRepoProvider +extension GitHubAPIProvider { + + func request(route: Route) -> Promise { + Promise { seal in + + let task = httpService.request(route: makeHTTPRoute(for: route)) + + task.responseDecodable(type: Route.Response.self, decoder: responseDecoder) { response in + switch response.result { + case let .failure(error): + seal.reject(self.handleHTTPError(error)) + + case let .success(value): + seal.fulfill(value) + } + } + } + } + + func request(route: Route) -> Promise where Route.Response == GitHubAPIEmptyResponse { + Promise { seal in + + let task = httpService.request(route: makeHTTPRoute(for: route)) + + task.responseJSON { response in + switch response.result { + case let .failure(error): + seal.reject(self.handleHTTPError(error)) + + case .success: + seal.fulfill(Void()) + } + } + } + } +} + +extension HTTPHeader { + + // MARK: - Type Methods + + fileprivate static func gitHubAccessToken(_ value: String) -> HTTPHeader { + .authorization(bearerToken: value) + } +} + +extension DateFormatter { + + // MARK: - Type Properties + + fileprivate static func gitHubAPI(withMilliseconds: Bool) -> DateFormatter { + let dateFormatter = DateFormatter() + + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + if withMilliseconds { + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" + } else { + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + } + + return dateFormatter + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/GitHubHTTPService.swift b/Sources/FigmaGen/Providers/GitHubApi/GitHubHTTPService.swift new file mode 100644 index 0000000..529119d --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/GitHubHTTPService.swift @@ -0,0 +1,11 @@ +import Foundation +import FigmaGenTools + +public protocol GitHubHTTPService { + + // MARK: - Instance Methods + + func request(route: HTTPRoute) -> HTTPTask +} + +extension HTTPService: GitHubHTTPService { } diff --git a/Sources/FigmaGen/Providers/GitHubApi/RemoteRepoProvider.swift b/Sources/FigmaGen/Providers/GitHubApi/RemoteRepoProvider.swift new file mode 100644 index 0000000..bf34272 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/RemoteRepoProvider.swift @@ -0,0 +1,10 @@ +import Foundation +import PromiseKit + +protocol RemoteRepoProvider { + + // MARK: - Instance Methods + + func request(route: Route) -> Promise + func request(route: Route) -> Promise where Route.Response == GitHubAPIEmptyResponse +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIFileRoute.swift b/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIFileRoute.swift new file mode 100644 index 0000000..49b411b --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIFileRoute.swift @@ -0,0 +1,23 @@ +import Foundation +import FigmaGenTools + +struct GitHubAPIFileRoute: GitHubAPIRoute { + + typealias Response = GitHubFile + + private let owner: String + private let repo: String + private let branch: String + private let filePath: String + + let accessToken: String? + var urlPath: String { "\(owner)/\(repo)/\(branch)/\(filePath)" } + + init(owner: String, repo: String, branch: String, filePath: String, accessToken: String?) { + self.owner = owner + self.repo = repo + self.branch = branch + self.filePath = filePath + self.accessToken = accessToken + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIRoute.swift b/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIRoute.swift new file mode 100644 index 0000000..3c944ba --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIRoute.swift @@ -0,0 +1,28 @@ +import Foundation +import FigmaGenTools + +protocol GitHubAPIRoute { + + // MARK: - Nested Types + + associatedtype Response: Decodable + + // MARK: - Instance Properties + + var httpMethod: HTTPMethod { get } + var urlPath: String { get } + var accessToken: String? { get } +} + +extension GitHubAPIRoute { + + // MARK: - Instance Properties + + var httpMethod: HTTPMethod { + .get + } + + var accessToken: String? { + nil + } +} diff --git a/Sources/FigmaGen/Providers/Images/Render/DefaultImageRenderProvider.swift b/Sources/FigmaGen/Providers/Images/Render/DefaultImageRenderProvider.swift index 71464c2..9b5795d 100644 --- a/Sources/FigmaGen/Providers/Images/Render/DefaultImageRenderProvider.swift +++ b/Sources/FigmaGen/Providers/Images/Render/DefaultImageRenderProvider.swift @@ -101,12 +101,18 @@ final class DefaultImageRenderProvider: ImageRenderProvider { return .value([]) } - let promises = scales.map { scale in - renderImages(of: file, nodes: nodes, format: format, scale: scale, useAbsoluteBounds: useAbsoluteBounds) - .map { imageURLs in - (scale: scale, imageURLs: imageURLs) - } - } + let promises = scales + .map { scale in + nodes + .chunked(size: 100) + .map { nodesPars in + renderImages(of: file, nodes: nodesPars, format: format, scale: scale, useAbsoluteBounds: useAbsoluteBounds) + .map { imageURLs in + (scale: scale, imageURLs: imageURLs) + } + } + } + .flatMap { $0 } return firstly { when(fulfilled: promises) diff --git a/Sources/FigmaGen/Providers/Tokens/DefaultTokensProvider.swift b/Sources/FigmaGen/Providers/Tokens/DefaultTokensProvider.swift index a3a184f..c3457f0 100644 --- a/Sources/FigmaGen/Providers/Tokens/DefaultTokensProvider.swift +++ b/Sources/FigmaGen/Providers/Tokens/DefaultTokensProvider.swift @@ -5,15 +5,17 @@ final class DefaultTokensProvider: TokensProvider { // MARK: - Instance Properties - let apiProvider: FigmaAPIProvider + let figmaApiProvider: FigmaAPIProvider + let gitHubApiProvider: RemoteRepoProvider let dictionaryDecoder = DictionaryDecoder() let jsonDecoder = JSONDecoder() // MARK: - Initializers - init(apiProvider: FigmaAPIProvider) { - self.apiProvider = apiProvider + init(figmaApiProvider: FigmaAPIProvider, gitHubApiProvider: RemoteRepoProvider) { + self.figmaApiProvider = figmaApiProvider + self.gitHubApiProvider = gitHubApiProvider } // MARK: - Instance Methods @@ -46,6 +48,21 @@ final class DefaultTokensProvider: TokensProvider { return try jsonDecoder.decode(TokenValues.self, from: valuesData) } + private func extractTokens(from file: GitHubFile) throws -> TokenValues { + guard let valuesData = try? JSONEncoder().encode(file) else { + throw TokensProviderError(code: .failedCreateData) + } + + let json = try? JSONSerialization.jsonObject(with: valuesData, options: .mutableContainers) + + let jsonData = json.flatMap { try? JSONSerialization.data(withJSONObject: $0, options: .prettyPrinted) } + if let jsonData { + logger.debug(String(decoding: jsonData, as: UTF8.self)) + } + + return try jsonDecoder.decode(TokenValues.self, from: valuesData) + } + private func fetchFile(_ file: FileParameters) async throws -> FigmaFile { let route = FigmaAPIFileRoute( accessToken: file.accessToken, @@ -55,7 +72,21 @@ final class DefaultTokensProvider: TokensProvider { pluginData: "shared" ) - return try await apiProvider + return try await figmaApiProvider + .request(route: route) + .async() + } + + private func fetchFile(_ file: RemoteFileParameters) async throws -> GitHubFile { + let route = GitHubAPIFileRoute( + owner: file.owner, + repo: file.repo, + branch: file.branch, + filePath: file.filePath, + accessToken: file.accessToken + ) + + return try await gitHubApiProvider .request(route: route) .async() } @@ -67,4 +98,10 @@ final class DefaultTokensProvider: TokensProvider { return try extractTokens(from: figmaFile) } + + func fetchTokens(from remoteFile: RemoteFileParameters) async throws -> TokenValues { + let gitHubFile = try await fetchFile(remoteFile) + + return try extractTokens(from: gitHubFile) + } } diff --git a/Sources/FigmaGen/Providers/Tokens/TokensProvider.swift b/Sources/FigmaGen/Providers/Tokens/TokensProvider.swift index 51e4522..eac7120 100644 --- a/Sources/FigmaGen/Providers/Tokens/TokensProvider.swift +++ b/Sources/FigmaGen/Providers/Tokens/TokensProvider.swift @@ -5,4 +5,6 @@ protocol TokensProvider { // MARK: - Instance Methods func fetchTokens(from file: FileParameters) async throws -> TokenValues + + func fetchTokens(from remoteFile: RemoteFileParameters) async throws -> TokenValues } diff --git a/Sources/FigmaGen/Resolvers/AccessToken/DefaultAccessTokenResolver.swift b/Sources/FigmaGen/Resolvers/AccessToken/DefaultAccessTokenResolver.swift index 9b167ab..1fc3cdf 100644 --- a/Sources/FigmaGen/Resolvers/AccessToken/DefaultAccessTokenResolver.swift +++ b/Sources/FigmaGen/Resolvers/AccessToken/DefaultAccessTokenResolver.swift @@ -1,16 +1,24 @@ import Foundation +#if os(macOS) +import KeychainAccess +#endif final class DefaultAccessTokenResolver: AccessTokenResolver { func resolveAccessToken(from configuration: AccessTokenConfiguration?) -> String? { - switch configuration { - case let .value(accessToken): + if let accessToken = configuration?.value { return accessToken - - case let .environmentVariable(environmentVariable): - return ProcessInfo.processInfo.environment[environmentVariable] - - case nil: + } else if let environmentVariable = configuration?.environmentVariable, + let accessToken = ProcessInfo.processInfo.environment[environmentVariable] { + return accessToken + } else if let parameters = configuration?.keychainParameters { + #if os(macOS) + let accessToken = try? Keychain(service: parameters.service).getString(parameters.key) + return accessToken + #else + return nil + #endif + } else { return nil } } diff --git a/Sources/FigmaGenTools/Extensions/Array+Extensions.swift b/Sources/FigmaGenTools/Extensions/Array+Extensions.swift new file mode 100644 index 0000000..aca52fd --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Array+Extensions.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Array { + + public func chunked(size: Int) -> [[Element]] { + stride(from: 0, to: count, by: size).map { + Array(self[$0 ..< Swift.min($0 + size, count)]) + } + } +} diff --git a/tokens.json b/tokens.json new file mode 100644 index 0000000..936090c --- /dev/null +++ b/tokens.json @@ -0,0 +1,176 @@ +{ + "core": { + "core": { + "fontSize": { + "10": { + "value": "10", + "type": "fontSizes" + }, + "12": { + "value": "12", + "type": "fontSizes" + } + }, + "lineHeights": { + "12": { + "value": "12", + "type": "lineHeights" + }, + "14": { + "value": "14", + "type": "lineHeights" + }, + "16": { + "value": "16", + "type": "lineHeights" + } + }, + "paragraphSpacing": { + "0": { + "value": "0", + "type": "paragraphSpacing" + }, + "4": { + "value": "4", + "type": "paragraphSpacing" + } + }, + "a11yScale": { + "0": { + "value": "0", + "type": "a11yScales" + }, + "1": { + "value": "1", + "type": "a11yScales" + } + }, + "scale": { + "1": { + "value": "1", + "type": "scaling" + }, + "2": { + "value": "0.96", + "type": "scaling" + } + }, + "size": { + "0-x": { + "value": "{core.2-x-base} * 0", + "type": "sizing" + }, + "1-x": { + "value": "{core.2-x-base} * 1", + "type": "sizing" + } + }, + "borderRadius": { + "0-x": { + "value": "{core.2-x-base} * 0", + "type": "borderRadius" + }, + "1-x": { + "value": "{core.2-x-base} * 1", + "type": "borderRadius" + } + }, + "space": { + "0-x": { + "value": "{core.2-x-base} * 0", + "type": "spacing" + }, + "1-x": { + "value": "{core.2-x-base} * 1", + "type": "spacing" + } + }, + "x-base": { + "value": "2", + "type": "core" + }, + "2-x-base": { + "value": "{core.x-base} * 2", + "type": "core" + }, + "opacity": { + "0": { + "value": "0%", + "type": "opacity" + }, + "16": { + "value": "16%", + "type": "opacity" + } + }, + "letterSpacing": { + "0": { + "value": "0.00%", + "type": "letterSpacing" + }, + "negative": { + "50": { + "value": "-0.50%", + "type": "letterSpacing" + } + }, + "positive": { + "50": { + "value": "0.50%", + "type": "letterSpacing" + } + } + }, + "textCase": { + "none": { + "value": "none", + "type": "textCase" + } + }, + "textDecoration": { + "none": { + "value": "none", + "type": "textDecoration" + } + }, + "paragraphIndent": { + "0": { + "value": "0px", + "type": "dimension" + } + }, + "borderWidth": { + "10": { + "value": "1", + "type": "borderWidth" + }, + "15": { + "value": "1.5", + "type": "borderWidth" + } + } + } + }, + "semantic": { + "semantic": { }, + "static": { } + }, + "colors": { + "color": { + "base": { + "white": { + "value": "#ffffff", + "type": "color" + }, + "black": { + "value": "#000000", + "type": "color" + } + } + } + }, + "typography": { }, + "hh-day": { }, + "hh-night": { }, + "zp-day": { } +} \ No newline at end of file