Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

29 changed files with 163 additions and 849 deletions

View file

@ -2,39 +2,31 @@ on:
release:
types: [created]
name: Create new formula version
name: Upload tarball for release
jobs:
upload-release:
name: Create new formula version
name: Upload Release Asset
runs-on: ubuntu-latest
steps:
- name: Checkout formula repo
uses: actions/checkout@v2
with:
repository: ${{ github.repository_owner }}/homebrew-makecolors
token: ${{ secrets.PUBLISH_FORMULA_TOKEN }}
- name: Update formula
- name: Set release tarball name
run: |
cat << EOF > Formula/make-colors.rb
class MakeColors < Formula
desc "Converts a simple list of color definitions to asset catalogs for Xcode and resource XML for Android"
homepage "https://github.com/${{ github.repository }}"
url "https://github.com/${{ github.repository }}.git", :tag => "${{ github.event.release.tag_name }}", :revision => "${{ github.sha }}"
head "https://github.com/${{ github.repository }}.git"
license "MIT"
echo "TARBALL_NAME=$(echo MakeColors-${GITHUB_REF##*/v})" >> $GITHUB_ENV
depends_on :xcode => ["14.0", :build]
- name: Checkout code
uses: actions/checkout@v2
def install
system "make", "install", "prefix=#{prefix}"
end
end
EOF
- name: Pack tarball
run: |
git archive HEAD --prefix=${{ env.TARBALL_NAME }}/ | bzip2 > ${{ env.TARBALL_NAME }}.tar.bz2
git config user.name github-actions
git config user.email github-actions@github.com
git add Formula/make-colors.rb
git commit -m "Update formula for ${{ github.event.release.tag_name }}"
git push
- name: Upload Release Asset
id: upload
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./${{ env.TARBALL_NAME }}.tar.bz2
asset_name: ${{ env.TARBALL_NAME }}.tar.bz2
asset_content_type: application/x-bzip2

View file

@ -5,7 +5,7 @@ on:
jobs:
BuildAndTest:
runs-on: macos-12
runs-on: macos-latest
steps:
- uses: actions/checkout@v1

View file

@ -1 +1 @@
5.7
5.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

2
Example/.gitignore vendored
View file

@ -1,2 +0,0 @@
Example.*
!Example.txt

View file

@ -1,10 +0,0 @@
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

View file

@ -1,17 +0,0 @@
prefix ?= /usr/local
bindir = $(prefix)/bin
build:
swift build -c release --disable-sandbox
install: build
install -d "$(bindir)"
install ".build/release/MakeColors" "$(bindir)/make-colors"
uninstall:
rm -rf "$(bindir)/make-colors"
clean:
rm -rf .build
.PHONY: build install uninstall clean

View file

@ -1,23 +1,16 @@
{
"pins" : [
{
"identity" : "rbbjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/robb/RBBJSON",
"state" : {
"branch" : "main",
"revision" : "102c970283e105d7c5be2e29630db29c808c20eb"
"object": {
"pins": [
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
"version": "0.3.1"
}
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1",
"version" : "1.1.4"
}
}
],
"version" : 2
]
},
"version": 1
}

View file

