Compare commits
No commits in common. "main" and "v0.3.0" have entirely different histories.
23 changed files with 90 additions and 297 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
||||||
head "https://github.com/${{ github.repository }}.git"
|
head "https://github.com/${{ github.repository }}.git"
|
||||||
license "MIT"
|
license "MIT"
|
||||||
|
|
||||||
depends_on :xcode => ["14.0", :build]
|
depends_on :xcode => ["12.0", :build]
|
||||||
|
|
||||||
def install
|
def install
|
||||||
system "make", "install", "prefix=#{prefix}"
|
system "make", "install", "prefix=#{prefix}"
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
BuildAndTest:
|
BuildAndTest:
|
||||||
runs-on: macos-12
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
5.7
|
5.3
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
{
|
{
|
||||||
"pins" : [
|
"object": {
|
||||||
|
"pins": [
|
||||||
{
|
{
|
||||||
"identity" : "rbbjson",
|
"package": "RBBJSON",
|
||||||
"kind" : "remoteSourceControl",
|
"repositoryURL": "https://github.com/robb/RBBJSON",
|
||||||
"location" : "https://github.com/robb/RBBJSON",
|
"state": {
|
||||||
"state" : {
|
"branch": "main",
|
||||||
"branch" : "main",
|
"revision": "102c970283e105d7c5be2e29630db29c808c20eb",
|
||||||
"revision" : "102c970283e105d7c5be2e29630db29c808c20eb"
|
"version": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-argument-parser",
|
"package": "swift-argument-parser",
|
||||||
"kind" : "remoteSourceControl",
|
"repositoryURL": "https://github.com/apple/swift-argument-parser",
|
||||||
"location" : "https://github.com/apple/swift-argument-parser",
|
"state": {
|
||||||
"state" : {
|
"branch": null,
|
||||||
"revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1",
|
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
|
||||||
"version" : "1.1.4"
|
"version": "0.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"version" : 2
|
},
|
||||||
|
"version": 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version:5.7
|
// swift-tools-version:5.3
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,15 +6,21 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "MakeColors",
|
name: "MakeColors",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS("12.0"),
|
.macOS("10.15.4"),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.1.4")),
|
.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: [
|
targets: [
|
||||||
.executableTarget(
|
.target(
|
||||||
name: "MakeColors",
|
name: "MakeColors",
|
||||||
|
dependencies: [
|
||||||
|
"LibMakeColors",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "LibMakeColors",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
]
|
]
|
||||||
|
@ -22,7 +28,7 @@ let package = Package(
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "MakeColorsTests",
|
name: "MakeColorsTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"MakeColors",
|
"LibMakeColors",
|
||||||
.product(name: "RBBJSON", package: "RBBJSON"),
|
.product(name: "RBBJSON", package: "RBBJSON"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
|
@ -109,6 +109,7 @@ The generated HTML looks like this:
|
||||||
|
|
||||||
## Future work
|
## Future work
|
||||||
|
|
||||||
|
- Support other color formats (HSV, ...)
|
||||||
- Calculate derived colors (blend, change hue/saturation/brightness/alpha)
|
- Calculate derived colors (blend, change hue/saturation/brightness/alpha)
|
||||||
- Support for dark/light mode
|
- Support for dark/light mode
|
||||||
- Improved error reporting in the parser
|
- Improved error reporting in the parser
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol Generator: AnyObject {
|
protocol Generator: class {
|
||||||
static var defaultExtension: String { get }
|
static var defaultExtension: String { get }
|
||||||
static var option: String { get }
|
static var option: String { get }
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ protocol Generator: AnyObject {
|
||||||
func generate(data: [String: ColorDef]) throws -> FileWrapper
|
func generate(data: [String: ColorDef]) throws -> FileWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol Context: AnyObject {
|
protocol Context: class {
|
||||||
var prefix: String? { get }
|
var prefix: String? { get }
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,39 +19,6 @@ 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)
|
|
||||||
|
|
||||||
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 {
|
enum Errors: Error {
|
||||||
case syntaxError
|
case syntaxError
|
||||||
case duplicateColor(String)
|
case duplicateColor(String)
|
||||||
|
@ -80,17 +47,13 @@ enum HelpTexts {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@main
|
public final class MakeColors: ParsableCommand, Context {
|
||||||
public final class MakeColors: AsyncParsableCommand, Context {
|
|
||||||
@Argument(help: HelpTexts.input)
|
@Argument(help: HelpTexts.input)
|
||||||
var input: String
|
var input: String
|
||||||
|
|
||||||
@Flag(help: "The formatter to use.")
|
@Flag(help: "The formatter to use.")
|
||||||
private var formatter = GeneratorOption.allCases[0]
|
private var formatter = GeneratorOption.allCases[0]
|
||||||
|
|
||||||
@Option(help: "The importer to use.")
|
|
||||||
private var importer = ImporterOption.list
|
|
||||||
|
|
||||||
@Option(help: "Prefix for color names.")
|
@Option(help: "Prefix for color names.")
|
||||||
var prefix: String?
|
var prefix: String?
|
||||||
|
|
||||||
|
@ -102,9 +65,11 @@ public final class MakeColors: AsyncParsableCommand, Context {
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public func run() async throws {
|
public func run() throws {
|
||||||
let importer = try importer.type.init(source: input)
|
let scanner = Scanner(string: try readInput())
|
||||||
let data = try await importer.read()
|
scanner.charactersToBeSkipped = .whitespaces
|
||||||
|
|
||||||
|
let data = try scanner.colorList()
|
||||||
|
|
||||||
if dump {
|
if dump {
|
||||||
try dump(data: data)
|
try dump(data: data)
|
||||||
|
@ -113,7 +78,27 @@ public final class MakeColors: AsyncParsableCommand, Context {
|
||||||
let generator = formatter.type.init(context: self)
|
let generator = formatter.type.init(context: self)
|
||||||
let fileWrapper = try generator.generate(data: data)
|
let fileWrapper = try generator.generate(data: data)
|
||||||
|
|
||||||
try writeOutput(fileWrapper, name: output ?? "\(importer.outputName).\(formatter.type.defaultExtension)")
|
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 {
|
func dump(data: [String: ColorDef]) throws {
|
||||||
|
@ -133,7 +118,7 @@ public final class MakeColors: AsyncParsableCommand, Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeOutput(_ wrapper: FileWrapper, name: String) throws {
|
func writeOutput(_ wrapper: FileWrapper) throws {
|
||||||
if shouldWriteToStdout {
|
if shouldWriteToStdout {
|
||||||
guard wrapper.isRegularFile, let contents = wrapper.regularFileContents else {
|
guard wrapper.isRegularFile, let contents = wrapper.regularFileContents else {
|
||||||
throw Errors.cannotWriteWrapperToStdout
|
throw Errors.cannotWriteWrapperToStdout
|
||||||
|
@ -141,10 +126,19 @@ public final class MakeColors: AsyncParsableCommand, Context {
|
||||||
|
|
||||||
FileHandle.standardOutput.write(contents)
|
FileHandle.standardOutput.write(contents)
|
||||||
} else {
|
} else {
|
||||||
let writeURL = URL(fileURLWithPath: name)
|
let writeURL = outputURL(extension: formatter.type.defaultExtension)
|
||||||
try wrapper.write(to: writeURL, options: .atomic, originalContentsURL: nil)
|
try wrapper.write(to: writeURL, options: .atomic, originalContentsURL: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldWriteToStdout: Bool { output == "-" || (input == "-" && output == 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`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,35 +2,26 @@ extension Color {
|
||||||
init(hue: Int, saturation: UInt8, value: UInt8, alpha: UInt8 = 0xFF) {
|
init(hue: Int, saturation: UInt8, value: UInt8, alpha: UInt8 = 0xFF) {
|
||||||
let degrees = abs(hue % 360)
|
let degrees = abs(hue % 360)
|
||||||
|
|
||||||
let saturation = Double(saturation) / 0xFF
|
let s = Double(saturation) / 0xFF
|
||||||
let value = Double(value) / 0xFF
|
let v = Double(value) / 0xFF
|
||||||
|
let C = s * v
|
||||||
// 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 X = C * (1 - abs((Double(degrees) / 60).truncatingRemainder(dividingBy: 2) - 1))
|
||||||
let m = value - C
|
let m = v - C
|
||||||
// swiftlint:enable identifier_name
|
|
||||||
|
|
||||||
let result: (r: Double, g: Double, b: Double)
|
let result: (r: Double, g: Double, b: Double)
|
||||||
switch degrees {
|
switch degrees {
|
||||||
case 0..<60:
|
case 0..<60:
|
||||||
result = (C, X, 0)
|
result = (C, X, 0)
|
||||||
|
|
||||||
case 60..<120:
|
case 60..<120:
|
||||||
result = (X, C, 0)
|
result = (X, C, 0)
|
||||||
|
|
||||||
case 120..<180:
|
case 120..<180:
|
||||||
result = (0, C, X)
|
result = (0, C, X)
|
||||||
|
|
||||||
case 180..<240:
|
case 180..<240:
|
||||||
result = (0, X, C)
|
result = (0, X, C)
|
||||||
|
|
||||||
case 240..<300:
|
case 240..<300:
|
||||||
result = (X, 0, C)
|
result = (X, 0, C)
|
||||||
|
|
||||||
case 300..<360:
|
case 300..<360:
|
||||||
result = (C, 0, X)
|
result = (C, 0, X)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
fatalError("Degrees out of range")
|
fatalError("Degrees out of range")
|
||||||
}
|
}
|
|
@ -110,7 +110,7 @@ extension Scanner {
|
||||||
|
|
||||||
func colorLine() -> (String, ColorDef)? {
|
func colorLine() -> (String, ColorDef)? {
|
||||||
guard
|
guard
|
||||||
let name = name(),
|
let name = self.name(),
|
||||||
let def = colorDef(),
|
let def = colorDef(),
|
||||||
endOfLine()
|
endOfLine()
|
||||||
else {
|
else {
|
||||||
|
@ -168,7 +168,7 @@ extension Scanner {
|
||||||
func commaSeparated() -> [UInt8]? {
|
func commaSeparated() -> [UInt8]? {
|
||||||
var result: [UInt8] = []
|
var result: [UInt8] = []
|
||||||
repeat {
|
repeat {
|
||||||
guard let component = component() else {
|
guard let component = self.component() else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
result.append(component)
|
result.append(component)
|
|
@ -1,151 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum FigmaErrors: Error {
|
|
||||||
case invalidUrl
|
|
||||||
case missingToken
|
|
||||||
case invalidResponse
|
|
||||||
case missingColor(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
class FigmaImporter: Importer {
|
|
||||||
let key: String
|
|
||||||
let token: String
|
|
||||||
let outputName: 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]
|
|
||||||
outputName = url.pathComponents[3]
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
protocol Importer {
|
|
||||||
init(source: String) throws
|
|
||||||
|
|
||||||
func read() async throws -> [String: ColorDef]
|
|
||||||
|
|
||||||
var outputName: String { get }
|
|
||||||
|
|
||||||
static var option: String { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Importer {
|
|
||||||
static var option: String {
|
|
||||||
String(describing: self).droppingSuffix("Importer").lowercased()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
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] {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
3
Sources/MakeColors/main.swift
Normal file
3
Sources/MakeColors/main.swift
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import LibMakeColors
|
||||||
|
|
||||||
|
MakeColors.main()
|
|
@ -1,4 +1,4 @@
|
||||||
@testable import MakeColors
|
@testable import LibMakeColors
|
||||||
import RBBJSON
|
import RBBJSON
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@testable import MakeColors
|
@testable import LibMakeColors
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class ColorHSVTest: XCTestCase {
|
final class ColorHSVTest: XCTestCase {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@testable import MakeColors
|
@testable import LibMakeColors
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class ColorParserTest: XCTestCase {
|
final class ColorParserTest: XCTestCase {
|
||||||
|
|
Loading…
Add table
Reference in a new issue