Quick and dirty figma importer

This commit is contained in:
Sven Weidauer 2022-10-08 12:41:02 +02:00
parent 956c1f5d27
commit 2b1f21dbab
3 changed files with 151 additions and 1 deletions

View file

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "MakeColors",
platforms: [
.macOS("10.15.4"),
.macOS("12.0"),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.1.4")),

View file

@ -0,0 +1,149 @@
import Foundation
enum FigmaErrors: Error {
case invalidUrl
case missingToken
case invalidResponse
case missingColor(String)
}
class FigmaImporter: Importer {
let key: String
let token: String
required init(source: String) throws {
// https://www.figma.com/file/:key/:title
guard
let url = URL(string: source),
url.host == "www.figma.com",
url.pathComponents.count >= 4,
url.pathComponents[1] == "file"
else {
throw FigmaErrors.invalidUrl
}
key = url.pathComponents[2]
guard let token = ProcessInfo.processInfo.environment["FIGMA_TOKEN"] else {
throw FigmaErrors.missingToken
}
self.token = token
}
func read() async throws -> [String: ColorDef] {
let styles = try await request(StylesResponse.self, path: "/v1/files/\(key)/styles").meta.styles
.filter { $0.styleType == "FILL" }
let ids = styles.map(\.nodeId).joined(separator: ",")
let nodes = try await request(
NodesResponse.self,
path: "/v1/files/\(key)/nodes",
query: [URLQueryItem(name: "ids", value: ids)]
)
.nodes
var result: [String: ColorDef] = [:]
result.reserveCapacity(styles.count)
for style in styles {
guard
let node = nodes[style.nodeId],
let fill = node.document.fills.first(where: { $0.type == "SOLID" })
else {
throw FigmaErrors.missingColor(style.name)
}
if node.document.fills.count > 1 {
print("Warning: Multiple fills defined for \(style.name)")
}
if fill.blendMode != "NORMAL" {
print("Warning: Blend mode \(fill.blendMode) used for \(style.name)")
}
guard !result.keys.contains(style.name) else {
throw Errors.duplicateColor(style.name)
}
result[style.name] = .color(Color(fill.color))
}
return result
}
func request<T: Decodable>(_: T.Type = T.self, path: String, query: [URLQueryItem]? = nil) async throws -> T {
var components = URLComponents()
components.scheme = "https"
components.host = "api.figma.com"
components.path = path
components.queryItems = query
guard let url = components.url else {
fatalError("Cannot create url. Components: \(components)")
}
var request = URLRequest(url: url)
request.setValue(token, forHTTPHeaderField: "X-Figma-Token")
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
fatalError("Non-HTTP-Response received: \(response)")
}
guard response.statusCode == 200 else {
throw FigmaErrors.invalidResponse
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(T.self, from: data)
}
}
struct StylesResponse: Decodable {
var meta: Meta
struct Meta: Decodable {
var styles: [Style]
}
struct Style: Decodable {
var nodeId: String
var styleType: String
var name: String
var description: String
}
}
struct NodesResponse: Decodable {
var nodes: [String: Node]
struct Node: Decodable {
var document: Document
}
struct Document: Decodable {
var fills: [Fill]
}
struct Fill: Decodable {
var blendMode: String
var type: String
var color: Color
}
struct Color: Decodable {
var r, g, b, a: Float
}
}
extension Color {
init(_ color: NodesResponse.Color) {
red = UInt8(truncatingIfNeeded: Int(color.r * 0xFF))
green = UInt8(truncatingIfNeeded: Int(color.g * 0xFF))
blue = UInt8(truncatingIfNeeded: Int(color.b * 0xFF))
alpha = UInt8(truncatingIfNeeded: Int(color.a * 0xFF))
}
}

View file

@ -22,6 +22,7 @@ private struct GeneratorOption: EnumerableFlag, CustomStringConvertible {
private struct ImporterOption: CaseIterable, ExpressibleByArgument, CustomStringConvertible {
static let allCases: [ImporterOption] = [
.list,
.init(type: FigmaImporter.self),
]
static let list = ImporterOption(type: ListImporter.self)