From e65f68465412c11aa89f1f7da15298bb29c97212 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Fri, 1 Jan 2021 12:52:20 +0100 Subject: [PATCH 01/27] Support giving color values as percentage --- .../Model/Scanner+ColorParser.swift | 14 ++++++++- Tests/MakeColorsTests/ColorParserTest.swift | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift b/Sources/LibMakeColors/Model/Scanner+ColorParser.swift index fb0bd1c..84957cd 100644 --- a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift +++ b/Sources/LibMakeColors/Model/Scanner+ColorParser.swift @@ -119,13 +119,25 @@ extension Scanner { func commaSeparated() -> [UInt8]? { var result: [UInt8] = [] repeat { - guard let int = scanInt(), let component = UInt8(exactly: int) else { + guard let component = self.component() else { return nil } result.append(component) } while string(",") return result } + + func component() -> UInt8? { + guard var int = scanInt() else { + return nil + } + + if string("%") { + int = int * 0xFF / 100 + } + + return UInt8(exactly: int) + } } private extension Scanner { diff --git a/Tests/MakeColorsTests/ColorParserTest.swift b/Tests/MakeColorsTests/ColorParserTest.swift index 708c77d..edcb587 100644 --- a/Tests/MakeColorsTests/ColorParserTest.swift +++ b/Tests/MakeColorsTests/ColorParserTest.swift @@ -57,6 +57,35 @@ final class ColorParserTest: XCTestCase { XCTAssertNil(color) } + func testScanningColorWithPercentage() throws { + let color = scanColor("rgba(100%, 0, 50%, 100%)") + XCTAssertEqual(color, Color(red: 255, green: 0, blue: 127, alpha: 255)) + } + + func testReadingComponentAsByte() throws { + let scanner = Scanner(string: "128") + XCTAssertEqual(scanner.component(), 128) + XCTAssertTrue(scanner.isAtEnd) + } + + func testReadingComponentAs100Percent() throws { + let scanner = Scanner(string: "100%") + XCTAssertEqual(scanner.component(), 0xFF) + XCTAssertTrue(scanner.isAtEnd) + } + + func testReadingComponentAs0Percent() throws { + let scanner = Scanner(string: "0%") + XCTAssertEqual(scanner.component(), 0) + XCTAssertTrue(scanner.isAtEnd) + } + + func testReadingComponentAs50PercentRoundsDown() throws { + let scanner = Scanner(string: "50%") + XCTAssertEqual(scanner.component(), 127) + XCTAssertTrue(scanner.isAtEnd) + } + private func scanColor(_ input: String) -> Color? { let scanner = Scanner(string: input) return scanner.color() From 0b94872101087ebc96254f95b7821877799de09b Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Fri, 1 Jan 2021 13:33:11 +0100 Subject: [PATCH 02/27] Add HSV conversion --- Sources/LibMakeColors/Model/Color+HSV.swift | 36 +++++++++++++++++++ Tests/MakeColorsTests/ColorHSVTest.swift | 39 +++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 Sources/LibMakeColors/Model/Color+HSV.swift create mode 100644 Tests/MakeColorsTests/ColorHSVTest.swift diff --git a/Sources/LibMakeColors/Model/Color+HSV.swift b/Sources/LibMakeColors/Model/Color+HSV.swift new file mode 100644 index 0000000..8b9383f --- /dev/null +++ b/Sources/LibMakeColors/Model/Color+HSV.swift @@ -0,0 +1,36 @@ +extension Color { + init(hue: Int, saturation: UInt8, value: UInt8, alpha: UInt8 = 0xFF) { + let degrees = abs(hue % 360) + + let s = Double(saturation) / 0xFF + let v = Double(value) / 0xFF + let C = s * v + let X = C * (1 - abs((Double(degrees) / 60).truncatingRemainder(dividingBy: 2) - 1)) + let m = v - C + + let result: (r: Double, g: Double, b: Double) + switch degrees { + case 0..<60: + result = (C, X, 0) + case 60..<120: + result = (X, C, 0) + case 120..<180: + result = (0, C, X) + case 180..<240: + result = (0, X, C) + case 240..<300: + result = (X, 0, C) + case 300..<360: + result = (C, 0, X) + default: + fatalError("Degrees out of range") + } + + self.init( + red: UInt8(((result.r + m) * 0xFF).rounded()), + green: UInt8(((result.g + m) * 0xFF).rounded()), + blue: UInt8(((result.b + m) * 0xFF).rounded()), + alpha: alpha + ) + } +} diff --git a/Tests/MakeColorsTests/ColorHSVTest.swift b/Tests/MakeColorsTests/ColorHSVTest.swift new file mode 100644 index 0000000..d7bd9dc --- /dev/null +++ b/Tests/MakeColorsTests/ColorHSVTest.swift @@ -0,0 +1,39 @@ +@testable import LibMakeColors +import XCTest + +final class ColorHSVTest: XCTestCase { + func testHSV0Degrees() { + let color = Color(hue: 0, saturation: 0xFF, value: 0xFF) + XCTAssertEqual(color, Color(red: 0xFF, green: 0, blue: 0)) + } + + func testHSV60Degrees() { + let color = Color(hue: 60, saturation: 0xFF, value: 0xFF) + XCTAssertEqual(color, Color(red: 0xFF, green: 0xFF, blue: 0)) + } + + func testHSV120Degrees() { + let color = Color(hue: 120, saturation: 0xFF, value: 0xFF) + XCTAssertEqual(color, Color(red: 0, green: 0xFF, blue: 0)) + } + + func testHSV180Degrees() { + let color = Color(hue: 180, saturation: 0xFF, value: 0xFF) + XCTAssertEqual(color, Color(red: 0, green: 0xFF, blue: 0xFF)) + } + + func testHSV240Degrees() { + let color = Color(hue: 240, saturation: 0xFF, value: 0xFF) + XCTAssertEqual(color, Color(red: 0, green: 0, blue: 0xFF)) + } + + func testHSV300Degrees() { + let color = Color(hue: 300, saturation: 0xFF, value: 0xFF) + XCTAssertEqual(color, Color(red: 0xFF, green: 0, blue: 0xFF)) + } + + func testHSV360Degrees() { + let color = Color(hue: 360, saturation: 0xFF, value: 0xFF) + XCTAssertEqual(color, Color(red: 0xFF, green: 0, blue: 0)) + } +} From 75a3bc265dd07d9f7e0b721b4c3969dd4c4bfe7e Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Fri, 1 Jan 2021 13:49:44 +0100 Subject: [PATCH 03/27] Parse hsv colors --- .../Model/Scanner+ColorParser.swift | 49 +++++++++++++++++++ Tests/MakeColorsTests/ColorParserTest.swift | 32 +++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift b/Sources/LibMakeColors/Model/Scanner+ColorParser.swift index 84957cd..c0d54d2 100644 --- a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift +++ b/Sources/LibMakeColors/Model/Scanner+ColorParser.swift @@ -31,9 +31,58 @@ extension Scanner { return Color(white: arguments) } + if string("hsva") { + return readHSV(alpha: true) + } + + if string("hsv") { + return readHSV(alpha: false) + } + return nil } + private func readHSV(alpha readAlpha: Bool) -> Color? { + guard + string("("), + let hue = degrees(), + string(","), + let saturation = component(), + string(","), + let value = component() + else { return nil } + + let alpha: UInt8 + + if readAlpha { + guard + string(","), + let value = component() + else { return nil } + alpha = value + } else { + alpha = 0xFF + } + + guard string(")") else { return nil } + + return Color(hue: hue, saturation: saturation, value: value, alpha: alpha) + } + + func degrees() -> Int? { + guard let int = scanInt() else { return nil } + + if string("%") { + guard 0...100 ~= int else { return nil } + return (360 * int) / 100 + } else if string("°") || string("deg") { + return int + } else { + guard 0...0xFF ~= int else { return nil } + return (360 * int) / 0xFF + } + } + func colorReference() -> String? { guard string("@") else { return nil } return name() diff --git a/Tests/MakeColorsTests/ColorParserTest.swift b/Tests/MakeColorsTests/ColorParserTest.swift index edcb587..075c72d 100644 --- a/Tests/MakeColorsTests/ColorParserTest.swift +++ b/Tests/MakeColorsTests/ColorParserTest.swift @@ -86,8 +86,38 @@ final class ColorParserTest: XCTestCase { XCTAssertTrue(scanner.isAtEnd) } + func testScanningDegreesAsByte() throws { + let scanner = Scanner(string: "128") + XCTAssertEqual(scanner.degrees(), 180) + } + + func testScanningDegreesAsPercentage() throws { + let scanner = Scanner(string: "50%") + XCTAssertEqual(scanner.degrees(), 180) + } + + func testScanningDegrees() throws { + let scanner = Scanner(string: "120°") + XCTAssertEqual(scanner.degrees(), 120) + } + + func testScanningDegreesWithDegSuffix() throws { + let scanner = Scanner(string: "120 deg") + XCTAssertEqual(scanner.degrees(), 120) + } + + func testScanningHSVColor() throws { + XCTAssertEqual(scanColor("hsv(60°, 255, 100%)"), Color(red: 0xFF, green: 0xFF, blue: 0)) + } + + func testScanningHSVAColor() throws { + XCTAssertEqual(scanColor("hsva(60°, 50%, 255, 99)"), Color(red: 0xFF, green: 0xFF, blue: 128, alpha: 99)) + } + private func scanColor(_ input: String) -> Color? { let scanner = Scanner(string: input) - return scanner.color() + let result = scanner.color() + XCTAssertTrue(result == nil || scanner.isAtEnd) + return result } } From 04943d8f63b2f486a0f732d42c474c3bf3ac9884 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Fri, 1 Jan 2021 15:17:50 +0100 Subject: [PATCH 04/27] Use simplified argument list parser --- Sources/LibMakeColors/Model/Scanner+ColorParser.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift b/Sources/LibMakeColors/Model/Scanner+ColorParser.swift index c0d54d2..d114d4a 100644 --- a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift +++ b/Sources/LibMakeColors/Model/Scanner+ColorParser.swift @@ -19,11 +19,11 @@ extension Scanner { } } - if string("rgba"), string("("), let components = commaSeparated(), components.count == 4, string(")") { + if string("rgba"), let components = argumentList(4) { return Color(components) } - if string("rgb"), string("("), let components = commaSeparated(), components.count == 3, string(")") { + if string("rgb"), let components = argumentList(3) { return Color(components) } From 8262760bc733275d9a95399e143f7f85250d4bee Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Fri, 1 Jan 2021 15:18:16 +0100 Subject: [PATCH 05/27] Limit percent value from 0 to 100. --- Sources/LibMakeColors/Model/Scanner+ColorParser.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift b/Sources/LibMakeColors/Model/Scanner+ColorParser.swift index d114d4a..9870824 100644 --- a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift +++ b/Sources/LibMakeColors/Model/Scanner+ColorParser.swift @@ -182,6 +182,7 @@ extension Scanner { } if string("%") { + guard 0...100 ~= int else { return nil } int = int * 0xFF / 100 } From be2a655c405fea4ffc4786b5bdb17f19f17f89e2 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 2 Jan 2021 21:17:41 +0100 Subject: [PATCH 06/27] Match formatting used by Xcode. Turns out Xcode requires two digits for hex colors values. Single digits get repeated. --- .../Generators/AssetCatalogGenerator.swift | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift b/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift index 964c6f5..9cc8698 100644 --- a/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift +++ b/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift @@ -67,10 +67,10 @@ private extension Color { "color" : { "color-space" : "srgb", "components" : { - "alpha" : "0x\(String(alpha, radix: 16))", - "blue" : "0x\(String(blue, radix: 16))", - "green" : "0x\(String(green, radix: 16))", - "red" : "0x\(String(red, radix: 16))" + "alpha" : "\(Double(alpha) / 0xFF)", + "blue" : "0x\(blue, radix: 16, width: 2)", + "green" : "0x\(green, radix: 16, width: 2)", + "red" : "0x\(red, radix: 16, width: 2)" } }, "idiom" : "universal" @@ -88,6 +88,22 @@ private extension Color { } } +extension String.StringInterpolation { + mutating func appendInterpolation( + _ value: I, + radix: Int, + width: Int = 0, + uppercase: Bool = true + ) { + var string = String(value, radix: radix, uppercase: uppercase) + if width > string.count { + string.insert(contentsOf: String(repeating: "0", count: width - string.count), at: string.startIndex) + } + + appendLiteral(string) + } +} + private let group = """ { "properties" : { From 1c31c11f49b5dbcde7b39eeaf098baa6d348c29a Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 2 Jan 2021 22:03:41 +0100 Subject: [PATCH 07/27] Create unit test for color JSON --- Package.resolved | 9 +++++++ Package.swift | 6 ++++- .../Generators/AssetCatalogGenerator.swift | 2 +- .../AssetCatalogFormattingTest.swift | 26 +++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 Tests/MakeColorsTests/AssetCatalogFormattingTest.swift diff --git a/Package.resolved b/Package.resolved index 228d9c6..a0484d0 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "RBBJSON", + "repositoryURL": "https://github.com/robb/RBBJSON", + "state": { + "branch": "main", + "revision": "102c970283e105d7c5be2e29630db29c808c20eb", + "version": null + } + }, { "package": "swift-argument-parser", "repositoryURL": "https://github.com/apple/swift-argument-parser", diff --git a/Package.swift b/Package.swift index 47180f3..d0b3184 100644 --- a/Package.swift +++ b/Package.swift @@ -10,6 +10,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.1")), + .package(url: "https://github.com/robb/RBBJSON", .branch("main")), ], targets: [ .target( @@ -26,7 +27,10 @@ let package = Package( ), .testTarget( name: "MakeColorsTests", - dependencies: ["LibMakeColors"] + dependencies: [ + "LibMakeColors", + .product(name: "RBBJSON", package: "RBBJSON"), + ] ), ] ) diff --git a/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift b/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift index 9cc8698..f21d153 100644 --- a/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift +++ b/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift @@ -58,7 +58,7 @@ private let infoTag = """ } """ -private extension Color { +extension Color { func json() -> String { """ { diff --git a/Tests/MakeColorsTests/AssetCatalogFormattingTest.swift b/Tests/MakeColorsTests/AssetCatalogFormattingTest.swift new file mode 100644 index 0000000..4c0e8e5 --- /dev/null +++ b/Tests/MakeColorsTests/AssetCatalogFormattingTest.swift @@ -0,0 +1,26 @@ +@testable import LibMakeColors +import RBBJSON +import XCTest + +class AssetCatalogFormattingTest: XCTestCase { + func testColorProducedValidJSON() throws { + let color = Color(red: 0xFF, green: 0xF0, blue: 0x0F) + let data = Data(color.json().utf8) + + let json = try JSONDecoder().decode(RBBJSON.self, from: data) + + XCTAssertEqual(json.info.author, .string("de.5sw.MakeColors")) + XCTAssertEqual(json.info.version, .number(1)) + + XCTAssertEqual(Array(json.colors[any: .child]).count, 1) + + XCTAssertEqual(json.colors[0].idiom, .string("universal")) + + let jsonColor = json.colors[0].color + XCTAssertEqual(jsonColor["color-space"], .string("srgb")) + XCTAssertEqual(jsonColor.components.alpha, .string("1.0")) + XCTAssertEqual(jsonColor.components.red, .string("0xFF")) + XCTAssertEqual(jsonColor.components.green, .string("0xF0")) + XCTAssertEqual(jsonColor.components.blue, .string("0x0F")) + } +} From 404566c36b6487c7b21bc60cbacab37f2f7d140d Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 2 Jan 2021 22:20:55 +0100 Subject: [PATCH 08/27] Add example file --- Example/.gitignore | 2 ++ Example/Example.txt | 10 ++++++++++ 2 files changed, 12 insertions(+) create mode 100644 Example/.gitignore create mode 100644 Example/Example.txt diff --git a/Example/.gitignore b/Example/.gitignore new file mode 100644 index 0000000..5d1cee6 --- /dev/null +++ b/Example/.gitignore @@ -0,0 +1,2 @@ +Example.* +!Example.txt diff --git a/Example/Example.txt b/Example/Example.txt new file mode 100644 index 0000000..c2be7ac --- /dev/null +++ b/Example/Example.txt @@ -0,0 +1,10 @@ +Base/Green #8fd151 +Base/PaleGreen #d0f9a9 +Base/Red rgb(249, 39, 7) +TransparentRed rgba(255, 0, 0, 128) +Base/Yellow #ff0 + +Error @Base/Red +Warning @Base/Yellow +Good @Base/Green + From 78fcdef196248eca16615bd5becfd663ba764500 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 2 Jan 2021 22:22:35 +0100 Subject: [PATCH 09/27] Update asset catalog image --- Docs/assetcatalog.png | Bin 14349 -> 14267 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Docs/assetcatalog.png b/Docs/assetcatalog.png index a5107eabdabd9d7df29645dee2ff079340d15ed7..0340a76f419a3ea641b0d174f875c430548e0b35 100644 GIT binary patch delta 13926 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 Date: Sat, 2 Jan 2021 22:33:52 +0100 Subject: [PATCH 10/27] Add info on HSV colors to README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index afa53a7..3e4a710 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Base/Red rgb(249, 39, 7) TransparentRed rgba(255, 0, 0, 128) Base/Yellow #ff0 ``` + Grayscale colors can be produced with the `white(value)` and `white(value, alpha)` syntax. A value of zero means black while a value of 255 is pure white. ``` @@ -53,6 +54,12 @@ MediumGray white(128) TransparentGray white(128, 128) ``` +HSV colors can be specified as `hsv(hue, saturation, value)` or `hsva(hue, saturation, value, alpha)` syntax. Hue is specified as degrees with the `°` or `deg` suffix. + +``` +HSV/Yellow hsv(60°, 255, 255) +``` + Colors can also reference other colors by prefixing them with an `@` sign: ``` From ba7c0c04fa3fec4144a0de4393371f4bc758a3b1 Mon Sep 17 00:00:00 2001 From: Sven Date: Sat, 2 Jan 2021 22:35:45 +0100 Subject: [PATCH 11/27] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 3e4a710..b9b8980 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,6 @@ The generated HTML looks like this: ## Future work -- Support other color formats (HSV, ...) - Calculate derived colors (blend, change hue/saturation/brightness/alpha) - Support for dark/light mode - Improved error reporting in the parser From e6f44f5be27498dea59e241eff3cfbd6bba90fff Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 2 Jan 2021 22:43:26 +0100 Subject: [PATCH 12/27] Fix lint warnings --- Sources/LibMakeColors/Model/Color+HSV.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Sources/LibMakeColors/Model/Color+HSV.swift b/Sources/LibMakeColors/Model/Color+HSV.swift index 8b9383f..9251c03 100644 --- a/Sources/LibMakeColors/Model/Color+HSV.swift +++ b/Sources/LibMakeColors/Model/Color+HSV.swift @@ -2,26 +2,35 @@ extension Color { init(hue: Int, saturation: UInt8, value: UInt8, alpha: UInt8 = 0xFF) { let degrees = abs(hue % 360) - let s = Double(saturation) / 0xFF - let v = Double(value) / 0xFF - let C = s * v + let saturation = Double(saturation) / 0xFF + let value = Double(value) / 0xFF + + // swiftlint:disable identifier_name - Wish I knew what these actually mean. + let C = saturation * value let X = C * (1 - abs((Double(degrees) / 60).truncatingRemainder(dividingBy: 2) - 1)) - let m = v - C + let m = value - C + // swiftlint:enable identifier_name let result: (r: Double, g: Double, b: Double) switch degrees { case 0..<60: result = (C, X, 0) + case 60..<120: result = (X, C, 0) + case 120..<180: result = (0, C, X) + case 180..<240: result = (0, X, C) + case 240..<300: result = (X, 0, C) + case 300..<360: result = (C, 0, X) + default: fatalError("Degrees out of range") } From e28bee804f091399c94b0e15320e325924e3f995 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 11:03:28 +0200 Subject: [PATCH 13/27] Make importer protocol. --- .../LibMakeColors/Importers/Importer.swift | 5 +++ .../Importers/List/ListImporter.swift | 36 +++++++++++++++++++ .../List}/Scanner+ColorParser.swift | 4 +-- Sources/LibMakeColors/MakeColors.swift | 26 ++------------ 4 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 Sources/LibMakeColors/Importers/Importer.swift create mode 100644 Sources/LibMakeColors/Importers/List/ListImporter.swift rename Sources/LibMakeColors/{Model => Importers/List}/Scanner+ColorParser.swift (98%) diff --git a/Sources/LibMakeColors/Importers/Importer.swift b/Sources/LibMakeColors/Importers/Importer.swift new file mode 100644 index 0000000..7528edb --- /dev/null +++ b/Sources/LibMakeColors/Importers/Importer.swift @@ -0,0 +1,5 @@ +protocol Importer { + init(source: String) throws + + func read() throws -> [String: ColorDef] +} diff --git a/Sources/LibMakeColors/Importers/List/ListImporter.swift b/Sources/LibMakeColors/Importers/List/ListImporter.swift new file mode 100644 index 0000000..d0e3d50 --- /dev/null +++ b/Sources/LibMakeColors/Importers/List/ListImporter.swift @@ -0,0 +1,36 @@ +import Foundation + +struct ListImporter: Importer { + let input: String + + init(source: String) { + input = source + } + + func read() throws -> [String: ColorDef] { + let scanner = Scanner(string: try readInput()) + scanner.charactersToBeSkipped = .whitespaces + + return try scanner.colorList() + } + + func readInput() throws -> String { + if input == "-" { + return try readStdin() + } + + let url = URL(fileURLWithPath: input) + return try String(contentsOf: url) + } + + func readStdin() throws -> String { + guard + let data = try FileHandle.standardInput.readToEnd(), + let input = String(data: data, encoding: .utf8) + else { + throw Errors.cannotReadStdin + } + + return input + } +} diff --git a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift b/Sources/LibMakeColors/Importers/List/Scanner+ColorParser.swift similarity index 98% rename from Sources/LibMakeColors/Model/Scanner+ColorParser.swift rename to Sources/LibMakeColors/Importers/List/Scanner+ColorParser.swift index 9870824..b3d43d7 100644 --- a/Sources/LibMakeColors/Model/Scanner+ColorParser.swift +++ b/Sources/LibMakeColors/Importers/List/Scanner+ColorParser.swift @@ -110,7 +110,7 @@ extension Scanner { func colorLine() -> (String, ColorDef)? { guard - let name = self.name(), + let name = name(), let def = colorDef(), endOfLine() else { @@ -168,7 +168,7 @@ extension Scanner { func commaSeparated() -> [UInt8]? { var result: [UInt8] = [] repeat { - guard let component = self.component() else { + guard let component = component() else { return nil } result.append(component) diff --git a/Sources/LibMakeColors/MakeColors.swift b/Sources/LibMakeColors/MakeColors.swift index a2db649..810a7c8 100644 --- a/Sources/LibMakeColors/MakeColors.swift +++ b/Sources/LibMakeColors/MakeColors.swift @@ -66,10 +66,8 @@ public final class MakeColors: ParsableCommand, Context { public init() {} public func run() throws { - let scanner = Scanner(string: try readInput()) - scanner.charactersToBeSkipped = .whitespaces - - let data = try scanner.colorList() + let importer = ListImporter(source: input) + let data = try importer.read() if dump { try dump(data: data) @@ -81,26 +79,6 @@ public final class MakeColors: ParsableCommand, Context { try writeOutput(fileWrapper) } - func readInput() throws -> String { - if input == "-" { - return try readStdin() - } - - let url = URL(fileURLWithPath: input) - return try String(contentsOf: url) - } - - func readStdin() throws -> String { - guard - let data = try FileHandle.standardInput.readToEnd(), - let input = String(data: data, encoding: .utf8) - else { - throw Errors.cannotReadStdin - } - - return input - } - func dump(data: [String: ColorDef]) throws { for (key, color) in data.sorted() { let resolved = try data.resolve(key) From 02d2350b29a1bf69e25d9d19d169f110bf3a200e Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 11:04:24 +0200 Subject: [PATCH 14/27] Fix warnings --- Sources/LibMakeColors/Generators/Generator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/LibMakeColors/Generators/Generator.swift b/Sources/LibMakeColors/Generators/Generator.swift index 8561ac3..085021b 100644 --- a/Sources/LibMakeColors/Generators/Generator.swift +++ b/Sources/LibMakeColors/Generators/Generator.swift @@ -1,6 +1,6 @@ import Foundation -protocol Generator: class { +protocol Generator: AnyObject { static var defaultExtension: String { get } static var option: String { get } @@ -9,7 +9,7 @@ protocol Generator: class { func generate(data: [String: ColorDef]) throws -> FileWrapper } -protocol Context: class { +protocol Context: AnyObject { var prefix: String? { get } } From dc00d87a32cd7ca9992b626faf4bb94edac50641 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 11:20:00 +0200 Subject: [PATCH 15/27] Add option to select importer. --- .../LibMakeColors/Importers/Importer.swift | 8 +++++ Sources/LibMakeColors/MakeColors.swift | 34 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Sources/LibMakeColors/Importers/Importer.swift b/Sources/LibMakeColors/Importers/Importer.swift index 7528edb..3fb9da2 100644 --- a/Sources/LibMakeColors/Importers/Importer.swift +++ b/Sources/LibMakeColors/Importers/Importer.swift @@ -2,4 +2,12 @@ protocol Importer { init(source: String) throws func read() throws -> [String: ColorDef] + + static var option: String { get } +} + +extension Importer { + static var option: String { + String(String(describing: self).droppingSuffix("Importer")) + } } diff --git a/Sources/LibMakeColors/MakeColors.swift b/Sources/LibMakeColors/MakeColors.swift index 810a7c8..cfe0fea 100644 --- a/Sources/LibMakeColors/MakeColors.swift +++ b/Sources/LibMakeColors/MakeColors.swift @@ -19,6 +19,35 @@ private struct GeneratorOption: EnumerableFlag, CustomStringConvertible { } } +private struct ImporterOption: CaseIterable, ExpressibleByArgument, CustomStringConvertible { + static let allCases: [ImporterOption] = [ + .list, + ] + + static let list = ImporterOption(type: ListImporter.self) + + let type: Importer.Type + + init(type: Importer.Type) { + self.type = type + } + + init?(argument: String) { + guard let found = Self.allCases.first(where: { $0.description.caseInsensitiveCompare(argument) == .orderedSame }) else { + return nil + } + self = found + } + + var description: String { + type.option + } + + static func == (lhs: ImporterOption, rhs: ImporterOption) -> Bool { + lhs.type == rhs.type + } +} + enum Errors: Error { case syntaxError case duplicateColor(String) @@ -54,6 +83,9 @@ public final class MakeColors: ParsableCommand, Context { @Flag(help: "The formatter to use.") private var formatter = GeneratorOption.allCases[0] + @Option(help: "The importer to use.") + private var importer = ImporterOption.list + @Option(help: "Prefix for color names.") var prefix: String? @@ -66,7 +98,7 @@ public final class MakeColors: ParsableCommand, Context { public init() {} public func run() throws { - let importer = ListImporter(source: input) + let importer = try importer.type.init(source: input) let data = try importer.read() if dump { From d7d34812cf00830b71d285a427a5abcb2d4c8dc7 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 11:42:02 +0200 Subject: [PATCH 16/27] Format --- Sources/LibMakeColors/MakeColors.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/LibMakeColors/MakeColors.swift b/Sources/LibMakeColors/MakeColors.swift index cfe0fea..949bc52 100644 --- a/Sources/LibMakeColors/MakeColors.swift +++ b/Sources/LibMakeColors/MakeColors.swift @@ -33,7 +33,10 @@ private struct ImporterOption: CaseIterable, ExpressibleByArgument, CustomString } init?(argument: String) { - guard let found = Self.allCases.first(where: { $0.description.caseInsensitiveCompare(argument) == .orderedSame }) else { + guard + let found = Self.allCases + .first(where: { $0.description.caseInsensitiveCompare(argument) == .orderedSame }) + else { return nil } self = found From 012203a4f367ef36437557b43fca14ba7ff74c35 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 11:42:42 +0200 Subject: [PATCH 17/27] Lowercase importer names --- Sources/LibMakeColors/Importers/Importer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/LibMakeColors/Importers/Importer.swift b/Sources/LibMakeColors/Importers/Importer.swift index 3fb9da2..426fec8 100644 --- a/Sources/LibMakeColors/Importers/Importer.swift +++ b/Sources/LibMakeColors/Importers/Importer.swift @@ -8,6 +8,6 @@ protocol Importer { extension Importer { static var option: String { - String(String(describing: self).droppingSuffix("Importer")) + String(describing: self).droppingSuffix("Importer").lowercased() } } From 0bb53ea949764cb8ada68284746dd7ddf763833f Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 11:46:52 +0200 Subject: [PATCH 18/27] Remove library target --- Package.swift | 14 ++++---------- .../Extensions/FileWrapper+Extensions.swift | 0 .../Extensions/StringProtocol+Extensions.swift | 0 .../Generators/AndroidGenerator.swift | 0 .../Generators/AssetCatalogGenerator.swift | 0 .../Generators/Generator.swift | 0 .../Generators/HTMLGenerator.swift | 0 .../Importers/Importer.swift | 0 .../Importers/List/ListImporter.swift | 0 .../Importers/List/Scanner+ColorParser.swift | 0 .../{LibMakeColors => MakeColors}/MakeColors.swift | 1 + .../Model/Color+HSV.swift | 0 .../Model/Color.swift | 0 Sources/MakeColors/main.swift | 3 --- .../AssetCatalogFormattingTest.swift | 2 +- Tests/MakeColorsTests/ColorHSVTest.swift | 2 +- Tests/MakeColorsTests/ColorParserTest.swift | 2 +- 17 files changed, 8 insertions(+), 16 deletions(-) rename Sources/{LibMakeColors => MakeColors}/Extensions/FileWrapper+Extensions.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Extensions/StringProtocol+Extensions.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Generators/AndroidGenerator.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Generators/AssetCatalogGenerator.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Generators/Generator.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Generators/HTMLGenerator.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Importers/Importer.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Importers/List/ListImporter.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Importers/List/Scanner+ColorParser.swift (100%) rename Sources/{LibMakeColors => MakeColors}/MakeColors.swift (99%) rename Sources/{LibMakeColors => MakeColors}/Model/Color+HSV.swift (100%) rename Sources/{LibMakeColors => MakeColors}/Model/Color.swift (100%) delete mode 100644 Sources/MakeColors/main.swift diff --git a/Package.swift b/Package.swift index d0b3184..f6195f4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -10,17 +10,11 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.1")), - .package(url: "https://github.com/robb/RBBJSON", .branch("main")), + .package(url: "https://github.com/robb/RBBJSON", branch: "main"), ], targets: [ - .target( + .executableTarget( name: "MakeColors", - dependencies: [ - "LibMakeColors", - ] - ), - .target( - name: "LibMakeColors", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), ] @@ -28,7 +22,7 @@ let package = Package( .testTarget( name: "MakeColorsTests", dependencies: [ - "LibMakeColors", + "MakeColors", .product(name: "RBBJSON", package: "RBBJSON"), ] ), diff --git a/Sources/LibMakeColors/Extensions/FileWrapper+Extensions.swift b/Sources/MakeColors/Extensions/FileWrapper+Extensions.swift similarity index 100% rename from Sources/LibMakeColors/Extensions/FileWrapper+Extensions.swift rename to Sources/MakeColors/Extensions/FileWrapper+Extensions.swift diff --git a/Sources/LibMakeColors/Extensions/StringProtocol+Extensions.swift b/Sources/MakeColors/Extensions/StringProtocol+Extensions.swift similarity index 100% rename from Sources/LibMakeColors/Extensions/StringProtocol+Extensions.swift rename to Sources/MakeColors/Extensions/StringProtocol+Extensions.swift diff --git a/Sources/LibMakeColors/Generators/AndroidGenerator.swift b/Sources/MakeColors/Generators/AndroidGenerator.swift similarity index 100% rename from Sources/LibMakeColors/Generators/AndroidGenerator.swift rename to Sources/MakeColors/Generators/AndroidGenerator.swift diff --git a/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift b/Sources/MakeColors/Generators/AssetCatalogGenerator.swift similarity index 100% rename from Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift rename to Sources/MakeColors/Generators/AssetCatalogGenerator.swift diff --git a/Sources/LibMakeColors/Generators/Generator.swift b/Sources/MakeColors/Generators/Generator.swift similarity index 100% rename from Sources/LibMakeColors/Generators/Generator.swift rename to Sources/MakeColors/Generators/Generator.swift diff --git a/Sources/LibMakeColors/Generators/HTMLGenerator.swift b/Sources/MakeColors/Generators/HTMLGenerator.swift similarity index 100% rename from Sources/LibMakeColors/Generators/HTMLGenerator.swift rename to Sources/MakeColors/Generators/HTMLGenerator.swift diff --git a/Sources/LibMakeColors/Importers/Importer.swift b/Sources/MakeColors/Importers/Importer.swift similarity index 100% rename from Sources/LibMakeColors/Importers/Importer.swift rename to Sources/MakeColors/Importers/Importer.swift diff --git a/Sources/LibMakeColors/Importers/List/ListImporter.swift b/Sources/MakeColors/Importers/List/ListImporter.swift similarity index 100% rename from Sources/LibMakeColors/Importers/List/ListImporter.swift rename to Sources/MakeColors/Importers/List/ListImporter.swift diff --git a/Sources/LibMakeColors/Importers/List/Scanner+ColorParser.swift b/Sources/MakeColors/Importers/List/Scanner+ColorParser.swift similarity index 100% rename from Sources/LibMakeColors/Importers/List/Scanner+ColorParser.swift rename to Sources/MakeColors/Importers/List/Scanner+ColorParser.swift diff --git a/Sources/LibMakeColors/MakeColors.swift b/Sources/MakeColors/MakeColors.swift similarity index 99% rename from Sources/LibMakeColors/MakeColors.swift rename to Sources/MakeColors/MakeColors.swift index 949bc52..3254d4e 100644 --- a/Sources/LibMakeColors/MakeColors.swift +++ b/Sources/MakeColors/MakeColors.swift @@ -79,6 +79,7 @@ enum HelpTexts { ) } +@main public final class MakeColors: ParsableCommand, Context { @Argument(help: HelpTexts.input) var input: String diff --git a/Sources/LibMakeColors/Model/Color+HSV.swift b/Sources/MakeColors/Model/Color+HSV.swift similarity index 100% rename from Sources/LibMakeColors/Model/Color+HSV.swift rename to Sources/MakeColors/Model/Color+HSV.swift diff --git a/Sources/LibMakeColors/Model/Color.swift b/Sources/MakeColors/Model/Color.swift similarity index 100% rename from Sources/LibMakeColors/Model/Color.swift rename to Sources/MakeColors/Model/Color.swift diff --git a/Sources/MakeColors/main.swift b/Sources/MakeColors/main.swift deleted file mode 100644 index db25b90..0000000 --- a/Sources/MakeColors/main.swift +++ /dev/null @@ -1,3 +0,0 @@ -import LibMakeColors - -MakeColors.main() diff --git a/Tests/MakeColorsTests/AssetCatalogFormattingTest.swift b/Tests/MakeColorsTests/AssetCatalogFormattingTest.swift index 4c0e8e5..d4f9f10 100644 --- a/Tests/MakeColorsTests/AssetCatalogFormattingTest.swift +++ b/Tests/MakeColorsTests/AssetCatalogFormattingTest.swift @@ -1,4 +1,4 @@ -@testable import LibMakeColors +@testable import MakeColors import RBBJSON import XCTest diff --git a/Tests/MakeColorsTests/ColorHSVTest.swift b/Tests/MakeColorsTests/ColorHSVTest.swift index d7bd9dc..9eba183 100644 --- a/Tests/MakeColorsTests/ColorHSVTest.swift +++ b/Tests/MakeColorsTests/ColorHSVTest.swift @@ -1,4 +1,4 @@ -@testable import LibMakeColors +@testable import MakeColors import XCTest final class ColorHSVTest: XCTestCase { diff --git a/Tests/MakeColorsTests/ColorParserTest.swift b/Tests/MakeColorsTests/ColorParserTest.swift index 075c72d..8c3756e 100644 --- a/Tests/MakeColorsTests/ColorParserTest.swift +++ b/Tests/MakeColorsTests/ColorParserTest.swift @@ -1,4 +1,4 @@ -@testable import LibMakeColors +@testable import MakeColors import XCTest final class ColorParserTest: XCTestCase { From 3adefbf70e1291aaa645a37bb3200b9ebc0164c6 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 11:47:32 +0200 Subject: [PATCH 19/27] Update argument parser --- Package.resolved | 42 ++++++++++++++++++++---------------------- Package.swift | 2 +- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/Package.resolved b/Package.resolved index a0484d0..717a74e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,25 +1,23 @@ { - "object": { - "pins": [ - { - "package": "RBBJSON", - "repositoryURL": "https://github.com/robb/RBBJSON", - "state": { - "branch": "main", - "revision": "102c970283e105d7c5be2e29630db29c808c20eb", - "version": null - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser", - "state": { - "branch": null, - "revision": "92646c0cdbaca076c8d3d0207891785b3379cbff", - "version": "0.3.1" - } + "pins" : [ + { + "identity" : "rbbjson", + "kind" : "remoteSourceControl", + "location" : "https://github.com/robb/RBBJSON", + "state" : { + "branch" : "main", + "revision" : "102c970283e105d7c5be2e29630db29c808c20eb" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1", + "version" : "1.1.4" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index f6195f4..0c21422 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let package = Package( .macOS("10.15.4"), ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.1")), + .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.1.4")), .package(url: "https://github.com/robb/RBBJSON", branch: "main"), ], targets: [ From 956c1f5d2766cd0968858af9ec2fef2c196021eb Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 11:48:33 +0200 Subject: [PATCH 20/27] Make importer async --- Sources/MakeColors/Importers/Importer.swift | 2 +- Sources/MakeColors/MakeColors.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/MakeColors/Importers/Importer.swift b/Sources/MakeColors/Importers/Importer.swift index 426fec8..5ba54c2 100644 --- a/Sources/MakeColors/Importers/Importer.swift +++ b/Sources/MakeColors/Importers/Importer.swift @@ -1,7 +1,7 @@ protocol Importer { init(source: String) throws - func read() throws -> [String: ColorDef] + func read() async throws -> [String: ColorDef] static var option: String { get } } diff --git a/Sources/MakeColors/MakeColors.swift b/Sources/MakeColors/MakeColors.swift index 3254d4e..c19563f 100644 --- a/Sources/MakeColors/MakeColors.swift +++ b/Sources/MakeColors/MakeColors.swift @@ -80,7 +80,7 @@ enum HelpTexts { } @main -public final class MakeColors: ParsableCommand, Context { +public final class MakeColors: AsyncParsableCommand, Context { @Argument(help: HelpTexts.input) var input: String @@ -101,9 +101,9 @@ public final class MakeColors: ParsableCommand, Context { public init() {} - public func run() throws { + public func run() async throws { let importer = try importer.type.init(source: input) - let data = try importer.read() + let data = try await importer.read() if dump { try dump(data: data) From 2b1f21dbab08c36266aed891aeeee76fc5220f95 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 12:41:02 +0200 Subject: [PATCH 21/27] Quick and dirty figma importer --- Package.swift | 2 +- .../Importers/Figma/FigmaImporter.swift | 149 ++++++++++++++++++ Sources/MakeColors/MakeColors.swift | 1 + 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 Sources/MakeColors/Importers/Figma/FigmaImporter.swift diff --git a/Package.swift b/Package.swift index 0c21422..05a09d5 100644 --- a/Package.swift +++ b/Package.swift @@ -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")), diff --git a/Sources/MakeColors/Importers/Figma/FigmaImporter.swift b/Sources/MakeColors/Importers/Figma/FigmaImporter.swift new file mode 100644 index 0000000..488f4d8 --- /dev/null +++ b/Sources/MakeColors/Importers/Figma/FigmaImporter.swift @@ -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.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)) + } +} diff --git a/Sources/MakeColors/MakeColors.swift b/Sources/MakeColors/MakeColors.swift index c19563f..12a5ff3 100644 --- a/Sources/MakeColors/MakeColors.swift +++ b/Sources/MakeColors/MakeColors.swift @@ -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) From ac708f4ae2497c0357333cb5f33b84658ee0a5ef Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 12:50:55 +0200 Subject: [PATCH 22/27] Let importer determine output file name. --- .../Importers/Figma/FigmaImporter.swift | 2 ++ Sources/MakeColors/Importers/Importer.swift | 2 ++ .../MakeColors/Importers/List/ListImporter.swift | 2 ++ Sources/MakeColors/MakeColors.swift | 15 +++------------ 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Sources/MakeColors/Importers/Figma/FigmaImporter.swift b/Sources/MakeColors/Importers/Figma/FigmaImporter.swift index 488f4d8..c84339f 100644 --- a/Sources/MakeColors/Importers/Figma/FigmaImporter.swift +++ b/Sources/MakeColors/Importers/Figma/FigmaImporter.swift @@ -10,6 +10,7 @@ enum FigmaErrors: Error { class FigmaImporter: Importer { let key: String let token: String + let outputName: String required init(source: String) throws { // https://www.figma.com/file/:key/:title @@ -23,6 +24,7 @@ class FigmaImporter: Importer { } key = url.pathComponents[2] + outputName = url.pathComponents[3] guard let token = ProcessInfo.processInfo.environment["FIGMA_TOKEN"] else { throw FigmaErrors.missingToken diff --git a/Sources/MakeColors/Importers/Importer.swift b/Sources/MakeColors/Importers/Importer.swift index 5ba54c2..6c4d5d8 100644 --- a/Sources/MakeColors/Importers/Importer.swift +++ b/Sources/MakeColors/Importers/Importer.swift @@ -3,6 +3,8 @@ protocol Importer { func read() async throws -> [String: ColorDef] + var outputName: String { get } + static var option: String { get } } diff --git a/Sources/MakeColors/Importers/List/ListImporter.swift b/Sources/MakeColors/Importers/List/ListImporter.swift index d0e3d50..ef291b1 100644 --- a/Sources/MakeColors/Importers/List/ListImporter.swift +++ b/Sources/MakeColors/Importers/List/ListImporter.swift @@ -2,9 +2,11 @@ import Foundation struct ListImporter: Importer { let input: String + var outputName: String init(source: String) { input = source + outputName = URL(fileURLWithPath: source).deletingPathExtension().lastPathComponent } func read() throws -> [String: ColorDef] { diff --git a/Sources/MakeColors/MakeColors.swift b/Sources/MakeColors/MakeColors.swift index 12a5ff3..8b88849 100644 --- a/Sources/MakeColors/MakeColors.swift +++ b/Sources/MakeColors/MakeColors.swift @@ -113,7 +113,7 @@ public final class MakeColors: AsyncParsableCommand, Context { let generator = formatter.type.init(context: self) let fileWrapper = try generator.generate(data: data) - try writeOutput(fileWrapper) + try writeOutput(fileWrapper, name: output ?? "\(importer.outputName).\(formatter.type.defaultExtension)") } func dump(data: [String: ColorDef]) throws { @@ -133,7 +133,7 @@ public final class MakeColors: AsyncParsableCommand, Context { } } - func writeOutput(_ wrapper: FileWrapper) throws { + func writeOutput(_ wrapper: FileWrapper, name: String) throws { if shouldWriteToStdout { guard wrapper.isRegularFile, let contents = wrapper.regularFileContents else { throw Errors.cannotWriteWrapperToStdout @@ -141,19 +141,10 @@ public final class MakeColors: AsyncParsableCommand, Context { FileHandle.standardOutput.write(contents) } else { - let writeURL = outputURL(extension: formatter.type.defaultExtension) + let writeURL = URL(fileURLWithPath: name) try wrapper.write(to: writeURL, options: .atomic, originalContentsURL: nil) } } var shouldWriteToStdout: Bool { output == "-" || (input == "-" && output == nil) } - - func outputURL(extension: String) -> URL { - if let output = output { - return URL(fileURLWithPath: output) - } else { - let basename = URL(fileURLWithPath: input).deletingPathExtension().lastPathComponent - return URL(fileURLWithPath: basename).appendingPathExtension(`extension`) - } - } } From 4a489ccd20396c5e760213e9baba53463a3e617f Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 12:59:18 +0200 Subject: [PATCH 23/27] Update .swift-version --- .swift-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swift-version b/.swift-version index d346e2a..760606e 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.3 +5.7 From fe95a6fd35e7894b755dd2bac03323304c030f07 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 13:00:03 +0200 Subject: [PATCH 24/27] Need Xcode 14 to build --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36deebd..961f0ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: head "https://github.com/${{ github.repository }}.git" license "MIT" - depends_on :xcode => ["12.0", :build] + depends_on :xcode => ["14.0", :build] def install system "make", "install", "prefix=#{prefix}" From 2271198f471971adf162d2c02f7f48110a7444c4 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 13:04:59 +0200 Subject: [PATCH 25/27] Select Xcode 14 --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be00eaa..7331788 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,10 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v1 + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '14.0' - name: Cache Swift packages uses: actions/cache@v2 From 028095f00e973b8df40aebe361f7009834a9c319 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 13:08:03 +0200 Subject: [PATCH 26/27] Use macos-12 image --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7331788..28f1422 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ on: jobs: BuildAndTest: - runs-on: macos-latest + runs-on: macos-12 steps: - uses: actions/checkout@v1 From fa9da2263367cb1d7b57c59f8b4fc86b8bdb76cb Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Sat, 8 Oct 2022 13:09:40 +0200 Subject: [PATCH 27/27] Remove setup-xcode, should not be needed any more --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28f1422..3b150ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,6 @@ jobs: runs-on: macos-12 steps: - uses: actions/checkout@v1 - - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '14.0' - name: Cache Swift packages uses: actions/cache@v2