@ -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.
import PackageDescription
@ -6,25 +6,27 @@ import PackageDescription
let package = Package(
name: "MakeColors",
platforms: [
.macOS("12.0"),
.macOS(.v10_15),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.1.4")),
.package(url: "https://github.com/robb/RBBJSON", branch: "main"),
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.1")),
],
targets: [
.executableTarget(
.target(
name: "MakeColors",
dependencies: [
"LibMakeColors",
]
),
.target(
name: "LibMakeColors",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.testTarget(
name: "MakeColorsTests",
dependencies: [
"MakeColors",
.product(name: "RBBJSON", package: "RBBJSON"),
]
dependencies: ["LibMakeColors"]
),
]
)

View file

@ -2,70 +2,21 @@
Converts a simple list of color definitions to asset catalogs for Xcode, resource XML for Android or an HTML preview.
## Installation
Install via [Homebrew](https://brew.sh):
```
brew tap 5sw/makecolors
brew install make-colors
```
If you dont use Homebrew you can also install directly from source. Clone the repository or download the release and run `make install` inside the working copy.
## Usage
```
USAGE: make-colors <input> [--ios] [--android] [--html] [--prefix <prefix>] [--output <output>] [--dump]
ARGUMENTS:
<input> The color list to process.
Use - to process the standard input.
OPTIONS:
--ios/--android/--html The formatter to use. (default: ios)
--prefix <prefix> Prefix for color names.
--output <output> Output file to write.
Use - for standard output.
Default is the input file name with the appropriate file extension. If
the input is - the default is standard output.
Note that asset catalogs cannot be written to standard output.
--dump List read colors on console.
-h, --help Show help information.
```
## Input format
Each line in your input contains one color definition. That is a name followed by the actual color. We support RGB colors in a few formats similar to CSS:
```
Base/Green #8fd151
Base/PaleGreen #d0f9a9
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.
```
Black white(0)
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)
Color/Color1 #fff
Color/Color2 #abcdeff0
Color/Color3 rgb(12, 13, 53)
Color/Color4 rgba(250, 250, 250, 128)
```
Colors can also reference other colors by prefixing them with an `@` sign:
```
Error @Base/Red
Warning @Base/Yellow
Good @Base/Green
ColorAlias @Color/Color1
```
## Output format
@ -74,41 +25,17 @@ Good @Base/Green
The optional prefix followed by a `/` is added in front of the color name. Then for each part separate by / a new folder that provides namespace is inserted in the asset catalogs. Spaces are inserted between CamelCase words. Color references are inserted as copies of the original color.
For the given example input the generated asset catalog looks like this:
![](Docs/assetcatalog.png)
### Android resource XML (`--android`)
The optional prefix, followed by a underscore is added in front of the name. Names are converted from CamelCase to snake_case and / is replaced by underscores as well. Color references the generated color resource also references the original color.
The generated XML for the example input looks like this:
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="base_green">#8FD151</color>
<color name="base_pale_green">#D0F9A9</color>
<color name="base_red">#F92707</color>
<color name="base_yellow">#FFFF00</color>
<color name="transparent_red">#FF000080</color>
<color name="error">@color/base_red</color>
<color name="good">@color/base_green</color>
<color name="warning">@color/base_yellow</color>
</resources>
</xml>
```
### HTML preview (`--html`)
Generates a simple HTML table with the color names, values and a sample.
The generated HTML looks like this:
![](Docs/html.png)
## 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

View file

@ -1,7 +1,7 @@
import Foundation
final class AssetCatalogGenerator: Generator {
static let defaultExtension = "xcassets"
static let defaultExtension = "xcasset"
static let option = "ios"
let context: Context
@ -58,7 +58,7 @@ private let infoTag = """
}
"""
extension Color {
private extension Color {
func json() -> String {
"""
{
@ -67,10 +67,10 @@ extension Color {
"color" : {
"color-space" : "srgb",
"components" : {
"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)"
"alpha" : "\(Float(alpha) / 256)",
"blue" : "0x\(String(blue, radix: 16))",
"green" : "0x\(String(green, radix: 16))",
"red" : "0x\(String(red, radix: 16))"
}
},
"idiom" : "universal"
@ -88,22 +88,6 @@ extension Color {
}
}
extension String.StringInterpolation {
mutating func appendInterpolation<I: BinaryInteger>(
_ 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" : {

View file

@ -1,6 +1,6 @@
import Foundation
protocol Generator: AnyObject {
protocol Generator: class {
static var defaultExtension: String { get }
static var option: String { get }
@ -9,7 +9,7 @@ protocol Generator: AnyObject {
func generate(data: [String: ColorDef]) throws -> FileWrapper
}
protocol Context: AnyObject {
protocol Context: class {
var prefix: String? { get }
}

View file

@ -0,0 +1,83 @@
import ArgumentParser
import Foundation
private struct GeneratorOption: EnumerableFlag, CustomStringConvertible {
static let allCases: [GeneratorOption] = [
.init(type: AssetCatalogGenerator.self),
.init(type: AndroidGenerator.self),
.init(type: HTMLGenerator.self),
]
let type: Generator.Type
var description: String {
type.option
}
static func == (lhs: GeneratorOption, rhs: GeneratorOption) -> Bool {
lhs.type == rhs.type
}
}
enum Errors: Error {
case syntaxError
case duplicateColor(String)
case missingReference(String)
case cyclicReference(String)
}
public final class MakeColors: ParsableCommand, Context {
@Argument(help: "The color list to proces")
var input: String
@Flag(help: "The formatter to use")
private var formatter = GeneratorOption.allCases[0]
@Option(help: "Prefix for color names")
var prefix: String?
@Option(help: "Output file")
var output: String?
public init() {}
public func run() throws {
let url = URL(fileURLWithPath: input)
let string = try String(contentsOf: url)
let scanner = Scanner(string: string)
scanner.charactersToBeSkipped = .whitespaces
let data = try scanner.colorList()
for (key, color) in data.sorted() {
let resolved = try data.resolve(key)
switch color {
case .color:
print(key.insertCamelCaseSeparators(), resolved, separator: ": ")
case let .reference(referenced):
print(
"\(key.insertCamelCaseSeparators()) (@\(referenced.insertCamelCaseSeparators()))",
resolved,
separator: ": "
)
}
}
let generator = formatter.type.init(context: self)
let fileWrapper = try generator.generate(data: data)
let writeURL = outputURL(extension: formatter.type.defaultExtension)
try fileWrapper.write(to: writeURL, options: .atomic, originalContentsURL: 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`)
}
}
}

View file

@ -19,14 +19,6 @@ struct Color: CustomStringConvertible, Equatable {
alpha = array.count == 4 ? array[3] : 0xFF
}
init(white array: [UInt8]) {
precondition(array.count == 1 || array.count == 2)
red = array[0]
green = array[0]
blue = array[0]
alpha = array.count == 2 ? array[1] : 0xFF
}
var description: String {
let alphaSuffix = alpha != 0xFF ? String(format: "%02X", alpha) : ""
return String(format: "#%02X%02X%02X%@", red, green, blue, alphaSuffix)

View file

@ -19,70 +19,17 @@ extension Scanner {
}
}
if string("rgba"), let components = argumentList(4) {
if string("rgba"), string("("), let components = commaSeparated(), components.count == 4, string(")") {
return Color(components)
}
if string("rgb"), let components = argumentList(3) {
if string("rgb"), string("("), let components = commaSeparated(), components.count == 3, string(")") {
return Color(components)
}
if string("white"), let arguments = argumentList(min: 1, max: 2) {
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()
@ -110,7 +57,7 @@ extension Scanner {
func colorLine() -> (String, ColorDef)? {
guard
let name = name(),
let name = self.name(),
let def = colorDef(),
endOfLine()
else {
@ -144,50 +91,17 @@ extension Scanner {
return result
}
// swiftlint:disable:next discouraged_optional_collection
func argumentList(_ count: Int) -> [UInt8]? {
argumentList(min: count, max: count)
}
// swiftlint:disable:next discouraged_optional_collection
func argumentList(min: Int, max: Int? = nil) -> [UInt8]? {
let max = max ?? Int.max
guard
string("("),
let arguments = commaSeparated(),
string(")"),
(min...max) ~= arguments.count
else {
return nil
}
return arguments
}
// swiftlint:disable:next discouraged_optional_collection
func commaSeparated() -> [UInt8]? {
var result: [UInt8] = []
repeat {
guard let component = component() else {
guard let int = scanInt(), let component = UInt8(exactly: int) else {
return nil
}
result.append(component)
} while string(",")
return result
}
func component() -> UInt8? {
guard var int = scanInt() else {
return nil
}
if string("%") {
guard 0...100 ~= int else { return nil }
int = int * 0xFF / 100
}
return UInt8(exactly: int)
}
}
private extension Scanner {
@ -197,7 +111,7 @@ private extension Scanner {
}
private extension CharacterSet {
static let hex = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
static let hex = CharacterSet(charactersIn: "0123456789abcdef")
static let name = alphanumerics.union(CharacterSet(charactersIn: "_/"))
}

View file

@ -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))
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -1,150 +0,0 @@
import ArgumentParser
import Foundation
private struct GeneratorOption: EnumerableFlag, CustomStringConvertible {
static let allCases: [GeneratorOption] = [
.init(type: AssetCatalogGenerator.self),
.init(type: AndroidGenerator.self),
.init(type: HTMLGenerator.self),
]
let type: Generator.Type
var description: String {
type.option
}
static func == (lhs: GeneratorOption, rhs: GeneratorOption) -> Bool {
lhs.type == rhs.type
}
}
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 {
case syntaxError
case duplicateColor(String)
case missingReference(String)
case cyclicReference(String)
case cannotWriteWrapperToStdout
case cannotReadStdin
}
enum HelpTexts {
static let input = ArgumentHelp(
"The color list to process.",
discussion: """
Use - to process the standard input.
"""
)
static let output = ArgumentHelp(
"Output file to write.",
discussion: """
Use - for standard output.
Default is the input file name with the appropriate file extension. \
If the input is - the default is standard output.
Note that asset catalogs cannot be written to standard output.
"""
)
}
@main
public final class MakeColors: AsyncParsableCommand, Context {
@Argument(help: HelpTexts.input)
var input: String
@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?
@Option(help: HelpTexts.output)
var output: String?
@Flag(help: "List read colors on console.")
var dump = false
public init() {}
public func run() async throws {
let importer = try importer.type.init(source: input)
let data = try await importer.read()
if dump {
try dump(data: data)
}
let generator = formatter.type.init(context: self)
let fileWrapper = try generator.generate(data: data)
try writeOutput(fileWrapper, name: output ?? "\(importer.outputName).\(formatter.type.defaultExtension)")
}
func dump(data: [String: ColorDef]) throws {
for (key, color) in data.sorted() {
let resolved = try data.resolve(key)
switch color {
case .color:
print(key.insertCamelCaseSeparators(), resolved, separator: ": ")
case let .reference(referenced):
print(
"\(key.insertCamelCaseSeparators()) (@\(referenced.insertCamelCaseSeparators()))",
resolved,
separator: ": "
)
}
}
}
func writeOutput(_ wrapper: FileWrapper, name: String) throws {
if shouldWriteToStdout {
guard wrapper.isRegularFile, let contents = wrapper.regularFileContents else {
throw Errors.cannotWriteWrapperToStdout
}
FileHandle.standardOutput.write(contents)
} else {
let writeURL = URL(fileURLWithPath: name)
try wrapper.write(to: writeURL, options: .atomic, originalContentsURL: nil)
}
}
var shouldWriteToStdout: Bool { output == "-" || (input == "-" && output == nil) }
}

View file

@ -1,45 +0,0 @@
extension Color {
init(hue: Int, saturation: UInt8, value: UInt8, alpha: UInt8 = 0xFF) {
let degrees = abs(hue % 360)
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 = 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")
}
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
)
}
}

View file

@ -0,0 +1,3 @@
import LibMakeColors
MakeColors.main()

View file

@ -1,26 +0,0 @@
@testable import MakeColors
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"))
}
}

View file

@ -1,39 +0,0 @@
@testable import MakeColors
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))
}
}

View file

@ -1,123 +1,40 @@
@testable import MakeColors
@testable import LibMakeColors
import XCTest
final class ColorParserTest: XCTestCase {
func testScanningThreeDigitColor() throws {
let color = scanColor("#abc")
XCTAssertEqual(Color(red: 0xAA, green: 0xBB, blue: 0xCC), color)
}
func testScanningThreeDigitColorUppercase() throws {
let color = scanColor("#ABc")
let scanner = Scanner(string: "#abc")
let color = scanner.color()
XCTAssertEqual(Color(red: 0xAA, green: 0xBB, blue: 0xCC), color)
}
func testScanningFourDigitColor() throws {
let color = scanColor("#abcd")
let scanner = Scanner(string: "#abcd")
let color = scanner.color()
XCTAssertEqual(Color(red: 0xAA, green: 0xBB, blue: 0xCC, alpha: 0xDD), color)
}
func testScanningSixDigitColor() throws {
let color = scanColor("#abcdef")
let scanner = Scanner(string: "#abcdef")
let color = scanner.color()
XCTAssertEqual(Color(red: 0xAB, green: 0xCD, blue: 0xEF), color)
}
func testScanningEightDigitColor() throws {
let color = scanColor("#abcdef17")
let scanner = Scanner(string: "#abcdef17")
let color = scanner.color()
XCTAssertEqual(Color(red: 0xAB, green: 0xCD, blue: 0xEF, alpha: 0x17), color)
}
func testScanningRGBColor() throws {
let color = scanColor("rgb(1,2,3)")
let scanner = Scanner(string: "rgb(1,2,3)")
let color = scanner.color()
XCTAssertEqual(Color(red: 1, green: 2, blue: 3), color)
}
func testScanningRGBAColor() throws {
let color = scanColor("rgba(1,2,3,4)")
let scanner = Scanner(string: "rgba(1,2,3,4)")
let color = scanner.color()
XCTAssertEqual(Color(red: 1, green: 2, blue: 3, alpha: 4), color)
}
func testScanningWhite() throws {
let color = scanColor("white(255)")
XCTAssertEqual(Color(red: 255, green: 255, blue: 255, alpha: 255), color)
}
func testScanningWhiteWithAlpha() throws {
let color = scanColor("white(255, 128)")
XCTAssertEqual(Color(red: 255, green: 255, blue: 255, alpha: 128), color)
}
func testWhiteFailsWithoutArguments() throws {
let color = scanColor("white()")
XCTAssertNil(color)
}
func testWhiteFailsWith3Arguments() throws {
let color = scanColor("white(1,2,3)")
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)
}
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)
let result = scanner.color()
XCTAssertTrue(result == nil || scanner.isAtEnd)
return result
}
}