Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Sources/FigmaGen/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ enum Dependencies {

// MARK: -

static let tokensResolver: TokensResolver = DefaultTokensResolver()

// MARK: -

static let templateContextCoder: TemplateContextCoder = DefaultTemplateContextCoder()

static let stencilExtensions: [StencilExtension] = [
Expand Down Expand Up @@ -116,6 +120,7 @@ enum Dependencies {
)

static let tokensGenerator: TokensGenerator = DefaultTokensGenerator(
tokensProvider: tokensProvider
tokensProvider: tokensProvider,
tokensResolver: tokensResolver
)
}
128 changes: 10 additions & 118 deletions Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,132 +7,20 @@ final class DefaultTokensGenerator: TokensGenerator, GenerationParametersResolvi
// MARK: - Instance Properties

let tokensProvider: TokensProvider
let tokensResolver: TokensResolver

let defaultTemplateType = RenderTemplateType.native(name: "Tokens")
let defaultDestination = RenderDestination.console

// MARK: - Initializers

init(tokensProvider: TokensProvider) {
init(tokensProvider: TokensProvider, tokensResolver: TokensResolver) {
self.tokensProvider = tokensProvider
self.tokensResolver = tokensResolver
}

// MARK: - Instance Methods

private func evaluteValue(_ value: String) -> String {
let expression = AnyExpression(value)

do {
return try expression.evaluate()
} catch {
return value
}
}

private func resolveValue(_ value: String, tokenValues: TokenValues) throws -> String {
let allTokens = tokenValues.all

let resolvedValue = try value.replacingOccurrences(matchingPattern: #"\{.*?\}"#) { referenceName in
let referenceName = referenceName
.removingFirst()
.removingLast()

guard let token = allTokens.first(where: { $0.name == referenceName }) else {
throw TokensGeneratorError(code: .referenceNotFound(name: referenceName))
}

guard let value = token.type.stringValue else {
throw TokensGeneratorError(code: .unexpectedTokenValueType(name: referenceName))
}

return try resolveValue(value, tokenValues: tokenValues)
}

return evaluteValue(resolvedValue)
}

private func makeColor(hex: String, alpha: CGFloat) throws -> Color {
let hex = hex
.trimmingCharacters(in: .whitespacesAndNewlines)
.uppercased()
.filter { $0 != "#" }

guard hex.count == 6 else {
throw TokensGeneratorError(code: .invalidHEXComponent(hex: hex))
}

var rgbValue: UInt64 = 0
Scanner(string: hex).scanHexInt64(&rgbValue)

return Color(
red: Double((rgbValue & 0xFF0000) >> 16) / 255.0,
green: Double((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: Double(rgbValue & 0x0000FF) / 255.0,
alpha: alpha
)
}

private func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues) throws -> Color {
let components = value
.slice(from: "(", to: ")", includingBounds: false)?
.components(separatedBy: ", ")

guard let components, components.count == 2 else {
throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: value))
}

let hex = components[0]
let alphaPercent = components[1]

guard let alpha = Double(alphaPercent.dropLast()) else {
throw TokensGeneratorError(code: .invalidAlphaComponent(alpha: alphaPercent))
}

return try makeColor(hex: hex, alpha: alpha / 100.0)
}

private func resolveColorValue(_ value: String, tokenValues: TokenValues) throws -> Color {
if value.hasPrefix("rgba") {
return try resolveRGBAColorValue(value, tokenValues: tokenValues)
}

return try makeColor(hex: value, alpha: 1.0)
}

private func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues) throws -> LinearGradient {
guard let startFunctionIndex = value.firstIndex(of: "("), let endFunctionIndex = value.lastIndex(of: ")") else {
throw TokensGeneratorError(code: .failedToExtractLinearGradientParams(linearGradient: value))
}

let rawParams = value[value.index(after: startFunctionIndex)..<endFunctionIndex]
let pattern = #",(?![^(]*\))(?![^"']*["'](?:[^"']*["'][^"']*["'])*[^"']*$)"#

let params = try String(rawParams)
.split(usingRegex: pattern)
.map { $0.trimmingCharacters(in: .whitespaces) }

let angle = params[0]

let colorStopList = try params
.removingFirst()
.map { rawColorStop in
guard let separatorRange = rawColorStop.range(of: " ", options: .backwards) else {
throw TokensGeneratorError(code: .failedToExtractLinearGradientParams(linearGradient: value))
}

let percentage = String(rawColorStop[separatorRange.upperBound...])
let rawColor = String(rawColorStop[...separatorRange.lowerBound])
let color = try resolveColorValue(rawColor, tokenValues: tokenValues)

return LinearGradient.LinearColorStop(color: color, percentage: percentage)
}

return LinearGradient(
angle: angle,
colorStopList: colorStopList
)
}

