Refactor in one unique command with subcommand + add a new command to generate ressources from a configuration file
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
This commit is contained in:
121
Sources/ResgenSwift/Colors/Colors.swift
Normal file
121
Sources/ResgenSwift/Colors/Colors.swift
Normal file
@ -0,0 +1,121 @@
|
||||
//
|
||||
// main.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 20/12/2021.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Colors: ParsableCommand {
|
||||
|
||||
// MARK: - CommandConfiguration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "A utility for generate colors assets and their getters.",
|
||||
version: ResgenSwiftVersion
|
||||
)
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static let toolName = "Color"
|
||||
static let defaultExtensionName = "UIColor"
|
||||
static let assetsColorsFolderName = "Colors"
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var extensionFileName: String {
|
||||
if options.extensionSuffix.isEmpty == false {
|
||||
return "\(options.extensionName)+\(options.extensionSuffix).swift"
|
||||
}
|
||||
return "\(options.extensionName).swift"
|
||||
}
|
||||
var extensionFilePath: String { "\(options.extensionOutputPath)/\(extensionFileName)" }
|
||||
var generateStaticVariable: Bool {
|
||||
options.extensionName == Self.defaultExtensionName
|
||||
}
|
||||
|
||||
// MARK: - Command options
|
||||
|
||||
@OptionGroup var options: ColorsToolOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
public func run() throws {
|
||||
print("[\(Self.toolName)] Starting colors generation")
|
||||
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate \(options.colorStyle) colors in xcassets \(options.xcassetsPath)")
|
||||
|
||||
// Check requirements
|
||||
guard checkRequirements() else { return }
|
||||
|
||||
print("[\(Self.toolName)] Will generate colors")
|
||||
|
||||
// Delete current colors
|
||||
deleteCurrentColors()
|
||||
|
||||
// Get colors to generate
|
||||
let parsedColors = ColorFileParser.parse(options.inputFile,
|
||||
colorStyle: options.colorStyle)
|
||||
|
||||
// Generate all colors in xcassets
|
||||
ColorXcassetHelper.generateXcassetColors(colors: parsedColors,
|
||||
to: options.xcassetsPath)
|
||||
|
||||
// Generate extension
|
||||
ColorExtensionGenerator.writeExtensionFile(colors: parsedColors,
|
||||
staticVar: generateStaticVariable,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: extensionFilePath)
|
||||
|
||||
print("[\(Self.toolName)] Colors generated")
|
||||
}
|
||||
|
||||
// MARK: - Requirements
|
||||
|
||||
private func checkRequirements() -> Bool {
|
||||
let fileManager = FileManager()
|
||||
|
||||
// Check if input file exists
|
||||
guard fileManager.fileExists(atPath: options.inputFile) else {
|
||||
let error = ColorsToolError.fileNotExists(options.inputFile)
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
|
||||
// Check if xcassets file exists
|
||||
guard fileManager.fileExists(atPath: options.xcassetsPath) else {
|
||||
let error = ColorsToolError.fileNotExists(options.xcassetsPath)
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
|
||||
// Check if needed to regenerate
|
||||
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, inputFilePath: options.inputFile, extensionFilePath: extensionFilePath) else {
|
||||
print("[\(Self.toolName)] Colors are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func deleteCurrentColors() {
|
||||
Shell.shell("rm", "-rf", "\(options.xcassetsPath)/Colors/*")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Command samples:
|
||||
|
||||
1. UIColor extension without suffix
|
||||
swift run -c release ColorToolCore -f ./SampleFiles/Colors/sampleColors1.txt --style all --xcassets-path "./SampleFiles/Colors/colors.xcassets" --extension-output-path "./SampleFiles/Colors/Generated/" --extension-name "UIColor"
|
||||
|
||||
2. UIColor extension with custom suffix
|
||||
swift run -c release ColorToolCore -f ./SampleFiles/Colors/sampleColors1.txt --style all --xcassets-path "./SampleFiles/Colors/colors.xcassets" --extension-output-path "./SampleFiles/Colors/Generated/" --extension-name "UIColor" --extension-suffix "SampleApp"
|
||||
|
||||
3. Custom extension with only light theme colors (R2Color)
|
||||
swift run -c release ColorToolCore -f ./SampleFiles/Colors/sampleColors1.txt --style light --xcassets-path "./SampleFiles/Colors/colors.xcassets" --extension-output-path "./SampleFiles/Colors/Generated/" --extension-name "R2Color"
|
||||
*/
|
35
Sources/ResgenSwift/Colors/ColorsToolError.swift
Normal file
35
Sources/ResgenSwift/Colors/ColorsToolError.swift
Normal file
@ -0,0 +1,35 @@
|
||||
//
|
||||
// ColorsToolError.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 20/12/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ColorsToolError: Error {
|
||||
case badFormat(String)
|
||||
case writeAsset(String)
|
||||
case writeExtension(String, String)
|
||||
case fileNotExists(String)
|
||||
case badColorDefinition(String, String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .badFormat(let info):
|
||||
return "error:[\(Colors.toolName)] Bad line format: \(info). Accepted format are: colorName=\"#RGB/#ARGB\"; colorName \"#RGB/#ARGB\"; colorName \"#RGB/#ARGB\" \"#RGB/#ARGB\""
|
||||
|
||||
case .writeAsset(let info):
|
||||
return "error:[\(Colors.toolName)] An error occured while writing color in Xcasset: \(info)"
|
||||
|
||||
case .writeExtension(let filename, let info):
|
||||
return "error:[\(Colors.toolName)] An error occured while writing extension in \(filename): \(info)"
|
||||
|
||||
case .fileNotExists(let filename):
|
||||
return "error:[\(Colors.toolName)] File \(filename) does not exists"
|
||||
|
||||
case .badColorDefinition(let lightColor, let darkColor):
|
||||
return "error:[\(Colors.toolName)]One of these two colors has invalid synthax: -\(lightColor)- or -\(darkColor)-"
|
||||
}
|
||||
}
|
||||
}
|
38
Sources/ResgenSwift/Colors/ColorsToolOptions.swift
Normal file
38
Sources/ResgenSwift/Colors/ColorsToolOptions.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// ColorsToolOptions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 17/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct ColorsToolOptions: ParsableArguments {
|
||||
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
|
||||
var forceGeneration = false
|
||||
|
||||
@Argument(help: "Input files where colors ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var inputFile: String
|
||||
|
||||
@Option(help: "Color style to generate: light for light colors only, or all for dark and light colors")
|
||||
fileprivate var style: String
|
||||
|
||||
@Option(help: "Path of xcassets where to generate colors", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var xcassetsPath: String
|
||||
|
||||
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var extensionOutputPath: String
|
||||
|
||||
@Option(help: "Extension name. If not specified, it will generate an UIColor extension. Using default extension name will generate static property.")
|
||||
var extensionName: String = Colors.defaultExtensionName
|
||||
|
||||
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift")
|
||||
var extensionSuffix: String = ""
|
||||
}
|
||||
|
||||
extension ColorsToolOptions {
|
||||
var colorStyle: ColorStyle {
|
||||
ColorStyle(rawValue: style) ?? .all
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
//
|
||||
// ColorExtensionGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 20/12/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
|
||||
struct ColorExtensionGenerator {
|
||||
|
||||
let colors: [ParsedColor]
|
||||
let extensionClassname: String
|
||||
|
||||
static func writeExtensionFile(colors: [ParsedColor], staticVar: Bool, extensionName: String, extensionFilePath: String) {
|
||||
// Create file if not exists
|
||||
let fileManager = FileManager()
|
||||
if fileManager.fileExists(atPath: extensionFilePath) == false {
|
||||
Shell.shell("touch", "\(extensionFilePath)")
|
||||
}
|
||||
|
||||
// Create extension content
|
||||
let extensionContent = [
|
||||
Self.getHeader(extensionClassname: extensionName),
|
||||
Self.getProperties(for: colors, withStaticVar: staticVar),
|
||||
Self.getFooter()
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionContent.write(to: extensionFilePathURL, atomically: true, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = ColorsToolError.writeExtension(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func getHeader(extensionClassname: String) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.\(Colors.toolName) \(ResgenSwiftVersion)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension \(extensionClassname) {\n
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getProperties(for colors: [ParsedColor], withStaticVar staticVar: Bool) -> String {
|
||||
colors.map {
|
||||
if staticVar {
|
||||
return $0.getColorStaticProperty()
|
||||
}
|
||||
return $0.getColorProperty()
|
||||
}
|
||||
.joined(separator: "\n\n")
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
//
|
||||
// ColorXcassetHelper.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 20/12/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
|
||||
struct ColorXcassetHelper {
|
||||
|
||||
static func generateXcassetColors(colors: [ParsedColor], to xcassetsPath: String) {
|
||||
colors.forEach {
|
||||
Self.generateColorSetAssets(from: $0, to: xcassetsPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate ColorSet in XCAssets file
|
||||
private static func generateColorSetAssets(from color: ParsedColor, to xcassetsPath: String) {
|
||||
// Create ColorSet
|
||||
let colorSetPath = "\(xcassetsPath)/Colors/\(color.name).colorset"
|
||||
Shell.shell("mkdir", "-p", "\(colorSetPath)")
|
||||
|
||||
// Create Contents.json in ColorSet
|
||||
let contentsJsonPath = "\(colorSetPath)/Contents.json"
|
||||
Shell.shell("touch", "\(contentsJsonPath)")
|
||||
|
||||
// Write content in Contents.json
|
||||
let contentsJsonPathURL = URL(fileURLWithPath: contentsJsonPath)
|
||||
do {
|
||||
try color.contentsJSON().write(to: contentsJsonPathURL, atomically: true, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = ColorsToolError.writeAsset(error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
}
|
||||
}
|
13
Sources/ResgenSwift/Colors/Model/ColorStyle.swift
Normal file
13
Sources/ResgenSwift/Colors/Model/ColorStyle.swift
Normal file
@ -0,0 +1,13 @@
|
||||
//
|
||||
// ColorStyle.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 29/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ColorStyle: String, Decodable {
|
||||
case light
|
||||
case all
|
||||
}
|
92
Sources/ResgenSwift/Colors/Model/ParsedColor.swift
Normal file
92
Sources/ResgenSwift/Colors/Model/ParsedColor.swift
Normal file
@ -0,0 +1,92 @@
|
||||
//
|
||||
// ParsedColor.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 20/12/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ParsedColor {
|
||||
let name: String
|
||||
let light: String
|
||||
let dark: String
|
||||
|
||||
// Generate Contents.json content
|
||||
func contentsJSON() -> String {
|
||||
let lightARGB = light.colorComponent()
|
||||
let darkARGB = dark.colorComponent()
|
||||
|
||||
let allComponents = [
|
||||
lightARGB.alpha, lightARGB.red, lightARGB.green, lightARGB.blue,
|
||||
darkARGB.alpha, darkARGB.red, darkARGB.green, darkARGB.blue
|
||||
].map {
|
||||
$0.isEmpty
|
||||
}
|
||||
|
||||
guard allComponents.contains(true) == false else {
|
||||
let error = ColorsToolError.badColorDefinition(light, dark)
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
|
||||
return """
|
||||
{
|
||||
"colors": [
|
||||
{
|
||||
"color": {
|
||||
"color-space": "srgb",
|
||||
"components": {
|
||||
"alpha": "0x\(lightARGB.alpha)",
|
||||
"blue": "0x\(lightARGB.blue)",
|
||||
"green": "0x\(lightARGB.green)",
|
||||
"red": "0x\(lightARGB.red)",
|
||||
}
|
||||
},
|
||||
"idiom": "universal"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"color": {
|
||||
"color-space": "srgb",
|
||||
"components": {
|
||||
"alpha": "0x\(darkARGB.alpha)",
|
||||
"blue": "0x\(darkARGB.blue)",
|
||||
"green": "0x\(darkARGB.green)",
|
||||
"red": "0x\(darkARGB.red)",
|
||||
}
|
||||
},
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
func getColorProperty() -> String {
|
||||
"""
|
||||
/// Color \(name) is \(light) (light) or \(dark) (dark)"
|
||||
@objc var \(name): UIColor {
|
||||
UIColor(named: "\(name)")!
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
func getColorStaticProperty() -> String {
|
||||
"""
|
||||
/// Color \(name) is \(light) (light) or \(dark) (dark)"
|
||||
static var \(name): UIColor {
|
||||
UIColor(named: "\(name)")!
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
49
Sources/ResgenSwift/Colors/Parser/ColorFileParser.swift
Normal file
49
Sources/ResgenSwift/Colors/Parser/ColorFileParser.swift
Normal file
@ -0,0 +1,49 @@
|
||||
//
|
||||
// ColorFileParser.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 29/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ColorFileParser {
|
||||
static func parse(_ inputFile: String, colorStyle: ColorStyle) -> [ParsedColor] {
|
||||
// Get content of input file
|
||||
let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8)
|
||||
let colorsByLines = inputFileContent.components(separatedBy: CharacterSet.newlines)
|
||||
|
||||
// Iterate on each line of input file
|
||||
return colorsByLines.enumerated().compactMap { lineNumber, colorLine in
|
||||
// Required format:
|
||||
// colorName="#RGB/#ARGB", colorName "#RGB/#ARGB", colorName "#RGB/#ARGB" "#RGB/#ARGB"
|
||||
let colorLineCleanedUp = colorLine
|
||||
.removeTrailingWhitespace()
|
||||
.replacingOccurrences(of: "=", with: "") // Keep compat with current file format
|
||||
|
||||
guard colorLineCleanedUp.hasPrefix("#") == false, colorLineCleanedUp.isEmpty == false else {
|
||||
print("[\(Colors.toolName)] ⚠️ BadFormat or empty line (line number: \(lineNumber + 1)). Skip this line")
|
||||
return nil
|
||||
}
|
||||
|
||||
let colorContent = colorLineCleanedUp.split(separator: " ")
|
||||
|
||||
guard colorContent.count >= 2 else {
|
||||
let error = ColorsToolError.badFormat(colorLine)
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
|
||||
switch colorStyle {
|
||||
case .light:
|
||||
return ParsedColor(name: String(colorContent[0]), light: String(colorContent[1]), dark: String(colorContent[1]))
|
||||
|
||||
case .all:
|
||||
if colorContent.count == 3 {
|
||||
return ParsedColor(name: String(colorContent[0]), light: String(colorContent[1]), dark: String(colorContent[2]))
|
||||
}
|
||||
return ParsedColor(name: String(colorContent[0]), light: String(colorContent[1]), dark: String(colorContent[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
Sources/ResgenSwift/Fonts/FontOptions.swift
Normal file
43
Sources/ResgenSwift/Fonts/FontOptions.swift
Normal file
@ -0,0 +1,43 @@
|
||||
//
|
||||
// FontsOptions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 17/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct FontsOptions: ParsableArguments {
|
||||
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
|
||||
var forceGeneration = false
|
||||
|
||||
@Argument(help: "Input files where fonts ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var inputFile: String
|
||||
|
||||
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var extensionOutputPath: String
|
||||
|
||||
@Option(help: "Extension name. If not specified, it will generate an UIFont extension. Using default extension name will generate static property.")
|
||||
var extensionName: String = Fonts.defaultExtensionName
|
||||
|
||||
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+FontsMyApp.swift")
|
||||
var extensionSuffix: String = ""
|
||||
}
|
||||
|
||||
extension FontsOptions {
|
||||
var extensionFileName: String {
|
||||
if extensionSuffix.isEmpty == false {
|
||||
return "\(extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(extensionName).swift"
|
||||
}
|
||||
|
||||
var extensionFilePath: String {
|
||||
"\(extensionOutputPath)/\(extensionFileName)"
|
||||
}
|
||||
|
||||
var generateStaticVariable: Bool {
|
||||
extensionName == Fonts.defaultExtensionName
|
||||
}
|
||||
}
|
93
Sources/ResgenSwift/Fonts/Fonts.swift
Normal file
93
Sources/ResgenSwift/Fonts/Fonts.swift
Normal file
@ -0,0 +1,93 @@
|
||||
//
|
||||
// Fonts.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 13/12/2021.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Fonts: ParsableCommand {
|
||||
|
||||
// MARK: - CommandConfiguration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "A utility to generate an helpful etension to access your custom font from code and also Info.plist UIAppsFont content.",
|
||||
version: ResgenSwiftVersion
|
||||
)
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static let toolName = "Fonts"
|
||||
static let defaultExtensionName = "UIFont"
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var extensionFileName: String {
|
||||
if options.extensionSuffix.isEmpty == false {
|
||||
return "\(options.extensionName)+\(options.extensionSuffix).swift"
|
||||
}
|
||||
return "\(options.extensionName).swift"
|
||||
}
|
||||
var extensionFilePath: String { "\(options.extensionOutputPath)/\(extensionFileName)" }
|
||||
var generateStaticVariable: Bool {
|
||||
options.extensionName == Self.defaultExtensionName
|
||||
}
|
||||
|
||||
// MARK: - Command Options
|
||||
|
||||
@OptionGroup var options: FontsOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
public func run() throws {
|
||||
print("[\(Self.toolName)] Starting fonts generation")
|
||||
|
||||
// Check requirements
|
||||
guard checkRequirements() else { return }
|
||||
|
||||
print("[\(Self.toolName)] Will generate fonts")
|
||||
|
||||
// Get fonts to generate
|
||||
let fontsToGenerate = FontFileParser.parse(options.inputFile)
|
||||
|
||||
// Get real font names
|
||||
let inputFolder = URL(fileURLWithPath: options.inputFile).deletingLastPathComponent().relativePath
|
||||
let fontsNames = FontsToolHelper.getFontPostScriptName(for: fontsToGenerate,
|
||||
inputFolder: inputFolder)
|
||||
|
||||
// Generate extension
|
||||
FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames,
|
||||
staticVar: generateStaticVariable,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: extensionFilePath)
|
||||
|
||||
print("Info.plist information:")
|
||||
print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames))")
|
||||
|
||||
print("[\(Self.toolName)] Fonts generated")
|
||||
}
|
||||
|
||||
// MARK: - Requirements
|
||||
|
||||
private func checkRequirements() -> Bool {
|
||||
let fileManager = FileManager()
|
||||
|
||||
// Check input file exists
|
||||
guard fileManager.fileExists(atPath: options.inputFile) else {
|
||||
let error = FontsToolError.fileNotExists(options.inputFile)
|
||||
print(error.localizedDescription)
|
||||
Fonts.exit(withError: error)
|
||||
}
|
||||
|
||||
// Check if needed to regenerate
|
||||
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, inputFilePath: options.inputFile, extensionFilePath: extensionFilePath) else {
|
||||
print("[\(Self.toolName)] Fonts are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
31
Sources/ResgenSwift/Fonts/FontsToolError.swift
Normal file
31
Sources/ResgenSwift/Fonts/FontsToolError.swift
Normal file
@ -0,0 +1,31 @@
|
||||
//
|
||||
// FontsToolError.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 13/12/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum FontsToolError: Error {
|
||||
case fcScan(String, Int32, String?)
|
||||
case inputFolderNotFound(String)
|
||||
case fileNotExists(String)
|
||||
case writeExtension(String, String)
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .fcScan(let path, let code, let output):
|
||||
return "error:[\(Fonts.toolName)] Error while getting fontName (fc-scan --format %{postscriptname} \(path). fc-scan exit with \(code) and output is: \(output ?? "no output")"
|
||||
|
||||
case .inputFolderNotFound(let inputFolder):
|
||||
return " error:[\(Fonts.toolName)] Input folder not found: \(inputFolder)"
|
||||
|
||||
case .fileNotExists(let filename):
|
||||
return " error:[\(Fonts.toolName)] File \(filename) does not exists"
|
||||
|
||||
case .writeExtension(let filename, let info):
|
||||
return "error:[\(Fonts.toolName)] An error occured while writing extension in \(filename): \(info)"
|
||||
}
|
||||
}
|
||||
}
|
73
Sources/ResgenSwift/Fonts/FontsToolHelper.swift
Normal file
73
Sources/ResgenSwift/Fonts/FontsToolHelper.swift
Normal file
@ -0,0 +1,73 @@
|
||||
//
|
||||
// FontsToolHelper.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 13/12/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
|
||||
class FontsToolHelper {
|
||||
|
||||
static func getFontPostScriptName(for fonts: [String], inputFolder: String) -> [FontName] {
|
||||
let fontsFilenames = Self.getFontsFilenames(fromInputFolder: inputFolder)
|
||||
.filter { fontNameWithPath in
|
||||
let fontName = URL(fileURLWithPath: fontNameWithPath)
|
||||
.deletingPathExtension()
|
||||
.lastPathComponent
|
||||
|
||||
if fonts.contains(fontName) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let fontsFilesnamesWithPath = fontsFilenames.map {
|
||||
"\(inputFolder)/\($0)"
|
||||
}
|
||||
|
||||
return fontsFilesnamesWithPath.compactMap {
|
||||
Self.getFontName(atPath: $0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private static func getFontsFilenames(fromInputFolder inputFolder: String) -> [String] {
|
||||
// Get a enumerator for all files
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.fileExists(atPath: inputFolder) else {
|
||||
let error = FontsToolError.inputFolderNotFound(inputFolder)
|
||||
print(error.localizedDescription)
|
||||
Fonts.exit(withError: error)
|
||||
}
|
||||
|
||||
let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: inputFolder)!
|
||||
|
||||
// Filters font files
|
||||
let fontsFileNames: [String] = (enumerator.allObjects as! [String])
|
||||
.filter {
|
||||
if $0.hasSuffix(".ttf") || $0.hasSuffix(".otf") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return fontsFileNames
|
||||
}
|
||||
|
||||
private static func getFontName(atPath path: String) -> String {
|
||||
//print("fc-scan --format %{postscriptname} \(path)")
|
||||
// Get real font name
|
||||
let task = Shell.shell("fc-scan", "--format", "%{postscriptname}", path)
|
||||
|
||||
guard let fontName = task.output, task.terminationStatus == 0 else {
|
||||
let error = FontsToolError.fcScan(path, task.terminationStatus, task.output)
|
||||
print(error.localizedDescription)
|
||||
Fonts.exit(withError: error)
|
||||
}
|
||||
|
||||
return fontName
|
||||
}
|
||||
}
|
22
Sources/ResgenSwift/Fonts/Generator/FontPlistGenerator.swift
Normal file
22
Sources/ResgenSwift/Fonts/Generator/FontPlistGenerator.swift
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// FontPlistGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 29/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class FontPlistGenerator {
|
||||
static func generatePlistUIAppsFontContent(for fonts: [FontName]) -> String {
|
||||
var plistData = "<key>UIAppFonts</key>\n\t<array>\n"
|
||||
fonts
|
||||
.compactMap { $0 }
|
||||
.forEach {
|
||||
plistData += "\t\t<string>\($0)</string>\n"
|
||||
}
|
||||
plistData += "\t</array>\n*/"
|
||||
|
||||
return plistData
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
//
|
||||
// FontToolContentGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 13/12/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
|
||||
class FontExtensionGenerator {
|
||||
|
||||
static func writeExtensionFile(fontsNames: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
|
||||
// Check file if not exists
|
||||
let fileManager = FileManager()
|
||||
if fileManager.fileExists(atPath: extensionFilePath) == false {
|
||||
Shell.shell("touch", "\(extensionFilePath)")
|
||||
}
|
||||
|
||||
// Create extension content
|
||||
let extensionContent = [
|
||||
Self.getHeader(extensionClassname: extensionName),
|
||||
Self.getFontNameEnum(fontsNames: fontsNames),
|
||||
Self.getFontMethods(fontsNames: fontsNames, staticVar: staticVar),
|
||||
Self.getFooter()
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionContent.write(to: extensionFilePathURL, atomically: true, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Fonts.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func getHeader(extensionClassname: String) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.\(Fonts.toolName) \(ResgenSwiftVersion)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension \(extensionClassname) {\n
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getFontNameEnum(fontsNames: [String]) -> String {
|
||||
var enumDefinition = " enum FontName: String {\n"
|
||||
|
||||
fontsNames.forEach {
|
||||
enumDefinition += " case \($0.removeCharacters(from: "[]+-_")) = \"\($0)\"\n"
|
||||
}
|
||||
enumDefinition += " }\n"
|
||||
|
||||
return enumDefinition
|
||||
}
|
||||
|
||||
private static func getFontMethods(fontsNames: [FontName], staticVar: Bool) -> String {
|
||||
let pragma = " // MARK: - Getter"
|
||||
|
||||
var propertiesOrMethods: [String] = fontsNames
|
||||
.unique()
|
||||
.map {
|
||||
if staticVar {
|
||||
return $0.staticProperty
|
||||
} else {
|
||||
return $0.method
|
||||
}
|
||||
}
|
||||
|
||||
propertiesOrMethods.insert(pragma, at: 0)
|
||||
return propertiesOrMethods.joined(separator: "\n\n")
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
32
Sources/ResgenSwift/Fonts/Model/FontName.swift
Normal file
32
Sources/ResgenSwift/Fonts/Model/FontName.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// FontName.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 29/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias FontName = String
|
||||
|
||||
extension FontName {
|
||||
var fontNameSanitize: String {
|
||||
self.removeCharacters(from: "[]+-_")
|
||||
}
|
||||
|
||||
var method: String {
|
||||
"""
|
||||
func \(fontNameSanitize)(withSize size: CGFloat) -> UIFont {
|
||||
UIFont(name: FontName.\(fontNameSanitize).rawValue, size: size)!
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
var staticProperty: String {
|
||||
"""
|
||||
static let \(fontNameSanitize): ((_ size: CGFloat) -> UIFont) = { size in
|
||||
UIFont(name: FontName.\(fontNameSanitize).rawValue, size: size)!
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
16
Sources/ResgenSwift/Fonts/Parser/FontFileParser.swift
Normal file
16
Sources/ResgenSwift/Fonts/Parser/FontFileParser.swift
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// FontFileParser.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 29/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class FontFileParser {
|
||||
static func parse(_ inputFile: String) -> [String] {
|
||||
let inputFileContent = try! String(contentsOfFile: inputFile,
|
||||
encoding: .utf8)
|
||||
return inputFileContent.components(separatedBy: CharacterSet.newlines)
|
||||
}
|
||||
}
|
51
Sources/ResgenSwift/Generate/Generate.swift
Normal file
51
Sources/ResgenSwift/Generate/Generate.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// Generate.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Generate: ParsableCommand {
|
||||
|
||||
// MARK: - CommandConfiguration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "A utility to generate ressources based on a configuration file",
|
||||
version: ResgenSwiftVersion
|
||||
)
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static let toolName = "Generate"
|
||||
|
||||
// MARK: - Command Options
|
||||
|
||||
@OptionGroup var options: GenerateOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
public func run() throws {
|
||||
print("[\(Self.toolName)] Starting Resgen with configuration: \(options.configurationFile)")
|
||||
|
||||
// Parse
|
||||
let configuration = ConfigurationFileParser.parse(options.configurationFile)
|
||||
print("Found configurations :")
|
||||
print(" - \(configuration.colors.count) colors configuration")
|
||||
print(" - \(configuration.fonts.count) fonts configuration")
|
||||
print(" - \(configuration.images.count) images configuration")
|
||||
print(" - \(configuration.strings.count) strings configuration")
|
||||
print(" - \(configuration.tags.count) tags configuration")
|
||||
print()
|
||||
// Execute commands
|
||||
configuration.runnableConfigurations
|
||||
.forEach {
|
||||
$0.run(force: options.forceGeneration)
|
||||
}
|
||||
|
||||
print("[\(Self.toolName)] Resgen ended")
|
||||
}
|
||||
}
|
30
Sources/ResgenSwift/Generate/GenerateError.swift
Normal file
30
Sources/ResgenSwift/Generate/GenerateError.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// ResgenSwiftError.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum GenerateError: Error {
|
||||
case fileNotExists(String)
|
||||
case invalidConfigurationFile(String)
|
||||
case commandError([String], String)
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .fileNotExists(let filename):
|
||||
return " error:[\(Generate.toolName)] File \(filename) does not exists"
|
||||
|
||||
case .invalidConfigurationFile(let filename):
|
||||
return " error:[\(Generate.toolName)] File \(filename) is not a valid configuration file"
|
||||
|
||||
case .commandError(let command, let terminationStatus):
|
||||
let readableCommand = command
|
||||
.map { $0 }
|
||||
.joined(separator: " ")
|
||||
return "error:[\(Generate.toolName)] An error occured while running command '\(readableCommand)'. Command terminate with status code: \(terminationStatus)"
|
||||
}
|
||||
}
|
||||
}
|
19
Sources/ResgenSwift/Generate/GenerateOptions.swift
Normal file
19
Sources/ResgenSwift/Generate/GenerateOptions.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// GenerateOptions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct GenerateOptions: ParsableArguments {
|
||||
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
|
||||
var forceGeneration = false
|
||||
|
||||
@Argument(help: "Configuration file.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var configurationFile: String
|
||||
}
|
140
Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift
Normal file
140
Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift
Normal file
@ -0,0 +1,140 @@
|
||||
//
|
||||
// ConfigurationFile.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ConfigurationFile: Codable, CustomDebugStringConvertible {
|
||||
var colors: [ColorsConfiguration]
|
||||
var fonts: [FontsConfiguration]
|
||||
var images: [ImagesConfiguration]
|
||||
var strings: [StringsConfiguration]
|
||||
var tags: [TagsConfiguration]
|
||||
|
||||
var runnableConfigurations: [Runnable] {
|
||||
let runnables: [[Runnable]] = [colors, fonts, images, strings, tags]
|
||||
return Array(runnables.joined())
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
\(colors)
|
||||
-----------
|
||||
-----------
|
||||
\(fonts)
|
||||
-----------
|
||||
-----------
|
||||
\(images)
|
||||
-----------
|
||||
-----------
|
||||
\(strings)
|
||||
-----------
|
||||
-----------
|
||||
\(tags)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
|
||||
let inputFile: String
|
||||
let style: String
|
||||
let xcassetsPath: String
|
||||
let extensionOutputPath: String
|
||||
let extensionName: String?
|
||||
let extensionSuffix: String?
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
Colors configuration:
|
||||
- Input file: \(inputFile)
|
||||
- Style: \(style)
|
||||
- Xcassets path: \(xcassetsPath)
|
||||
- Extension output path: \(extensionOutputPath)
|
||||
- Extension name: \(extensionName ?? "-")
|
||||
- Extension suffix: \(extensionSuffix ?? "-")
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
struct FontsConfiguration: Codable, CustomDebugStringConvertible {
|
||||
let inputFile: String
|
||||
let extensionOutputPath: String
|
||||
let extensionName: String?
|
||||
let extensionSuffix: String?
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
Fonts configuration:
|
||||
- Input file: \(inputFile)
|
||||
- Extension output path: \(extensionOutputPath)
|
||||
- Extension name: \(extensionName ?? "-")
|
||||
- Extension suffix: \(extensionSuffix ?? "-")
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
|
||||
let inputFile: String
|
||||
let xcassetsPath: String
|
||||
let extensionOutputPath: String
|
||||
let extensionName: String?
|
||||
let extensionSuffix: String?
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
Images configuration:
|
||||
- Input file: \(inputFile)
|
||||
- Xcassets path: \(xcassetsPath)
|
||||
- Extension output path: \(extensionOutputPath)
|
||||
- Extension name: \(extensionName ?? "-")
|
||||
- Extension suffix: \(extensionSuffix ?? "-")
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
struct StringsConfiguration: Codable, CustomDebugStringConvertible {
|
||||
let inputFile: String
|
||||
let outputPath: String
|
||||
let langs: String
|
||||
let defaultLang: String
|
||||
let extensionOutputPath: String
|
||||
let extensionName: String?
|
||||
let extensionSuffix: String?
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
Strings configuration:
|
||||
- Input file: \(inputFile)
|
||||
- Output path: \(outputPath)
|
||||
- Langs: \(langs)
|
||||
- Default lang: \(defaultLang)
|
||||
- Extension output path: \(extensionOutputPath)
|
||||
- Extension name: \(extensionName ?? "-")
|
||||
- Extension suffix: \(extensionSuffix ?? "-")
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
struct TagsConfiguration: Codable, CustomDebugStringConvertible {
|
||||
let inputFile: String
|
||||
let lang: String
|
||||
let extensionOutputPath: String
|
||||
let extensionName: String?
|
||||
let extensionSuffix: String?
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
Tags configuration:
|
||||
- Input file: \(inputFile)
|
||||
- Lang: \(lang)
|
||||
- Extension output path: \(extensionOutputPath)
|
||||
- Extension name: \(extensionName ?? "-")
|
||||
- Extension suffix: \(extensionSuffix ?? "-")
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
//
|
||||
// ConfigurationFileParser.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
class ConfigurationFileParser {
|
||||
static func parse(_ configurationFile: String) -> ConfigurationFile {
|
||||
guard let data = FileManager().contents(atPath: configurationFile) else {
|
||||
let error = GenerateError.fileNotExists(configurationFile)
|
||||
print(error.localizedDescription)
|
||||
Generate.exit(withError: error)
|
||||
}
|
||||
|
||||
guard let configuration = try? YAMLDecoder().decode(ConfigurationFile.self, from: data) else {
|
||||
let error = GenerateError.invalidConfigurationFile(configurationFile)
|
||||
print(error.localizedDescription)
|
||||
Generate.exit(withError: error)
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
//
|
||||
// ColorsConfiguration+ShellCommandable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ColorsConfiguration: Runnable {
|
||||
func run(force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile,
|
||||
"--style",
|
||||
style,
|
||||
"--xcassets-path",
|
||||
xcassetsPath,
|
||||
"--extension-output-path",
|
||||
extensionOutputPath
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Colors.main(args)
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
//
|
||||
// FontsConfiguration+ShellCommandable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FontsConfiguration: Runnable {
|
||||
func run(force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile,
|
||||
"--extension-output-path",
|
||||
extensionOutputPath
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Fonts.main(args)
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
//
|
||||
// ImagesConfiguration+ShellCommandable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ImagesConfiguration: Runnable {
|
||||
func run(force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile,
|
||||
"--xcassets-path",
|
||||
xcassetsPath,
|
||||
"--extension-output-path",
|
||||
extensionOutputPath
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Images.main(args)
|
||||
}
|
||||
}
|
13
Sources/ResgenSwift/Generate/ShellCommandable/Runnable.swift
Normal file
13
Sources/ResgenSwift/Generate/ShellCommandable/Runnable.swift
Normal file
@ -0,0 +1,13 @@
|
||||
//
|
||||
// ShellCommandable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol Runnable {
|
||||
func run(force: Bool)
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
//
|
||||
// StringsConfiguration+ShellCommandable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension StringsConfiguration: Runnable {
|
||||
func run(force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile,
|
||||
"--output-path",
|
||||
outputPath,
|
||||
"--langs",
|
||||
langs,
|
||||
"--default-lang",
|
||||
defaultLang,
|
||||
"--extension-output-path",
|
||||
extensionOutputPath
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Stringium.main(args)
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
//
|
||||
// TagsConfiguration+ShellCommandable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TagsConfiguration: Runnable {
|
||||
func run(force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile,
|
||||
"--lang",
|
||||
lang,
|
||||
"--extension-output-path",
|
||||
extensionOutputPath
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Tags.main(args)
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
//
|
||||
// FileManagerExtensions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 24/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager {
|
||||
func getAllRegularFileIn(directory: String) -> [String] {
|
||||
var files = [String]()
|
||||
guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
|
||||
let error = ImagesError.unknown("Cannot enumerate file in \(directory)")
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
|
||||
for case let fileURL as URL in enumerator {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey])
|
||||
if fileAttributes.isRegularFile! {
|
||||
files.append(fileURL.relativePath)
|
||||
}
|
||||
} catch {
|
||||
let error = ImagesError.getFileAttributed(fileURL.relativePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func getAllImageSetFolderIn(directory: String) -> [String] {
|
||||
var files = [String]()
|
||||
guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
|
||||
let error = ImagesError.unknown("Cannot enumerate imageset directory in \(directory)")
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
|
||||
for case let fileURL as URL in enumerator {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys:[.isDirectoryKey])
|
||||
if fileAttributes.isDirectory! && fileURL.lastPathComponent.hasSuffix(".imageset") {
|
||||
files.append(fileURL.lastPathComponent)
|
||||
}
|
||||
} catch {
|
||||
let error = ImagesError.getFileAttributed(fileURL.relativePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
//
|
||||
// ImageExtensionGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 14/02/2022.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
|
||||
class ImageExtensionGenerator {
|
||||
|
||||
// MARK: - Extension files
|
||||
|
||||
static func writeStringsFiles(images: [ParsedImage], staticVar: Bool, inputFilename: String, extensionName: String, extensionFilePath: String) {
|
||||
// Get header/footer
|
||||
let extensionHeader = Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName)
|
||||
let extensionFooter = Self.getFooter()
|
||||
|
||||
// Create content
|
||||
let extensionContent: String = {
|
||||
var content = ""
|
||||
images.forEach { img in
|
||||
if staticVar {
|
||||
content += "\n\(img.getStaticImageProperty())"
|
||||
} else {
|
||||
content += "\n\(img.getImageProperty())"
|
||||
}
|
||||
content += "\n "
|
||||
}
|
||||
return content
|
||||
}()
|
||||
|
||||
// Create file if not exists
|
||||
let fileManager = FileManager()
|
||||
if fileManager.fileExists(atPath: extensionFilePath) == false {
|
||||
Shell.shell("touch", "\(extensionFilePath)")
|
||||
}
|
||||
|
||||
// Generate extension
|
||||
Self.generateExtensionFile(extensionFilePath: extensionFilePath, extensionHeader, extensionContent, extensionFooter)
|
||||
}
|
||||
|
||||
// MARK: - pragm
|
||||
|
||||
private static func generateExtensionFile(extensionFilePath: String, _ args: String...) {
|
||||
// Create file if not exists
|
||||
let fileManager = FileManager()
|
||||
if fileManager.fileExists(atPath: extensionFilePath) == false {
|
||||
Shell.shell("touch", "\(extensionFilePath)")
|
||||
}
|
||||
|
||||
// Create extension content
|
||||
let extensionContent = args.joined(separator: "\n")
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionContent.write(to: extensionFilePathURL, atomically: true, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func getHeader(inputFilename: String, extensionClassname: String) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.Imagium \(ResgenSwiftVersion)
|
||||
// Images from \(inputFilename)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension \(extensionClassname) {
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
//@objc var onboarding_foreground3: UIImage {
|
||||
// return UIImage(named: "onboarding_foreground3")!
|
||||
// }
|
189
Sources/ResgenSwift/Images/Generator/XcassetsGenerator.swift
Normal file
189
Sources/ResgenSwift/Images/Generator/XcassetsGenerator.swift
Normal file
@ -0,0 +1,189 @@
|
||||
//
|
||||
// XcassetsGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 24/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
|
||||
class XcassetsGenerator {
|
||||
|
||||
static let outputImageExtension = "png"
|
||||
|
||||
let forceGeneration: Bool
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(forceGeneration: Bool) {
|
||||
self.forceGeneration = forceGeneration
|
||||
}
|
||||
|
||||
// MARK: - Assets generation
|
||||
|
||||
func generateXcassets(inputPath: String, imagesToGenerate: [ParsedImage], xcassetsPath: String) {
|
||||
let fileManager = FileManager()
|
||||
let svgConverter = Images.getSvgConverterPath()
|
||||
let allSubFiles = fileManager.getAllRegularFileIn(directory: inputPath)
|
||||
|
||||
var generatedAssetsPaths = [String]()
|
||||
|
||||
// Generate new assets
|
||||
imagesToGenerate.forEach { parsedImage in
|
||||
// Get image path
|
||||
let imageData: (path: String, ext: String) = {
|
||||
for subfile in allSubFiles {
|
||||
if subfile.hasSuffix("/" + parsedImage.name + ".svg") {
|
||||
return (subfile, "svg")
|
||||
}
|
||||
if subfile.hasSuffix("/" + parsedImage.name + ".png") {
|
||||
return (subfile, "png")
|
||||
}
|
||||
if subfile.hasSuffix("/" + parsedImage.name + ".jpg") {
|
||||
return (subfile, "jpg")
|
||||
}
|
||||
if subfile.hasSuffix("/" + parsedImage.name + ".jepg") {
|
||||
return (subfile, "jepg")
|
||||
}
|
||||
}
|
||||
let error = ImagesError.unknownImageExtension(parsedImage.name)
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}()
|
||||
|
||||
// Create imageset folder
|
||||
let imagesetName = "\(parsedImage.name).imageset"
|
||||
let imagesetPath = "\(xcassetsPath)/\(imagesetName)"
|
||||
Shell.shell("mkdir", "-p", imagesetPath)
|
||||
|
||||
// Store managed images path
|
||||
generatedAssetsPaths.append(imagesetName)
|
||||
|
||||
// Generate output images path
|
||||
let output1x = "\(imagesetPath)/\(parsedImage.name).\(XcassetsGenerator.outputImageExtension)"
|
||||
let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(XcassetsGenerator.outputImageExtension)"
|
||||
let output3x = "\(imagesetPath)/\(parsedImage.name)@3x.\(XcassetsGenerator.outputImageExtension)"
|
||||
|
||||
// Check if we need to convert image
|
||||
if self.shouldBypassGeneration(for: parsedImage, xcassetImagePath: output1x) {
|
||||
print("\(parsedImage.name) -> Not regenerating")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert image
|
||||
let convertArguments = parsedImage.convertArguments
|
||||
if imageData.ext == "svg" {
|
||||
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -h 300 -o path/to/output.png
|
||||
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -o path/to/output.png
|
||||
// /usr/local/bin/rsvg-convert path/to/image.png -h 300 -o path/to/output.png
|
||||
var command1x = ["\(svgConverter)", "\(imageData.path)"]
|
||||
var command2x = ["\(svgConverter)", "\(imageData.path)"]
|
||||
var command3x = ["\(svgConverter)", "\(imageData.path)"]
|
||||
|
||||
self.addConvertArgument(command: &command1x, convertArgument: convertArguments.x1)
|
||||
self.addConvertArgument(command: &command2x, convertArgument: convertArguments.x2)
|
||||
self.addConvertArgument(command: &command3x, convertArgument: convertArguments.x3)
|
||||
|
||||
command1x.append(contentsOf: ["-o", output1x])
|
||||
command2x.append(contentsOf: ["-o", output2x])
|
||||
command3x.append(contentsOf: ["-o", output3x])
|
||||
|
||||
Shell.shell(command1x)
|
||||
Shell.shell(command2x)
|
||||
Shell.shell(command3x)
|
||||
} else {
|
||||
// convert path/to/image.png -resize 200x300 path/to/output.png
|
||||
// convert path/to/image.png -resize 200x path/to/output.png
|
||||
// convert path/to/image.png -resize x300 path/to/output.png
|
||||
Shell.shell("convert", "\(imageData.path)", "-resize", "\(convertArguments.x1.width ?? "")x\(convertArguments.x1.height ?? "")", output1x)
|
||||
Shell.shell("convert", "\(imageData.path)", "-resize", "\(convertArguments.x2.width ?? "")x\(convertArguments.x2.height ?? "")", output2x)
|
||||
Shell.shell("convert", "\(imageData.path)", "-resize", "\(convertArguments.x3.width ?? "")x\(convertArguments.x3.height ?? "")", output3x)
|
||||
}
|
||||
|
||||
// Write Content.json
|
||||
let imagesetContentJson = parsedImage.contentJson
|
||||
let contentJsonFilePath = "\(imagesetPath)/Contents.json"
|
||||
if fileManager.fileExists(atPath: contentJsonFilePath) == false {
|
||||
Shell.shell("touch", "\(contentJsonFilePath)")
|
||||
}
|
||||
|
||||
let contentJsonFilePathURL = URL(fileURLWithPath: contentJsonFilePath)
|
||||
try! imagesetContentJson.write(to: contentJsonFilePathURL, atomically: true, encoding: .utf8)
|
||||
|
||||
print("\(parsedImage.name) -> Generated")
|
||||
}
|
||||
|
||||
// Success info
|
||||
let generatedAssetsCount = generatedAssetsPaths.count
|
||||
print("Images generated: \(generatedAssetsCount)")
|
||||
|
||||
// Delete old assets
|
||||
let allImagesetName = Set(fileManager.getAllImageSetFolderIn(directory: xcassetsPath))
|
||||
let imagesetToRemove = allImagesetName.subtracting(Set(generatedAssetsPaths))
|
||||
|
||||
imagesetToRemove.forEach {
|
||||
print("Will remove: \($0)")
|
||||
}
|
||||
|
||||
imagesetToRemove.forEach { itemToRemove in
|
||||
try! fileManager.removeItem(atPath: "\(xcassetsPath)/\(itemToRemove)")
|
||||
}
|
||||
print("Removed \(imagesetToRemove.count) images")
|
||||
}
|
||||
|
||||
// MARK: - Helpers: SVG command
|
||||
|
||||
private func addConvertArgument(command: inout [String], convertArgument: ConvertArgument) {
|
||||
if let width = convertArgument.width, width.isEmpty == false {
|
||||
command.append("-w")
|
||||
command.append("\(width)")
|
||||
}
|
||||
if let height = convertArgument.height, height.isEmpty == false {
|
||||
command.append("-h")
|
||||
command.append("\(height)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers: bypass generation
|
||||
|
||||
private func shouldBypassGeneration(for image: ParsedImage, xcassetImagePath: String) -> Bool {
|
||||
guard forceGeneration == false else {
|
||||
return false
|
||||
}
|
||||
|
||||
let fileManager = FileManager()
|
||||
|
||||
// File not exists -> do not bypass
|
||||
guard fileManager.fileExists(atPath: xcassetImagePath) else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Info unavailable -> do not bypass
|
||||
let taskWidth = Shell.shell("identify", "-format", "%w", xcassetImagePath)
|
||||
let taskHeight = Shell.shell("identify", "-format", "%h", xcassetImagePath)
|
||||
guard taskWidth.terminationStatus == 0,
|
||||
taskHeight.terminationStatus == 0 else {
|
||||
return false
|
||||
}
|
||||
|
||||
let currentWidth = Int(taskWidth.output ?? "-1") ?? -1
|
||||
let currentheight = Int(taskHeight.output ?? "-1") ?? -1
|
||||
|
||||
// Info unavailable -> do not bypass
|
||||
guard currentWidth > 0 && currentheight > 0 else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check width and height
|
||||
if image.width != -1 && currentWidth == image.width {
|
||||
return true
|
||||
}
|
||||
if image.height != -1 && currentheight == image.height {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
114
Sources/ResgenSwift/Images/Images.swift
Normal file
114
Sources/ResgenSwift/Images/Images.swift
Normal file
@ -0,0 +1,114 @@
|
||||
//
|
||||
// Images.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 24/01/2022.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Images: ParsableCommand {
|
||||
|
||||
// MARK: - CommandConfiguration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "A utility for generate images and an extension to access them easily.",
|
||||
version: ResgenSwiftVersion
|
||||
)
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static let toolName = "Images"
|
||||
static let defaultExtensionName = "UIImage"
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var extensionFileName: String { "\(options.extensionName)+\(options.extensionSuffix).swift" }
|
||||
var extensionFilePath: String { "\(options.extensionOutputPath)/\(extensionFileName)" }
|
||||
var inputFilenameWithoutExt: String {
|
||||
URL(fileURLWithPath: options.inputFile)
|
||||
.deletingPathExtension()
|
||||
.lastPathComponent
|
||||
}
|
||||
|
||||
// MARK: - Command Options
|
||||
|
||||
@OptionGroup var options: ImagesOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
mutating func run() {
|
||||
print("[\(Self.toolName)] Starting images generation")
|
||||
|
||||
// Check requirements
|
||||
guard checkRequirements() else { return }
|
||||
|
||||
print("[\(Self.toolName)] Will generate images")
|
||||
|
||||
// Parse input file
|
||||
let imagesToGenerate = ImageFileParser.parse(options.inputFile, platform: PlatormTag.ios)
|
||||
|
||||
// Generate xcassets files
|
||||
let inputFolder = URL(fileURLWithPath: options.inputFile)
|
||||
.deletingLastPathComponent()
|
||||
.relativePath
|
||||
let xcassetsGenerator = XcassetsGenerator(forceGeneration: options.forceExecutionAndGeneration)
|
||||
xcassetsGenerator.generateXcassets(inputPath: inputFolder,
|
||||
imagesToGenerate: imagesToGenerate,
|
||||
xcassetsPath: options.xcassetsPath)
|
||||
|
||||
// Generate extension
|
||||
ImageExtensionGenerator.writeStringsFiles(images: imagesToGenerate,
|
||||
staticVar: options.extensionName == Self.defaultExtensionName,
|
||||
inputFilename: inputFilenameWithoutExt,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: extensionFilePath)
|
||||
|
||||
|
||||
print("[\(Self.toolName)] Images generated")
|
||||
}
|
||||
|
||||
// MARK: - Requirements
|
||||
|
||||
private func checkRequirements() -> Bool {
|
||||
guard options.forceExecutionAndGeneration == false else {
|
||||
return true
|
||||
}
|
||||
|
||||
let fileManager = FileManager()
|
||||
|
||||
// Input file
|
||||
guard fileManager.fileExists(atPath: options.inputFile) else {
|
||||
let error = ImagesError.fileNotExists(options.inputFile)
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
|
||||
// RSVG-Converter
|
||||
_ = Images.getSvgConverterPath()
|
||||
|
||||
// Check if needed to regenerate
|
||||
guard GeneratorChecker.shouldGenerate(force: options.forceExecution, inputFilePath: options.inputFile, extensionFilePath: extensionFilePath) else {
|
||||
print("[\(Self.toolName)] Images are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@discardableResult
|
||||
static func getSvgConverterPath() -> String {
|
||||
let taskSvgConverter = Shell.shell("which", "rsvg-convert")
|
||||
if taskSvgConverter.terminationStatus == 0 {
|
||||
return taskSvgConverter.output!.removeCharacters(from: CharacterSet.whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
let error = ImagesError.rsvgConvertNotFound
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
}
|
43
Sources/ResgenSwift/Images/ImagesError.swift
Normal file
43
Sources/ResgenSwift/Images/ImagesError.swift
Normal file
@ -0,0 +1,43 @@
|
||||
//
|
||||
// ImagesError.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 24/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ImagesError: Error {
|
||||
case inputFolderNotFound(String)
|
||||
case fileNotExists(String)
|
||||
case unknownImageExtension(String)
|
||||
case getFileAttributed(String, String)
|
||||
case rsvgConvertNotFound
|
||||
case writeFile(String, String)
|
||||
case unknown(String)
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .inputFolderNotFound(let inputFolder):
|
||||
return " error:[\(Images.toolName)] Input folder not found: \(inputFolder)"
|
||||
|
||||
case .fileNotExists(let filename):
|
||||
return " error:[\(Images.toolName)] File \(filename) does not exists"
|
||||
|
||||
case .unknownImageExtension(let filename):
|
||||
return " error:[\(Images.toolName)] File \(filename) have an unhandled file extension. Cannot generate image."
|
||||
|
||||
case .getFileAttributed(let filename, let errorDescription):
|
||||
return " error:[\(Images.toolName)] Getting file attributes of \(filename) failed with error: \(errorDescription)"
|
||||
|
||||
case .rsvgConvertNotFound:
|
||||
return " error:[\(Images.toolName)] Can't find rsvg-convert (can be installed with 'brew remove imagemagick && brew install imagemagick --with-librsvg')"
|
||||
|
||||
case .writeFile(let subErrorDescription, let filename):
|
||||
return " error:[\(Images.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
|
||||
|
||||
case .unknown(let errorDescription):
|
||||
return " error:[\(Images.toolName)] Unknown error: \(errorDescription)"
|
||||
}
|
||||
}
|
||||
}
|
41
Sources/ResgenSwift/Images/ImagesOptions.swift
Normal file
41
Sources/ResgenSwift/Images/ImagesOptions.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// ImagiumOptions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 24/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct ImagesOptions: ParsableArguments {
|
||||
@Flag(name: .customShort("f"), help: "Should force script execution")
|
||||
var forceExecution = false
|
||||
|
||||
@Flag(name: .customShort("F"), help: "Regenerate all images")
|
||||
var forceExecutionAndGeneration = false
|
||||
|
||||
@Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var inputFile: String
|
||||
|
||||
@Option(help: "Xcassets path where to generate images.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var xcassetsPath: String
|
||||
|
||||
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var extensionOutputPath: String
|
||||
|
||||
@Option(help: "Extension name. If not specified, it will generate an UIImage extension. Using default extension name will generate static property.")
|
||||
var extensionName: String = Images.defaultExtensionName
|
||||
|
||||
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Image{extensionSuffix}.swift")
|
||||
var extensionSuffix: String = ""
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
swift run -c release Imagium $FORCE_FLAG "./Images/sampleImages.txt" \
|
||||
--xcassets-path "./Images/imagium.xcassets" \
|
||||
--extension-output-path "./Images/Generated" \
|
||||
--extension-name "UIImage" \
|
||||
--extension-suffix "GenAllScript"
|
||||
*/
|
13
Sources/ResgenSwift/Images/Model/ConvertArgument.swift
Normal file
13
Sources/ResgenSwift/Images/Model/ConvertArgument.swift
Normal file
@ -0,0 +1,13 @@
|
||||
//
|
||||
// ConvertArgument.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 24/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ConvertArgument {
|
||||
let width: String?
|
||||
let height: String?
|
||||
}
|
90
Sources/ResgenSwift/Images/Model/ParsedImage.swift
Normal file
90
Sources/ResgenSwift/Images/Model/ParsedImage.swift
Normal file
@ -0,0 +1,90 @@
|
||||
//
|
||||
// ParsedImage.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 24/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ParsedImage {
|
||||
let name: String
|
||||
let tags: String
|
||||
let width: Int
|
||||
let height: Int
|
||||
|
||||
// MARK: - Convert
|
||||
|
||||
var convertArguments: (x1: ConvertArgument, x2: ConvertArgument, x3: ConvertArgument) {
|
||||
var width1x = ""
|
||||
var height1x = ""
|
||||
var width2x = ""
|
||||
var height2x = ""
|
||||
var width3x = ""
|
||||
var height3x = ""
|
||||
|
||||
if width != -1 {
|
||||
width1x = "\(width)"
|
||||
width2x = "\(width * 2)"
|
||||
width3x = "\(width * 3)"
|
||||
}
|
||||
|
||||
if height != -1 {
|
||||
height1x = "\(height)"
|
||||
height2x = "\(height * 2)"
|
||||
height3x = "\(height * 3)"
|
||||
}
|
||||
|
||||
return (x1: ConvertArgument(width: width1x, height: height1x),
|
||||
x2: ConvertArgument(width: width2x, height: height2x),
|
||||
x3: ConvertArgument(width: width3x, height: height3x))
|
||||
}
|
||||
|
||||
// MARK: - Assets
|
||||
|
||||
var contentJson: String {
|
||||
"""
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "\(name).\(XcassetsGenerator.outputImageExtension)"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x",
|
||||
"filename" : "\(name)@2x.\(XcassetsGenerator.outputImageExtension)"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x",
|
||||
"filename" : "\(name)@3x.\(XcassetsGenerator.outputImageExtension)"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "ResgenSwift-Imagium"
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
// MARK: - Extension property
|
||||
|
||||
func getImageProperty() -> String {
|
||||
"""
|
||||
var \(name): UIImage {
|
||||
UIImage(named: "\(name)")!
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
func getStaticImageProperty() -> String {
|
||||
"""
|
||||
static var \(name): UIImage {
|
||||
UIImage(named: "\(name)")!
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
13
Sources/ResgenSwift/Images/Model/PlatormTag.swift
Normal file
13
Sources/ResgenSwift/Images/Model/PlatormTag.swift
Normal file
@ -0,0 +1,13 @@
|
||||
//
|
||||
// PlatormTag.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 29/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PlatormTag: String {
|
||||
case droid = "d"
|
||||
case ios = "i"
|
||||
}
|
47
Sources/ResgenSwift/Images/Parser/ImageFileParser.swift
Normal file
47
Sources/ResgenSwift/Images/Parser/ImageFileParser.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// ImageFileParser.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 24/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ImageFileParser {
|
||||
|
||||
static func parse(_ inputFile: String, platform: PlatormTag) -> [ParsedImage] {
|
||||
let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8)
|
||||
let stringsByLines = inputFileContent.components(separatedBy: .newlines)
|
||||
|
||||
var imagesToGenerate = [ParsedImage]()
|
||||
|
||||
// Parse file
|
||||
stringsByLines.forEach {
|
||||
guard $0.removeLeadingTrailingWhitespace().isEmpty == false, $0.first != "#" else {
|
||||
return
|
||||
}
|
||||
|
||||
let splittedLine = $0.split(separator: " ")
|
||||
|
||||
let width: Int = {
|
||||
if splittedLine[2] == "?" {
|
||||
return -1
|
||||
}
|
||||
return Int(splittedLine[2])!
|
||||
}()
|
||||
let height: Int = {
|
||||
if splittedLine[3] == "?" {
|
||||
return -1
|
||||
}
|
||||
return Int(splittedLine[3])!
|
||||
}()
|
||||
|
||||
let image = ParsedImage(name: String(splittedLine[1]), tags: String(splittedLine[0]), width: width, height: height)
|
||||
imagesToGenerate.append(image)
|
||||
}
|
||||
|
||||
return imagesToGenerate.filter {
|
||||
$0.tags.contains(platform.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
155
Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift
Normal file
155
Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift
Normal file
@ -0,0 +1,155 @@
|
||||
//
|
||||
// StringsFileGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 04/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
|
||||
class StringsFileGenerator {
|
||||
|
||||
// MARK: - Strings Files
|
||||
|
||||
static func writeStringsFiles(sections: [Section], langs: [String], defaultLang: String, tags: [String], outputPath: String, inputFilenameWithoutExt: String) {
|
||||
var stringsFilesContent = [String: String]()
|
||||
for lang in langs {
|
||||
stringsFilesContent[lang] = Self.generateStringsFileContent(lang: lang,
|
||||
defaultLang: defaultLang,
|
||||
tags: tags,
|
||||
sections: sections)
|
||||
}
|
||||
|
||||
// Write strings file content
|
||||
langs.forEach { lang in
|
||||
guard let fileContent = stringsFilesContent[lang] else { return }
|
||||
|
||||
let stringsFilePath = "\(outputPath)/\(lang).lproj/\(inputFilenameWithoutExt).strings"
|
||||
let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
|
||||
do {
|
||||
try fileContent.write(to: stringsFilePathURL, atomically: true, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func generateStringsFileContent(lang: String, defaultLang: String, tags inputTags: [String], sections: [Section]) -> String {
|
||||
var stringsFileContent = """
|
||||
/**
|
||||
* Apple Strings File
|
||||
* Generated by ResgenSwift \(ResgenSwiftVersion)
|
||||
* Language: \(lang)
|
||||
*/\n
|
||||
"""
|
||||
|
||||
sections.forEach { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: inputTags) else {
|
||||
return // Go to next section
|
||||
}
|
||||
|
||||
stringsFileContent += "\n/********** \(section.name) **********/\n\n"
|
||||
section.definitions.forEach { definition in
|
||||
var skipDefinition = false // Set to true if not matching tag
|
||||
let translationOpt: String? = {
|
||||
// If no matching tag => skip
|
||||
if definition.hasOneOrMoreMatchingTags(inputTags: inputTags) == false {
|
||||
skipDefinition = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// If tags contains `noTranslationTag` => get default lang
|
||||
if definition.tags.contains(Stringium.noTranslationTag) {
|
||||
return definition.translations[defaultLang]
|
||||
}
|
||||
|
||||
// Else: get specific lang
|
||||
return definition.translations[lang]
|
||||
}()
|
||||
|
||||
if let translation = translationOpt {
|
||||
stringsFileContent += "\"\(definition.name)\" = \"\(translation)\";\n\n"
|
||||
} else if skipDefinition == false {
|
||||
let error = StringiumError.langNotDefined(lang, definition.name, definition.reference != nil)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stringsFileContent
|
||||
}
|
||||
|
||||
// MARK: - Extension file
|
||||
|
||||
static func writeExtensionFiles(sections: [Section], defaultLang lang: String, tags: [String], staticVar: Bool, inputFilename: String, extensionName: String, extensionFilePath: String) {
|
||||
let extensionHeader = Self.getHeader(stringsFilename: inputFilename, extensionClassname: extensionName)
|
||||
let extensionFooter = Self.getFooter()
|
||||
|
||||
let extensionContent: String = {
|
||||
var content = ""
|
||||
sections.forEach { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
|
||||
return // Go to next section
|
||||
}
|
||||
|
||||
content += "\n\t// MARK: - \(section.name)"
|
||||
section.definitions.forEach { definition in
|
||||
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
|
||||
return // Go to next definition
|
||||
}
|
||||
|
||||
if staticVar {
|
||||
content += "\n\n\(definition.getNSLocalizedStringStaticProperty(forLang: lang))"
|
||||
} else {
|
||||
content += "\n\n\(definition.getNSLocalizedStringProperty(forLang: lang))"
|
||||
}
|
||||
}
|
||||
content += "\n"
|
||||
}
|
||||
return content
|
||||
}()
|
||||
|
||||
// Create file if not exists
|
||||
let fileManager = FileManager()
|
||||
if fileManager.fileExists(atPath: extensionFilePath) == false {
|
||||
Shell.shell("touch", "\(extensionFilePath)")
|
||||
}
|
||||
|
||||
// Create extension content
|
||||
let extensionFileContent = [extensionHeader, extensionContent, extensionFooter].joined(separator: "\n")
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionFileContent.write(to: extensionFilePathURL, atomically: true, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func getHeader(stringsFilename: String, extensionClassname: String) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.Strings.\(Stringium.toolName) \(ResgenSwiftVersion)
|
||||
|
||||
import UIKit
|
||||
|
||||
fileprivate let kStringsFileName = "\(stringsFilename)"
|
||||
|
||||
extension \(extensionClassname) {
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
73
Sources/ResgenSwift/Strings/Generator/TagsGenerator.swift
Normal file
73
Sources/ResgenSwift/Strings/Generator/TagsGenerator.swift
Normal file
@ -0,0 +1,73 @@
|
||||
//
|
||||
// TagsGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
import CoreVideo
|
||||
|
||||
class TagsGenerator {
|
||||
static func writeExtensionFiles(sections: [Section], lang: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
|
||||
let extensionHeader = Self.getHeader(extensionClassname: extensionName, staticVar: staticVar)
|
||||
let extensionFooter = Self.getFooter()
|
||||
|
||||
let extensionContent: String = {
|
||||
var content = ""
|
||||
sections.forEach { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
|
||||
return // Go to next section
|
||||
}
|
||||
|
||||
content += "\n // MARK: - \(section.name)"
|
||||
section.definitions.forEach { definition in
|
||||
if staticVar {
|
||||
content += "\n\n\(definition.getStaticProperty(forLang: lang))"
|
||||
} else {
|
||||
content += "\n\n\(definition.getProperty(forLang: lang))"
|
||||
}
|
||||
}
|
||||
content += "\n"
|
||||
}
|
||||
return content
|
||||
}()
|
||||
|
||||
// Create file if not exists
|
||||
let fileManager = FileManager()
|
||||
if fileManager.fileExists(atPath: extensionFilePath) == false {
|
||||
Shell.shell("touch", "\(extensionFilePath)")
|
||||
}
|
||||
|
||||
// Create extension content
|
||||
let extensionFileContent = [extensionHeader, extensionContent, extensionFooter].joined(separator: "\n")
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionFileContent.write(to: extensionFilePathURL, atomically: true, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func getHeader(extensionClassname: String, staticVar: Bool) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.Strings.Tags \(ResgenSwiftVersion)
|
||||
|
||||
\(staticVar ? "typelias Tags = String\n\n" : "")import UIKit
|
||||
|
||||
extension \(extensionClassname) {
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
189
Sources/ResgenSwift/Strings/Model/Definition.swift
Normal file
189
Sources/ResgenSwift/Strings/Model/Definition.swift
Normal file
@ -0,0 +1,189 @@
|
||||
//
|
||||
// Definition.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 04/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Definition {
|
||||
let name: String
|
||||
var tags = [String]()
|
||||
var comment: String?
|
||||
var translations = [String: String]()
|
||||
var reference: String?
|
||||
var isPlurals = false
|
||||
|
||||
var isValid: Bool {
|
||||
name.isEmpty == false &&
|
||||
translations.isEmpty == false
|
||||
}
|
||||
|
||||
init(name: String) {
|
||||
self.name = name
|
||||
}
|
||||
|
||||
static func match(_ line: String) -> Definition? {
|
||||
guard line.range(of: "\\[(.*?)]$", options: .regularExpression, range: nil, locale: nil) != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let definitionName = line
|
||||
.replacingOccurrences(of: ["[", "]"], with: "")
|
||||
.removeLeadingTrailingWhitespace()
|
||||
|
||||
return Definition(name: definitionName)
|
||||
}
|
||||
|
||||
func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool {
|
||||
if Set(inputTags).intersection(Set(self.tags)).isEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func getStringParameters(input: String) -> (inputParameters: [String], translationArguments: [String])? {
|
||||
var methodsParameters = [String]()
|
||||
|
||||
let printfPlaceholderRegex = try! NSRegularExpression(pattern: "%(?:\\d+\\$)?[+-]?(?:[ 0]|'.{1})?-?\\d*(?:\\.\\d+)?[blcdeEufFgGosxX@]*")
|
||||
printfPlaceholderRegex.enumerateMatches(in: input, options: [], range: NSRange(location: 0, length: input.count)) { match, _, stop in
|
||||
guard let match = match else { return }
|
||||
|
||||
if let range = Range(match.range, in: input), let last = input[range].last {
|
||||
debugPrint("Found: \(input[range])")
|
||||
switch last {
|
||||
case "d", "u":
|
||||
methodsParameters.append("Int")
|
||||
case "f", "F":
|
||||
methodsParameters.append("Double")
|
||||
case "@", "s", "c":
|
||||
methodsParameters.append("String")
|
||||
case "%":
|
||||
// if you need to print %, you have to add %%
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if methodsParameters.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var inputParameters = [String]()
|
||||
var translationArguments = [String]()
|
||||
for (index, paramType) in methodsParameters.enumerated() {
|
||||
let paramName = "arg\(index)"
|
||||
translationArguments.append(paramName)
|
||||
inputParameters.append("\(paramName): \(paramType)")
|
||||
}
|
||||
|
||||
return (inputParameters: inputParameters, translationArguments: translationArguments)
|
||||
}
|
||||
|
||||
private func getBaseProperty(lang: String, translation: String, isStatic: Bool) -> String {
|
||||
"""
|
||||
/// Translation in \(lang) :
|
||||
/// \(translation)
|
||||
\(isStatic ? "static ": "")var \(name): String {
|
||||
NSLocalizedString("\(name)", tableName: kStringsFileName, bundle: Bundle.main, value: "\(translation)", comment: "")
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private func getBaseMethod(lang: String, translation: String, isStatic: Bool, inputParameters: [String], translationArguments: [String]) -> String {
|
||||
"""
|
||||
|
||||
|
||||
/// Translation in \(lang) :
|
||||
/// \(translation)
|
||||
\(isStatic ? "static ": "")func \(name)(\(inputParameters.joined(separator: ", "))) -> String {
|
||||
String(format: \(isStatic ? "Self" : "self").\(name), \(translationArguments.joined(separator: ", ")))
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
func getNSLocalizedStringProperty(forLang lang: String) -> String {
|
||||
guard let translation = translations[lang] else {
|
||||
let error = StringiumError.langNotDefined(lang, name, reference != nil)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
|
||||
// Generate property
|
||||
let property = getBaseProperty(lang: lang, translation: translation, isStatic: false)
|
||||
|
||||
// Generate method
|
||||
var method = ""
|
||||
if let parameters = self.getStringParameters(input: translation) {
|
||||
method = getBaseMethod(lang: lang,
|
||||
translation: translation,
|
||||
isStatic: false,
|
||||
inputParameters: parameters.inputParameters,
|
||||
translationArguments: parameters.translationArguments)
|
||||
}
|
||||
|
||||
return property + method
|
||||
}
|
||||
|
||||
func getNSLocalizedStringStaticProperty(forLang lang: String) -> String {
|
||||
guard let translation = translations[lang] else {
|
||||
let error = StringiumError.langNotDefined(lang, name, reference != nil)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
|
||||
// Generate property
|
||||
let property = getBaseProperty(lang: lang, translation: translation, isStatic: true)
|
||||
|
||||
// Generate method
|
||||
var method = ""
|
||||
if let parameters = self.getStringParameters(input: translation) {
|
||||
method = getBaseMethod(lang: lang,
|
||||
translation: translation,
|
||||
isStatic: true,
|
||||
inputParameters: parameters.inputParameters,
|
||||
translationArguments: parameters.translationArguments)
|
||||
}
|
||||
|
||||
return property + method
|
||||
}
|
||||
|
||||
// MARK: - Raw strings
|
||||
|
||||
func getProperty(forLang lang: String) -> String {
|
||||
guard let translation = translations[lang] else {
|
||||
let error = StringiumError.langNotDefined(lang, name, reference != nil)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
|
||||
return """
|
||||
/// Translation in \(lang) :
|
||||
/// \(translation)
|
||||
var \(name): String {
|
||||
"\(translation)"
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
func getStaticProperty(forLang lang: String) -> String {
|
||||
guard let translation = translations[lang] else {
|
||||
let error = StringiumError.langNotDefined(lang, name, reference != nil)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
|
||||
return """
|
||||
/// Translation in \(lang) :
|
||||
/// \(translation)
|
||||
static var \(name): String {
|
||||
"\(translation)"
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
39
Sources/ResgenSwift/Strings/Model/Section.swift
Normal file
39
Sources/ResgenSwift/Strings/Model/Section.swift
Normal file
@ -0,0 +1,39 @@
|
||||
//
|
||||
// Section.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 04/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Section {
|
||||
let name: String // OnBoarding
|
||||
var definitions = [Definition]()
|
||||
|
||||
init(name: String) {
|
||||
self.name = name
|
||||
}
|
||||
|
||||
static func match(_ line: String) -> Section? {
|
||||
guard line.range(of: "\\[\\[(.*?)]]$", options: .regularExpression, range: nil, locale: nil) != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let sectionName = line
|
||||
.replacingOccurrences(of: ["[", "]"], with: "")
|
||||
.removeLeadingTrailingWhitespace()
|
||||
return Section(name: sectionName)
|
||||
}
|
||||
|
||||
func hasOneOrMoreMatchingTags(tags: [String]) -> Bool {
|
||||
let allTags = definitions.flatMap { $0.tags }
|
||||
let allTagsSet = Set(allTags)
|
||||
|
||||
let intersection = Set(tags).intersection(allTagsSet)
|
||||
if intersection.isEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
92
Sources/ResgenSwift/Strings/Parser/TwineFileParser.swift
Normal file
92
Sources/ResgenSwift/Strings/Parser/TwineFileParser.swift
Normal file
@ -0,0 +1,92 @@
|
||||
//
|
||||
// TwineFileParser.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TwineFileParser {
|
||||
static func parse(_ inputFile: String) -> [Section] {
|
||||
let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8)
|
||||
let stringsByLines = inputFileContent.components(separatedBy: .newlines)
|
||||
|
||||
var sections = [Section]()
|
||||
|
||||
// Parse file
|
||||
stringsByLines.forEach {
|
||||
// Section
|
||||
if let section = Section.match($0) {
|
||||
sections.append(section)
|
||||
return
|
||||
}
|
||||
|
||||
// Definition
|
||||
if let definition = Definition.match($0) {
|
||||
sections.last?.definitions.append(definition)
|
||||
return
|
||||
}
|
||||
|
||||
// Definition content
|
||||
if $0.isEmpty == false {
|
||||
// fr = Test => ["fr ", " Test"]
|
||||
let splitLine = $0
|
||||
.removeLeadingTrailingWhitespace()
|
||||
.split(separator: "=")
|
||||
|
||||
guard let lastDefinition = sections.last?.definitions.last,
|
||||
let leftElement = splitLine.first else {
|
||||
return
|
||||
}
|
||||
|
||||
let rightElement: String = splitLine.dropFirst().joined(separator: "=")
|
||||
|
||||
// "fr " => "fr"
|
||||
let leftHand = String(leftElement).removeTrailingWhitespace()
|
||||
// " Test" => "Test"
|
||||
let rightHand = String(rightElement).removeLeadingWhitespace()
|
||||
|
||||
// Handle comments, tags and translation
|
||||
switch leftHand {
|
||||
case "comments":
|
||||
lastDefinition.comment = rightHand
|
||||
|
||||
case "tags":
|
||||
lastDefinition.tags = rightHand
|
||||
.split(separator: ",")
|
||||
.map { String($0) }
|
||||
|
||||
case "ref":
|
||||
lastDefinition.reference = rightHand
|
||||
|
||||
default:
|
||||
lastDefinition.translations[leftHand] = rightHand.escapeDoubleQuote()
|
||||
// Is a plurals strings (fr:one = Test)
|
||||
// Will be handle later
|
||||
//if leftHand.split(separator: ":").count > 1 {
|
||||
// lastDefinition.isPlurals = true
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep only valid definition
|
||||
var invalidDefinitionNames = [String]()
|
||||
sections.forEach { section in
|
||||
section.definitions = section.definitions
|
||||
.filter {
|
||||
if $0.isValid == false {
|
||||
invalidDefinitionNames.append($0.name)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if invalidDefinitionNames.count > 0 {
|
||||
print(" warning:[\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
}
|
118
Sources/ResgenSwift/Strings/Stringium/Stringium.swift
Normal file
118
Sources/ResgenSwift/Strings/Stringium/Stringium.swift
Normal file
@ -0,0 +1,118 @@
|
||||
//
|
||||
// Stringium.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Stringium: ParsableCommand {
|
||||
|
||||
// MARK: - Command Configuration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "Generate strings with custom scripts.",
|
||||
version: ResgenSwiftVersion
|
||||
)
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static let toolName = "Stringium"
|
||||
static let defaultExtensionName = "String"
|
||||
static let noTranslationTag: String = "notranslation"
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var extensionFileName: String {
|
||||
if let extensionSuffix = options.extensionSuffix {
|
||||
return "\(options.extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(options.extensionName).swift"
|
||||
}
|
||||
|
||||
var extensionFilePath: String { "\(options.extensionOutputPath)/\(extensionFileName)" }
|
||||
|
||||
var inputFilenameWithoutExt: String {
|
||||
URL(fileURLWithPath: options.inputFile)
|
||||
.deletingPathExtension()
|
||||
.lastPathComponent
|
||||
}
|
||||
|
||||
var generateStaticVariable: Bool {
|
||||
options.extensionName == Self.defaultExtensionName
|
||||
}
|
||||
|
||||
// MARK: - Command options
|
||||
|
||||
@OptionGroup var options: StringiumOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
mutating func run() {
|
||||
print("[\(Self.toolName)] Starting strings generation")
|
||||
|
||||
// Check requirements
|
||||
guard checkRequirements() else { return }
|
||||
|
||||
print("[\(Self.toolName)] Will generate strings")
|
||||
|
||||
// Parse input file
|
||||
let sections = TwineFileParser.parse(options.inputFile)
|
||||
|
||||
// Generate strings files
|
||||
StringsFileGenerator.writeStringsFiles(sections: sections,
|
||||
langs: options.langs,
|
||||
defaultLang: options.defaultLang,
|
||||
tags: options.tags,
|
||||
outputPath: options.stringsFileOutputPath,
|
||||
inputFilenameWithoutExt: inputFilenameWithoutExt)
|
||||
|
||||
// Generate extension
|
||||
StringsFileGenerator.writeExtensionFiles(sections: sections,
|
||||
defaultLang: options.defaultLang,
|
||||
tags: options.tags,
|
||||
staticVar: generateStaticVariable,
|
||||
inputFilename: inputFilenameWithoutExt,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: extensionFilePath)
|
||||
|
||||
print("[\(Self.toolName)] Strings generated")
|
||||
}
|
||||
|
||||
// MARK: - Requirements
|
||||
|
||||
private func checkRequirements() -> Bool {
|
||||
let fileManager = FileManager()
|
||||
|
||||
// Input file
|
||||
guard fileManager.fileExists(atPath: options.inputFile) else {
|
||||
let error = StringiumError.fileNotExists(options.inputFile)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
|
||||
// Langs
|
||||
guard options.langs.isEmpty == false else {
|
||||
let error = StringiumError.langsListEmpty
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
|
||||
guard options.langs.contains(options.defaultLang) else {
|
||||
let error = StringiumError.defaultLangsNotInLangs
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
|
||||
// Check if needed to regenerate
|
||||
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, inputFilePath: options.inputFile, extensionFilePath: extensionFilePath) else {
|
||||
print("[\(Self.toolName)] Strings are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
38
Sources/ResgenSwift/Strings/Stringium/StringiumError.swift
Normal file
38
Sources/ResgenSwift/Strings/Stringium/StringiumError.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// StringToolError.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 05/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum StringiumError: Error {
|
||||
case fileNotExists(String)
|
||||
case langsListEmpty
|
||||
case defaultLangsNotInLangs
|
||||
case writeFile(String, String)
|
||||
case langNotDefined(String, String, Bool)
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .fileNotExists(let filename):
|
||||
return " error:[\(Stringium.toolName)] File \(filename) does not exists "
|
||||
|
||||
case .langsListEmpty:
|
||||
return " error:[\(Stringium.toolName)] Langs list is empty"
|
||||
|
||||
case .defaultLangsNotInLangs:
|
||||
return " error:[\(Stringium.toolName)] Langs list does not contains the default lang"
|
||||
|
||||
case .writeFile(let subErrorDescription, let filename):
|
||||
return " error:[\(Stringium.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
|
||||
|
||||
case .langNotDefined(let lang, let definitionName, let isReference):
|
||||
if isReference {
|
||||
return " error:[\(Stringium.toolName)] Reference are handled only by TwineTool. Please use it or remove reference from you strings file."
|
||||
}
|
||||
return " error:[\(Stringium.toolName)] Lang \"\(lang)\" not found for \"\(definitionName)\""
|
||||
}
|
||||
}
|
||||
}
|
60
Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift
Normal file
60
Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift
Normal file
@ -0,0 +1,60 @@
|
||||
//
|
||||
// StringiumOptions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct StringiumOptions: ParsableArguments {
|
||||
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
|
||||
var forceGeneration = false
|
||||
|
||||
@Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var inputFile: String
|
||||
|
||||
@Option(name: .customLong("output-path"), help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
fileprivate var outputPathRaw: String
|
||||
|
||||
@Option(name: .customLong("langs"), help: "Langs to generate.")
|
||||
fileprivate var langsRaw: String
|
||||
|
||||
@Option(help: "Default langs.")
|
||||
var defaultLang: String
|
||||
|
||||
@Option(name: .customLong("tags"), help: "Tags to generate.")
|
||||
fileprivate var tagsRaw: String = "ios iosonly iosOnly notranslation"
|
||||
|
||||
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var extensionOutputPath: String
|
||||
|
||||
@Option(help: "Extension name. If not specified, it will generate an String extension. Using default extension name will generate static property.")
|
||||
var extensionName: String = Stringium.defaultExtensionName
|
||||
|
||||
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+{extensionSuffix}.swift")
|
||||
var extensionSuffix: String?
|
||||
}
|
||||
|
||||
extension StringiumOptions {
|
||||
var stringsFileOutputPath: String {
|
||||
var outputPath = outputPathRaw
|
||||
if outputPath.last == "/" {
|
||||
outputPath = String(outputPath.dropLast())
|
||||
}
|
||||
return outputPath
|
||||
}
|
||||
|
||||
var langs: [String] {
|
||||
langsRaw
|
||||
.split(separator: " ")
|
||||
.map { String($0) }
|
||||
}
|
||||
|
||||
var tags: [String] {
|
||||
tagsRaw
|
||||
.split(separator: " ")
|
||||
.map { String($0) }
|
||||
}
|
||||
}
|
30
Sources/ResgenSwift/Strings/Strings.swift
Normal file
30
Sources/ResgenSwift/Strings/Strings.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// main.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Strings: ParsableCommand {
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "A utility for generate strings.",
|
||||
version: ResgenSwiftVersion,
|
||||
|
||||
// Pass an array to `subcommands` to set up a nested tree of subcommands.
|
||||
// With language support for type-level introspection, this could be
|
||||
// provided by automatically finding nested `ParsableCommand` types.
|
||||
subcommands: [Twine.self, Stringium.self, Tags.self]
|
||||
|
||||
// A default subcommand, when provided, is automatically selected if a
|
||||
// subcommand is not given on the command line.
|
||||
//defaultSubcommand: Twine.self
|
||||
)
|
||||
}
|
||||
|
||||
//Strings.main()
|
||||
|
89
Sources/ResgenSwift/Strings/Tag/Tags.swift
Normal file
89
Sources/ResgenSwift/Strings/Tag/Tags.swift
Normal file
@ -0,0 +1,89 @@
|
||||
//
|
||||
// Tag.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Tags: ParsableCommand {
|
||||
|
||||
// MARK: - Command Configuration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "Generate tags extension file.",
|
||||
version: ResgenSwiftVersion
|
||||
)
|
||||
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static let toolName = "Tags"
|
||||
static let defaultExtensionName = "Tags"
|
||||
static let noTranslationTag: String = "notranslation"
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var extensionFileName: String {
|
||||
if let extensionSuffix = options.extensionSuffix {
|
||||
return "\(options.extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(options.extensionName).swift"
|
||||
}
|
||||
var extensionFilePath: String { "\(options.extensionOutputPath)/\(extensionFileName)" }
|
||||
var generateStaticVariable: Bool {
|
||||
options.extensionName == Self.defaultExtensionName
|
||||
}
|
||||
|
||||
// MARK: - Command Options
|
||||
|
||||
@OptionGroup var options: TagsOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
mutating func run() {
|
||||
print("[\(Self.toolName)] Starting tagss generation")
|
||||
|
||||
// Check requirements
|
||||
guard checkRequirements() else { return }
|
||||
|
||||
print("[\(Self.toolName)] Will generate tags")
|
||||
|
||||
// Parse input file
|
||||
let sections = TwineFileParser.parse(options.inputFile)
|
||||
|
||||
// Generate extension
|
||||
TagsGenerator.writeExtensionFiles(sections: sections,
|
||||
lang: options.lang,
|
||||
tags: ["ios", "iosonly", Self.noTranslationTag],
|
||||
staticVar: generateStaticVariable,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: extensionFilePath)
|
||||
|
||||
print("[\(Self.toolName)] Tags generated")
|
||||
}
|
||||
|
||||
// MARK: - Requirements
|
||||
|
||||
private func checkRequirements() -> Bool {
|
||||
let fileManager = FileManager()
|
||||
|
||||
// Input file
|
||||
guard fileManager.fileExists(atPath: options.inputFile) else {
|
||||
let error = StringiumError.fileNotExists(options.inputFile)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
|
||||
// Check if needed to regenerate
|
||||
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, inputFilePath: options.inputFile, extensionFilePath: extensionFilePath) else {
|
||||
print("[\(Self.toolName)] Tags are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
29
Sources/ResgenSwift/Strings/Tag/TagsOptions.swift
Normal file
29
Sources/ResgenSwift/Strings/Tag/TagsOptions.swift
Normal file
@ -0,0 +1,29 @@
|
||||
//
|
||||
// TagOptions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct TagsOptions: ParsableArguments {
|
||||
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
|
||||
var forceGeneration = false
|
||||
|
||||
@Argument(help: "Input files where tags ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var inputFile: String
|
||||
|
||||
@Option(help: "Lang to generate. (\"ium\" by default)")
|
||||
var lang: String = "ium"
|
||||
|
||||
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var extensionOutputPath: String
|
||||
|
||||
@Option(help: "Extension name. If not specified, it will generate a Tag extension. Using default extension name will generate static property.")
|
||||
var extensionName: String = Tags.defaultExtensionName
|
||||
|
||||
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Tag{extensionSuffix}.swift")
|
||||
var extensionSuffix: String?
|
||||
}
|
105
Sources/ResgenSwift/Strings/Twine/Twine.swift
Normal file
105
Sources/ResgenSwift/Strings/Twine/Twine.swift
Normal file
@ -0,0 +1,105 @@
|
||||
//
|
||||
// Twine.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Twine: ParsableCommand {
|
||||
|
||||
// MARK: - Command Configuration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "Generate strings with twine.",
|
||||
version: ResgenSwiftVersion
|
||||
)
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static let toolName = "Twine"
|
||||
static let defaultExtensionName = "String"
|
||||
static let twineExecutable = "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)/scripts/twine/twine"
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var inputFilenameWithoutExt: String { URL(fileURLWithPath: options.inputFile)
|
||||
.deletingPathExtension()
|
||||
.lastPathComponent
|
||||
}
|
||||
|
||||
// MARK: - Command Options
|
||||
|
||||
@OptionGroup var options: TwineOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
mutating func run() {
|
||||
print("[\(Self.toolName)] Starting strings generation")
|
||||
|
||||
// Check requirements
|
||||
guard checkRequirements() else { return }
|
||||
|
||||
print("[\(Self.toolName)] Will generate strings")
|
||||
|
||||
// Generate strings files (lproj files)
|
||||
for lang in options.langs {
|
||||
Shell.shell(Self.twineExecutable,
|
||||
"generate-localization-file", options.inputFile,
|
||||
"--lang", "\(lang)",
|
||||
"\(options.outputPath)/\(lang).lproj/\(inputFilenameWithoutExt).strings",
|
||||
"--tags=ios,iosonly,iosOnly")
|
||||
}
|
||||
|
||||
// Generate extension
|
||||
var extensionFilePath: String { "\(options.extensionOutputPath)/\(inputFilenameWithoutExt).swift" }
|
||||
Shell.shell(Self.twineExecutable,
|
||||
"generate-localization-file", options.inputFile,
|
||||
"--format", "apple-swift",
|
||||
"--lang", "\(options.defaultLang)",
|
||||
extensionFilePath,
|
||||
"--tags=ios,iosonly,iosOnly")
|
||||
|
||||
print("[\(Self.toolName)] Strings generated")
|
||||
}
|
||||
|
||||
// MARK: - Requirements
|
||||
|
||||
private func checkRequirements() -> Bool {
|
||||
let fileManager = FileManager()
|
||||
|
||||
// Input file
|
||||
guard fileManager.fileExists(atPath: options.inputFile) else {
|
||||
let error = TwineError.fileNotExists(options.inputFile)
|
||||
print(error.localizedDescription)
|
||||
Twine.exit(withError: error)
|
||||
}
|
||||
|
||||
// Langs
|
||||
guard options.langs.isEmpty == false else {
|
||||
let error = TwineError.langsListEmpty
|
||||
print(error.localizedDescription)
|
||||
Twine.exit(withError: error)
|
||||
}
|
||||
|
||||
guard options.langs.contains(options.defaultLang) else {
|
||||
let error = TwineError.defaultLangsNotInLangs
|
||||
print(error.localizedDescription)
|
||||
Twine.exit(withError: error)
|
||||
}
|
||||
|
||||
// "R2String+" is hardcoded in Twine formatter
|
||||
let extensionFilePathGenerated = "\(options.extensionOutputPath)/R2String+\(inputFilenameWithoutExt).swift"
|
||||
|
||||
// Check if needed to regenerate
|
||||
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, inputFilePath: options.inputFile, extensionFilePath: extensionFilePathGenerated) else {
|
||||
print("[\(Self.toolName)] Strings are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
27
Sources/ResgenSwift/Strings/Twine/TwineError.swift
Normal file
27
Sources/ResgenSwift/Strings/Twine/TwineError.swift
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// TwineError.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TwineError: Error {
|
||||
case fileNotExists(String)
|
||||
case langsListEmpty
|
||||
case defaultLangsNotInLangs
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .fileNotExists(let filename):
|
||||
return " error:[\(Twine.toolName)] File \(filename) does not exists "
|
||||
|
||||
case .langsListEmpty:
|
||||
return " error:[\(Twine.toolName)] Langs list is empty"
|
||||
|
||||
case .defaultLangsNotInLangs:
|
||||
return " error:[\(Twine.toolName)] Langs list does not contains the default lang"
|
||||
}
|
||||
}
|
||||
}
|
37
Sources/ResgenSwift/Strings/Twine/TwineOptions.swift
Normal file
37
Sources/ResgenSwift/Strings/Twine/TwineOptions.swift
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// TwineOptions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 10/01/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct TwineOptions: ParsableArguments {
|
||||
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
|
||||
var forceGeneration = false
|
||||
|
||||
@Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var inputFile: String
|
||||
|
||||
@Option(help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var outputPath: String
|
||||
|
||||
@Option(name: .customLong("langs"), help: "Langs to generate.")
|
||||
fileprivate var langsRaw: String
|
||||
|
||||
@Option(help: "Default langs.")
|
||||
var defaultLang: String
|
||||
|
||||
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var extensionOutputPath: String
|
||||
}
|
||||
|
||||
extension TwineOptions {
|
||||
var langs: [String] {
|
||||
langsRaw
|
||||
.split(separator: " ")
|
||||
.map { String($0) }
|
||||
}
|
||||
}
|
@ -1 +1,35 @@
|
||||
print("Welcome ResgenSwift")
|
||||
//
|
||||
// ResgenSwift.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 13/12/2021.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct ResgenSwift: ParsableCommand {
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "A utility for generate ressources.",
|
||||
version: ResgenSwiftVersion,
|
||||
|
||||
// Pass an array to `subcommands` to set up a nested tree of subcommands.
|
||||
// With language support for type-level introspection, this could be
|
||||
// provided by automatically finding nested `ParsableCommand` types.
|
||||
subcommands: [
|
||||
Colors.self,
|
||||
Fonts.self,
|
||||
Images.self,
|
||||
Strings.self,
|
||||
Generate.self
|
||||
]
|
||||
|
||||
// A default subcommand, when provided, is automatically selected if a
|
||||
// subcommand is not given on the command line.
|
||||
//defaultSubcommand: Twine.self
|
||||
)
|
||||
}
|
||||
|
||||
ResgenSwift.main()
|
||||
|
Reference in New Issue
Block a user