Publish v1.0
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
Reviewed-on: #1
This commit is contained in:
110
Sources/ResgenSwift/Colors/Colors.swift
Normal file
110
Sources/ResgenSwift/Colors/Colors.swift
Normal file
@ -0,0 +1,110 @@
|
||||
//
|
||||
// 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: - Command options
|
||||
|
||||
@OptionGroup var options: ColorsToolOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
public func run() throws {
|
||||
print("[\(Self.toolName)] Starting colors generation")
|
||||
|
||||
// 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)
|
||||
// -> Time: 0.0020350217819213867 seconds
|
||||
|
||||
// Generate all colors in xcassets
|
||||
ColorXcassetHelper.generateXcassetColors(colors: parsedColors,
|
||||
to: options.xcassetsPath)
|
||||
// -> Time: 3.4505380392074585 seconds
|
||||
|
||||
// Generate extension
|
||||
ColorExtensionGenerator.writeExtensionFile(colors: parsedColors,
|
||||
staticVar: options.staticMembers,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: options.extensionFilePath)
|
||||
// -> Time: 0.0010340213775634766 seconds
|
||||
|
||||
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: options.extensionFilePath) else {
|
||||
print("[\(Self.toolName)] Colors are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func deleteCurrentColors() {
|
||||
let fileManager = FileManager()
|
||||
let assetsColorPath = "\(options.xcassetsPath)/Colors"
|
||||
|
||||
if fileManager.fileExists(atPath: assetsColorPath) {
|
||||
do {
|
||||
try fileManager.removeItem(atPath: assetsColorPath)
|
||||
} catch {
|
||||
let error = ColorsToolError.deleteExistingColors("\(options.xcassetsPath)/Colors")
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
Sources/ResgenSwift/Colors/ColorsToolError.swift
Normal file
43
Sources/ResgenSwift/Colors/ColorsToolError.swift
Normal file
@ -0,0 +1,43 @@
|
||||
//
|
||||
// ColorsToolError.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 20/12/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ColorsToolError: Error {
|
||||
case badFormat(String)
|
||||
case writeAsset(String)
|
||||
case createAssetFolder(String)
|
||||
case writeExtension(String, String)
|
||||
case fileNotExists(String)
|
||||
case badColorDefinition(String, String)
|
||||
case deleteExistingColors(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 .createAssetFolder(let assetsFolder):
|
||||
return "error:[\(Colors.toolName)] An error occured while creating colors folder `\(assetsFolder)`"
|
||||
|
||||
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)-"
|
||||
|
||||
case .deleteExistingColors(let assetsFolder):
|
||||
return "error:[\(Colors.toolName)] An error occured while deleting colors folder `\(assetsFolder)`"
|
||||
}
|
||||
}
|
||||
}
|
54
Sources/ResgenSwift/Colors/ColorsToolOptions.swift
Normal file
54
Sources/ResgenSwift/Colors/ColorsToolOptions.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// 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: "Tell if it will generate static properties or not")
|
||||
var staticMembers: Bool = false
|
||||
|
||||
@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?
|
||||
}
|
||||
|
||||
// MARK: - Computed var
|
||||
|
||||
extension ColorsToolOptions {
|
||||
var colorStyle: ColorStyle {
|
||||
ColorStyle(rawValue: style) ?? .all
|
||||
}
|
||||
|
||||
var extensionFileName: String {
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
return "\(extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(extensionName).swift"
|
||||
}
|
||||
|
||||
var extensionFilePath: String {
|
||||
"\(extensionOutputPath)/\(extensionFileName)"
|
||||
}
|
||||
}
|
@ -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 extension content
|
||||
let extensionContent = Self.getExtensionContent(colors: colors,
|
||||
staticVar: staticVar,
|
||||
extensionName: extensionName)
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = ColorsToolError.writeExtension(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
static func getExtensionContent(colors: [ParsedColor], staticVar: Bool, extensionName: String) -> String {
|
||||
[
|
||||
Self.getHeader(extensionClassname: extensionName),
|
||||
Self.getProperties(for: colors, withStaticVar: staticVar),
|
||||
Self.getFooter()
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
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,47 @@
|
||||
//
|
||||
// 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"
|
||||
let contentsJsonPath = "\(colorSetPath)/Contents.json"
|
||||
|
||||
let fileManager = FileManager()
|
||||
if fileManager.fileExists(atPath: colorSetPath) == false {
|
||||
do {
|
||||
try fileManager.createDirectory(atPath: colorSetPath,
|
||||
withIntermediateDirectories: true)
|
||||
} catch {
|
||||
let error = ColorsToolError.createAssetFolder(colorSetPath)
|
||||
print(error.localizedDescription)
|
||||
Colors.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// Write content in Contents.json
|
||||
let contentsJsonPathURL = URL(fileURLWithPath: contentsJsonPath)
|
||||
do {
|
||||
try color.contentsJSON().write(to: contentsJsonPathURL, atomically: false, 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)")!
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
56
Sources/ResgenSwift/Colors/Parser/ColorFileParser.swift
Normal file
56
Sources/ResgenSwift/Colors/Parser/ColorFileParser.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// 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 parseLines(lines: colorsByLines, colorStyle: colorStyle)
|
||||
}
|
||||
|
||||
static func parseLines(lines: [String], colorStyle: ColorStyle) -> [ParsedColor] {
|
||||
lines
|
||||
.enumerated()
|
||||
.compactMap { lineNumber, colorLine in
|
||||
// Required format:
|
||||
// colorName = "#RGB/#ARGB", colorName "#RGB/#ARGB", colorName "#RGB/#ARGB" "#RGB/#ARGB"
|
||||
let colorLineCleanedUp = colorLine
|
||||
.removeLeadingWhitespace()
|
||||
.removeTrailingWhitespace()
|
||||
.replacingOccurrences(of: "=", with: "") // Keep compat with current file format
|
||||
|
||||
guard colorLineCleanedUp.hasPrefix("#") == false, colorLineCleanedUp.isEmpty == false else {
|
||||
// debugPrint("[\(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]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
Sources/ResgenSwift/Fonts/FontOptions.swift
Normal file
44
Sources/ResgenSwift/Fonts/FontOptions.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// 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: "Tell if it will generate static properties or methods")
|
||||
var staticMembers: Bool = false
|
||||
|
||||
@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 = ""
|
||||
}
|
||||
|
||||
// MARK: - Computed var
|
||||
|
||||
extension FontsOptions {
|
||||
var extensionFileName: String {
|
||||
if extensionSuffix.isEmpty == false {
|
||||
return "\(extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(extensionName).swift"
|
||||
}
|
||||
|
||||
var extensionFilePath: String {
|
||||
"\(extensionOutputPath)/\(extensionFileName)"
|
||||
}
|
||||
}
|
83
Sources/ResgenSwift/Fonts/Fonts.swift
Normal file
83
Sources/ResgenSwift/Fonts/Fonts.swift
Normal file
@ -0,0 +1,83 @@
|
||||
//
|
||||
// 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: - Command Options
|
||||
|
||||
@OptionGroup var options: FontsOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
public func run() throws {
|
||||
print("[\(Self.toolName)] Starting fonts generation")
|
||||
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate fonts")
|
||||
|
||||
// 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: options.staticMembers,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: options.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: options.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>"
|
||||
|
||||
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) {
|
||||
// Create extension content
|
||||
let extensionContent = Self.getExtensionContent(fontsNames: fontsNames,
|
||||
staticVar: staticVar,
|
||||
extensionName: extensionName)
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Fonts.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
static func getExtensionContent(fontsNames: [String], staticVar: Bool, extensionName: String) -> String {
|
||||
[
|
||||
Self.getHeader(extensionClassname: extensionName),
|
||||
Self.getFontNameEnum(fontsNames: fontsNames),
|
||||
Self.getFontMethods(fontsNames: fontsNames, staticVar: staticVar),
|
||||
Self.getFooter()
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
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.fontNameSanitize) = \"\($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)
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
//
|
||||
// StringExtensions.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 31/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
func prependIfRelativePath(_ prependPath: String) -> String {
|
||||
if self.hasPrefix("/") {
|
||||
return self
|
||||
}
|
||||
return prependPath + self
|
||||
}
|
||||
}
|
57
Sources/ResgenSwift/Generate/Generate.swift
Normal file
57
Sources/ResgenSwift/Generate/Generate.swift
Normal file
@ -0,0 +1,57 @@
|
||||
//
|
||||
// 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()
|
||||
|
||||
print("Input file: \(configuration.colors.first?.inputFile ?? "no input file")")
|
||||
|
||||
// Execute commands
|
||||
configuration.runnableConfigurations
|
||||
.forEach {
|
||||
let begin = Date()
|
||||
$0.run(projectDirectory: options.projectDirectory,
|
||||
force: options.forceGeneration)
|
||||
print("Took: \(Date().timeIntervalSince(begin))s\n")
|
||||
}
|
||||
|
||||
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)"
|
||||
}
|
||||
}
|
||||
}
|
28
Sources/ResgenSwift/Generate/GenerateOptions.swift
Normal file
28
Sources/ResgenSwift/Generate/GenerateOptions.swift
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@Option(help: "Project directory. It will be added to every relative path (path that does not start with `/`",
|
||||
transform: {
|
||||
if $0.last == "/" {
|
||||
return $0
|
||||
}
|
||||
return $0 + "/"
|
||||
})
|
||||
var projectDirectory: String
|
||||
}
|
180
Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift
Normal file
180
Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift
Normal file
@ -0,0 +1,180 @@
|
||||
//
|
||||
// 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?
|
||||
private let staticMembers: Bool?
|
||||
|
||||
var staticMembersOptions: Bool {
|
||||
if let staticMembers = staticMembers {
|
||||
return staticMembers
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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?
|
||||
private let staticMembers: Bool?
|
||||
|
||||
var staticMembersOptions: Bool {
|
||||
if let staticMembers = staticMembers {
|
||||
return staticMembers
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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?
|
||||
private let staticMembers: Bool?
|
||||
|
||||
var staticMembersOptions: Bool {
|
||||
if let staticMembers = staticMembers {
|
||||
return staticMembers
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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?
|
||||
private let staticMembers: Bool?
|
||||
|
||||
var staticMembersOptions: Bool {
|
||||
if let staticMembers = staticMembers {
|
||||
return staticMembers
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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?
|
||||
private let staticMembers: Bool?
|
||||
|
||||
var staticMembersOptions: Bool {
|
||||
if let staticMembers = staticMembers {
|
||||
return staticMembers
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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,45 @@
|
||||
//
|
||||
// ColorsConfiguration+Runnable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ColorsConfiguration: Runnable {
|
||||
func run(projectDirectory: String, force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile.prependIfRelativePath(projectDirectory),
|
||||
"--style",
|
||||
style,
|
||||
"--xcassets-path",
|
||||
xcassetsPath.prependIfRelativePath(projectDirectory),
|
||||
"--extension-output-path",
|
||||
extensionOutputPath.prependIfRelativePath(projectDirectory),
|
||||
"--static-members",
|
||||
"\(staticMembersOptions)"
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Colors.main(args)
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
//
|
||||
// FontsConfiguration+Runnable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FontsConfiguration: Runnable {
|
||||
func run(projectDirectory: String, force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile.prependIfRelativePath(projectDirectory),
|
||||
"--extension-output-path",
|
||||
extensionOutputPath.prependIfRelativePath(projectDirectory),
|
||||
"--static-members",
|
||||
"\(staticMembersOptions)"
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Fonts.main(args)
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
//
|
||||
// ImagesConfiguration+Runnable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ImagesConfiguration: Runnable {
|
||||
func run(projectDirectory: String, force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"] // Images has a -f and -F options
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile.prependIfRelativePath(projectDirectory),
|
||||
"--xcassets-path",
|
||||
xcassetsPath.prependIfRelativePath(projectDirectory),
|
||||
"--extension-output-path",
|
||||
extensionOutputPath.prependIfRelativePath(projectDirectory),
|
||||
"--static-members",
|
||||
"\(staticMembersOptions)"
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Images.main(args)
|
||||
}
|
||||
}
|
13
Sources/ResgenSwift/Generate/Runnable/Runnable.swift
Normal file
13
Sources/ResgenSwift/Generate/Runnable/Runnable.swift
Normal file
@ -0,0 +1,13 @@
|
||||
//
|
||||
// ShellCommandable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol Runnable {
|
||||
func run(projectDirectory: String, force: Bool)
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
//
|
||||
// StringsConfiguration+Runnable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension StringsConfiguration: Runnable {
|
||||
func run(projectDirectory: String, force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile.prependIfRelativePath(projectDirectory),
|
||||
"--output-path",
|
||||
outputPath.prependIfRelativePath(projectDirectory),
|
||||
"--langs",
|
||||
langs,
|
||||
"--default-lang",
|
||||
defaultLang,
|
||||
"--extension-output-path",
|
||||
extensionOutputPath.prependIfRelativePath(projectDirectory),
|
||||
"--static-members",
|
||||
"\(staticMembersOptions)"
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Stringium.main(args)
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
//
|
||||
// TagsConfiguration+Runnable.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 30/08/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TagsConfiguration: Runnable {
|
||||
func run(projectDirectory: String, force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile.prependIfRelativePath(projectDirectory),
|
||||
"--lang",
|
||||
lang,
|
||||
"--extension-output-path",
|
||||
extensionOutputPath.prependIfRelativePath(projectDirectory),
|
||||
"--static-members",
|
||||
"\(staticMembersOptions)"
|
||||
]
|
||||
|
||||
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,77 @@
|
||||
//
|
||||
// ImageExtensionGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 14/02/2022.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
|
||||
class ImageExtensionGenerator {
|
||||
|
||||
// MARK: - pragm
|
||||
|
||||
static func generateExtensionFile(images: [ParsedImage],
|
||||
staticVar: Bool,
|
||||
inputFilename: String,
|
||||
extensionName: String,
|
||||
extensionFilePath: String) {
|
||||
// Create extension conten1t
|
||||
let extensionContent = Self.getExtensionContent(images: images,
|
||||
staticVar: staticVar,
|
||||
extensionName: extensionName,
|
||||
inputFilename: inputFilename)
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extension content
|
||||
|
||||
static func getExtensionContent(images: [ParsedImage], staticVar: Bool, extensionName: String, inputFilename: String) -> String {
|
||||
[
|
||||
Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName),
|
||||
Self.getProperties(images: images, staticVar: staticVar),
|
||||
Self.getFooter()
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
// MARK: - Extension part
|
||||
|
||||
private static func getHeader(inputFilename: String, extensionClassname: String) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.\(Images.toolName) \(ResgenSwiftVersion)
|
||||
// Images from \(inputFilename)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension \(extensionClassname) {
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getProperties(images: [ParsedImage], staticVar: Bool) -> String {
|
||||
if staticVar {
|
||||
return images
|
||||
.map { "\n\($0.getStaticImageProperty())" }
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
return images
|
||||
.map { "\n\($0.getImageProperty())" }
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
171
Sources/ResgenSwift/Images/Generator/XcassetsGenerator.swift
Normal file
171
Sources/ResgenSwift/Images/Generator/XcassetsGenerator.swift
Normal file
@ -0,0 +1,171 @@
|
||||
//
|
||||
// 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 name
|
||||
let imagesetName = "\(parsedImage.name).imageset"
|
||||
let imagesetPath = "\(xcassetsPath)/\(imagesetName)"
|
||||
|
||||
// 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
|
||||
guard self.shouldGenerate(inputImagePath: imageData.path, xcassetImagePath: output1x) else {
|
||||
//print("\(parsedImage.name) -> Not regenerating")
|
||||
return
|
||||
}
|
||||
|
||||
// Create imageset folder
|
||||
if fileManager.fileExists(atPath: imagesetPath) == false {
|
||||
do {
|
||||
try fileManager.createDirectory(atPath: imagesetPath,
|
||||
withIntermediateDirectories: true)
|
||||
} catch {
|
||||
let error = ImagesError.createAssetFolder(imagesetPath)
|
||||
print(error.localizedDescription)
|
||||
Images.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
let contentJsonFilePathURL = URL(fileURLWithPath: contentJsonFilePath)
|
||||
try! imagesetContentJson.write(to: contentJsonFilePathURL, atomically: false, 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 shouldGenerate(inputImagePath: String, xcassetImagePath: String) -> Bool {
|
||||
if forceGeneration {
|
||||
return true
|
||||
}
|
||||
|
||||
return GeneratorChecker.isFile(inputImagePath, moreRecenThan: xcassetImagePath)
|
||||
}
|
||||
}
|
108
Sources/ResgenSwift/Images/Images.swift
Normal file
108
Sources/ResgenSwift/Images/Images.swift
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// 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: - Command Options
|
||||
|
||||
@OptionGroup var options: ImagesOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
mutating func run() {
|
||||
print("[\(Self.toolName)] Starting images generation")
|
||||
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate images in xcassets \(options.xcassetsPath)")
|
||||
|
||||
// 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.generateExtensionFile(images: imagesToGenerate,
|
||||
staticVar: options.staticMembers,
|
||||
inputFilename: options.inputFilenameWithoutExt,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: options.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: options.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)
|
||||
}
|
||||
}
|
47
Sources/ResgenSwift/Images/ImagesError.swift
Normal file
47
Sources/ResgenSwift/Images/ImagesError.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// 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 createAssetFolder(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 .createAssetFolder(let folder):
|
||||
return "error:[\(Colors.toolName)] An error occured while creating folder `\(folder)`"
|
||||
|
||||
case .unknown(let errorDescription):
|
||||
return " error:[\(Images.toolName)] Unknown error: \(errorDescription)"
|
||||
}
|
||||
}
|
||||
}
|
56
Sources/ResgenSwift/Images/ImagesOptions.swift
Normal file
56
Sources/ResgenSwift/Images/ImagesOptions.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// 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: "Tell if it will generate static properties or not")
|
||||
var staticMembers: Bool = false
|
||||
|
||||
@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?
|
||||
}
|
||||
|
||||
// MARK: - Computed var
|
||||
|
||||
extension ImagesOptions {
|
||||
var extensionFileName: String {
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
return "\(extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(extensionName).swift"
|
||||
}
|
||||
|
||||
var extensionFilePath: String {
|
||||
"\(extensionOutputPath)/\(extensionFileName)"
|
||||
}
|
||||
|
||||
var inputFilenameWithoutExt: String {
|
||||
URL(fileURLWithPath: inputFile)
|
||||
.deletingPathExtension()
|
||||
.lastPathComponent
|
||||
}
|
||||
}
|
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"
|
||||
}
|
50
Sources/ResgenSwift/Images/Parser/ImageFileParser.swift
Normal file
50
Sources/ResgenSwift/Images/Parser/ImageFileParser.swift
Normal file
@ -0,0 +1,50 @@
|
||||
//
|
||||
// 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)
|
||||
|
||||
return Self.parseLines(stringsByLines, platform: platform)
|
||||
}
|
||||
|
||||
static func parseLines(_ lines: [String], platform: PlatormTag) -> [ParsedImage] {
|
||||
var imagesToGenerate = [ParsedImage]()
|
||||
|
||||
lines.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)
|
||||
}
|
||||
}
|
||||
}
|
163
Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift
Normal file
163
Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift
Normal file
@ -0,0 +1,163 @@
|
||||
//
|
||||
// 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: false, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Get extension content
|
||||
let extensionFileContent = Self.getExtensionContent(sections: sections,
|
||||
defaultLang: lang,
|
||||
tags: tags,
|
||||
staticVar: staticVar,
|
||||
inputFilename: inputFilename,
|
||||
extensionName: extensionName)
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extension content
|
||||
|
||||
static func getExtensionContent(sections: [Section], defaultLang lang: String, tags: [String], staticVar: Bool, inputFilename: String, extensionName: String) -> String {
|
||||
[
|
||||
Self.getHeader(stringsFilename: inputFilename, extensionClassname: extensionName),
|
||||
Self.getProperties(sections: sections, defaultLang: lang, tags: tags, staticVar: staticVar),
|
||||
Self.getFooter()
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
// MARK: - Extension part
|
||||
|
||||
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 getProperties(sections: [Section], defaultLang lang: String, tags: [String], staticVar: Bool) -> String {
|
||||
sections.compactMap { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
|
||||
return nil // Go to next section
|
||||
}
|
||||
|
||||
var res = "\n // MARK: - \(section.name)\n"
|
||||
res += section.definitions.compactMap { definition in
|
||||
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
|
||||
return nil // Go to next definition
|
||||
}
|
||||
|
||||
if staticVar {
|
||||
return "\n\(definition.getNSLocalizedStringStaticProperty(forLang: lang))"
|
||||
}
|
||||
return "\n\(definition.getNSLocalizedStringProperty(forLang: lang))"
|
||||
}
|
||||
.joined(separator: "\n")
|
||||
return res
|
||||
}
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
85
Sources/ResgenSwift/Strings/Generator/TagsGenerator.swift
Normal file
85
Sources/ResgenSwift/Strings/Generator/TagsGenerator.swift
Normal file
@ -0,0 +1,85 @@
|
||||
//
|
||||
// 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) {
|
||||
// Get extension content
|
||||
let extensionFileContent = Self.getExtensionContent(sections: sections,
|
||||
lang: lang,
|
||||
tags: tags,
|
||||
staticVar: staticVar,
|
||||
extensionName: extensionName)
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
|
||||
print(error.localizedDescription)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extension content
|
||||
|
||||
static func getExtensionContent(sections: [Section], lang: String, tags: [String], staticVar: Bool, extensionName: String) -> String {
|
||||
[
|
||||
Self.getHeader(extensionClassname: extensionName, staticVar: staticVar),
|
||||
Self.getProperties(sections: sections, lang: lang, tags: tags, staticVar: staticVar),
|
||||
Self.getFooter()
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
// MARK: - Extension part
|
||||
|
||||
private static func getHeader(extensionClassname: String, staticVar: Bool) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.Strings.\(Tags.toolName) \(ResgenSwiftVersion)
|
||||
|
||||
\(staticVar ? "typelias Tags = String\n\n" : "")import UIKit
|
||||
|
||||
extension \(extensionClassname) {
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getProperties(sections: [Section], lang: String, tags: [String], staticVar: Bool) -> String {
|
||||
sections
|
||||
.compactMap { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
|
||||
return nil// Go to next section
|
||||
}
|
||||
|
||||
var res = "\n // MARK: - \(section.name)"
|
||||
section.definitions.forEach { definition in
|
||||
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
|
||||
return // Go to next definition
|
||||
}
|
||||
|
||||
if staticVar {
|
||||
res += "\n\n\(definition.getStaticProperty(forLang: lang))"
|
||||
} else {
|
||||
res += "\n\n\(definition.getProperty(forLang: lang))"
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
188
Sources/ResgenSwift/Strings/Model/Definition.swift
Normal file
188
Sources/ResgenSwift/Strings/Model/Definition.swift
Normal file
@ -0,0 +1,188 @@
|
||||
//
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
}
|
100
Sources/ResgenSwift/Strings/Stringium/Stringium.swift
Normal file
100
Sources/ResgenSwift/Strings/Stringium/Stringium.swift
Normal file
@ -0,0 +1,100 @@
|
||||
//
|
||||
// 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: - Command options
|
||||
|
||||
@OptionGroup var options: StringiumOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
mutating func run() {
|
||||
print("[\(Self.toolName)] Starting strings generation")
|
||||
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate strings for \(options.langs) (default lang: \(options.defaultLang)")
|
||||
|
||||
// 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: options.inputFilenameWithoutExt)
|
||||
|
||||
// Generate extension
|
||||
StringsFileGenerator.writeExtensionFiles(sections: sections,
|
||||
defaultLang: options.defaultLang,
|
||||
tags: options.tags,
|
||||
staticVar: options.staticMembers,
|
||||
inputFilename: options.inputFilenameWithoutExt,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: options.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: options.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)\""
|
||||
}
|
||||
}
|
||||
}
|
86
Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift
Normal file
86
Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift
Normal file
@ -0,0 +1,86 @@
|
||||
//
|
||||
// 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: "Tell if it will generate static properties or not")
|
||||
var staticMembers: Bool = false
|
||||
|
||||
@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?
|
||||
}
|
||||
|
||||
// MARK: - Private var getter
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Computed var
|
||||
|
||||
extension StringiumOptions {
|
||||
var extensionFileName: String {
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
return "\(extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(extensionName).swift"
|
||||
}
|
||||
|
||||
var extensionFilePath: String {
|
||||
"\(extensionOutputPath)/\(extensionFileName)"
|
||||
}
|
||||
|
||||
var inputFilenameWithoutExt: String {
|
||||
URL(fileURLWithPath: inputFile)
|
||||
.deletingPathExtension()
|
||||
.lastPathComponent
|
||||
}
|
||||
}
|
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()
|
||||
|
79
Sources/ResgenSwift/Strings/Tag/Tags.swift
Normal file
79
Sources/ResgenSwift/Strings/Tag/Tags.swift
Normal file
@ -0,0 +1,79 @@
|
||||
//
|
||||
// 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: - Command Options
|
||||
|
||||
@OptionGroup var options: TagsOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
mutating func run() {
|
||||
print("[\(Self.toolName)] Starting tags generation")
|
||||
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate strings for lang: \(options.lang)")
|
||||
|
||||
// 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: options.staticMembers,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: options.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: options.extensionFilePath) else {
|
||||
print("[\(Self.toolName)] Tags are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
47
Sources/ResgenSwift/Strings/Tag/TagsOptions.swift
Normal file
47
Sources/ResgenSwift/Strings/Tag/TagsOptions.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// 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: "Tell if it will generate static properties or not")
|
||||
var staticMembers: Bool = false
|
||||
|
||||
@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?
|
||||
}
|
||||
|
||||
// MARK: - Computed var
|
||||
|
||||
extension TagsOptions {
|
||||
var extensionFileName: String {
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
return "\(extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(extensionName).swift"
|
||||
}
|
||||
|
||||
var extensionFilePath: String {
|
||||
"\(extensionOutputPath)/\(extensionFileName)"
|
||||
}
|
||||
}
|
96
Sources/ResgenSwift/Strings/Twine/Twine.swift
Normal file
96
Sources/ResgenSwift/Strings/Twine/Twine.swift
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// 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: - 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/\(options.inputFilenameWithoutExt).strings",
|
||||
"--tags=ios,iosonly,iosOnly"])
|
||||
}
|
||||
|
||||
// Generate extension
|
||||
Shell.shell([Self.twineExecutable,
|
||||
"generate-localization-file", options.inputFile,
|
||||
"--format", "apple-swift",
|
||||
"--lang", "\(options.defaultLang)",
|
||||
options.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)
|
||||
}
|
||||
|
||||
// Check if needed to regenerate
|
||||
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration,
|
||||
inputFilePath: options.inputFile,
|
||||
extensionFilePath: options.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"
|
||||
}
|
||||
}
|
||||
}
|
58
Sources/ResgenSwift/Strings/Twine/TwineOptions.swift
Normal file
58
Sources/ResgenSwift/Strings/Twine/TwineOptions.swift
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
// MARK: - Private var getter
|
||||
|
||||
extension TwineOptions {
|
||||
var langs: [String] {
|
||||
langsRaw
|
||||
.split(separator: " ")
|
||||
.map { String($0) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Computed var
|
||||
|
||||
extension TwineOptions {
|
||||
var inputFilenameWithoutExt: String {
|
||||
URL(fileURLWithPath: inputFile)
|
||||
.deletingPathExtension()
|
||||
.lastPathComponent
|
||||
}
|
||||
|
||||
var extensionFilePath: String {
|
||||
"\(extensionOutputPath)/\(inputFilenameWithoutExt).swift"
|
||||
}
|
||||
|
||||
// "R2String+" is hardcoded in Twine formatter
|
||||
var extensionFilePathGenerated: String {
|
||||
"\(extensionOutputPath)/R2String+\(inputFilenameWithoutExt).swift"
|
||||
}
|
||||
}
|
@ -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