private func generate(parameters: GenerationParameters) async throws {
let tokenValues = try await tokensProvider.fetchTokens(from: parameters.file)

Expand All @@ -142,13 +30,17 @@ final class DefaultTokensGenerator: TokensGenerator, GenerationParametersResolvi
return nil
}

return (tokenValue, try resolveValue(value, tokenValues: tokenValues))
return (tokenValue, try tokensResolver.resolveValue(value, tokenValues: tokenValues))
}
.forEach { tokenValue, value in
if value.hasPrefix("rgba") {
print("[\(tokenValue.name)] \(try resolveRGBAColorValue(value, tokenValues: tokenValues))")
let color = try tokensResolver.resolveRGBAColorValue(value, tokenValues: tokenValues)

print("[\(tokenValue.name)] \(color)")
} else if value.hasPrefix("linear-gradient") {
print("[\(tokenValue.name)] \(try resolveLinearGradientValue(value, tokenValues: tokenValues))")
let gradient = try tokensResolver.resolveLinearGradientValue(value, tokenValues: tokenValues)

print("[\(tokenValue.name)] \(gradient)")
} else {
print("[\(tokenValue.name)] \(value)")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Foundation
import Expression

final class DefaultTokensResolver: TokensResolver {

// MARK: - Instance Methods

private func evaluteValue(_ value: String) -> String {
let expression = AnyExpression(value)

do {
return try expression.evaluate()
} catch {
return value
}
}

private func makeColor(hex: String, alpha: CGFloat) throws -> Color {
let hex = hex
.trimmingCharacters(in: .whitespacesAndNewlines)
.uppercased()
.filter { $0 != "#" }

guard hex.count == 6 else {
throw TokensGeneratorError(code: .invalidHEXComponent(hex: hex))
}

var rgbValue: UInt64 = 0
Scanner(string: hex).scanHexInt64(&rgbValue)

return Color(
red: Double((rgbValue & 0xFF0000) >> 16) / 255.0,
green: Double((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: Double(rgbValue & 0x0000FF) / 255.0,
alpha: alpha
)
}

private func resolveColorValue(_ value: String, tokenValues: TokenValues) throws -> Color {
if value.hasPrefix("rgba") {
return try resolveRGBAColorValue(value, tokenValues: tokenValues)
}

return try makeColor(hex: value, alpha: 1.0)
}

// MARK: - TokensResolver

func resolveValue(_ value: String, tokenValues: TokenValues) throws -> String {
let allTokens = tokenValues.all

let resolvedValue = try value.replacingOccurrences(matchingPattern: #"\{.*?\}"#) { referenceName in
let referenceName = referenceName
.removingFirst()
.removingLast()

guard let token = allTokens.first(where: { $0.name == referenceName }) else {
throw TokensGeneratorError(code: .referenceNotFound(name: referenceName))
}

guard let value = token.type.stringValue else {
throw TokensGeneratorError(code: .unexpectedTokenValueType(name: referenceName))
}

return try resolveValue(value, tokenValues: tokenValues)
}

return evaluteValue(resolvedValue)
}

func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues) throws -> Color {
let components = try resolveValue(value, tokenValues: tokenValues)
.slice(from: "(", to: ")", includingBounds: false)?
.components(separatedBy: ", ")

guard let components, components.count == 2 else {
throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: value))
}

let hex = components[0]
let alphaPercent = components[1]

guard let alpha = Double(alphaPercent.dropLast()) else {
throw TokensGeneratorError(code: .invalidAlphaComponent(alpha: alphaPercent))
}

return try makeColor(hex: hex, alpha: alpha / 100.0)
}

func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues) throws -> LinearGradient {
let value = try resolveValue(value, tokenValues: tokenValues)

guard let startFunctionIndex = value.firstIndex(of: "("), let endFunctionIndex = value.lastIndex(of: ")") else {
throw TokensGeneratorError(code: .failedToExtractLinearGradientParams(linearGradient: value))
}

let rawParams = value[value.index(after: startFunctionIndex)..<endFunctionIndex]
let pattern = #",(?![^(]*\))(?![^"']*["'](?:[^"']*["'][^"']*["'])*[^"']*$)"#

let params = try String(rawParams)
.split(usingRegex: pattern)
.map { $0.trimmingCharacters(in: .whitespaces) }

let angle = params[0]

let colorStopList = try params
.removingFirst()
.map { rawColorStop in
guard let separatorRange = rawColorStop.range(of: " ", options: .backwards) else {
throw TokensGeneratorError(code: .failedToExtractLinearGradientParams(linearGradient: value))
}

let percentage = String(rawColorStop[separatorRange.upperBound...])
let rawColor = String(rawColorStop[...separatorRange.lowerBound])
let color = try resolveColorValue(rawColor, tokenValues: tokenValues)

return LinearGradient.LinearColorStop(color: color, percentage: percentage)
}

return LinearGradient(
angle: angle,
colorStopList: colorStopList
)
}
}
60 changes: 60 additions & 0 deletions Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation

