Skip to content
This repository has been archived by the owner on Sep 6, 2018. It is now read-only.

Commit

Permalink
Merge pull request #40 from SwiftGen/feature/split-color-parser
Browse files Browse the repository at this point in the history
Split colors parser into sub-parsers & support multiple palettes (files)
  • Loading branch information
djbe authored Jun 4, 2017
2 parents faa37e3 + 8b6e987 commit df6219d
Show file tree
Hide file tree
Showing 18 changed files with 556 additions and 392 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Due to the removal of legacy code, there are a few breaking changes in this new
* Storyboards now provide a `platform` identifier (iOS, macOS, tvOS, watchOS).
[David Jennes](https://github.com/djbe)
[#45](https://github.com/SwiftGen/templates/issues/45)
* Added support for multiple color palettes.
[David Jennes](https://github.com/djbe)
[#41](https://github.com/SwiftGen/templates/issues/40)

### Internal Changes

Expand All @@ -47,6 +50,9 @@ Due to the removal of legacy code, there are a few breaking changes in this new
* Images: switch back from `actool` to an internal parser to fix numerous issues with the former.
[David Jennes](https://github.com/djbe)
[#43](https://github.com/SwiftGen/templates/issues/43)
* Refactor the colors parser into a generic parser that will invoke the correct type-specific parser based on the file extension. This allows us to support multiple input files.
[David Jennes](https://github.com/djbe)
[#40](https://github.com/SwiftGen/templates/issues/40)

## 1.1.0

Expand Down
14 changes: 8 additions & 6 deletions Documentation/Colors.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ The colors parser supports multiple input file types:

The output context has the following structure:

- `colors`: `Array` — list of colors:
- `name` : `String` — name of the color
- `red` : `String` — hex value of the red component
- `green`: `String` — hex value of the green component
- `blue` : `String` — hex value of the blue component
- `alpha`: `String` — hex value of the alpha component
- `palettes`: `Array` of:
- `name` : `String` — name of the palette
- `colors`: `Array` of:
- `name` : `String` — name of each color
- `red` : `String` — hex value of the red component
- `green`: `String` — hex value of the green component
- `blue` : `String` — hex value of the blue component
- `alpha`: `String` — hex value of the alpha component
168 changes: 96 additions & 72 deletions Pods/Pods.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

241 changes: 70 additions & 171 deletions Sources/Parsers/ColorsFileParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,82 @@
// MIT Licence
//

import AppKit.NSColor
import Foundation
import Kanna
import PathKit

public protocol ColorsFileParser {
var colors: [String: UInt32] { get }
}

public enum ColorsParserError: Error, CustomStringConvertible {
case invalidHexColor(string: String, key: String?)
case invalidFile(reason: String)
case duplicateExtensionParser(ext: String, existing: String, new: String)
case invalidHexColor(path: Path, string: String, key: String?)
case invalidFile(path: Path, reason: String)
case unsupportedFileType(path: Path, supported: [String])

public var description: String {
switch self {
case .invalidHexColor(string: let string, key: let key):
case .duplicateExtensionParser(let ext, let existing, let new):
return "error: Parser \(new) tried to register the file type '\(ext)' already registered by \(existing)."
case .invalidHexColor(let path, let string, let key):
let keyInfo = key.flatMap { " for key \"\($0)\"" } ?? ""
return "error: Invalid hex color \"\(string)\" found\(keyInfo)."
case .invalidFile(reason: let reason):
return "error: Unable to parse file. \(reason)"
return "error: Invalid hex color \"\(string)\" found\(keyInfo) (\(path))."
case .invalidFile(let path, let reason):
return "error: Unable to parse file at \(path). \(reason)"
case .unsupportedFileType(let path, let supported):
return "error: Unsupported file type for \(path). " +
"The supported file types are: \(supported.joined(separator: ", "))"
}
}
}

struct Palette {
let name: String
let colors: [String: UInt32]
}

protocol ColorsFileTypeParser: class {
static var extensions: [String] { get }

init()
func parseFile(at path: Path) throws -> Palette
}

public final class ColorsFileParser {
private var parsers = [String: ColorsFileTypeParser.Type]()
var palettes = [Palette]()

public init() throws {
try register(parser: ColorsCLRFileParser.self)
try register(parser: ColorsJSONFileParser.self)
try register(parser: ColorsTextFileParser.self)
try register(parser: ColorsXMLFileParser.self)
}

public func parseFile(at path: Path) throws {
guard let parserType = parsers[path.extension?.lowercased() ?? ""] else {
throw ColorsParserError.unsupportedFileType(path: path, supported: Array(parsers.keys))
}

let parser = parserType.init()
let palette = try parser.parseFile(at: path)

palettes += [palette]
}

func register(parser: ColorsFileTypeParser.Type) throws {
for ext in parser.extensions {
guard parsers[ext] == nil else {
throw ColorsParserError.duplicateExtensionParser(ext: ext,
existing: String(describing: parsers[ext]!),
new: String(describing: parser))
}
parsers[ext] = parser
}
}
}

// MARK: - Private Helpers

internal func parse(hex hexString: String, key: String? = nil) throws -> UInt32 {
func parse(hex hexString: String, key: String? = nil, path: Path) throws -> UInt32 {
let scanner = Scanner(string: hexString)

let prefixLen: Int
if scanner.scanString("#", into: nil) {
prefixLen = 1
Expand All @@ -43,7 +91,7 @@ internal func parse(hex hexString: String, key: String? = nil) throws -> UInt32

var value: UInt32 = 0
guard scanner.scanHexInt32(&value) else {
throw ColorsParserError.invalidHexColor(string: hexString, key: key)
throw ColorsParserError.invalidHexColor(path: path, string: hexString, key: key)
}

let len = hexString.lengthOfBytes(using: .utf8) - prefixLen
Expand All @@ -55,170 +103,21 @@ internal func parse(hex hexString: String, key: String? = nil) throws -> UInt32
return value
}

// MARK: - Text File Parser

public final class ColorsTextFileParser: ColorsFileParser {
public private(set) var colors = [String: UInt32]()

public init() {}

public func addColor(named name: String, value: String) throws {
try addColor(named: name, value: parse(hex: value, key: name))
}

public func addColor(named name: String, value: UInt32) {
colors[name] = value
}

public func keyValueDict(from path: Path, withSeperator seperator: String = ":") throws -> [String:String] {

let content = try path.read(.utf8)
let lines = content.components(separatedBy: CharacterSet.newlines)
let whitespace = CharacterSet.whitespaces
let skippedCharacters = NSMutableCharacterSet()
skippedCharacters.formUnion(with: whitespace)
skippedCharacters.formUnion(with: skippedCharacters as CharacterSet)

var dict: [String: String] = [:]
for line in lines {
let scanner = Scanner(string: line)
scanner.charactersToBeSkipped = skippedCharacters as CharacterSet

var key: NSString?
var value: NSString?
guard scanner.scanUpTo(seperator, into: &key) &&
scanner.scanString(seperator, into: nil) &&
scanner.scanUpToCharacters(from: whitespace, into: &value) else {
continue
}

if let key: String = key?.trimmingCharacters(in: whitespace),
let value: String = value?.trimmingCharacters(in: whitespace) {
dict[key] = value
}
}

return dict
}

private func colorValue(forKey key: String, onDict dict: [String: String]) -> String {
var currentKey = key
var stringValue: String = ""
while let value = dict[currentKey]?.trimmingCharacters(in: CharacterSet.whitespaces) {
currentKey = value
stringValue = value
}

return stringValue
}

// Text file expected to be:
// - One line per entry
// - Each line composed by the color name, then ":", then the color hex representation
// - Extra spaces will be skipped
public func parseFile(at path: Path, separator: String = ":") throws {
do {
let dict = try keyValueDict(from: path, withSeperator: separator)
for key in dict.keys {
try addColor(named: key, value: colorValue(forKey: key, onDict: dict))
}
} catch let error as ColorsParserError {
throw error
} catch let error {
throw ColorsParserError.invalidFile(reason: error.localizedDescription)
}
}
}

// MARK: - CLR File Parser

public final class ColorsCLRFileParser: ColorsFileParser {
public private(set) var colors = [String: UInt32]()

public init() {}

public func parseFile(at path: Path) throws {
if let colorsList = NSColorList(name: "UserColors", fromFile: path.string) {
for colorName in colorsList.allKeys {
colors[colorName] = colorsList.color(withKey: colorName)?.rgbColor?.hexValue
}
} else {
throw ColorsParserError.invalidFile(reason: "Invalid color list")
}
}

}

extension NSColor {

fileprivate var rgbColor: NSColor? {
var rgbColor: NSColor? {
guard colorSpace.colorSpaceModel != .RGB else { return self }

return usingColorSpaceName(NSCalibratedRGBColorSpace)
}

internal var hexValue: UInt32 {
let hexRed = UInt32(round(redComponent * 0xFF)) << 24
let hexGreen = UInt32(round(greenComponent * 0xFF)) << 16
let hexBlue = UInt32(round(blueComponent * 0xFF)) << 8
let hexAlpha = UInt32(round(alphaComponent * 0xFF))
return hexRed | hexGreen | hexBlue | hexAlpha
}

}

// MARK: - Android colors.xml File Parser

public final class ColorsXMLFileParser: ColorsFileParser {
private enum XML {
static let colorXPath = "/resources/color"
static let nameAttribute = "name"
}

public private(set) var colors = [String: UInt32]()

public init() {}

public func parseFile(at path: Path) throws {
guard let document = Kanna.XML(xml: try path.read(), encoding: .utf8) else {
throw ColorsParserError.invalidFile(reason: "Unknown XML parser error.")
}
var hexValue: UInt32 {
guard let rgb = rgbColor else { return 0 }

for color in document.xpath(XML.colorXPath) {
guard let value = color.text else {
throw ColorsParserError.invalidFile(reason: "Invalid structure, color must have a value.")
}
guard let name = color["name"], !name.isEmpty else {
throw ColorsParserError.invalidFile(reason: "Invalid structure, color \(value) must have a name.")
}

colors[name] = try parse(hex: value, key: name)
}
}
}
let hexRed = UInt32(round(rgb.redComponent * 0xFF)) << 24
let hexGreen = UInt32(round(rgb.greenComponent * 0xFF)) << 16
let hexBlue = UInt32(round(rgb.blueComponent * 0xFF)) << 8
let hexAlpha = UInt32(round(rgb.alphaComponent * 0xFF))

// MARK: - JSON File Parser

public final class ColorsJSONFileParser: ColorsFileParser {
public private(set) var colors = [String: UInt32]()

public init() {}

public func parseFile(at path: Path) throws {
do {
let json = try JSONSerialization.jsonObject(with: try path.read(), options: [])

guard let dict = json as? [String: String] else {
throw ColorsParserError.invalidFile(reason: "Invalid structure, must be an object with string values.")
}

for (key, value) in dict {
colors[key] = try parse(hex: value, key: key)
}
} catch let error as ColorsParserError {
throw error
} catch let error {
throw ColorsParserError.invalidFile(reason: error.localizedDescription)
}
return hexRed | hexGreen | hexBlue | hexAlpha
}
}
31 changes: 31 additions & 0 deletions Sources/Parsers/ColorsFileParsers/ColorsCLRFileParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// SwiftGenKit
// Copyright (c) 2017 SwiftGen
// MIT Licence
//

import Foundation
import PathKit

final class ColorsCLRFileParser: ColorsFileTypeParser {
static let extensions = ["clr"]

private enum Keys {
static let userColors = "UserColors"
}

func parseFile(at path: Path) throws -> Palette {
if let colorsList = NSColorList(name: Keys.userColors, fromFile: path.string) {
var colors = [String: UInt32]()

for colorName in colorsList.allKeys {
colors[colorName] = colorsList.color(withKey: colorName)?.hexValue
}

let name = path.lastComponentWithoutExtension
return Palette(name: name, colors: colors)
} else {
throw ColorsParserError.invalidFile(path: path, reason: "Invalid color list")
}
}
}
34 changes: 34 additions & 0 deletions Sources/Parsers/ColorsFileParsers/ColorsJSONFileParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// SwiftGenKit
// Copyright (c) 2017 SwiftGen
// MIT Licence
//

import Foundation
import PathKit

final class ColorsJSONFileParser: ColorsFileTypeParser {
static let extensions = ["json"]

func parseFile(at path: Path) throws -> Palette {
do {
let json = try JSONSerialization.jsonObject(with: try path.read(), options: [])
guard let dict = json as? [String: String] else {
throw ColorsParserError.invalidFile(path: path,
reason: "Invalid structure, must be an object with string values.")
}

var colors = [String: UInt32]()
for (key, value) in dict {
colors[key] = try parse(hex: value, key: key, path: path)
}

let name = path.lastComponentWithoutExtension
return Palette(name: name, colors: colors)
} catch let error as ColorsParserError {
throw error
} catch let error {
throw ColorsParserError.invalidFile(path: path, reason: error.localizedDescription)
}
}
}
Loading

0 comments on commit df6219d

Please sign in to comment.