diff --git a/Sources/LibMakeColors/Color.swift b/Sources/LibMakeColors/Color.swift
deleted file mode 100644
index c96c8c2..0000000
--- a/Sources/LibMakeColors/Color.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-struct Color: CustomStringConvertible, Equatable {
- var description: String {
- return a != 0xFF ? String(format: "#%02X%02X%02X%02X", r, g, b, a): String(format: "#%02X%02X%02X", r, g, b)
- }
-
- let r, g, b, a: UInt8
-
- init(r: UInt8, g: UInt8, b: UInt8, a: UInt8 = 0xFF) {
- self.r = r
- self.g = g
- self.b = b
- self.a = a
- }
-
- init?(_ array: [UInt8]) {
- guard array.count >= 3 else { return nil }
- r = array[0]
- g = array[1]
- b = array[2]
- a = array.count >= 4 ? array[3] : 0xFF
- }
-}
-
-enum ColorDef {
- case reference(String)
- case color(Color)
-}
diff --git a/Sources/LibMakeColors/Extensions/FileWrapper+Extensions.swift b/Sources/LibMakeColors/Extensions/FileWrapper+Extensions.swift
new file mode 100644
index 0000000..1dbb70e
--- /dev/null
+++ b/Sources/LibMakeColors/Extensions/FileWrapper+Extensions.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+extension FileWrapper {
+ convenience init(_ string: String) {
+ self.init(regularFileWithContents: Data(string.utf8))
+ }
+}
diff --git a/Sources/LibMakeColors/Extensions/String+Extensions.swift b/Sources/LibMakeColors/Extensions/String+Extensions.swift
new file mode 100644
index 0000000..66c6ee0
--- /dev/null
+++ b/Sources/LibMakeColors/Extensions/String+Extensions.swift
@@ -0,0 +1,40 @@
+extension StringProtocol {
+ var capitalizeFirst: String {
+ guard !isEmpty else {
+ return String(self)
+ }
+
+ return prefix(1).uppercased() + dropFirst()
+ }
+
+ var lowercasedFirst: String {
+ guard !isEmpty else {
+ return String(self)
+ }
+
+ return prefix(1).lowercased() + dropFirst()
+ }
+
+ func droppingSuffix(_ suffix: String) -> SubSequence {
+ guard hasSuffix(suffix) else {
+ return self[...]
+ }
+
+ return dropLast(suffix.count)
+ }
+
+ func insertCamelCaseSeparators(separator: String = " ") -> String {
+ replacingOccurrences(
+ of: "(?<=[a-z0-9])([A-Z])",
+ with: "\(separator)$1",
+ options: .regularExpression,
+ range: nil
+ )
+ }
+
+ func camelCasePathToSnakeCase() -> String {
+ insertCamelCaseSeparators(separator: "_")
+ .replacingOccurrences(of: "/", with: "_")
+ .lowercased()
+ }
+}
diff --git a/Sources/LibMakeColors/Generators/AndroidGenerator.swift b/Sources/LibMakeColors/Generators/AndroidGenerator.swift
new file mode 100644
index 0000000..ecd225d
--- /dev/null
+++ b/Sources/LibMakeColors/Generators/AndroidGenerator.swift
@@ -0,0 +1,44 @@
+import Foundation
+
+final class AndroidGenerator: Generator {
+ static let defaultExtension = "xml"
+
+ let context: Context
+
+ init(context: Context) {
+ self.context = context
+ }
+
+ func generate(data: [String : ColorDef]) throws -> FileWrapper {
+ var xml = """
+
+
+
+ """
+
+ let prefix = context.prefix.map { $0.camelCasePathToSnakeCase() + "_" } ?? ""
+
+ for (key, color) in data.sorted() {
+ _ = try data.resolve(key)
+
+ let value: String
+ switch color {
+ case .color(let colorValue): value = colorValue.description
+ case .reference(let ref): value = "@color/\(prefix)\(ref.camelCasePathToSnakeCase())"
+ }
+
+ xml += """
+ \(value)
+
+ """
+ }
+
+ xml += """
+
+
+
+ """
+
+ return FileWrapper(xml)
+ }
+}
diff --git a/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift b/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift
new file mode 100644
index 0000000..f17623b
--- /dev/null
+++ b/Sources/LibMakeColors/Generators/AssetCatalogGenerator.swift
@@ -0,0 +1,107 @@
+import Foundation
+
+final class AssetCatalogGenerator: Generator {
+ static let defaultExtension = "xcasset"
+ static let option = "ios"
+
+ let context: Context
+
+ init(context: Context) {
+ self.context = context
+ }
+
+ func generate(data: [String : ColorDef]) throws -> FileWrapper {
+ let root = FileWrapper(directoryWithFileWrappers: ["Contents.json" : FileWrapper(catalog)])
+ let colorRoot: FileWrapper
+
+ if let prefix = context.prefix?.insertCamelCaseSeparators() {
+ colorRoot = FileWrapper(directoryWithFileWrappers: ["Contents.json": FileWrapper(group)])
+ colorRoot.filename = prefix
+ colorRoot.preferredFilename = prefix
+ root.addFileWrapper(colorRoot)
+ } else {
+ colorRoot = root
+ }
+
+ for key in data.keys {
+ var path = key.insertCamelCaseSeparators().split(separator: "/").map(\.capitalizeFirst)
+ let colorSet = path.removeLast()
+
+ var current = colorRoot
+ for pathSegment in path {
+ if let next = current.fileWrappers?[pathSegment] {
+ current = next
+ } else {
+ let next = FileWrapper(directoryWithFileWrappers: ["Contents.json": FileWrapper(group)])
+ next.filename = pathSegment
+ next.preferredFilename = pathSegment
+ _ = current.addFileWrapper(next)
+ current = next
+ }
+ }
+
+ let colorWrapper = try data.resolve(key).fileWrapper()
+ colorWrapper.filename = "\(colorSet).colorset"
+ colorWrapper.preferredFilename = "\(colorSet).colorset"
+
+ current.addFileWrapper(colorWrapper)
+ }
+
+ return root
+ }
+}
+
+private extension Color {
+ func json() -> String {
+ return """
+ {
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "\(Float(a) / 256)",
+ "blue" : "0x\(String(b, radix: 16))",
+ "green" : "0x\(String(g, radix: 16))",
+ "red" : "0x\(String(r, radix: 16))"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+ }
+ """
+ }
+
+ func fileWrapper() -> FileWrapper {
+ FileWrapper(directoryWithFileWrappers: [
+ "Contents.json": FileWrapper(json())
+ ])
+ }
+}
+
+
+private let group = """
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
+"""
+
+private let catalog = """
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
+"""
diff --git a/Sources/LibMakeColors/Generators/Generator.swift b/Sources/LibMakeColors/Generators/Generator.swift
new file mode 100644
index 0000000..8561ac3
--- /dev/null
+++ b/Sources/LibMakeColors/Generators/Generator.swift
@@ -0,0 +1,20 @@
+import Foundation
+
+protocol Generator: class {
+ static var defaultExtension: String { get }
+ static var option: String { get }
+
+ init(context: Context)
+
+ func generate(data: [String: ColorDef]) throws -> FileWrapper
+}
+
+protocol Context: class {
+ var prefix: String? { get }
+}
+
+extension Generator {
+ static var option: String {
+ String(String(describing: self).droppingSuffix("Generator"))
+ }
+}
diff --git a/Sources/LibMakeColors/Generators/HTMLGenerator.swift b/Sources/LibMakeColors/Generators/HTMLGenerator.swift
new file mode 100644
index 0000000..43d0d08
--- /dev/null
+++ b/Sources/LibMakeColors/Generators/HTMLGenerator.swift
@@ -0,0 +1,74 @@
+import Foundation
+
+final class HTMLGenerator: Generator {
+ static let defaultExtension = "html"
+ let context: Context
+
+ init(context: Context) {
+ self.context = context
+ }
+
+ func generate(data: [String : ColorDef]) throws -> FileWrapper {
+ var html = """
+
+
+
+
+
+
+
+ |
+ Name |
+ Value |
+
+
+
+
+ """
+
+ for (key, color) in data.sorted() {
+ let actualColor = try data.resolve(key)
+ let value: String
+
+ switch color {
+ case let .reference(name): value = """
+ \(name.insertCamelCaseSeparators())
\(actualColor)
+ """
+ case .color: value = actualColor.description
+ }
+
+ html += """
+
+ |
+ \(key.insertCamelCaseSeparators()) |
+ \(value) |
+
+
+ """
+ }
+
+ html += """
+
+
+
+
+ """
+
+ return FileWrapper(html)
+ }
+}
diff --git a/Sources/LibMakeColors/MakeColors.swift b/Sources/LibMakeColors/MakeColors.swift
index 89090b4..79e68d3 100644
--- a/Sources/LibMakeColors/MakeColors.swift
+++ b/Sources/LibMakeColors/MakeColors.swift
@@ -1,10 +1,22 @@
import ArgumentParser
import Foundation
-enum Formatter: String, EnumerableFlag {
- case ios
- case android
- case html
+private struct GeneratorOption: EnumerableFlag, CustomStringConvertible {
+ let type: Generator.Type
+
+ var description: String {
+ type.option
+ }
+
+ static let allCases: [GeneratorOption] = [
+ .init(type: AssetCatalogGenerator.self),
+ .init(type: AndroidGenerator.self),
+ .init(type: HTMLGenerator.self)
+ ]
+
+ static func == (lhs: GeneratorOption, rhs: GeneratorOption) -> Bool {
+ lhs.type == rhs.type
+ }
}
enum Errors: Error {
@@ -14,12 +26,12 @@ enum Errors: Error {
case cyclicReference(String)
}
-public struct MakeColors: ParsableCommand {
+public final class MakeColors: ParsableCommand, Context {
@Argument(help: "The color list to proces")
var input: String
@Flag(help: "The formatter to use")
- var formatter = Formatter.ios
+ private var formatter = GeneratorOption.allCases[0]
@Option(help: "Prefix for color names")
var prefix: String?
@@ -38,18 +50,19 @@ public struct MakeColors: ParsableCommand {
let data = try scanner.colorList()
- print(data)
-
- switch formatter {
- case .ios:
- try writeAssetCatalog(data: data)
-
- case .android:
- try writeAndroidXML(data: data)
-
- case .html:
- try writeHtmlPreview(data: data)
+ for (key, color) in data.sorted() {
+ let resolved = try data.resolve(key)
+ switch color {
+ case .color: print(key.insertCamelCaseSeparators(), resolved, separator: ": ")
+ case .reference(let r): print("\(key.insertCamelCaseSeparators()) (@\(r.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 {
@@ -59,270 +72,5 @@ public struct MakeColors: ParsableCommand {
return URL(fileURLWithPath: input).deletingPathExtension().appendingPathExtension(`extension`)
}
}
-
- func mapColorName(_ name: String) -> String {
- mapSpaceColorName(name, separator: "_")
- .replacingOccurrences(of: "/", with: "_")
- .lowercased()
- }
-
- func mapSpaceColorName(_ name: String, separator: String = " ") -> String {
- name.replacingOccurrences(
- of: "(?<=[a-z0-9])([A-Z])",
- with: "\(separator)$1",
- options: .regularExpression,
- range: nil
- )
- }
-
- func writeAndroidXML(data: [String: ColorDef]) throws {
- var xml = """
-
-
-
- """
-
- let prefix = self.prefix.map { mapColorName($0) + "_" } ?? ""
-
- for (key, color) in data.sorted(by: compare) {
- _ = try data.resolve(key)
-
- let value: String
- switch color {
- case .color(let colorValue): value = colorValue.description
- case .reference(let ref): value = "@color/\(prefix)\(mapColorName(ref))"
- }
-
- xml += """
- \(value)
-
- """
- }
-
- xml += """
-
-
-
- """
-
- try xml.write(to: outputURL(extension: "xml"), atomically: true, encoding: .utf8)
- }
-
- func writeHtmlPreview(data: [String: ColorDef]) throws {
- var html = """
-
-
-
-
-
-
-
- |
- Name |
- Value |
-
-
-
-
- """
-
- for (key, color) in data.sorted(by: compare) {
- let actualColor = try data.resolve(key)
- let value: String
-
- switch color {
- case let .reference(name): value = """
- \(mapSpaceColorName(name))
\(actualColor)
- """
- case .color: value = actualColor.description
- }
-
- html += """
-
- |
- \(mapSpaceColorName(key)) |
- \(value) |
-
-
- """
- }
-
- html += """
-
-
-
-
- """
-
- try html.write(to: outputURL(extension: "html"), atomically: true, encoding: .utf8)
- }
-
- func writeAssetCatalog(data: [String: ColorDef]) throws {
- let root = FileWrapper(directoryWithFileWrappers: ["Contents.json" : FileWrapper(catalog)])
- let colorRoot: FileWrapper
-
- if let prefix = prefix {
- let name = mapSpaceColorName(prefix)
- colorRoot = FileWrapper(directoryWithFileWrappers: ["Contents.json": FileWrapper(group)])
- colorRoot.filename = name
- colorRoot.preferredFilename = name
- root.addFileWrapper(colorRoot)
- } else {
- colorRoot = root
- }
-
- for key in data.keys {
- var path = mapSpaceColorName(key).split(separator: "/").map(\.capitalizeFirst)
- let colorSet = path.removeLast()
-
- var current = colorRoot
- for pathSegment in path {
- if let next = current.fileWrappers?[pathSegment] {
- current = next
- } else {
- let next = FileWrapper(directoryWithFileWrappers: ["Contents.json": FileWrapper(group)])
- next.filename = pathSegment
- next.preferredFilename = pathSegment
- _ = current.addFileWrapper(next)
- current = next
- }
- }
-
- let colorWrapper = try data.resolve(key).fileWrapper()
- colorWrapper.filename = "\(colorSet).colorset"
- colorWrapper.preferredFilename = "\(colorSet).colorset"
-
- current.addFileWrapper(colorWrapper)
- }
-
- let outputUrl = outputURL(extension: "xcassets")
-
- try root.write(to: outputUrl, options: .atomic, originalContentsURL: nil)
- }
}
-extension StringProtocol {
- var capitalizeFirst: String {
- guard !isEmpty else {
- return String(self)
- }
-
- return prefix(1).uppercased() + dropFirst()
- }
-}
-
-func compareDef(_ a: ColorDef, _ b: ColorDef) -> ComparisonResult {
- switch (a, b) {
- case (.color, .reference): return .orderedAscending
- case (.reference, .color): return .orderedDescending
- case (.color, .color), (.reference, .reference): return .orderedSame
- }
-}
-
-func compare(_ a: (String, ColorDef), b: (String, ColorDef)) -> Bool {
- switch (compareDef(a.1, b.1)) {
- case .orderedAscending: return true
- case .orderedDescending: return false
- case .orderedSame: return a.0.localizedStandardCompare(b.0) == .orderedAscending
- }
-}
-
-
-
-
-extension Dictionary where Key == String, Value == ColorDef {
- func resolve(_ name: String, visited: Set = []) throws -> Color {
- var visited = visited
- guard visited.insert(name).inserted else {
- throw Errors.cyclicReference(name)
- }
-
- switch self[name] {
- case nil:
- throw Errors.missingReference(name)
-
- case .color(let color):
- return color
-
- case .reference(let referenced):
- return try resolve(referenced, visited: visited)
- }
- }
-
-}
-
-extension Color {
- func json() -> String {
- return """
- {
- "colors" : [
- {
- "color" : {
- "color-space" : "srgb",
- "components" : {
- "alpha" : "\(Float(a) / 256)",
- "blue" : "0x\(String(b, radix: 16))",
- "green" : "0x\(String(g, radix: 16))",
- "red" : "0x\(String(r, radix: 16))"
- }
- },
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
- }
- """
- }
-
- func fileWrapper() -> FileWrapper {
- FileWrapper(directoryWithFileWrappers: [
- "Contents.json": FileWrapper(json())
- ])
- }
-}
-
-extension FileWrapper {
- convenience init(_ string: String) {
- self.init(regularFileWithContents: Data(string.utf8))
- }
-}
-
-let group = """
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- },
- "properties" : {
- "provides-namespace" : true
- }
-}
-"""
-
-let catalog = """
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
-"""
-
diff --git a/Sources/LibMakeColors/Model/Color.swift b/Sources/LibMakeColors/Model/Color.swift
new file mode 100644
index 0000000..6c5c641
--- /dev/null
+++ b/Sources/LibMakeColors/Model/Color.swift
@@ -0,0 +1,60 @@
+struct Color: CustomStringConvertible, Equatable {
+ let r, g, b, a: UInt8
+
+ init(r: UInt8, g: UInt8, b: UInt8, a: UInt8 = 0xFF) {
+ self.r = r
+ self.g = g
+ self.b = b
+ self.a = a
+ }
+
+ init?(_ array: [UInt8]) {
+ guard array.count >= 3 else { return nil }
+ r = array[0]
+ g = array[1]
+ b = array[2]
+ a = array.count >= 4 ? array[3] : 0xFF
+ }
+
+ var description: String {
+ return a != 0xFF ? String(format: "#%02X%02X%02X%02X", r, g, b, a): String(format: "#%02X%02X%02X", r, g, b)
+ }
+}
+
+enum ColorDef {
+ case reference(String)
+ case color(Color)
+}
+
+extension Dictionary where Key == String, Value == ColorDef {
+ func resolve(_ name: String, visited: Set = []) throws -> Color {
+ var visited = visited
+ guard visited.insert(name).inserted else {
+ throw Errors.cyclicReference(name)
+ }
+
+ switch self[name] {
+ case nil:
+ throw Errors.missingReference(name)
+
+ case .color(let color):
+ return color
+
+ case .reference(let referenced):
+ return try resolve(referenced, visited: visited)
+ }
+ }
+
+ func sorted() -> [Element] {
+ sorted(by: Self.compare)
+ }
+
+ static func compare(_ a: (String, ColorDef), _ b: (String, ColorDef)) -> Bool {
+ switch (a, b) {
+ case ((_, .color), (_, .reference)): return true
+ case ((_, .reference), (_, .color)): return false
+ case let ((left, _), (right, _)): return left.localizedStandardCompare(right) == .orderedAscending
+ }
+ }
+
+}
diff --git a/Sources/LibMakeColors/Parser.swift b/Sources/LibMakeColors/Model/Parser.swift
similarity index 98%
rename from Sources/LibMakeColors/Parser.swift
rename to Sources/LibMakeColors/Model/Parser.swift
index 9cce424..390e88c 100644
--- a/Sources/LibMakeColors/Parser.swift
+++ b/Sources/LibMakeColors/Model/Parser.swift
@@ -1,12 +1,11 @@
import Foundation
-
-extension CharacterSet {
+private extension CharacterSet {
static let hex = CharacterSet(charactersIn: "0123456789abcdef")
static let name = alphanumerics.union(CharacterSet.init(charactersIn: "_/"))
}
-extension Collection {
+private extension Collection {
func chunks(size: Int) -> UnfoldSequence {
sequence(state: startIndex) { state -> SubSequence? in
guard state != endIndex else { return nil }