protocol TokensResolver {

// MARK: - Instance Methods

/// Resolving references and mathematical expressions in `value` from `tokenValues`.
///
/// Reference example: `{core.space.1-x} + {core.space.1-x} / 2`
/// where `core.space.1-x == 1` the resolved value would be `1 + 1 / 2`
/// and after evaluating the mathematical expression, the function will return `1.5`
///
/// - Parameters:
/// - value: String value to resolve
/// - tokenValues: All token values
/// - Returns: Resolved value.
func resolveValue(_ value: String, tokenValues: TokenValues) throws -> String

/// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)``
/// and convert `rgba()` to ``Color`` object
///
/// Supported formats:
/// - `rgba(hex_color, alpha-value-percentage)`
/// - TO DO: Support more formats
///
/// [Color tokens examples and should be supported later](https://docs.tokens.studio/available-tokens/color-tokens#solid-colors)
///
/// - Parameters:
/// - value: Raw `rgba()` with references, e.g.:
/// ```
/// rgba(
/// {color.base.white},
/// {semantic.opacity.disabled}
/// )
/// ```
/// - tokenValues: All token values
/// - Returns: ``Color`` object with values resolved from `rgba()`
func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues) throws -> Color

/// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)``
/// and convert `linear-gradient()` to ``LinearGradient`` object
///
/// Supported formats:
/// - `linear-gradient(angle, [hex_color||rgba() length-percentage])`
///
/// [Gradients tokens examples](https://docs.tokens.studio/available-tokens/color-tokens#gradients)
///
/// - Parameters:
/// - value: Raw `linear-gradient()` with references, e.g:
/// ```
/// linear-gradient(
/// 0deg,
/// rgba({color.base.red.50}, {semantic.opacity.transparent}) 0%,
/// {color.base.red.50} {semantic.opacity.visible}
/// )
/// ```
/// - tokenValues: All token values
/// - Returns: ``LinearGradient`` object with values resolved from `linear-gradient()`
func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues) throws -> LinearGradient
}
Loading