diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4e96cc2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -os: osx -osx_image: xcode11.3 -language: swift -sudo: required -script: -- swift test \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 106f5e3..532e8ea 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/tadija/AEXML", "state": { "branch": null, - "revision": "e4d517844dd03dac557e35d77a8e9ab438de91a6", - "version": "4.4.0" + "revision": "8623e73b193386909566a9ca20203e33a09af142", + "version": "4.5.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/kylef/Spectre.git", "state": { "branch": null, - "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", - "version": "0.9.0" + "revision": "f79d4ecbf8bc4e1579fbd86c3e1d652fb6876c53", + "version": "0.9.2" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/apple/swift-argument-parser", "state": { "branch": null, - "revision": "f6ac7b8118ff5d1bc0faee7f37bf6f8fd8f95602", - "version": "0.0.1" + "revision": "92646c0cdbaca076c8d3d0207891785b3379cbff", + "version": "0.3.1" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/tuist/xcodeproj.git", "state": { "branch": null, - "revision": "02e4ee305baf0ba23af7522210250d8275200122", - "version": "7.6.0" + "revision": "82bf5efcaa27e94ed8c761c1eb3e397b6dea82b9", + "version": "7.18.0" } } ] diff --git a/README.md b/README.md index e080712..6678b71 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,143 @@ # Shark + [![Build Status](https://travis-ci.org/kaandedeoglu/Shark.svg?branch=master)](https://travis-ci.org/kaandedeoglu/Shark) -> Shark has been rewritten from scratch and now requires Xcode 10.2 / Swift 5 +Shark is a Swift command line tool that generates type safe enums for your image assets, color assets, localizations and fonts. + +Because Shark reads your `.xcodeproj` to find these assets, the setup is extremely simple. + +## Motivation + +Here's what a generated `Shark.swift` file looks like and how it is used in a codebase: + +```swift +// Shark.swift +// Generated by Shark https://github.com/kaandedeoglu/Shark + +import UIKit + +// swiftlint:disable all +public enum Shark { + private static let bundle: Bundle = { + class Custom {} + return Bundle(for: Custom.self) + }() + + public enum I { + public enum Button { + public static var profile: UIImage { return UIImage(named:"report_user", in: bundle, compatibleWith: nil)! } + public static var cancel: UIImage { return UIImage(named:"battery_swap_maintained", in: bundle, compatibleWith: nil)! } + public static var user_avatar: UIImage { return UIImage(named:"damage_check", in: bundle, compatibleWith: nil)! } + } + } + + public enum C { + public static var blue1: UIColor { return UIColor(named: "blue1", in: bundle, compatibleWith: nil)! } + public static var blue2: UIColor { return UIColor(named: "blue2", in: bundle, compatibleWith: nil)! } + public static var gray1: UIColor { return UIColor(named: "gray1", in: bundle, compatibleWith: nil)! } + public static var gray2: UIColor { return UIColor(named: "gray2", in: bundle, compatibleWith: nil)! } + public static var green1: UIColor { return UIColor(named: "green1", in: bundle, compatibleWith: nil)! } + public static var green2: UIColor { return UIColor(named: "green2", in: bundle, compatibleWith: nil)! } + } + + public enum F { + public static func gothamBold(ofSize size: CGFloat) -> UIFont { return UIFont(name: "Gotham-Bold", size: size)! } + public static func gothamMedium(ofSize size: CGFloat) -> UIFont { return UIFont(name: "Gotham-Medium", size: size)! } + public static func gothamRegular(ofSize size: CGFloat) -> UIFont { return UIFont(name: "Gotham-Regular", size: size)! } + } + + public enum L { + public enum button { + /// Login + public static var login: String { return NSLocalizedString("button.login", bundle: bundle, comment: "") } + + /// Logout + public static var logout: String { return NSLocalizedString("button.logout", bundle: bundle, comment: "") } + } + + public enum login { + /// Please log in to continue + public static var title: String { return NSLocalizedString("login.title", bundle: bundle, comment: "") } -Shark is a Swift command line tool that generates type safe enums for your image assets, color assets and localizations. + /// Skip login and continue + public static var skip: String { return NSLocalizedString("login.skip", bundle: bundle, comment: "") } -Because Shark reads your .xcodeproj to find these assets, the setup is extremely simple. + public enum error { + /// Login failed + public static var title: String { return NSLocalizedString("login.error.title", bundle: bundle, comment: "") } + + /// Operation failed with error: %@ + public static func message(_ value1: String) -> String { + return String(format: NSLocalizedString("login.error.message", bundle: bundle, comment: ""), value1) + } + } + } + } +} + +// At the call site +imageView.image = Shark.I.Button.profile +label.font = Shark.F.gothamBold(ofSize: 16.0) +label.text = Shark.L.login.title +view.backgroundColor = Shark.C.green1 + +// You can also make it prettier with typealiases +typealias I = Shark.I +typealias C = Shark.C +typealias F = Shark.F +typealias L = Shark.L + +imageView.image = I.Button.profile +label.font = F.gothamBold(ofSize: 16.0) +label.text = L.login.error.message("I disobeyed my masters") +view.backgroundColor = C.green1 +``` + +There are a few things to notice: + +- Image assets are namespaced by folder. For example all the images in your `Assets.xcassets/Buttons` folder will live under an enum called `Buttons`. +- Localizations are namespaced with separators. Currently Shark uses the dot symbol `.` as the separator. + As you can see localization keys are recursively namespaced until we get to the last component. ## Installation -#### Brew: + +### Brew + ```bash brew install kaandedeoglu/formulae/shark ``` -#### Manually: +### Manually + Clone the project, then do: + ```bash -> swift build -c release --disable-sandbox +> swift build -c release > cp ./build/release/Shark /usr/local/bin ``` You can then verify the installation by doing ```bash -shark --help +> shark --help ``` ## Setup -The easiest way to set up Shark is by adding a new Run Script phase to your build phases. This build phase should ideally run before the `Compile Sources` phase. +- Add a new Run Script phase to your target's build phases. This build phase should ideally run before the `Compile Sources` phase. The script body should look like the following: -- Add a new Run Script phase to your target. The script body should look like: ```bash if [ -x "$(command -v shark)" ]; then shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME fi ``` - the `if/else` block makes sure that Shark is run only if it's installed on the current machine. + the `if/fi` block makes sure that Shark runs only if it's installed on the current machine. - Build your project. You should now see a file named `Shark.swift` in your project folder. -- Add this file to your project. Voila! The file will be updated every time the project is built! +- Add this file to your target. Voila! `Shark.swift` will be updated every time you build the project. - Alternatively you can do the following: + ```bash # Write to a specific file called MyAssets.swift shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME/MyAssets.swift @@ -52,222 +148,44 @@ The easiest way to set up Shark is by adding a new Run Script phase to your buil shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME/Utility/MyAssets.swift ``` -## Flags -Shark also accepts some flags to configure behavior +## Options + +Shark also accepts the following command line options to configure behavior + +### --name -#### --name By default, the top level enum everything else lives under is called - you guessed it - `Shark`. You can change this by using the `--name` flag. ```bash shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME --name Assets ``` -#### --locale -By default, Shark will try to find English localizations to generate the localizations enum. If there are no English .strings file in your project, or you'd like Shark to take another localization as base, you can specify the language code with the `--locale` flag. +### --locale + +By default, Shark will try to find English localizations to generate the localizations enum. If there are no English `.strings` file in your project, or you'd like Shark to take another localization as base, you can specify the language code with the `--locale` flag. + ```bash # Use Spanish localizations for generation shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME --locale es ``` -#### --target +### --target + In case your Xcode project has multiple application targets, you should specify which one Shark should look at by using the `--target` flag. ```bash shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME --target MyAppTarget ``` -#### --help -Prints the overview, example usage and available flags to the console. +### --help -## Sample output - -Below is a sample output generated by Shark. As you can see, the top level `enum Shark` contains three enums inside. `I` (Images), `L` (Localizations) and `C` (Colors). Example usage looks like - -```swift -imageView.image = Shark.I.TaskIcons.task_icon_clean -label.text = Shark.L.button.login -view.backgroundColor = Shark.C.blue1 -``` - -There are a few things to notice: -- Image assets are namespaced by folder. For example all the images in your `.xcassets` folder that are contained in a folder called `TaskIcons` will be listed under an enum called `TaskIcons`. -- Localizations are namespaced with separators. Currently Shark uses the dot symbol `.` as the separator. - For example given two strings `"button.login"` and `"button.logout"`, Shark will generate the following: - -```swift -public enum L { - public enum button { - /// Login - public static var login: String { return NSLocalizedString("button.login", comment: "") } - - /// Logout - public static var logout: String { return NSLocalizedString("button.logout", comment: "") } - } -} -``` - -You can see the sample below: - -```swift -// Shark.swift -// Generated by Shark https://github.com/kaandedeoglu/Shark - -import UIKit - -public enum Shark { - public enum I { - public enum TaskIcons { - public static var task_icon_clean: UIImage { return UIImage(named:"task_icon_clean")! } - public static var task_icon_fallback: UIImage { return UIImage(named:"task_icon_fallback")! } - public static var task_icon_replacebatteries: UIImage { return UIImage(named:"task_icon_replacebatteries")! } - public static var task_icon_replacezipper: UIImage { return UIImage(named:"task_icon_replacezipper")! } - } - - public static var back_image: UIImage { return UIImage(named:"back_image")! } - public static var icon_banner_retry: UIImage { return UIImage(named:"icon-banner-retry")! } - public static var icon_no_internet: UIImage { return UIImage(named:"icon-no-internet")! } - public static var icon_paused_shift: UIImage { return UIImage(named:"icon-paused-shift")! } - public static var input_icon_camera: UIImage { return UIImage(named:"input-icon-camera")! } - public static var input_icon_invalid: UIImage { return UIImage(named:"input-icon-invalid")! } - public static var input_icon_show_password: UIImage { return UIImage(named:"input-icon-show-password")! } - public static var map_location_on: UIImage { return UIImage(named:"map-location-on")! } - public static var menu_icon: UIImage { return UIImage(named:"menu-icon")! } - public static var modal_checkbox_active: UIImage { return UIImage(named:"modal-checkbox-active")! } - public static var modal_checkbox_inactive: UIImage { return UIImage(named:"modal-checkbox-inactive")! } - public static var modal_icon_cancel: UIImage { return UIImage(named:"modal-icon-cancel")! } - public static var modal_icon_navigate: UIImage { return UIImage(named:"modal-icon-navigate")! } - } - - public enum C { - public static var backgroundColor: UIColor { return UIColor(named: "backgroundColor")! } - public static var blue1: UIColor { return UIColor(named: "blue1")! } - public static var blue2: UIColor { return UIColor(named: "blue2")! } - public static var blue3: UIColor { return UIColor(named: "blue3")! } - public static var gray1: UIColor { return UIColor(named: "gray1")! } - public static var gray2: UIColor { return UIColor(named: "gray2")! } - public static var gray3: UIColor { return UIColor(named: "gray3")! } - public static var green1: UIColor { return UIColor(named: "green1")! } - public static var green2: UIColor { return UIColor(named: "green2")! } - public static var green3: UIColor { return UIColor(named: "green3")! } - public static var red1: UIColor { return UIColor(named: "red1")! } - public static var red2: UIColor { return UIColor(named: "red2")! } - public static var red3: UIColor { return UIColor(named: "red3")! } - public static var violet1: UIColor { return UIColor(named: "violet1")! } - public static var violet2: UIColor { return UIColor(named: "violet2")! } - public static var violet3: UIColor { return UIColor(named: "violet3")! } - } - - public enum L { - public enum banner { - public enum bluetooth_controller_failed { - /// Cannot create the ble connection to the scooter, this is most likely a backend issue, please contact your fleet operator. - public static var title: String { return NSLocalizedString("banner.bluetooth_controller_failed.title", comment: "") } - } - - public enum check_out_next_step_fetch_failed { - /// Check out succeeded, but fetching the next step failed, please try again. - public static var message: String { return NSLocalizedString("banner.check_out_next_step_fetch_failed.message", comment: "") } - } - - public enum generic { - /// Error - public static var title: String { return NSLocalizedString("banner.generic.title", comment: "") } - } - - public enum invalid_credentials { - /// Wrong email & password combination - public static var title: String { return NSLocalizedString("banner.invalid_credentials.title", comment: "") } - } - - public enum login { - /// Login request failed, please try again - public static var message: String { return NSLocalizedString("banner.login.message", comment: "") } - } - - public enum no_network_connection { - /// No internet connection - public static var title: String { return NSLocalizedString("banner.no_network_connection.title", comment: "") } - } - - public enum request_failed { - /// Request failed, please try again - public static var title: String { return NSLocalizedString("banner.request_failed.title", comment: "") } - } - - public enum retry { - /// Please try again - public static var message: String { return NSLocalizedString("banner.retry.message", comment: "") } - } - } - - public enum button { - /// Connecting... - public static var connecting: String { return NSLocalizedString("button.connecting", comment: "") } - - /// Login - public static var login: String { return NSLocalizedString("button.login", comment: "") } - - /// Logout - public static var logout: String { return NSLocalizedString("button.logout", comment: "") } - - /// Navigate - public static var navigate: String { return NSLocalizedString("button.navigate", comment: "") } - - /// Okay - public static var okay: String { return NSLocalizedString("button.okay", comment: "") } - - /// Remember my choice - public static var remember_my_choice: String { return NSLocalizedString("button.remember_my_choice", comment: "") } - - /// Retry - public static var retry: String { return NSLocalizedString("button.retry", comment: "") } - - /// Start Task - public static var start_task: String { return NSLocalizedString("button.start_task", comment: "") } - - /// Unlock - public static var unlock: String { return NSLocalizedString("button.unlock", comment: "") } - - /// Unlocking... - public static var unlocking: String { return NSLocalizedString("button.unlocking", comment: "") } - } - - public enum error { - public enum generic { - /// Error - public static var title: String { return NSLocalizedString("error.generic.title", comment: "") } - } - - public enum location_required { - /// Turn on location services to allow Runner to determine your location - public static var message: String { return NSLocalizedString("error.location_required.message", comment: "") } - - /// Location required - public static var title: String { return NSLocalizedString("error.location_required.title", comment: "") } - } - - /// Not a valid email address - public static var invalid_email: String { return NSLocalizedString("error.invalid_email", comment: "") } - - /// Password must be at least 8 characters - /// include upper and lowercase letters, - /// numbers and special characters - public static var invalid_password: String { return NSLocalizedString("error.invalid_password", comment: "") } - - /// Starting Runner failed with error: %@ - public static func loading_failed(_ value1: String) -> String { - return String(format: NSLocalizedString("error.loading_failed", comment: ""), value1) - } - } - } -} -``` +Prints the overview, example usage and available flags to the console. ## License The MIT License (MIT) -Copyright (c) 2019 Kaan Dedeoglu +Copyright (c) 2020 Kaan Dedeoglu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Sources/Shark/ColorEnumBuilder.swift b/Sources/Shark/ColorEnumBuilder.swift index 254fefe..a8f9283 100644 --- a/Sources/Shark/ColorEnumBuilder.swift +++ b/Sources/Shark/ColorEnumBuilder.swift @@ -1,20 +1,14 @@ import Foundation -private enum ColorValue: Equatable, Comparable { - case color(name: String) +private struct ColorValue: Equatable, Comparable { + let name: String func declaration(indentLevel: Int) -> String { - switch self { - case .color(let name): - return #"\#(String(indentLevel: indentLevel))public static var \#(name.casenameSanitized): UIColor { return UIColor(named: "\#(name)", in: \#(SharkEnumBuilder.topLevelEnumName).bundle, compatibleWith: nil)! }"# - } + return #"\#(String.indent(indentLevel))public static var \#(name.casenameSanitized): UIColor { return UIColor(named: "\#(name)", in: bundle, compatibleWith: nil)! }"# } static func <(lhs: ColorValue, rhs: ColorValue) -> Bool { - switch (lhs, rhs) { - case (.color(let leftName), .color(let rightName)): - return leftName < rightName - } + return lhs.name < rhs.name } } @@ -26,13 +20,10 @@ enum ColorEnumBuilder { static func colorEnumString(forFilesAtPaths paths: [String], topLevelName: String) throws -> String? { let colorAssetPaths = try paths.flatMap { try FileManager.default.subpathsOfDirectory(atPath: $0).filter({ $0.pathExtension == Constants.colorSetExtension }) } guard colorAssetPaths.isEmpty == false else { return nil } - - var result = """ -public enum \(topLevelName) { -""" + var result = "public enum \(topLevelName) {\n" for name in colorAssetPaths.map({ $0.lastPathComponent.deletingPathExtension }).sorted() { - result += ColorValue.color(name: name).declaration(indentLevel: 1) + result += ColorValue(name: name).declaration(indentLevel: 1) result += "\n" } diff --git a/Sources/Shark/FontEnumBuilder.swift b/Sources/Shark/FontEnumBuilder.swift new file mode 100644 index 0000000..34877fb --- /dev/null +++ b/Sources/Shark/FontEnumBuilder.swift @@ -0,0 +1,49 @@ +import Foundation + +private struct FontValue: Equatable, Comparable { + let methodName: String + let fontName: String + + func declaration(indentLevel: Int) -> String { + #"\#(String.indent(indentLevel))public static func \#(methodName)(ofSize size: CGFloat) -> UIFont { return UIFont(name: "\#(fontName)", size: size)! }"# + } + + static func <(lhs: FontValue, rhs: FontValue) -> Bool { + lhs.methodName < rhs.methodName + } +} + +enum FontEnumBuilder { + static func fontsEnumString(forFilesAtPaths paths: [String], topLevelName: String) throws -> String? { + let fontValues: [FontValue] = paths.compactMap { path in + guard + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let font = CGDataProvider(data: data as CFData).flatMap(CGFont.init), + let fullName = font.fullName as String?, + let postScriptName = font.postScriptName as String? else { return nil } + + var components = fullName.split(separator: " ") + let first = components.removeFirst().lowercased() + let rest = components.map { $0.capitalized } + let methodName = ([first] + rest).joined() + + return FontValue(methodName: methodName, + fontName: postScriptName) + } + + guard fontValues.isEmpty == false else { return nil } + + var result = """ + public enum \(topLevelName) { + + """ + + for font in fontValues.sorted() { + result += font.declaration(indentLevel: 1) + result += "\n" + } + + result += "}" + return result + } +} diff --git a/Sources/Shark/ImageEnumBuilder.swift b/Sources/Shark/ImageEnumBuilder.swift index 7f416bb..c1cdcb6 100644 --- a/Sources/Shark/ImageEnumBuilder.swift +++ b/Sources/Shark/ImageEnumBuilder.swift @@ -1,18 +1,18 @@ import Foundation private enum ImageValue: Equatable, Comparable { - case namespace(name: String) case image(caseName: String, value: String) + case namespace(name: String) func declaration(withBody body: String = "", indentLevel: Int) throws -> String { switch self { case .image(let name, let value): - return #"\#(String(indentLevel: indentLevel))public static var \#(name): UIImage { return UIImage(named:"\#(value)", in: \#(SharkEnumBuilder.topLevelEnumName).bundle, compatibleWith: nil)! }"# + return #"\#(String.indent(indentLevel))public static var \#(name): UIImage { return UIImage(named:"\#(value)", in: bundle, compatibleWith: nil)! }"# case .namespace(let name): return #""" - \#(String(indentLevel: indentLevel))public enum \#(name) { + \#(String.indent(indentLevel))public enum \#(name) { \#(body) - \#(String(indentLevel: indentLevel))} + \#(String.indent(indentLevel))} """# } @@ -31,6 +31,24 @@ private enum ImageValue: Equatable, Comparable { } } +extension ImageValue: SanitizableValue { + var name: String { + switch self { + case .namespace(let name), .image(let name, _): + return name + } + } + + func underscoringName() -> ImageValue { + switch self { + case .image(let caseName, let value): + return .image(caseName: caseName.underscored, value: value) + case .namespace(let name): + return .namespace(name: name.underscored) + } + } +} + enum ImageEnumBuilder { private enum Constants { static let imageSetExtension = "imageset" @@ -67,43 +85,3 @@ enum ImageEnumBuilder { } } } - -private extension Node where Element == ImageValue { - func sanitize() { - //If two children have the same name, or if a child has the same name with its parent, underscore - var modified = false - repeat { - modified = false - var countedSet = CountedSet() - for child in children { - for _ in 0.. Self { + switch self { + case .localization(let name, let key, let value): + return .localization(name: name.underscored, key: key, value: value) + case .namespace(let name): + return .namespace(name: name.underscored) + } + } +} + +enum LocalizationBuilderError: LocalizedError { case invalidLocalizableStringsFile(path: String) - - var localizedDescription: String { + + var errorDescription: String? { switch self { case .invalidLocalizableStringsFile(let path): return "Invalid .strings file at \(path)" @@ -154,49 +172,9 @@ extension Array where Element == LocalizationValue.InterpolationType { let formatValuesString = (1...count).map { "\(variableName)\($0)"}.joined(separator: ", ") return #""" - \#(String(indentLevel: indentLevel))public static func \#(name)(\#(argumentsString)) -> String { - \#(String(indentLevel:indentLevel + 1))return String(format: NSLocalizedString("\#(key)", bundle: \#(SharkEnumBuilder.topLevelEnumName).bundle, comment: ""), \#(formatValuesString)) - \#(String(indentLevel: indentLevel))} + \#(String.indent(indentLevel))public static func \#(name)(\#(argumentsString)) -> String { + \#(String.indent(indentLevel + 1))return String(format: NSLocalizedString("\#(key)", bundle: bundle, comment: ""), \#(formatValuesString)) + \#(String.indent(indentLevel))} """# } } - -private extension Node where Element == LocalizationValue { - func sanitize() { - //If two children have the same name, or if a children has the same name with a parent, underscore - var modified = false - repeat { - modified = false - var countedSet = CountedSet() - for child in children { - for _ in 0.. { var value: Element @@ -44,3 +42,35 @@ extension Node: Comparable where Element: Comparable { return lhs.value < rhs.value } } + +extension Node where Element: SanitizableValue { + func sanitize() { + //If two children have the same name, or if a children has the same name with a parent, underscore + var modified = false + repeat { + modified = false + var countedSet = CountedSet() + for child in children { + for _ in 0.. Self +} diff --git a/Sources/Shark/Shark.swift b/Sources/Shark/Shark.swift new file mode 100644 index 0000000..053734b --- /dev/null +++ b/Sources/Shark/Shark.swift @@ -0,0 +1,75 @@ +import Foundation +import ArgumentParser + +struct Shark: ParsableCommand { + static var configuration: CommandConfiguration = .init(abstract:#""" +Paste the following line in a Xcode run phase script that runs after the "Compile Sources" run phase: +shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME +"""#) + + @OptionGroup() + private var options: Options + + func run() throws { + let enumString = try SharkEnumBuilder.sharkEnumString(forOptions: options) + + try FileBuilder + .fileContents(with: enumString, filename: options.outputPath.lastPathComponent) + .write(to: URL(fileURLWithPath: options.outputPath), atomically: true, encoding: .utf8) + } +} + +struct Options: ParsableArguments { + @Argument(help: "The .xcodeproj file path", transform: Self.transform(forProjectPath:)) + fileprivate(set) var projectPath: String + + @Argument(help: "The output file path", transform: Self.transform(forOutputPath:)) + fileprivate(set) var outputPath: String + + @Option(name: .customLong("name"), + help: "The top level enum name") + private(set) var topLevelEnumName: String = "Shark" + + @Option(name: .customLong("target"), + help: "Target name of the application, useful in case there are multiple application targets") + private(set) var targetName: String? + + @Option(name: .long, + help: "Localization code to use when selecting the Localizable.strings. i.e en, de, es.") + private(set) var locale: String = "en" +} + +extension Options { + private static func transform(forProjectPath path: String) throws -> String { + var isDirectory: ObjCBool = false + if path.pathExtension == "xcodeproj" { + return path + } else if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue { + let projectFiles = try FileManager + .default + .contentsOfDirectory(atPath: path).filter { $0.pathExtension == "xcodeproj" } + + if projectFiles.isEmpty { + throw ValidationError("\(path) should point to a .xcodeproj file") + } else if projectFiles.count == 1 { + return path.appendingPathComponent(projectFiles[0]) + } else { + throw ValidationError("There are multiple .xcodeproj files in directory: \(path). Please provide an exact path") + } + } else { + throw ValidationError("\(path) should point to a .xcodeproj file") + } + } + + private static func transform(forOutputPath path: String) throws -> String { + var path = path + var isDirectory: ObjCBool = false + if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue { + path.append("Shark.swift") + } else if path.pathExtension != "swift" { + throw ValidationError("The output path should either point to an existing folder or end with a .swift extension") + } + + return path + } +} diff --git a/Sources/Shark/SharkEnumBuilder.swift b/Sources/Shark/SharkEnumBuilder.swift index 6c38e07..b5b0de2 100644 --- a/Sources/Shark/SharkEnumBuilder.swift +++ b/Sources/Shark/SharkEnumBuilder.swift @@ -1,21 +1,23 @@ -import Foundation - enum SharkEnumBuilder { - static var topLevelEnumName = "Shark" static func sharkEnumString(forOptions options: Options) throws -> String { - SharkEnumBuilder.topLevelEnumName = options.topLevelEnumName let resourcePaths = try XcodeProjectHelper(options: options).resourcePaths() let imagesString = try ImageEnumBuilder.imageEnumString(forFilesAtPaths: resourcePaths.assetsPaths, topLevelName: "I") let colorsString = try ColorEnumBuilder.colorEnumString(forFilesAtPaths: resourcePaths.assetsPaths, topLevelName: "C") let localizationsString = try LocalizationEnumBuilder.localizationsEnumString(forFilesAtPaths: resourcePaths.localizationPaths, topLevelName: "L") - - let declarations = [imagesString, colorsString, localizationsString].compactMap({ $0?.indented(withLevel: 1) }).joined(separator: "\n\n") + let fontsString = try FontEnumBuilder.fontsEnumString(forFilesAtPaths: resourcePaths.fontPaths, topLevelName: "F") + + let declarations = [imagesString, colorsString, fontsString, localizationsString] + .compactMap({ $0?.indented(withLevel: 1) }) + .joined(separator: "\n\n") return """ - public enum \(topLevelEnumName) { - private class Custom {} - static var bundle: Bundle { return Bundle(for: Custom.self) } + public enum \(options.topLevelEnumName) { + private static let bundle: Bundle = { + class Custom {} + return Bundle(for: Custom.self) + }() + \(declarations) } """ diff --git a/Sources/Shark/String+Extensions.swift b/Sources/Shark/String+Extensions.swift index 9c84763..086ffc8 100644 --- a/Sources/Shark/String+Extensions.swift +++ b/Sources/Shark/String+Extensions.swift @@ -9,9 +9,9 @@ extension String { "#sourceLocation", "#warning", "associativity", "convenience", "dynamic", "didSet", "final", "get", "infix", "indirect", "lazy", "left", "mutating", "none", "nonmutating", "optional", "override", "postfix", "precedence", "prefix", "Protocol", "required", "right", "set", "Type", "unowned", "weak", "willSet", "some", "__COLUMN__", "__FILE__", "__FUNCTION__", "__LINE__"] - - init(indentLevel: Int) { - self.init(repeating: " ", count: indentLevel * 4) + + static func indent(_ level: Int) -> String { + String(repeating: " ", count: level * 4) } var expandingTildeInPath: String { @@ -63,7 +63,7 @@ extension String { } func indented(withLevel level: Int) -> String { - return mapLines { String(indentLevel: level) + $0 } + mapLines { $0.allSatisfy(\.isWhitespace) ? $0 : String.indent(level) + $0 } } func mapLines(_ transform: (String) -> String) -> String { diff --git a/Sources/Shark/XcodeProjectHelper.swift b/Sources/Shark/XcodeProjectHelper.swift index 6e7c26b..3669887 100644 --- a/Sources/Shark/XcodeProjectHelper.swift +++ b/Sources/Shark/XcodeProjectHelper.swift @@ -8,8 +8,9 @@ enum PBXFilePathError: String, Error { struct XcodeProjectHelper { struct ResourcePaths { - let localizationPaths: [String] - let assetsPaths: [String] + fileprivate(set) var localizationPaths: [String] = [] + fileprivate(set) var assetsPaths: [String] = [] + fileprivate(set) var fontPaths: [String] = [] } private let projectPath: Path @@ -49,23 +50,22 @@ struct XcodeProjectHelper { print("Cannot locate the resources build phase in the target") exit(EXIT_FAILURE) } - - var localizationPaths: [String] = [] - var assetPaths: [String] = [] - - let allPaths = try targetResourcesFiles.compactMap { $0.file }.flatMap { try paths(for: $0) } - for path in allPaths { - switch path.pathExtension { - case "xcassets": - assetPaths.append(path) - case "strings" where path.pathComponents.contains("\(locale).lproj"): - localizationPaths.append(path) - default: - break - } - } - return ResourcePaths(localizationPaths: localizationPaths, assetsPaths: assetPaths) + return try targetResourcesFiles + .compactMap { $0.file } + .flatMap(paths(for:)) + .reduce(into: ResourcePaths(), { result, path in + switch path.pathExtension { + case "xcassets": + result.assetsPaths.append(path) + case "strings" where path.pathComponents.contains("\(locale).lproj"): + result.localizationPaths.append(path) + case "ttf", "otf", "ttc": + result.fontPaths.append(path) + default: + break + } + }) } private func paths(for fileElement: PBXFileElement) throws -> [String] {