Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 105 additions & 24 deletions GenumKit/Parsers/AssetsCatalogParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,81 +8,162 @@ import Foundation
import PathKit

public final class AssetsCatalogParser {
var imageNames = [String]()
var entries = [Entry]()

public init() {}

@discardableResult
public func addImage(named name: String) -> Bool {
if imageNames.contains(name) {
return false
} else {
imageNames.append(name)
let found = entries.contains {
if case let .image(imageName, _) = $0, imageName == name {
return true
} else {
return false
}
}

guard !found else { return false }
entries.append(Entry.image(name: name, value: name))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation (from lines 17 to 26) seems wrong 😉


return true
}

public func parseCatalog(at path: Path) {
guard let items = loadAssetCatalog(at: path) else { return }

// process recursively
processCatalog(items: items)
entries = process(items: items)
}

enum Entry {
case image(name: String, value: String)
case namespace(name: String, items: [Entry])
}
}

// MARK: - Plist processing

private enum AssetCatalog: String {
case children = "children"
case filename = "filename"
case providesNamespace = "provides-namespace"
case root = "com.apple.actool.catalog-contents"
private enum AssetCatalog {
static let children = "children"
static let filename = "filename"
static let providesNamespace = "provides-namespace"
static let root = "com.apple.actool.catalog-contents"
}

extension AssetsCatalogParser {
static let imageSetExtension = "imageset"

fileprivate func processCatalog(items: [[String: AnyObject]], withPrefix prefix: String = "") {
/**
This method recursively parses a tree of nodes (similar to a directory structure)
resulting from the `actool` utility.

Each node in an asset catalog is either (there are more types, but we ignore those):
- An imageset, which is essentially a group containing a list of files (the latter is ignored).

<dict>
<key>children</key>
<array>
...actual file items here (for example the 1x, 2x and 3x images)...
</array>
<key>filename</key>
<string>Tomato.imageset</string>
</dict>

- A group, containing sub items such as imagesets or groups. A group can provide a namespaced,
which means that all the sub items will have to be prefixed with their parent's name.

<dict>
<key>children</key>
<array>
...sub items such as groups or imagesets...
</array>
<key>filename</key>
<string>Round</string>
<key>provides-namespace</key>
<true/>
</dict>

- Parameter items: The array of items to recursively process.
- Parameter prefix: The prefix to prepend values with (from namespaced groups).
- Returns: An array of processed Entry items.
*/
fileprivate func process(items: [[String: Any]], withPrefix prefix: String = "") -> [Entry] {
var result = [Entry]()

for item in items {
guard let filename = item[AssetCatalog.filename.rawValue] as? String else { continue }
guard let filename = item[AssetCatalog.filename] as? String else { continue }
let path = Path(filename)

if path.extension == AssetsCatalogParser.imageSetExtension {
// this is a simple imageset
let imageName = path.lastComponentWithoutExtension
addImage(named: "\(prefix)\(imageName)")

result += [.image(name: imageName, value: "\(prefix)\(imageName)")]
} else {
// this is a group/folder
let children = item[AssetCatalog.children.rawValue] as? [[String: AnyObject]] ?? []
let children = item[AssetCatalog.children] as? [[String: Any]] ?? []

if let providesNamespace = item[AssetCatalog.providesNamespace.rawValue] as? NSNumber,
providesNamespace.boolValue {
processCatalog(items: children, withPrefix: "\(prefix)\(filename)/")
if let providesNamespace = item[AssetCatalog.providesNamespace] as? Bool,
providesNamespace {
let processed = process(items: children, withPrefix: "\(prefix)\(filename)/")
result += [.namespace(name: filename, items: processed)]
} else {
processCatalog(items: children, withPrefix: prefix)
let processed = process(items: children, withPrefix: prefix)
result += [.namespace(name: filename, items: processed)]
}
}
}

return result
}
}

// MARK: - ACTool

extension AssetsCatalogParser {
fileprivate func loadAssetCatalog(at path: Path) -> [[String: AnyObject]]? {
let command = Command("xcrun", arguments: "actool", "--print-contents", String(describing: path))
/**
Try to parse an asset catalog using the `actool` utilty. While it supports parsing
multiple catalogs at once, we only use it to parse one at a time.

The output of the utility is a Plist and should be similar to this:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.actool.catalog-contents</key>
<array>
<dict>
<key>children</key>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ tabs indentation

<array>
...
</array>
<key>filename</key>
<string>Images.xcassets</string>
</dict>
</array>
</dict>
</plist>
```

- Parameter path: The path to the catalog to parse.
- Returns: An array of dictionaries, representing the tree of nodes in the catalog.
*/
fileprivate func loadAssetCatalog(at path: Path) -> [[String: Any]]? {
let command = Command("xcrun", arguments: "actool", "--print-contents", path.description)
let output = command.execute() as Data

// try to parse plist
guard let plist = try? PropertyListSerialization
.propertyList(from: output, format: nil) else { return nil }

// get first parsed catalog
guard let contents = plist as? [String: AnyObject],
let catalogs = contents[AssetCatalog.root.rawValue] as? [[String: AnyObject]],
guard let contents = plist as? [String: Any],
let catalogs = contents[AssetCatalog.root] as? [[String: Any]],
let catalog = catalogs.first else { return nil }

// get list of children
guard let children = catalog[AssetCatalog.children.rawValue] as? [[String: AnyObject]] else { return nil }
guard let children = catalog[AssetCatalog.children] as? [[String: Any]] else { return nil }

return children
}
Expand Down
2 changes: 1 addition & 1 deletion GenumKit/Parsers/ColorsFileParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public final class ColorsCLRFileParser: ColorsFileParser {
public init() {}

public func parseFile(at path: Path) {
if let colorsList = NSColorList(name: "UserColors", fromFile: String(describing: path)) {
if let colorsList = NSColorList(name: "UserColors", fromFile: path.description) {
for colorName in colorsList.allKeys {
colors[colorName] = colorsList.color(withKey: colorName)?.rgbColor?.hexValue
}
Expand Down
2 changes: 1 addition & 1 deletion GenumKit/Parsers/FontsFileParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public final class FontsFileParser {
public func parseFile(at path: Path) {
// PathKit does not support support enumeration with options yet
// see: https://github.com/kylef/PathKit/pull/25
let url = URL(fileURLWithPath: String(describing: path))
let url = URL(fileURLWithPath: path.description)

if let dirEnum = FileManager.default.enumerator(at: url,
includingPropertiesForKeys: [],
Expand Down
2 changes: 1 addition & 1 deletion GenumKit/Parsers/StringsFileParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class StringsFileParser {
// Localizable.strings files are generally UTF16, not UTF8!
public func parseFile(at path: Path) throws {
guard let data = try? path.read() else {
throw StringsFileParserError.FailureOnLoading(path: String(describing: path))
throw StringsFileParserError.FailureOnLoading(path: path.description)
}

let plist = try PropertyListSerialization
Expand Down
78 changes: 75 additions & 3 deletions GenumKit/Stencil/Contexts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ extension ColorsFileParser {
"rgb": String(hexChars[0..<6]),
"red": comps[0],
"green": comps[1],
"blue" : comps[2],
"blue": comps[2],
"alpha": comps[3],
]
}).sorted { $0["name"] ?? "" < $1["name"] ?? "" }
Expand All @@ -62,7 +62,79 @@ extension ColorsFileParser {
*/
extension AssetsCatalogParser {
public func stencilContext(enumName: String = "Asset") -> Context {
return Context(dictionary: ["enumName": enumName, "images": imageNames], namespace: GenumNamespace())
let images = justValues(entries: entries)
let imagesStructured = structure(entries: entries)

return Context(
dictionary: [
"enumName": enumName,
"images": images,
"structuredImages": imagesStructured
],
namespace: GenumNamespace()
)
}

private func justValues(entries: [Entry]) -> [String] {
var result = [String]()

for entry in entries {
switch entry {
case let .namespace(name: name, items: items):
result += justValues(entries: items)
case let .image(name: _, value: value):
result += [value]
}
}

return result
}

private func structure(entries: [Entry], currentLevel: Int = 0, maxLevel: Int = 5) -> [[String: Any]] {
return entries.map { entry in
switch entry {
case let .namespace(name: name, items: items):
if currentLevel + 1 >= maxLevel {
return [
"name": name,
"items": flatten(entries: items)
]
} else {
return [
"name": name,
"items": structure(entries: items, currentLevel: currentLevel + 1, maxLevel: maxLevel)
]
}
case let .image(name: name, value: value):
return [
"name": name,
"value": value
]
}
}
}

private func flatten(entries: [Entry]) -> [[String: Any]] {
var result = [[String: Any]]()

for entry in entries {
switch entry {
case let .namespace(name: name, items: items):
result += flatten(entries: items).map { item in
return [
"name": "\(name)/\(item["name"]!)",
"value": item["value"]!
]
}
case let .image(name: name, value: value):
result += [[
"name": name,
"value": value
]]
}
}

return result
}
}

Expand Down Expand Up @@ -140,7 +212,7 @@ extension StoryboardParser {
dictionary: [
"sceneEnumName": sceneEnumName,
"segueEnumName": segueEnumName,
"extraImports": extraImports,
"extraImports": extraImports,
"storyboards": storyboardsMap
],
namespace: GenumNamespace()
Expand Down
Loading