From 279f13dbd5666142a34052ef001d45f992cebbf4 Mon Sep 17 00:00:00 2001 From: Thibaut Schmitt Date: Wed, 30 Apr 2025 17:05:02 +0200 Subject: [PATCH] Add SwiftLint HARD rules --- .swiftlint.yml | 317 +++++++++++++++--- Package.resolved | 62 +--- Package.swift | 30 +- Sources/ResgenSwift/Analytics/Analytics.swift | 64 ++-- .../Analytics/AnalyticsError.swift | 11 +- .../Analytics/AnalyticsOptions.swift | 22 +- .../Generator/AnalyticsGenerator.swift | 176 ++++++---- .../Generator/FirebaseGenerator.swift | 28 +- .../Analytics/Generator/MatomoGenerator.swift | 48 +-- .../Analytics/Model/AnalyticsCategory.swift | 9 +- .../Analytics/Model/AnalyticsDefinition.swift | 37 +- .../Analytics/Model/AnalyticsFile.swift | 9 +- .../Analytics/Model/AnalyticsParameter.swift | 7 +- .../ResgenSwift/Analytics/Model/TagType.swift | 3 +- .../Analytics/Model/TargetType.swift | 4 +- .../Parser/AnalyticsFileParser.swift | 108 +++--- Sources/ResgenSwift/Colors/Colors.swift | 108 +++--- .../ResgenSwift/Colors/ColorsToolError.swift | 21 +- .../Colors/ColorsToolOptions.swift | 39 ++- .../Generator/ColorExtensionGenerator.swift | 62 ++-- .../Colors/Generator/ColorXcassetHelper.swift | 20 +- .../ResgenSwift/Colors/Model/ColorStyle.swift | 5 +- .../Colors/Model/ParsedColor.swift | 13 +- .../Colors/Parser/ColorFileParser.swift | 25 +- Sources/ResgenSwift/Fonts/FontOptions.swift | 31 +- Sources/ResgenSwift/Fonts/Fonts.swift | 64 ++-- .../ResgenSwift/Fonts/FontsToolError.swift | 15 +- .../ResgenSwift/Fonts/FontsToolHelper.swift | 32 +- .../Fonts/Generator/FontPlistGenerator.swift | 27 +- .../Generator/FontToolContentGenerator.swift | 62 ++-- .../ResgenSwift/Fonts/Model/FontName.swift | 5 +- .../Fonts/Parser/FontFileParser.swift | 9 +- .../Extensions/StringExtensions.swift | 2 +- Sources/ResgenSwift/Generate/Generate.swift | 40 ++- .../ResgenSwift/Generate/GenerateError.swift | 16 +- .../Generate/GenerateOptions.swift | 7 +- .../Generator/ArchitectureGenerator.swift | 13 +- .../Generate/Model/ConfigurationFile.swift | 168 ++++++---- .../Parser/ConfigurationFileParser.swift | 5 +- .../AnalyticsConfiguration+Runnable.swift | 13 +- .../ColorsConfiguration+Runnable.swift | 17 +- .../FontsConfiguration+Runnable.swift | 25 +- .../ImagesConfiguration+Runnable.swift | 21 +- .../Generate/Runnable/Runnable.swift | 1 + .../StringsConfiguration+Runnable.swift | 13 +- .../Runnable/TagsConfiguration+Runnable.swift | 13 +- .../Extensions/FileManagerExtensions.swift | 9 +- .../Generator/ImageExtensionGenerator.swift | 78 +++-- .../Images/Generator/XcassetsGenerator.swift | 105 +++--- Sources/ResgenSwift/Images/Images.swift | 108 +++--- Sources/ResgenSwift/Images/ImagesError.swift | 23 +- .../ResgenSwift/Images/ImagesOptions.swift | 43 +-- .../Images/Model/ConvertArgument.swift | 1 + .../Images/Model/ImageContent.swift | 12 +- .../Images/Model/ParsedImage.swift | 31 +- .../ResgenSwift/Images/Model/PlatormTag.swift | 1 + .../Images/Parser/ImageFileParser.swift | 20 +- .../Generator/StringsFileGenerator.swift | 171 ++++++---- .../Strings/Generator/TagsGenerator.swift | 83 +++-- .../Strings/Model/Definition.swift | 114 ++++--- .../ResgenSwift/Strings/Model/Section.swift | 17 +- .../ResgenSwift/Strings/Model/XcString.swift | 47 ++- .../Strings/Parser/TwineFileParser.swift | 48 ++- .../Strings/Stringium/Stringium.swift | 104 +++--- .../Strings/Stringium/StringiumError.swift | 15 +- .../Strings/Stringium/StringiumOptions.swift | 29 +- Sources/ResgenSwift/Strings/Strings.swift | 10 +- Sources/ResgenSwift/Strings/Tag/Tags.swift | 64 ++-- .../ResgenSwift/Strings/Tag/TagsOptions.swift | 22 +- Sources/ResgenSwift/Strings/Twine/Twine.swift | 99 +++--- .../Strings/Twine/TwineError.swift | 7 +- .../Strings/Twine/TwineOptions.swift | 21 +- Sources/ResgenSwift/main.swift | 8 +- Sources/ToolCore/GeneratorChecker.swift | 19 +- Sources/ToolCore/SequenceExtensions.swift | 5 +- Sources/ToolCore/Shell.swift | 24 +- Sources/ToolCore/StringExtensions.swift | 75 +++-- Sources/ToolCore/Version.swift | 2 + .../Analytics/AnalyticsGeneratorTests.swift | 111 +++--- 79 files changed, 1969 insertions(+), 1384 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index d09d0d3..1f42e64 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,43 +1,276 @@ -disabled_rules: # rule identifiers to exclude from running - - leading_whitespace - - trailing_whitespace - - identifier_name - - large_tuple - - file_length - - line_length - - force_try - - shorthand_operator - - type_body_length - - function_body_length - - function_parameter_count - - redundant_string_enum_value - - unused_closure_parameter - - cyclomatic_complexity - - syntactic_sugar - - empty_enum_arguments - - force_cast - - multiple_closures_with_trailing_closure - - private_over_fileprivate - - trailing_comma - - comment_spacing -excluded: # paths to ignore during linting. Takes precedence over `included`. - - DerivedData - - Carthage - - Pods - - vendor - - Vendor - - "*/R2Tag+tags.swift" -type_name: - min_length: 1 # only warning - max_length: # warning and error - warning: 50 - error: 60 - allowed_symbols: ["_"] -nesting: - type_level: - warning: 3 - error: 6 - statement_level: - warning: 5 - error: 10 +# All rules here : https://realm.github.io/SwiftLint/rule-directory.html +analyzer_rules: + - capture_variable + - typesafe_array_init + - unused_declaration + - unused_import + +included: + - Sources + +## Rules configuration + +attributes: + always_on_line_above: ["@InjectedValue", "@ViewBuilder", "@IBOutlet"] + always_on_same_line: ["@Environment", "@EnvironmentObject", "@StateObject", "@State"] + +identifier_name: + min_length: + - 2 + max_length: + - 60 + excluded: + - x + - y + +type_name: + min_length: 3 + max_length: 60 + excluded: + - T + allowed_symbols: + - _ + +# line_length: +# warning: 150 + +disabled_rules: + - blanket_disable_command # do not warn when rule is not re-enable later in the file + - type_contents_order + - legacy_objc_type + - indentation_width + - function_parameter_count + - line_length + - function_body_length + - cyclomatic_complexity + - optional_data_string_conversion + +opt_in_rules: + # Default rules : + - block_based_kvo + - class_delegate_protocol + - closing_brace + - closure_parameter_position + - colon + - comma + - comment_spacing + - compiler_protocol_init + - computed_accessors_order + - control_statement + - custom_rules + # - cyclomatic_complexity + - deployment_target + - discouraged_direct_init + - duplicate_enum_cases + - duplicate_imports + - duplicated_key_in_dictionary_literal + - dynamic_inline + - empty_enum_arguments + - empty_parameters + - empty_parentheses_with_trailing_closure + - file_length + - for_where + - force_unwrapping + - force_cast + - force_try + - trailing_whitespace + # - function_body_length + # - function_parameter_count + - generic_type_name + - identifier_name + - implicit_getter + - inclusive_language + - is_disjoint + - large_tuple + - leading_whitespace + - legacy_cggeometry_functions + - legacy_constant + - legacy_constructor + - legacy_hashing + - legacy_nsgeometry_functions + - legacy_random + # - line_length + - mark + - multiple_closures_with_trailing_closure + - nesting + - no_fallthrough_only + - no_space_in_method_call + - notification_center_detachment + - ns_number_init_as_function_reference + - nsobject_prefer_isequal + - opening_brace + - operator_whitespace + - orphaned_doc_comment + - private_over_fileprivate + - private_unit_test + - protocol_property_accessors_order + - reduce_boolean + - redundant_discardable_let + - redundant_objc_attribute + - redundant_optional_initialization + - redundant_set_access_control + - redundant_string_enum_value + - redundant_void_return + - return_arrow_whitespace + - self_in_property_initialization + - shorthand_operator + - statement_position + - superfluous_disable_command + - switch_case_alignment + - syntactic_sugar + - todo + - trailing_comma + - trailing_newline + - trailing_semicolon + - type_body_length + - type_name + - unavailable_condition + - unneeded_break_in_switch + - unused_closure_parameter + - unused_control_flow_label + - unused_enumerated + - unused_optional_binding + - unused_setter_value + - valid_ibinspectable + - vertical_parameter_alignment + - vertical_whitespace + - void_function_in_ternary + - void_return + - xctfail_message + - accessibility_trait_for_button + - array_init + - attributes + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - comma_inheritance + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discarded_notification_center_observer + - discouraged_assert + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - explicit_init + - fallthrough + - fatal_error_message + - file_header + - first_where + - flatmap_over_map_reduce + - ibinspectable_in_extension + - implicit_return + - implicitly_unwrapped_optional + - joined_default_parameter + - last_where + - legacy_multiple + - let_var_whitespace + - literal_expression_end_indentation + - lower_acl_than_parent + # - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - nimble_operator + - no_extension_access_modifier + - no_grouping_extension + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_in_static_references + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - prefixed_toplevel_constant + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - return_value_from_void_function + - self_binding + - shorthand_optional_binding + - single_test_class + - sorted_first_last + - sorted_imports + - strong_iboutlet + - test_case_accessibility + - toggle_bool + - trailing_closure + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_between_cases + - vertical_whitespace_closing_braces + - weak_delegate + - xct_specific_matcher + +custom_rules: + + # Empty line before and after MARK ------------------------------------------- + # mark_spacing: + # name: "Surround MARK by empty lines" + # regex: '\n[^\n]([^\n]*\/\/ MARK[^\n]*)\n[^\n]' + # message: "Surround MARK by empty lines" + # severity: warning + + # Empty line ----------------------------------------------------------------- + # no_empty_line_after_func: + # name: "No empty line after init or func" + # regex: '(func|init|let\s|var\s)[^\n]*\{[^\n\{\}]*\n\n' + # message: "No empty line after init or func" + # severity: warning + + # Empty line after canImport ----------------------------------------------------------------- + no_empty_line_after_can_import: + name: "Add empty line after #if canImport" + regex: '#if canImport\(.*\)\n[^\n]*(import|class|struct|enum|extension|protocol)' + message: "Add empty line after #if canImport" + severity: warning + + # Spacings ------------------------------------------------------------------- + empty_line_required: + name: "Add empty line after class, struct, enum, extension or protocol" + regex: '(class |struct |enum |extension |protocol )[^\n]*\{\n[^\n]*(class|struct|enum|extension|protocol|func|let|var|weak|private|internal|public|open|static|final|\/\/|init|case|@)' + message: "Add empty line after class, struct, enum, extension or protocol" + severity: warning + match_kinds: + - argument + - attribute.builtin + - attribute.id + - buildconfig.id + - buildconfig.keyword + - comment + - comment.mark + - comment.url + - identifier + - keyword + - number + - objectliteral + - parameter + - placeholder + - string + - string_interpolation_anchor + - typeidentifier + + diff --git a/Package.resolved b/Package.resolved index 824db60..23d472c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,32 +1,5 @@ { "pins" : [ - { - "identity" : "collectionconcurrencykit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", - "state" : { - "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", - "version" : "0.2.0" - } - }, - { - "identity" : "cryptoswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", - "state" : { - "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - "version" : "1.8.2" - } - }, - { - "identity" : "sourcekitten", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/SourceKitten.git", - "state" : { - "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", - "version" : "0.34.1" - } - }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -37,39 +10,12 @@ } }, { - "identity" : "swift-syntax", + "identity" : "swiftlintplugin", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" - } - }, - { - "identity" : "swiftlint", - "kind" : "remoteSourceControl", - "location" : "https://github.com/realm/SwiftLint.git", - "state" : { - "revision" : "f17a4f9dfb6a6afb0408426354e4180daaf49cee", - "version" : "0.54.0" - } - }, - { - "identity" : "swiftytexttable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", - "state" : { - "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", - "version" : "0.9.0" - } - }, - { - "identity" : "swxmlhash", - "kind" : "remoteSourceControl", - "location" : "https://github.com/drmohundro/SWXMLHash.git", - "state" : { - "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", - "version" : "7.0.2" + "revision" : "87454f5c9ff4d644086aec2a0df1ffba678e7f3c", + "version" : "0.57.1" } }, { diff --git a/Package.swift b/Package.swift index f9cc54c..d405a75 100644 --- a/Package.swift +++ b/Package.swift @@ -1,16 +1,25 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "ResgenSwift", - platforms: [.macOS(.v12)], + platforms: [.macOS(.v14)], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), - .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.1"), - .package(url: "https://github.com/realm/SwiftLint.git", .upToNextMajor(from: "0.54.0")), + .package( + url: "https://github.com/apple/swift-argument-parser", + from: "1.0.0" + ), + .package( + url: "https://github.com/jpsim/Yams.git", + from: "5.0.1" + ), + .package( + url: "https://github.com/lukepistrol/SwiftLintPlugin", + exact: "0.57.1" + ), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -19,10 +28,15 @@ let package = Package( name: "ResgenSwift", dependencies: [ "ToolCore", - .product(name: "ArgumentParser", package: "swift-argument-parser"), - "Yams" + "Yams", + .product( + name: "ArgumentParser", + package: "swift-argument-parser" + ) ], - plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")] + plugins: [ + .plugin(name: "SwiftLint", package: "SwiftLintPlugin") + ] ), // Helper targets diff --git a/Sources/ResgenSwift/Analytics/Analytics.swift b/Sources/ResgenSwift/Analytics/Analytics.swift index 2e51484..ab31f03 100644 --- a/Sources/ResgenSwift/Analytics/Analytics.swift +++ b/Sources/ResgenSwift/Analytics/Analytics.swift @@ -5,30 +5,30 @@ // Created by Loris Perret on 08/12/2023. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore struct Analytics: ParsableCommand { - + // MARK: - Command Configuration - + static var configuration = CommandConfiguration( abstract: "Generate analytics extension file.", version: ResgenSwiftVersion ) - + // MARK: - Static - + static let toolName = "Analytics" static let defaultExtensionName = "Analytics" - + // MARK: - Command Options - + @OptionGroup var options: AnalyticsOptions - + // MARK: - Run - + mutating func run() { print("[\(Self.toolName)] Starting analytics generation") print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate analytics for target: \(options.target)") @@ -37,50 +37,54 @@ struct Analytics: ParsableCommand { guard checkRequirements() else { return } print("[\(Self.toolName)] Will generate analytics") - + // Check requirements guard checkRequirements() else { return } - + // Parse input file - let sections = AnalyticsFileParser.parse(options.inputFile, target: options.target) - + let sections = AnalyticsFileParser().parse(options.inputFile, target: options.target) + // Generate extension - AnalyticsGenerator.writeExtensionFiles(sections: sections, - target: options.target, - tags: ["ios", "iosonly"], - staticVar: options.staticMembers, - extensionName: options.extensionName, - extensionFilePath: options.extensionFilePath) - + AnalyticsGenerator.writeExtensionFiles( + sections: sections, + target: options.target, + tags: ["ios", "iosonly"], + staticVar: options.staticMembers, + extensionName: options.extensionName, + extensionFilePath: options.extensionFilePath + ) + print("[\(Self.toolName)] Analytics generated") } - + // MARK: - Requirements - + private func checkRequirements() -> Bool { let fileManager = FileManager() - + // Input file guard fileManager.fileExists(atPath: options.inputFile) else { let error = AnalyticsError.fileNotExists(options.inputFile) print(error.description) - Analytics.exit(withError: error) + Self.exit(withError: error) } guard TrackerType.hasValidTarget(in: options.target) else { let error = AnalyticsError.noValidTracker(options.target) print(error.description) - Analytics.exit(withError: error) + Self.exit(withError: error) } // Check if needed to regenerate - guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, - inputFilePath: options.inputFile, - extensionFilePath: options.extensionFilePath) else { + guard GeneratorChecker.shouldGenerate( + force: options.forceGeneration, + inputFilePath: options.inputFile, + extensionFilePath: options.extensionFilePath + ) else { print("[\(Self.toolName)] Analytics are already up to date :) ") return false } - + return true } } diff --git a/Sources/ResgenSwift/Analytics/AnalyticsError.swift b/Sources/ResgenSwift/Analytics/AnalyticsError.swift index b32198d..dbc2de2 100644 --- a/Sources/ResgenSwift/Analytics/AnalyticsError.swift +++ b/Sources/ResgenSwift/Analytics/AnalyticsError.swift @@ -8,13 +8,14 @@ import Foundation enum AnalyticsError: Error { + case noValidTracker(String) case fileNotExists(String) case missingElement(String) case invalidParameter(String) case parseFailed(String) case writeFile(String, String) - + var description: String { switch self { case .noValidTracker(let inputTargets): @@ -22,17 +23,17 @@ enum AnalyticsError: Error { case .fileNotExists(let filename): return "error: [\(Analytics.toolName)] File \(filename) does not exists" - + case .missingElement(let element): return "error: [\(Analytics.toolName)] Missing \(element) for Matomo" - + case .invalidParameter(let reason): return "error: [\(Analytics.toolName)] Invalid parameter \(reason)" - + case .parseFailed(let baseError): return "error: [\(Analytics.toolName)] Parse input file failed: \(baseError)" - case .writeFile(let subErrorDescription, let filename): + case let .writeFile(subErrorDescription, filename): return "error: [\(Analytics.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)" } } diff --git a/Sources/ResgenSwift/Analytics/AnalyticsOptions.swift b/Sources/ResgenSwift/Analytics/AnalyticsOptions.swift index 0f3172e..b306aad 100644 --- a/Sources/ResgenSwift/Analytics/AnalyticsOptions.swift +++ b/Sources/ResgenSwift/Analytics/AnalyticsOptions.swift @@ -5,28 +5,31 @@ // Created by Loris Perret on 08/12/2023. // -import Foundation import ArgumentParser +import Foundation + +// swiftlint:disable no_grouping_extension struct AnalyticsOptions: 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: "Target(s) analytics to generate. (\"matomo\" | \"firebase\")") var target: 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 a Analytics extension.") var extensionName: String = Analytics.defaultExtensionName - + @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Analytics{extensionSuffix}.swift") var extensionSuffix: String? } @@ -34,13 +37,14 @@ struct AnalyticsOptions: ParsableArguments { // MARK: - Computed var extension AnalyticsOptions { + var extensionFileName: String { - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { return "\(extensionName)+\(extensionSuffix).swift" } return "\(extensionName).swift" } - + var extensionFilePath: String { "\(extensionOutputPath)/\(extensionFileName)" } diff --git a/Sources/ResgenSwift/Analytics/Generator/AnalyticsGenerator.swift b/Sources/ResgenSwift/Analytics/Generator/AnalyticsGenerator.swift index 02973c6..fdcc03e 100644 --- a/Sources/ResgenSwift/Analytics/Generator/AnalyticsGenerator.swift +++ b/Sources/ResgenSwift/Analytics/Generator/AnalyticsGenerator.swift @@ -5,112 +5,147 @@ // Created by Loris Perret on 08/12/2023. // +import CoreVideo import Foundation import ToolCore -import CoreVideo -class AnalyticsGenerator { - static var targets: [TrackerType] = [] +// Disabled cause it's a pain to handle in generated string - static func writeExtensionFiles(sections: [AnalyticsCategory], target: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) { +enum AnalyticsGenerator { + + // MARK: - Write content + + static func writeExtensionFiles( + sections: [AnalyticsCategory], + target: String, + tags: [String], + staticVar: Bool, + extensionName: String, + extensionFilePath: String + ) { // Get target type from enum let targetsString: [String] = target.components(separatedBy: " ") - - TrackerType.allCases.forEach { enumTarget in - if targetsString.contains(enumTarget.value) { - targets.append(enumTarget) + let targets = { + var targets = [TrackerType]() + TrackerType.allCases.forEach { enumTarget in + if targetsString.contains(enumTarget.value) { + targets.append(enumTarget) + } } - } - + return targets + }() + // Get extension content - let extensionFileContent = Self.getExtensionContent(sections: sections, - tags: tags, - staticVar: staticVar, - extensionName: extensionName) - + let extensionFileContent = getExtensionContent( + targets: targets, + sections: sections, + 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 { + } catch { let error = AnalyticsError.writeFile(extensionFilePath, error.localizedDescription) print(error.description) Analytics.exit(withError: error) } } - + // MARK: - Extension content - - static func getExtensionContent(sections: [AnalyticsCategory], tags: [String], staticVar: Bool, extensionName: String) -> String { + + static func getExtensionContent( + targets: [TrackerType], + sections: [AnalyticsCategory], + tags: [String], + staticVar: Bool, + extensionName: String + ) -> String { [ - Self.getHeader(extensionClassname: extensionName, staticVar: staticVar), - Self.getProperties(sections: sections, tags: tags, staticVar: staticVar), - Self.getFooter() + getHeader( + targets: targets, + extensionClassname: extensionName, + staticVar: staticVar + ), + getProperties( + sections: sections, + tags: tags, + staticVar: staticVar + ), + getFooter() ] .joined(separator: "\n") } - + // MARK: - Extension part - - private static func getHeader(extensionClassname: String, staticVar: Bool) -> String { + + private static func getHeader( + targets: [TrackerType], + extensionClassname: String, + staticVar: Bool + ) -> String { """ // Generated by ResgenSwift.\(Analytics.toolName) \(ResgenSwiftVersion) - - \(Self.getImport()) - - \(Self.getAnalyticsProtocol()) + + \(getImport(targets: targets)) + + \(getAnalyticsProtocol(targets: targets)) // MARK: - Manager - + class AnalyticsManager { + static var shared = AnalyticsManager() - + // MARK: - Properties - + var managers: [AnalyticsManagerProtocol] = [] - - \(Self.getEnabledContent()) - - \(Self.getAnalyticsProperties()) - - \(Self.getPrivateLogFunction()) + + \(getEnabledContent()) + + \(getAnalyticsProperties(targets: targets)) + + \(getPrivateLogFunction()) """ } - + private static func getEnabledContent() -> String { """ private var isEnabled: Bool = true - + // MARK: - Methods - + func setAnalyticsEnabled(_ enable: Bool) { isEnabled = enable } """ } - - private static func getImport() -> String { + + private static func getImport(targets: [TrackerType]) -> String { var result: [String] = [] - + if targets.contains(TrackerType.matomo) { result.append("import MatomoTracker") } if targets.contains(TrackerType.firebase) { result.append("import FirebaseAnalytics") } - + return result.joined(separator: "\n") } - + private static func getPrivateLogFunction() -> String { """ private func logScreen(name: String, path: String) { guard isEnabled else { return } - + managers.forEach { manager in manager.logScreen(name: name, path: path) } } - + private func logEvent( name: String, action: String, @@ -118,7 +153,7 @@ class AnalyticsGenerator { params: [String: Any]? ) { guard isEnabled else { return } - + managers.forEach { manager in manager.logEvent( name: name, @@ -130,18 +165,18 @@ class AnalyticsGenerator { } """ } - - private static func getAnalyticsProperties() -> String { + + private static func getAnalyticsProperties(targets: [TrackerType]) -> String { var header = "" var content: [String] = [] let footer = " }" - + if targets.contains(TrackerType.matomo) { header = "func configure(siteId: String, url: String) {" } else if targets.contains(TrackerType.firebase) { header = "func configure() {" } - + if targets.contains(TrackerType.matomo) { content.append(""" managers.append( @@ -155,7 +190,7 @@ class AnalyticsGenerator { if targets.contains(TrackerType.firebase) { content.append(" managers.append(FirebaseAnalyticsManager())") } - + return [ header, content.joined(separator: "\n"), @@ -163,12 +198,13 @@ class AnalyticsGenerator { ] .joined(separator: "\n") } - - private static func getAnalyticsProtocol() -> String { + + private static func getAnalyticsProtocol(targets: [TrackerType]) -> String { let proto = """ // MARK: - Protocol - + protocol AnalyticsManagerProtocol { + func logScreen(name: String, path: String) func logEvent( name: String, @@ -177,36 +213,40 @@ class AnalyticsGenerator { params: [String: Any]? ) } - + """ - + var result: [String] = [proto] - + if targets.contains(TrackerType.matomo) { result.append(MatomoGenerator.service) } - + if targets.contains(TrackerType.firebase) { result.append(FirebaseGenerator.service) } - + return result.joined(separator: "\n") } - - private static func getProperties(sections: [AnalyticsCategory], tags: [String], staticVar: Bool) -> String { + + private static func getProperties( + sections: [AnalyticsCategory], + 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.id)" section.definitions.forEach { definition in guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else { return // Go to next definition } - + if staticVar { res += "\n\n\(definition.getStaticProperty())" } else { @@ -217,11 +257,11 @@ class AnalyticsGenerator { } .joined(separator: "\n") } - + private static func getFooter() -> String { """ } - + """ } } diff --git a/Sources/ResgenSwift/Analytics/Generator/FirebaseGenerator.swift b/Sources/ResgenSwift/Analytics/Generator/FirebaseGenerator.swift index 1957156..e699a8e 100644 --- a/Sources/ResgenSwift/Analytics/Generator/FirebaseGenerator.swift +++ b/Sources/ResgenSwift/Analytics/Generator/FirebaseGenerator.swift @@ -11,14 +11,14 @@ enum FirebaseGenerator { static var service: String { [ - FirebaseGenerator.header, - FirebaseGenerator.logScreen, - FirebaseGenerator.logEvent, - FirebaseGenerator.footer + Self.header, + Self.logScreen, + Self.logEvent, + Self.footer ] .joined(separator: "\n") } - + // MARK: - Private vars private static var header: String { @@ -28,23 +28,23 @@ enum FirebaseGenerator { class FirebaseAnalyticsManager: AnalyticsManagerProtocol { """ } - + private static var logScreen: String { """ func logScreen(name: String, path: String) { var parameters = [ AnalyticsParameterScreenName: name as NSObject ] - + Analytics.logEvent( AnalyticsEventScreenView, parameters: parameters ) } - + """ } - + private static var logEvent: String { """ func logEvent( @@ -57,7 +57,7 @@ enum FirebaseGenerator { "action": action as NSObject, "category": category as NSObject, ] - + if let supplementaryParameters = params { for (newKey, newValue) in supplementaryParameters { if parameters.contains(where: { (key: String, value: NSObject) in @@ -65,11 +65,11 @@ enum FirebaseGenerator { }) { continue } - + parameters[newKey] = newValue as? NSObject } } - + Analytics.logEvent( name.replacingOccurrences(of: [" "], with: "_"), parameters: parameters @@ -77,11 +77,11 @@ enum FirebaseGenerator { } """ } - + private static var footer: String { """ } - + """ } } diff --git a/Sources/ResgenSwift/Analytics/Generator/MatomoGenerator.swift b/Sources/ResgenSwift/Analytics/Generator/MatomoGenerator.swift index 740cf95..4bd4320 100644 --- a/Sources/ResgenSwift/Analytics/Generator/MatomoGenerator.swift +++ b/Sources/ResgenSwift/Analytics/Generator/MatomoGenerator.swift @@ -1,6 +1,6 @@ // // MatomoGenerator.swift -// +// // // Created by Loris Perret on 05/12/2023. // @@ -11,15 +11,15 @@ enum MatomoGenerator { static var service: String { [ - MatomoGenerator.header, - MatomoGenerator.setup, - MatomoGenerator.logScreen, - MatomoGenerator.logEvent, - MatomoGenerator.footer + Self.header, + Self.setup, + Self.logScreen, + Self.logEvent, + Self.footer ] .joined(separator: "\n") } - + // MARK: - Private vars private static var header: String { @@ -27,18 +27,18 @@ enum MatomoGenerator { // MARK: - Matomo class MatomoAnalyticsManager: AnalyticsManagerProtocol { - + // MARK: - Properties - + private var tracker: MatomoTracker - + """ } - + private static var setup: String { """ // MARK: - Init - + init(siteId: String, url: String) { debugPrint("[Matomo service] Server URL: \\(url)") debugPrint("[Matomo service] Site ID: \\(siteId)") @@ -46,40 +46,40 @@ enum MatomoGenerator { siteId: siteId, baseURL: URL(string: url)! ) - + #if DEBUG tracker.dispatchInterval = 5 #endif - + #if DEBUG tracker.logger = DefaultLogger(minLevel: .verbose) #endif - + debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") } - + // MARK: - Methods - + """ } - + private static var logScreen: String { """ func logScreen(name: String, path: String) { guard !tracker.isOptedOut else { return } guard let trackerUrl = tracker.contentBase?.absoluteString else { return } - + let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS") tracker.track( view: [name], url: urlString ) } - + """ } - + private static var logEvent: String { """ func logEvent( @@ -89,7 +89,7 @@ enum MatomoGenerator { params: [String: Any]? ) { guard !tracker.isOptedOut else { return } - + tracker.track( eventWithCategory: category, action: action, @@ -100,11 +100,11 @@ enum MatomoGenerator { } """ } - + private static var footer: String { """ } - + """ } } diff --git a/Sources/ResgenSwift/Analytics/Model/AnalyticsCategory.swift b/Sources/ResgenSwift/Analytics/Model/AnalyticsCategory.swift index 9451d99..a2d4311 100644 --- a/Sources/ResgenSwift/Analytics/Model/AnalyticsCategory.swift +++ b/Sources/ResgenSwift/Analytics/Model/AnalyticsCategory.swift @@ -8,21 +8,24 @@ import Foundation class AnalyticsCategory { + + // MARK: - Properties + let id: String // OnBoarding var definitions = [AnalyticsDefinition]() - + // MARK: - Init init(id: String) { self.id = id } - + // MARK: - Methods 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 diff --git a/Sources/ResgenSwift/Analytics/Model/AnalyticsDefinition.swift b/Sources/ResgenSwift/Analytics/Model/AnalyticsDefinition.swift index 2f350cf..41ade03 100644 --- a/Sources/ResgenSwift/Analytics/Model/AnalyticsDefinition.swift +++ b/Sources/ResgenSwift/Analytics/Model/AnalyticsDefinition.swift @@ -9,6 +9,9 @@ import Foundation import ToolCore class AnalyticsDefinition { + + // MARK: - Properties + let id: String var name: String var path: String = "" @@ -18,7 +21,7 @@ class AnalyticsDefinition { var tags: [String] = [] var parameters: [AnalyticsParameter] = [] var type: TagType - + // MARK: - Init init(id: String, name: String, type: TagType) { @@ -26,7 +29,7 @@ class AnalyticsDefinition { self.name = name self.type = type } - + // MARK: - Methods func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool { @@ -35,7 +38,7 @@ class AnalyticsDefinition { } return true } - + // MARK: - Private Methods private func getFuncName() -> String { @@ -43,24 +46,24 @@ class AnalyticsDefinition { id.components(separatedBy: "_").forEach { word in pascalCaseTitle.append(contentsOf: word.uppercasedFirst()) } - + return "log\(type == .screen ? "Screen" : "Event")\(pascalCaseTitle)" } - + private func getParameters() -> String { var params = parameters var result: String - + if type == .screen { params = params.filter { param in !param.replaceIn.isEmpty } } - + let paramsString = params.map { parameter in "\(parameter.name): \(parameter.type)" } - + if paramsString.count > 2 { result = """ ( @@ -72,10 +75,10 @@ class AnalyticsDefinition { (\(paramsString.joined(separator: ", "))) """ } - + return result } - + private func replaceIn() { for parameter in parameters { for rep in parameter.replaceIn { @@ -89,15 +92,15 @@ class AnalyticsDefinition { } } } - + private func getlogFunction() -> String { var params: [String] = [] var result: String - + let supplementaryParams = parameters.filter { param in param.replaceIn.isEmpty } - + supplementaryParams.forEach { param in params.append("\"\(param.name)\": \(param.name)") } @@ -115,7 +118,7 @@ class AnalyticsDefinition { } else { result = "[:]" } - + if type == .screen { return """ logScreen( @@ -134,9 +137,9 @@ class AnalyticsDefinition { """ } } - + // MARK: - Raw strings - + func getProperty() -> String { replaceIn() return """ @@ -145,7 +148,7 @@ class AnalyticsDefinition { } """ } - + func getStaticProperty() -> String { replaceIn() return """ diff --git a/Sources/ResgenSwift/Analytics/Model/AnalyticsFile.swift b/Sources/ResgenSwift/Analytics/Model/AnalyticsFile.swift index d775768..bbfaba0 100644 --- a/Sources/ResgenSwift/Analytics/Model/AnalyticsFile.swift +++ b/Sources/ResgenSwift/Analytics/Model/AnalyticsFile.swift @@ -8,37 +8,42 @@ import Foundation struct AnalyticsFile: Codable { + var categories: [AnalyticsCategoryDTO] } struct AnalyticsCategoryDTO: Codable { + var id: String var screens: [AnalyticsDefinitionScreenDTO]? var events: [AnalyticsDefinitionEventDTO]? } struct AnalyticsDefinitionScreenDTO: Codable { + var id: String var name: String var tags: String var comments: String? var parameters: [AnalyticsParameterDTO]? - + var path: String? } struct AnalyticsDefinitionEventDTO: Codable { + var id: String var name: String var tags: String var comments: String? var parameters: [AnalyticsParameterDTO]? - + var category: String? var action: String? } struct AnalyticsParameterDTO: Codable { + var name: String var type: String var replaceIn: String? diff --git a/Sources/ResgenSwift/Analytics/Model/AnalyticsParameter.swift b/Sources/ResgenSwift/Analytics/Model/AnalyticsParameter.swift index 9c659c3..3bb7c3a 100644 --- a/Sources/ResgenSwift/Analytics/Model/AnalyticsParameter.swift +++ b/Sources/ResgenSwift/Analytics/Model/AnalyticsParameter.swift @@ -8,12 +8,15 @@ import Foundation class AnalyticsParameter { + + // MARK: - Properties + var name: String var type: String var replaceIn: [String] = [] - + // MARK: - Init - + init(name: String, type: String) { self.name = name self.type = type diff --git a/Sources/ResgenSwift/Analytics/Model/TagType.swift b/Sources/ResgenSwift/Analytics/Model/TagType.swift index 62d3d28..ae466ce 100644 --- a/Sources/ResgenSwift/Analytics/Model/TagType.swift +++ b/Sources/ResgenSwift/Analytics/Model/TagType.swift @@ -8,8 +8,9 @@ import Foundation extension AnalyticsDefinition { - + enum TagType { + case screen case event } diff --git a/Sources/ResgenSwift/Analytics/Model/TargetType.swift b/Sources/ResgenSwift/Analytics/Model/TargetType.swift index 412d5c3..b9d0ebb 100644 --- a/Sources/ResgenSwift/Analytics/Model/TargetType.swift +++ b/Sources/ResgenSwift/Analytics/Model/TargetType.swift @@ -7,7 +7,8 @@ import Foundation -enum TrackerType: CaseIterable { +enum TrackerType: CaseIterable, Sendable { + case matomo case firebase @@ -15,6 +16,7 @@ enum TrackerType: CaseIterable { switch self { case .matomo: "matomo" + case .firebase: "firebase" } diff --git a/Sources/ResgenSwift/Analytics/Parser/AnalyticsFileParser.swift b/Sources/ResgenSwift/Analytics/Parser/AnalyticsFileParser.swift index 1137f8e..6168b73 100644 --- a/Sources/ResgenSwift/Analytics/Parser/AnalyticsFileParser.swift +++ b/Sources/ResgenSwift/Analytics/Parser/AnalyticsFileParser.swift @@ -9,16 +9,54 @@ import Foundation import Yams class AnalyticsFileParser { - private static var inputFile: String = "" - private static var target: String = "" - - private static func parseYaml() -> AnalyticsFile { + + // MARK: - Properties + + private var inputFile: String = "" + private var target: String = "" + + // MARK: - Methods + + func parse(_ inputFile: String, target: String) -> [AnalyticsCategory] { + self.inputFile = inputFile + self.target = target + + let tagFile = parseYaml() + + return tagFile + .categories + .map { categorie in + let section = AnalyticsCategory(id: categorie.id) + + if let screens = categorie.screens { + section + .definitions + .append( + contentsOf: getTagDefinitionScreen(from: screens) + ) + } + + if let events = categorie.events { + section + .definitions + .append( + contentsOf: getTagDefinitionEvent(from: events) + ) + } + + return section + } + } + + // MARK: - Private methods + + private func parseYaml() -> AnalyticsFile { guard let data = FileManager().contents(atPath: inputFile) else { let error = AnalyticsError.fileNotExists(inputFile) print(error.description) Analytics.exit(withError: error) } - + do { let tagFile = try YAMLDecoder().decode(AnalyticsFile.self, from: data) return tagFile @@ -29,13 +67,12 @@ class AnalyticsFileParser { } } - private static func getParameters(from parameters: [AnalyticsParameterDTO]) -> [AnalyticsParameter] { + private func getParameters(from parameters: [AnalyticsParameterDTO]) -> [AnalyticsParameter] { parameters.map { dtoParameter in // Type - let type = dtoParameter.type.uppercasedFirst() - guard + guard type == "String" || type == "Int" || type == "Double" || @@ -59,7 +96,7 @@ class AnalyticsFileParser { } } - private static func getTagDefinition( + private func getTagDefinition( id: String, name: String, type: AnalyticsDefinition.TagType, @@ -72,20 +109,20 @@ class AnalyticsFileParser { .components(separatedBy: ",") .map { $0.removeLeadingTrailingWhitespace() } - if let comments = comments { + if let comments { definition.comments = comments } - - if let parameters = parameters { - definition.parameters = Self.getParameters(from: parameters) + + if let parameters { + definition.parameters = getParameters(from: parameters) } - + return definition } - - private static func getTagDefinitionScreen(from screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] { + + private func getTagDefinitionScreen(from screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] { screens.map { screen in - let definition: AnalyticsDefinition = Self.getTagDefinition( + let definition: AnalyticsDefinition = getTagDefinition( id: screen.id, name: screen.name, type: .screen, @@ -109,10 +146,10 @@ class AnalyticsFileParser { return definition } } - - private static func getTagDefinitionEvent(from events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] { + + private func getTagDefinitionEvent(from events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] { events.map { event in - let definition: AnalyticsDefinition = Self.getTagDefinition( + let definition: AnalyticsDefinition = getTagDefinition( id: event.id, name: event.name, type: .event, @@ -144,35 +181,4 @@ class AnalyticsFileParser { return definition } } - - static func parse(_ inputFile: String, target: String) -> [AnalyticsCategory] { - self.inputFile = inputFile - self.target = target - - let tagFile = Self.parseYaml() - - return tagFile - .categories - .map { categorie in - let section: AnalyticsCategory = AnalyticsCategory(id: categorie.id) - - if let screens = categorie.screens { - section - .definitions - .append( - contentsOf: Self.getTagDefinitionScreen(from: screens) - ) - } - - if let events = categorie.events { - section - .definitions - .append( - contentsOf: Self.getTagDefinitionEvent(from: events) - ) - } - - return section - } - } } diff --git a/Sources/ResgenSwift/Colors/Colors.swift b/Sources/ResgenSwift/Colors/Colors.swift index 722d08a..0d5cab2 100644 --- a/Sources/ResgenSwift/Colors/Colors.swift +++ b/Sources/ResgenSwift/Colors/Colors.swift @@ -1,124 +1,134 @@ // // main.swift -// +// // // Created by Thibaut Schmitt on 20/12/2021. // -import ToolCore +@preconcurrency import ArgumentParser import Foundation -import ArgumentParser +import ToolCore struct Colors: ParsableCommand { - + // MARK: - CommandConfiguration - - static var configuration = CommandConfiguration( + + static let configuration = CommandConfiguration( abstract: "A utility for generate colors assets and their getters.", version: ResgenSwiftVersion ) - + // MARK: - Static - + static let toolName = "Color" static let defaultExtensionName = "Color" static let defaultExtensionNameUIKit = "UIColor" static let assetsColorsFolderName = "Colors" - + // MARK: - Command options - + @OptionGroup var options: ColorsToolOptions - + // MARK: - Run - - public func run() throws { + + 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.style) + let parsedColors = ColorFileParser.parse( + options.inputFile, + colorStyle: options.style + ) // -> Time: 0.0020350217819213867 seconds // Generate all colors in xcassets - ColorXcassetHelper.generateXcassetColors(colors: parsedColors, - to: options.xcassetsPath) + 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, - isSwiftUI: true) - + ColorExtensionGenerator.writeExtensionFile( + colors: parsedColors, + staticVar: options.staticMembers, + extensionName: options.extensionName, + extensionFilePath: options.extensionFilePath, + isSwiftUI: true + ) + // Generate extension - ColorExtensionGenerator.writeExtensionFile(colors: parsedColors, - staticVar: options.staticMembers, - extensionName: options.extensionNameUIKit, - extensionFilePath: options.extensionFilePathUIKit, - isSwiftUI: false) - + ColorExtensionGenerator.writeExtensionFile( + colors: parsedColors, + staticVar: options.staticMembers, + extensionName: options.extensionNameUIKit, + extensionFilePath: options.extensionFilePathUIKit, + isSwiftUI: false + ) + 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.description) - Colors.exit(withError: error) + Self.exit(withError: error) } - + // Check if xcassets file exists guard fileManager.fileExists(atPath: options.xcassetsPath) else { let error = ColorsToolError.fileNotExists(options.xcassetsPath) print(error.description) - Colors.exit(withError: error) + Self.exit(withError: error) } - + // Extension for UIKit and SwiftUI should have different name guard options.extensionName != options.extensionNameUIKit else { let error = ColorsToolError.extensionNamesCollision(options.extensionName) print(error.description) - Colors.exit(withError: error) + Self.exit(withError: error) } - + // Check if needed to regenerate - guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, - inputFilePath: options.inputFile, - extensionFilePath: options.extensionFilePath) else { + 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.description) - Colors.exit(withError: error) + Self.exit(withError: error) } } } diff --git a/Sources/ResgenSwift/Colors/ColorsToolError.swift b/Sources/ResgenSwift/Colors/ColorsToolError.swift index e45f15c..2a54abf 100644 --- a/Sources/ResgenSwift/Colors/ColorsToolError.swift +++ b/Sources/ResgenSwift/Colors/ColorsToolError.swift @@ -8,6 +8,7 @@ import Foundation enum ColorsToolError: Error { + case extensionNamesCollision(String) case badFormat(String) case writeAsset(String) @@ -16,30 +17,30 @@ enum ColorsToolError: Error { case fileNotExists(String) case badColorDefinition(String, String) case deleteExistingColors(String) - + var description: String { switch self { case .extensionNamesCollision(let extensionName): return "error: [\(Fonts.toolName)] Error on extension names, extension name and SwiftUI extension name should be different (\(extensionName) is used on both)" - + 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): + + case let .writeExtension(filename, 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): + + case let .badColorDefinition(lightColor, 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)`" } diff --git a/Sources/ResgenSwift/Colors/ColorsToolOptions.swift b/Sources/ResgenSwift/Colors/ColorsToolOptions.swift index 868d753..97eb31d 100644 --- a/Sources/ResgenSwift/Colors/ColorsToolOptions.swift +++ b/Sources/ResgenSwift/Colors/ColorsToolOptions.swift @@ -1,38 +1,41 @@ // // ColorsToolOptions.swift -// +// // // Created by Thibaut Schmitt on 17/01/2022. // -import Foundation import ArgumentParser +import Foundation + +// swiftlint:disable no_grouping_extension 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") var style: ColorStyle - + @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 Color extension.") var extensionName: String = Colors.defaultExtensionName - + @Option(help: "SwiftUI Extension name. If not specified, it will generate an UIColor extension.") var extensionNameUIKit: String = Colors.defaultExtensionNameUIKit - + @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift") var extensionSuffix: String? } @@ -40,29 +43,29 @@ struct ColorsToolOptions: ParsableArguments { // MARK: - Computed var extension ColorsToolOptions { - + // MARK: - SwiftUI - + var extensionFileName: String { - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { return "\(extensionName)+\(extensionSuffix).swift" } return "\(extensionName).swift" } - + var extensionFilePath: String { "\(extensionOutputPath)/\(extensionFileName)" } - + // MARK: - UIKit - + var extensionFileNameUIKit: String { - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { return "\(extensionNameUIKit)+\(extensionSuffix).swift" } return "\(extensionNameUIKit).swift" } - + var extensionFilePathUIKit: String { "\(extensionOutputPath)/\(extensionFileNameUIKit)" } diff --git a/Sources/ResgenSwift/Colors/Generator/ColorExtensionGenerator.swift b/Sources/ResgenSwift/Colors/Generator/ColorExtensionGenerator.swift index f974b05..2daefac 100644 --- a/Sources/ResgenSwift/Colors/Generator/ColorExtensionGenerator.swift +++ b/Sources/ResgenSwift/Colors/Generator/ColorExtensionGenerator.swift @@ -1,6 +1,6 @@ // // ColorExtensionGenerator.swift -// +// // // Created by Thibaut Schmitt on 20/12/2021. // @@ -9,38 +9,44 @@ import Foundation import ToolCore struct ColorExtensionGenerator { - + let colors: [ParsedColor] let extensionClassname: String - + // MARK: - UIKit - - static func writeExtensionFile(colors: [ParsedColor], - staticVar: Bool, - extensionName: String, - extensionFilePath: String, - isSwiftUI: Bool) { + + static func writeExtensionFile( + colors: [ParsedColor], + staticVar: Bool, + extensionName: String, + extensionFilePath: String, + isSwiftUI: Bool + ) { // Create extension content - let extensionContent = Self.getExtensionContent(colors: colors, - staticVar: staticVar, - extensionName: extensionName, - isSwiftUI: isSwiftUI) - + let extensionContent = Self.getExtensionContent( + colors: colors, + staticVar: staticVar, + extensionName: extensionName, + isSwiftUI: isSwiftUI + ) + // Write content let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) do { try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) - } catch let error { + } catch { let error = ColorsToolError.writeExtension(extensionFilePath, error.localizedDescription) print(error.description) Colors.exit(withError: error) } } - - static func getExtensionContent(colors: [ParsedColor], - staticVar: Bool, - extensionName: String, - isSwiftUI: Bool) -> String { + + static func getExtensionContent( + colors: [ParsedColor], + staticVar: Bool, + extensionName: String, + isSwiftUI: Bool + ) -> String { [ Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI), Self.getProperties(for: colors, withStaticVar: staticVar, isSwiftUI: isSwiftUI), @@ -48,7 +54,7 @@ struct ColorExtensionGenerator { ] .joined(separator: "\n") } - + private static func getHeader(extensionClassname: String, isSwiftUI: Bool) -> String { """ // Generated by ResgenSwift.\(Colors.toolName) \(ResgenSwiftVersion) @@ -58,17 +64,19 @@ struct ColorExtensionGenerator { extension \(extensionClassname) {\n """ } - + private static func getFooter() -> String { """ } - + """ } - - private static func getProperties(for colors: [ParsedColor], - withStaticVar staticVar: Bool, - isSwiftUI: Bool) -> String { + + private static func getProperties( + for colors: [ParsedColor], + withStaticVar staticVar: Bool, + isSwiftUI: Bool + ) -> String { colors.map { $0.getColorProperty(isStatic: staticVar, isSwiftUI: isSwiftUI) } diff --git a/Sources/ResgenSwift/Colors/Generator/ColorXcassetHelper.swift b/Sources/ResgenSwift/Colors/Generator/ColorXcassetHelper.swift index 7824264..0daff91 100644 --- a/Sources/ResgenSwift/Colors/Generator/ColorXcassetHelper.swift +++ b/Sources/ResgenSwift/Colors/Generator/ColorXcassetHelper.swift @@ -1,6 +1,6 @@ // // ColorXcassetHelper.swift -// +// // // Created by Thibaut Schmitt on 20/12/2021. // @@ -8,37 +8,39 @@ import Foundation import ToolCore -struct ColorXcassetHelper { - +enum 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) + try fileManager.createDirectory( + atPath: colorSetPath, + withIntermediateDirectories: true + ) } catch { let error = ColorsToolError.createAssetFolder(colorSetPath) print(error.description) 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 { + } catch { let error = ColorsToolError.writeAsset(error.localizedDescription) print(error.description) Colors.exit(withError: error) diff --git a/Sources/ResgenSwift/Colors/Model/ColorStyle.swift b/Sources/ResgenSwift/Colors/Model/ColorStyle.swift index 78948aa..bff63bf 100644 --- a/Sources/ResgenSwift/Colors/Model/ColorStyle.swift +++ b/Sources/ResgenSwift/Colors/Model/ColorStyle.swift @@ -5,13 +5,14 @@ // Created by Thibaut Schmitt on 29/08/2022. // -import Foundation import ArgumentParser +import Foundation enum ColorStyle: String, Decodable, ExpressibleByArgument { + case light case all - + static var allValueStrings: [String] { [ Self.light.rawValue, diff --git a/Sources/ResgenSwift/Colors/Model/ParsedColor.swift b/Sources/ResgenSwift/Colors/Model/ParsedColor.swift index 98c7a0b..918f936 100644 --- a/Sources/ResgenSwift/Colors/Model/ParsedColor.swift +++ b/Sources/ResgenSwift/Colors/Model/ParsedColor.swift @@ -8,28 +8,29 @@ 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.description) Colors.exit(withError: error) } - + return """ { "colors": [ @@ -71,9 +72,9 @@ struct ParsedColor { } """ } - + // MARK: - UIKit - + func getColorProperty(isStatic: Bool, isSwiftUI: Bool) -> String { if isSwiftUI { return """ diff --git a/Sources/ResgenSwift/Colors/Parser/ColorFileParser.swift b/Sources/ResgenSwift/Colors/Parser/ColorFileParser.swift index 1db14c2..297f6aa 100644 --- a/Sources/ResgenSwift/Colors/Parser/ColorFileParser.swift +++ b/Sources/ResgenSwift/Colors/Parser/ColorFileParser.swift @@ -7,44 +7,45 @@ import Foundation -class ColorFileParser { +enum ColorFileParser { + static func parse(_ inputFile: String, colorStyle: ColorStyle) -> [ParsedColor] { // Get content of input file - let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8) + let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8) // swiftlint:disable:this force_try 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" + .compactMap { _, colorLine in // swiftlint:disable:this unused_enumerated + // 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.description) 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])) diff --git a/Sources/ResgenSwift/Fonts/FontOptions.swift b/Sources/ResgenSwift/Fonts/FontOptions.swift index 24d2021..42a3218 100644 --- a/Sources/ResgenSwift/Fonts/FontOptions.swift +++ b/Sources/ResgenSwift/Fonts/FontOptions.swift @@ -5,31 +5,34 @@ // Created by Thibaut Schmitt on 17/01/2022. // -import Foundation import ArgumentParser +import Foundation + +// swiftlint:disable no_grouping_extension 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 Font extension.") var extensionName: String = Fonts.defaultExtensionName - + @Option(help: "Extension name. If not specified, it will generate an UIFont extension.") var extensionNameUIKit: String = Fonts.defaultExtensionNameUIKit - + @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+FontsMyApp.swift") var extensionSuffix: String = "" - + @Option(name: .customLong("info-plist-paths"), help: "Info.plist paths (array). Will be used to update UIAppFonts content") fileprivate var infoPlistPathsRaw: String = "" } @@ -37,29 +40,29 @@ struct FontsOptions: ParsableArguments { // MARK: - Computed var extension FontsOptions { - + // MARK: - SwiftUI - + var extensionFileName: String { if extensionSuffix.isEmpty == false { return "\(extensionName)+\(extensionSuffix).swift" } return "\(extensionName).swift" } - + var extensionFilePath: String { "\(extensionOutputPath)/\(extensionFileName)" } - + // MARK: - UIKit - + var extensionFileNameUIKit: String { if extensionSuffix.isEmpty == false { return "\(extensionNameUIKit)+\(extensionSuffix).swift" } return "\(extensionNameUIKit).swift" } - + var extensionFilePathUIKit: String { "\(extensionOutputPath)/\(extensionFileNameUIKit)" } diff --git a/Sources/ResgenSwift/Fonts/Fonts.swift b/Sources/ResgenSwift/Fonts/Fonts.swift index e46a01a..9e4bbca 100644 --- a/Sources/ResgenSwift/Fonts/Fonts.swift +++ b/Sources/ResgenSwift/Fonts/Fonts.swift @@ -1,47 +1,47 @@ // // Fonts.swift -// +// // // Created by Thibaut Schmitt on 13/12/2021. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore 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 = "Font" static let defaultExtensionNameUIKit = "UIFont" - + // MARK: - Command Options - + @OptionGroup var options: FontsOptions - + // MARK: - Run - - public func run() throws { + + 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() @@ -51,7 +51,7 @@ struct Fonts: ParsableCommand { for: fontsToGenerate, inputFolder: inputFolder ) - + // Generate extension FontExtensionGenerator.writeExtensionFile( fontsNames: fontsNames, @@ -60,7 +60,7 @@ struct Fonts: ParsableCommand { extensionFilePath: options.extensionFilePath, isSwiftUI: true ) - + FontExtensionGenerator.writeExtensionFile( fontsNames: fontsNames, staticVar: options.staticMembers, @@ -68,40 +68,42 @@ struct Fonts: ParsableCommand { extensionFilePath: options.extensionFilePathUIKit, isSwiftUI: false ) - + print("Info.plist has been updated with:") print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames, infoPlistPaths: options.infoPlistPaths))") - + 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.description) - Fonts.exit(withError: error) + Self.exit(withError: error) } - + // Extension for UIKit and SwiftUI should have different name guard options.extensionName != options.extensionNameUIKit else { let error = FontsToolError.extensionNamesCollision(options.extensionName) print(error.description) - Fonts.exit(withError: error) + Self.exit(withError: error) } - + // Check if needed to regenerate - guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, - inputFilePath: options.inputFile, - extensionFilePath: options.extensionFilePath) else { + 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 } } diff --git a/Sources/ResgenSwift/Fonts/FontsToolError.swift b/Sources/ResgenSwift/Fonts/FontsToolError.swift index d553cd3..5e851e1 100644 --- a/Sources/ResgenSwift/Fonts/FontsToolError.swift +++ b/Sources/ResgenSwift/Fonts/FontsToolError.swift @@ -8,27 +8,28 @@ import Foundation enum FontsToolError: Error { + case extensionNamesCollision(String) case fcScan(String, Int32, String?) case inputFolderNotFound(String) case fileNotExists(String) case writeExtension(String, String) - + var description: String { switch self { case .extensionNamesCollision(let extensionName): return "error: [\(Fonts.toolName)] Error on extension names, extension name and SwiftUI extension name should be different (\(extensionName) is used on both)" - - case .fcScan(let path, let code, let output): + + case let .fcScan(path, code, 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): + + case let .writeExtension(filename, info): return "error: [\(Fonts.toolName)] An error occured while writing extension in \(filename): \(info)" } } diff --git a/Sources/ResgenSwift/Fonts/FontsToolHelper.swift b/Sources/ResgenSwift/Fonts/FontsToolHelper.swift index 017278d..26bd663 100644 --- a/Sources/ResgenSwift/Fonts/FontsToolHelper.swift +++ b/Sources/ResgenSwift/Fonts/FontsToolHelper.swift @@ -1,6 +1,6 @@ // // FontsToolHelper.swift -// +// // // Created by Thibaut Schmitt on 13/12/2021. // @@ -8,32 +8,32 @@ import Foundation import ToolCore -class FontsToolHelper { - +enum 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() @@ -42,26 +42,26 @@ class FontsToolHelper { print(error.description) Fonts.exit(withError: error) } - - let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: inputFolder)! - + + let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: inputFolder)! // swiftlint:disable:this force_unwrapping + // Filters font files - let fontsFileNames: [String] = (enumerator.allObjects as! [String]) + let fontsFileNames: [String] = (enumerator.allObjects as! [String]) // swiftlint:disable:this force_cast .filter { if $0.hasSuffix(".ttf") || $0.hasSuffix(".otf") { return true } return false } - + return fontsFileNames } - + private static func getFontName(atPath path: String) -> FontName { - //print("fc-scan --format %{postscriptname} \(path)") + // print("fc-scan --format %{postscriptname} \(path)") // Get real font name let task = Shell.shell(["fc-scan", "--format", "%{postscriptname}", path]) - + guard let postscriptName = task.output, task.terminationStatus == 0 else { let error = FontsToolError.fcScan(path, task.terminationStatus, task.output) print(error.description) diff --git a/Sources/ResgenSwift/Fonts/Generator/FontPlistGenerator.swift b/Sources/ResgenSwift/Fonts/Generator/FontPlistGenerator.swift index a9e5912..2ce8722 100644 --- a/Sources/ResgenSwift/Fonts/Generator/FontPlistGenerator.swift +++ b/Sources/ResgenSwift/Fonts/Generator/FontPlistGenerator.swift @@ -8,27 +8,34 @@ import Foundation import ToolCore -class FontPlistGenerator { +enum FontPlistGenerator { + static func generatePlistUIAppsFontContent(for fonts: [FontName], infoPlistPaths: [String]) -> String { let fontsToAddToPlist = fonts .compactMap { $0 } - + // Update each plist infoPlistPaths.forEach { infoPlist in // Remove UIAppFonts value - Shell.shell(launchPath: "/usr/libexec/PlistBuddy", - ["-c", "delete :UIAppFonts", infoPlist]) - + Shell.shell( + launchPath: "/usr/libexec/PlistBuddy", + ["-c", "delete :UIAppFonts", infoPlist] + ) + // Add UIAppFonts empty array debugPrint("Will PlistBuddy -c add :UIAppFonts array \(infoPlist)") - Shell.shell(launchPath: "/usr/libexec/PlistBuddy", - ["-c", "add :UIAppFonts array", infoPlist]) + Shell.shell( + launchPath: "/usr/libexec/PlistBuddy", + ["-c", "add :UIAppFonts array", infoPlist] + ) // Fill array with fonts fontsToAddToPlist .forEach { fontName in - Shell.shell(launchPath: "/usr/libexec/PlistBuddy", - ["-c", "add :UIAppFonts: string \(fontName.filename).\(fontName.fileExtension)", infoPlist]) + Shell.shell( + launchPath: "/usr/libexec/PlistBuddy", + ["-c", "add :UIAppFonts: string \(fontName.filename).\(fontName.fileExtension)", infoPlist] + ) } } @@ -38,7 +45,7 @@ class FontPlistGenerator { plistData += "\t\t\(fontName.filename).\(fontName.fileExtension)\n" } plistData += "\t" - + return plistData } } diff --git a/Sources/ResgenSwift/Fonts/Generator/FontToolContentGenerator.swift b/Sources/ResgenSwift/Fonts/Generator/FontToolContentGenerator.swift index cb52ca1..b0cd2bf 100644 --- a/Sources/ResgenSwift/Fonts/Generator/FontToolContentGenerator.swift +++ b/Sources/ResgenSwift/Fonts/Generator/FontToolContentGenerator.swift @@ -8,45 +8,51 @@ import Foundation import ToolCore -class FontExtensionGenerator { +enum FontExtensionGenerator { private static func getFontNameEnum(fontsNames: [FontName]) -> String { var enumDefinition = " enum FontName: String {\n" - + fontsNames.forEach { enumDefinition += " case \($0.fontNameSanitize) = \"\($0.postscriptName)\"\n" } enumDefinition += " }\n" - + return enumDefinition } - - static func writeExtensionFile(fontsNames: [FontName], - staticVar: Bool, - extensionName: String, - extensionFilePath: String, - isSwiftUI: Bool) { + + static func writeExtensionFile( + fontsNames: [FontName], + staticVar: Bool, + extensionName: String, + extensionFilePath: String, + isSwiftUI: Bool + ) { // Create extension content - let extensionContent = Self.getExtensionContent(fontsNames: fontsNames, - staticVar: staticVar, - extensionName: extensionName, - isSwiftUI: isSwiftUI) - + let extensionContent = Self.getExtensionContent( + fontsNames: fontsNames, + staticVar: staticVar, + extensionName: extensionName, + isSwiftUI: isSwiftUI + ) + // Write content let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) do { try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) - } catch let error { + } catch { let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription) print(error.description) Fonts.exit(withError: error) } } - - static func getExtensionContent(fontsNames: [FontName], - staticVar: Bool, - extensionName: String, - isSwiftUI: Bool) -> String { + + static func getExtensionContent( + fontsNames: [FontName], + staticVar: Bool, + extensionName: String, + isSwiftUI: Bool + ) -> String { [ Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI), Self.getFontNameEnum(fontsNames: fontsNames), @@ -55,34 +61,34 @@ class FontExtensionGenerator { ] .joined(separator: "\n") } - + private static func getHeader(extensionClassname: String, isSwiftUI: Bool) -> String { """ // Generated by ResgenSwift.\(Fonts.toolName) \(ResgenSwiftVersion) - + import \(isSwiftUI ? "SwiftUI" : "UIKit") - + extension \(extensionClassname) {\n """ } - + private static func getFontMethods(fontsNames: [FontName], staticVar: Bool, isSwiftUI: Bool) -> String { let pragma = " // MARK: - Getter" - + var propertiesOrMethods: [String] = fontsNames .unique() .map { $0.getProperty(isStatic: staticVar, isSwiftUI: isSwiftUI) } - + propertiesOrMethods.insert(pragma, at: 0) return propertiesOrMethods.joined(separator: "\n\n") } - + private static func getFooter() -> String { """ } - + """ } } diff --git a/Sources/ResgenSwift/Fonts/Model/FontName.swift b/Sources/ResgenSwift/Fonts/Model/FontName.swift index 6feb518..04f317c 100644 --- a/Sources/ResgenSwift/Fonts/Model/FontName.swift +++ b/Sources/ResgenSwift/Fonts/Model/FontName.swift @@ -7,7 +7,7 @@ import Foundation -//typealias FontName = String +// swiftlint:disable no_grouping_extension struct FontName: Hashable { @@ -17,10 +17,11 @@ struct FontName: Hashable { } extension FontName { + var fontNameSanitize: String { postscriptName.removeCharacters(from: "[]+-_") } - + func getProperty(isStatic: Bool, isSwiftUI: Bool) -> String { if isSwiftUI { if isStatic { diff --git a/Sources/ResgenSwift/Fonts/Parser/FontFileParser.swift b/Sources/ResgenSwift/Fonts/Parser/FontFileParser.swift index 61bb210..13b45ff 100644 --- a/Sources/ResgenSwift/Fonts/Parser/FontFileParser.swift +++ b/Sources/ResgenSwift/Fonts/Parser/FontFileParser.swift @@ -7,10 +7,13 @@ import Foundation -class FontFileParser { +enum FontFileParser { + static func parse(_ inputFile: String) -> [String] { - let inputFileContent = try! String(contentsOfFile: inputFile, - encoding: .utf8) + let inputFileContent = try! String( // swiftlint:disable:this force_try + contentsOfFile: inputFile, + encoding: .utf8 + ) return inputFileContent.components(separatedBy: CharacterSet.newlines) } } diff --git a/Sources/ResgenSwift/Generate/Extensions/StringExtensions.swift b/Sources/ResgenSwift/Generate/Extensions/StringExtensions.swift index faa5acd..361b623 100644 --- a/Sources/ResgenSwift/Generate/Extensions/StringExtensions.swift +++ b/Sources/ResgenSwift/Generate/Extensions/StringExtensions.swift @@ -8,7 +8,7 @@ import Foundation extension String { - + func prependIfRelativePath(_ prependPath: String) -> String { // If path starts with "/", it's an absolute path if self.hasPrefix("/") { diff --git a/Sources/ResgenSwift/Generate/Generate.swift b/Sources/ResgenSwift/Generate/Generate.swift index a051046..9ff7967 100644 --- a/Sources/ResgenSwift/Generate/Generate.swift +++ b/Sources/ResgenSwift/Generate/Generate.swift @@ -5,32 +5,32 @@ // Created by Thibaut Schmitt on 30/08/2022. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore 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 { + + func run() throws { print("[\(Self.toolName)] Starting Resgen with configuration: \(options.configurationFile)") - + // Parse let configuration = ConfigurationFileParser.parse(options.configurationFile) print("Found configurations :") @@ -41,18 +41,22 @@ struct Generate: ParsableCommand { print(" - \(configuration.strings.count) strings configuration(s)") print(" - \(configuration.tags.count) tags configuration(s)") print() - + if let architecture = configuration.architecture { - ArchitectureGenerator.writeArchitecture(architecture, - projectDirectory: options.projectDirectory) + ArchitectureGenerator.writeArchitecture( + architecture, + projectDirectory: options.projectDirectory + ) } - + // Execute commands configuration.runnableConfigurations .forEach { let begin = Date() - $0.run(projectDirectory: options.projectDirectory, - force: options.forceGeneration) + $0.run( + projectDirectory: options.projectDirectory, + force: options.forceGeneration + ) print("Took: \(Date().timeIntervalSince(begin))s\n") } diff --git a/Sources/ResgenSwift/Generate/GenerateError.swift b/Sources/ResgenSwift/Generate/GenerateError.swift index 03c3c16..f5791b9 100644 --- a/Sources/ResgenSwift/Generate/GenerateError.swift +++ b/Sources/ResgenSwift/Generate/GenerateError.swift @@ -8,26 +8,26 @@ import Foundation enum GenerateError: Error { + case fileNotExists(String) case invalidConfigurationFile(String, String) case commandError([String], String) case writeFile(String, String) - + var description: String { switch self { case .fileNotExists(let filename): return "error: [\(Generate.toolName)] File \(filename) does not exists" - - case .invalidConfigurationFile(let filename, let underneathErrorDescription): + + case let .invalidConfigurationFile(filename, underneathErrorDescription): return "error: [\(Generate.toolName)] File \(filename) is not a valid configuration file. Underneath error: \(underneathErrorDescription)" - - case .commandError(let command, let terminationStatus): + + case let .commandError(command, 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)" - - case .writeFile(let filename, let info): + + case let .writeFile(filename, info): return "error: [\(Generate.toolName)] An error occured while writing file in \(filename): \(info)" } } diff --git a/Sources/ResgenSwift/Generate/GenerateOptions.swift b/Sources/ResgenSwift/Generate/GenerateOptions.swift index da3c263..e6f044d 100644 --- a/Sources/ResgenSwift/Generate/GenerateOptions.swift +++ b/Sources/ResgenSwift/Generate/GenerateOptions.swift @@ -5,16 +5,17 @@ // Created by Thibaut Schmitt on 30/08/2022. // -import Foundation import ArgumentParser +import Foundation 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 == "/" { diff --git a/Sources/ResgenSwift/Generate/Generator/ArchitectureGenerator.swift b/Sources/ResgenSwift/Generate/Generator/ArchitectureGenerator.swift index 7670780..5acd5e4 100644 --- a/Sources/ResgenSwift/Generate/Generator/ArchitectureGenerator.swift +++ b/Sources/ResgenSwift/Generate/Generator/ArchitectureGenerator.swift @@ -5,10 +5,11 @@ // Created by Thibaut Schmitt on 18/11/2022. // -import ToolCore import Foundation +import ToolCore + +enum ArchitectureGenerator { -struct ArchitectureGenerator { static func writeArchitecture(_ architecture: ConfigurationArchitecture, projectDirectory: String) { // Create extension content var architectureContent = [ @@ -16,21 +17,21 @@ struct ArchitectureGenerator { architecture.getClass() ] .joined(separator: "\n\n") - + architectureContent += "\n" - + let filename = "\(architecture.classname).swift" guard let filePath = architecture.path?.prependIfRelativePath(projectDirectory) else { let error = GenerateError.writeFile(filename, "Path of file is not defined.") print(error.description) Generate.exit(withError: error) } - + // Write content let architectureFilePathURL = URL(fileURLWithPath: "\(filePath)/\(filename)") do { try architectureContent.write(to: architectureFilePathURL, atomically: false, encoding: .utf8) - } catch let error { + } catch { let error = GenerateError.writeFile(filename, error.localizedDescription) print(error.description) Generate.exit(withError: error) diff --git a/Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift b/Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift index e92f9d6..5a84654 100644 --- a/Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift +++ b/Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift @@ -8,6 +8,7 @@ import Foundation struct ConfigurationFile: Codable, CustomDebugStringConvertible { + var architecture: ConfigurationArchitecture? var analytics: [AnalyticsConfiguration] var colors: [ColorsConfiguration] @@ -15,12 +16,12 @@ struct ConfigurationFile: Codable, CustomDebugStringConvertible { var images: [ImagesConfiguration] var strings: [StringsConfiguration] var tags: [TagsConfiguration] - + var runnableConfigurations: [Runnable] { let runnables: [[Runnable]] = [analytics, colors, fonts, images, strings, tags] return Array(runnables.joined()) } - + var debugDescription: String { """ \(analytics) @@ -44,20 +45,21 @@ struct ConfigurationFile: Codable, CustomDebugStringConvertible { } struct ConfigurationArchitecture: Codable { + let property: String let classname: String let path: String? - let children: [ConfigurationArchitecture]? - + let children: [Self]? + func getProperty(isStatic: Bool) -> String { " \(isStatic ? "static " : "")let \(property) = \(classname)()" } - + func getClass(generateStaticProperty: Bool = true) -> String { guard children?.isEmpty == false else { return "final class \(classname): Sendable {}" } - + let classDefinition = [ "class \(classname) {", children?.map { $0.getProperty(isStatic: generateStaticProperty) }.joined(separator: "\n"), @@ -65,12 +67,12 @@ struct ConfigurationArchitecture: Codable { ] .compactMap { $0 } .joined(separator: "\n") - + return [classDefinition, "", getSubclass()] .compactMap { $0 } .joined(separator: "\n") } - + func getSubclass() -> String? { guard let children else { return nil } return children.compactMap { arch in @@ -81,26 +83,29 @@ struct ConfigurationArchitecture: Codable { } struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible { + let inputFile: String let target: String let extensionOutputPath: String let extensionName: String? let extensionSuffix: String? private let staticMembers: Bool? - + var staticMembersOptions: Bool { - if let staticMembers = staticMembers { + if let staticMembers { return staticMembers } return false } - - internal init(inputFile: String, - target: String, - extensionOutputPath: String, - extensionName: String?, - extensionSuffix: String?, - staticMembers: Bool?) { + + internal init( + inputFile: String, + target: String, + extensionOutputPath: String, + extensionName: String?, + extensionSuffix: String?, + staticMembers: Bool? + ) { self.inputFile = inputFile self.target = target self.extensionOutputPath = extensionOutputPath @@ -108,7 +113,7 @@ struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible { self.extensionSuffix = extensionSuffix self.staticMembers = staticMembers } - + var debugDescription: String { """ Analytics configuration: @@ -122,6 +127,7 @@ struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible { } struct ColorsConfiguration: Codable, CustomDebugStringConvertible { + let inputFile: String let style: String let xcassetsPath: String @@ -130,22 +136,24 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible { let extensionNameUIKit: String? let extensionSuffix: String? private let staticMembers: Bool? - + var staticMembersOptions: Bool { - if let staticMembers = staticMembers { + if let staticMembers { return staticMembers } return false } - - internal init(inputFile: String, - style: String, - xcassetsPath: String, - extensionOutputPath: String, - extensionName: String?, - extensionNameUIKit: String?, - extensionSuffix: String?, - staticMembers: Bool?) { + + internal init( + inputFile: String, + style: String, + xcassetsPath: String, + extensionOutputPath: String, + extensionName: String?, + extensionNameUIKit: String?, + extensionSuffix: String?, + staticMembers: Bool? + ) { self.inputFile = inputFile self.style = style self.xcassetsPath = xcassetsPath @@ -155,7 +163,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible { self.extensionSuffix = extensionSuffix self.staticMembers = staticMembers } - + var debugDescription: String { """ Colors configuration: @@ -171,6 +179,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible { } struct FontsConfiguration: Codable, CustomDebugStringConvertible { + let inputFile: String let extensionOutputPath: String let extensionName: String? @@ -178,21 +187,23 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible { let extensionSuffix: String? let infoPlistPaths: String? private let staticMembers: Bool? - + var staticMembersOptions: Bool { - if let staticMembers = staticMembers { + if let staticMembers { return staticMembers } return false } - - internal init(inputFile: String, - extensionOutputPath: String, - extensionName: String?, - extensionNameUIKit: String?, - extensionSuffix: String?, - infoPlistPaths: String?, - staticMembers: Bool?) { + + internal init( + inputFile: String, + extensionOutputPath: String, + extensionName: String?, + extensionNameUIKit: String?, + extensionSuffix: String?, + infoPlistPaths: String?, + staticMembers: Bool? + ) { self.inputFile = inputFile self.extensionOutputPath = extensionOutputPath self.extensionName = extensionName @@ -201,7 +212,7 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible { self.infoPlistPaths = infoPlistPaths self.staticMembers = staticMembers } - + var debugDescription: String { """ Fonts configuration: @@ -216,6 +227,7 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible { } struct ImagesConfiguration: Codable, CustomDebugStringConvertible { + let inputFile: String let xcassetsPath: String let extensionOutputPath: String @@ -223,21 +235,23 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible { let extensionNameUIKit: String? let extensionSuffix: String? private let staticMembers: Bool? - + var staticMembersOptions: Bool { - if let staticMembers = staticMembers { + if let staticMembers { return staticMembers } return false } - - internal init(inputFile: String, - xcassetsPath: String, - extensionOutputPath: String, - extensionName: String?, - extensionNameUIKit: String?, - extensionSuffix: String?, - staticMembers: Bool?) { + + internal init( + inputFile: String, + xcassetsPath: String, + extensionOutputPath: String, + extensionName: String?, + extensionNameUIKit: String?, + extensionSuffix: String?, + staticMembers: Bool? + ) { self.inputFile = inputFile self.xcassetsPath = xcassetsPath self.extensionOutputPath = extensionOutputPath @@ -246,7 +260,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible { self.extensionSuffix = extensionSuffix self.staticMembers = staticMembers } - + var debugDescription: String { """ Images configuration: @@ -261,6 +275,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible { } struct StringsConfiguration: Codable, CustomDebugStringConvertible { + let inputFile: String let outputPath: String let langs: String @@ -272,28 +287,30 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { private let xcStrings: Bool? var staticMembersOptions: Bool { - if let staticMembers = staticMembers { + if let staticMembers { return staticMembers } return false } var xcStringsOptions: Bool { - if let xcStrings = xcStrings { + if let xcStrings { return xcStrings } return false } - internal init(inputFile: String, - outputPath: String, - langs: String, - defaultLang: String, - extensionOutputPath: String, - extensionName: String?, - extensionSuffix: String?, - staticMembers: Bool?, - xcStrings: Bool?) { + internal init( + inputFile: String, + outputPath: String, + langs: String, + defaultLang: String, + extensionOutputPath: String, + extensionName: String?, + extensionSuffix: String?, + staticMembers: Bool?, + xcStrings: Bool? + ) { self.inputFile = inputFile self.outputPath = outputPath self.langs = langs @@ -304,7 +321,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { self.staticMembers = staticMembers self.xcStrings = xcStrings } - + var debugDescription: String { """ Strings configuration: @@ -320,26 +337,29 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { } 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 { + if let staticMembers { return staticMembers } return false } - - internal init(inputFile: String, - lang: String, - extensionOutputPath: String, - extensionName: String?, - extensionSuffix: String?, - staticMembers: Bool?) { + + internal init( + inputFile: String, + lang: String, + extensionOutputPath: String, + extensionName: String?, + extensionSuffix: String?, + staticMembers: Bool? + ) { self.inputFile = inputFile self.lang = lang self.extensionOutputPath = extensionOutputPath @@ -347,7 +367,7 @@ struct TagsConfiguration: Codable, CustomDebugStringConvertible { self.extensionSuffix = extensionSuffix self.staticMembers = staticMembers } - + var debugDescription: String { """ Tags configuration: diff --git a/Sources/ResgenSwift/Generate/Parser/ConfigurationFileParser.swift b/Sources/ResgenSwift/Generate/Parser/ConfigurationFileParser.swift index 4b61d31..c879a97 100644 --- a/Sources/ResgenSwift/Generate/Parser/ConfigurationFileParser.swift +++ b/Sources/ResgenSwift/Generate/Parser/ConfigurationFileParser.swift @@ -8,14 +8,15 @@ import Foundation import Yams -class ConfigurationFileParser { +enum ConfigurationFileParser { + static func parse(_ configurationFile: String) -> ConfigurationFile { guard let data = FileManager().contents(atPath: configurationFile) else { let error = GenerateError.fileNotExists(configurationFile) print(error.description) Generate.exit(withError: error) } - + do { return try YAMLDecoder().decode(ConfigurationFile.self, from: data) } catch { diff --git a/Sources/ResgenSwift/Generate/Runnable/AnalyticsConfiguration+Runnable.swift b/Sources/ResgenSwift/Generate/Runnable/AnalyticsConfiguration+Runnable.swift index 1689a24..ff492dc 100644 --- a/Sources/ResgenSwift/Generate/Runnable/AnalyticsConfiguration+Runnable.swift +++ b/Sources/ResgenSwift/Generate/Runnable/AnalyticsConfiguration+Runnable.swift @@ -8,13 +8,14 @@ import Foundation extension AnalyticsConfiguration: Runnable { + func run(projectDirectory: String, force: Bool) { var args = [String]() - + if force { args += ["-f"] } - + args += [ inputFile.prependIfRelativePath(projectDirectory), "--target", @@ -24,20 +25,20 @@ extension AnalyticsConfiguration: Runnable { "--static-members", "\(staticMembersOptions)" ] - - if let extensionName = extensionName { + + if let extensionName { args += [ "--extension-name", extensionName ] } - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { args += [ "--extension-suffix", extensionSuffix ] } - + Analytics.main(args) } } diff --git a/Sources/ResgenSwift/Generate/Runnable/ColorsConfiguration+Runnable.swift b/Sources/ResgenSwift/Generate/Runnable/ColorsConfiguration+Runnable.swift index 5c8f735..6f8bc11 100644 --- a/Sources/ResgenSwift/Generate/Runnable/ColorsConfiguration+Runnable.swift +++ b/Sources/ResgenSwift/Generate/Runnable/ColorsConfiguration+Runnable.swift @@ -8,18 +8,19 @@ import Foundation extension ColorsConfiguration: Runnable { + func run(projectDirectory: String, force: Bool) { let args = getArguments(projectDirectory: projectDirectory, force: force) Colors.main(args) } - + func getArguments(projectDirectory: String, force: Bool) -> [String] { var args = [String]() - + if force { args += ["-f"] } - + args += [ inputFile.prependIfRelativePath(projectDirectory), "--style", @@ -31,26 +32,26 @@ extension ColorsConfiguration: Runnable { "--static-members", "\(staticMembersOptions)" ] - - if let extensionName = extensionName { + + if let extensionName { args += [ "--extension-name", extensionName ] } - if let extensionNameUIKit = extensionNameUIKit { + if let extensionNameUIKit { args += [ "--extension-name-ui-kit", extensionNameUIKit ] } - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { args += [ "--extension-suffix", extensionSuffix ] } - + return args } } diff --git a/Sources/ResgenSwift/Generate/Runnable/FontsConfiguration+Runnable.swift b/Sources/ResgenSwift/Generate/Runnable/FontsConfiguration+Runnable.swift index a314c8e..6a9db9f 100644 --- a/Sources/ResgenSwift/Generate/Runnable/FontsConfiguration+Runnable.swift +++ b/Sources/ResgenSwift/Generate/Runnable/FontsConfiguration+Runnable.swift @@ -8,18 +8,19 @@ import Foundation extension FontsConfiguration: Runnable { + func run(projectDirectory: String, force: Bool) { let args = getArguments(projectDirectory: projectDirectory, force: force) Fonts.main(args) } - + func getArguments(projectDirectory: String, force: Bool) -> [String] { var args = [String]() - + if force { args += ["-f"] } - + args += [ inputFile.prependIfRelativePath(projectDirectory), "--extension-output-path", @@ -27,39 +28,39 @@ extension FontsConfiguration: Runnable { "--static-members", "\(staticMembersOptions)" ] - - if let extensionName = extensionName { + + if let extensionName { args += [ "--extension-name", extensionName ] } - if let extensionNameUIKit = extensionNameUIKit { + if let extensionNameUIKit { args += [ "--extension-name-ui-kit", extensionNameUIKit ] } - - if let extensionSuffix = extensionSuffix { + + if let extensionSuffix { args += [ "--extension-suffix", extensionSuffix ] } - - if let infoPlistPaths = infoPlistPaths { + + if let infoPlistPaths { let adjustedPlistPaths = infoPlistPaths .split(separator: " ") .map { String($0).prependIfRelativePath(projectDirectory) } .joined(separator: " ") - + args += [ "--info-plist-paths", adjustedPlistPaths ] } - + return args } } diff --git a/Sources/ResgenSwift/Generate/Runnable/ImagesConfiguration+Runnable.swift b/Sources/ResgenSwift/Generate/Runnable/ImagesConfiguration+Runnable.swift index 42b5ef8..91d9a43 100644 --- a/Sources/ResgenSwift/Generate/Runnable/ImagesConfiguration+Runnable.swift +++ b/Sources/ResgenSwift/Generate/Runnable/ImagesConfiguration+Runnable.swift @@ -1,6 +1,6 @@ // // ImagesConfiguration+Runnable.swift -// +// // // Created by Thibaut Schmitt on 30/08/2022. // @@ -8,18 +8,19 @@ import Foundation extension ImagesConfiguration: Runnable { + func run(projectDirectory: String, force: Bool) { let args = getArguments(projectDirectory: projectDirectory, force: force) Images.main(args) } - + func getArguments(projectDirectory: String, force: Bool) -> [String] { var args = [String]() - + if force { args += ["-f"] // Images has a -f and -F options } - + args += [ inputFile.prependIfRelativePath(projectDirectory), "--xcassets-path", @@ -29,26 +30,28 @@ extension ImagesConfiguration: Runnable { "--static-members", "\(staticMembersOptions)" ] - - if let extensionName = extensionName { + + if let extensionName { args += [ "--extension-name", extensionName ] } - if let extensionNameUIKit = extensionNameUIKit { + + if let extensionNameUIKit { args += [ "--extension-name-ui-kit", extensionNameUIKit ] } - if let extensionSuffix = extensionSuffix { + + if let extensionSuffix { args += [ "--extension-suffix", extensionSuffix ] } - + return args } } diff --git a/Sources/ResgenSwift/Generate/Runnable/Runnable.swift b/Sources/ResgenSwift/Generate/Runnable/Runnable.swift index 532b52f..160b137 100644 --- a/Sources/ResgenSwift/Generate/Runnable/Runnable.swift +++ b/Sources/ResgenSwift/Generate/Runnable/Runnable.swift @@ -8,5 +8,6 @@ import Foundation protocol Runnable { + func run(projectDirectory: String, force: Bool) } diff --git a/Sources/ResgenSwift/Generate/Runnable/StringsConfiguration+Runnable.swift b/Sources/ResgenSwift/Generate/Runnable/StringsConfiguration+Runnable.swift index 7da6a37..5e094e4 100644 --- a/Sources/ResgenSwift/Generate/Runnable/StringsConfiguration+Runnable.swift +++ b/Sources/ResgenSwift/Generate/Runnable/StringsConfiguration+Runnable.swift @@ -8,9 +8,10 @@ import Foundation extension StringsConfiguration: Runnable { + func run(projectDirectory: String, force: Bool) { var args = [String]() - + if force { args += ["-f"] } @@ -30,21 +31,21 @@ extension StringsConfiguration: Runnable { "--xc-strings", "\(xcStringsOptions)" ] - - if let extensionName = extensionName { + + if let extensionName { args += [ "--extension-name", extensionName ] } - - if let extensionSuffix = extensionSuffix { + + if let extensionSuffix { args += [ "--extension-suffix", extensionSuffix ] } - + Stringium.main(args) } } diff --git a/Sources/ResgenSwift/Generate/Runnable/TagsConfiguration+Runnable.swift b/Sources/ResgenSwift/Generate/Runnable/TagsConfiguration+Runnable.swift index 04c6648..2309ee8 100644 --- a/Sources/ResgenSwift/Generate/Runnable/TagsConfiguration+Runnable.swift +++ b/Sources/ResgenSwift/Generate/Runnable/TagsConfiguration+Runnable.swift @@ -8,13 +8,14 @@ import Foundation extension TagsConfiguration: Runnable { + func run(projectDirectory: String, force: Bool) { var args = [String]() - + if force { args += ["-f"] } - + args += [ inputFile.prependIfRelativePath(projectDirectory), "--lang", @@ -24,20 +25,20 @@ extension TagsConfiguration: Runnable { "--static-members", "\(staticMembersOptions)" ] - - if let extensionName = extensionName { + + if let extensionName { args += [ "--extension-name", extensionName ] } - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { args += [ "--extension-suffix", extensionSuffix ] } - + Tags.main(args) } } diff --git a/Sources/ResgenSwift/Images/Extensions/FileManagerExtensions.swift b/Sources/ResgenSwift/Images/Extensions/FileManagerExtensions.swift index bc4ccff..5b5bc5f 100644 --- a/Sources/ResgenSwift/Images/Extensions/FileManagerExtensions.swift +++ b/Sources/ResgenSwift/Images/Extensions/FileManagerExtensions.swift @@ -7,7 +7,10 @@ import Foundation +// swiftlint:disable force_unwrapping + 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 { @@ -15,7 +18,7 @@ extension FileManager { print(error.description) Images.exit(withError: error) } - + for case let fileURL as URL in enumerator { do { let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey]) @@ -30,7 +33,7 @@ extension FileManager { } 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 { @@ -38,7 +41,7 @@ extension FileManager { print(error.description) Images.exit(withError: error) } - + for case let fileURL as URL in enumerator { do { let fileAttributes = try fileURL.resourceValues(forKeys: [.isDirectoryKey]) diff --git a/Sources/ResgenSwift/Images/Generator/ImageExtensionGenerator.swift b/Sources/ResgenSwift/Images/Generator/ImageExtensionGenerator.swift index 47e760b..0676873 100644 --- a/Sources/ResgenSwift/Images/Generator/ImageExtensionGenerator.swift +++ b/Sources/ResgenSwift/Images/Generator/ImageExtensionGenerator.swift @@ -5,42 +5,48 @@ // Created by Thibaut Schmitt on 14/02/2022. // -import ToolCore import Foundation +import ToolCore + +enum ImageExtensionGenerator { -class ImageExtensionGenerator { - // MARK: - UIKit - - static func generateExtensionFile(images: [ParsedImage], - staticVar: Bool, - inputFilename: String, - extensionName: String, - extensionFilePath: String, - isSwiftUI: Bool) { + + static func generateExtensionFile( + images: [ParsedImage], + staticVar: Bool, + inputFilename: String, + extensionName: String, + extensionFilePath: String, + isSwiftUI: Bool + ) { // Create extension conten1t - let extensionContent = Self.getExtensionContent(images: images, - staticVar: staticVar, - extensionName: extensionName, - inputFilename: inputFilename, - isSwiftUI: isSwiftUI) - + let extensionContent = Self.getExtensionContent( + images: images, + staticVar: staticVar, + extensionName: extensionName, + inputFilename: inputFilename, + isSwiftUI: isSwiftUI + ) + // Write content let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) do { try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) - } catch let error { + } catch { let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription) print(error.description) Images.exit(withError: error) } } - - static func getExtensionContent(images: [ParsedImage], - staticVar: Bool, - extensionName: String, - inputFilename: String, - isSwiftUI: Bool) -> String { + + static func getExtensionContent( + images: [ParsedImage], + staticVar: Bool, + extensionName: String, + inputFilename: String, + isSwiftUI: Bool + ) -> String { [ Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName, isSwiftUI: isSwiftUI), Self.getProperties(images: images, staticVar: staticVar, isSwiftUI: isSwiftUI), @@ -48,30 +54,36 @@ class ImageExtensionGenerator { ] .joined(separator: "\n") } - - private static func getHeader(inputFilename: String, - extensionClassname: String, - isSwiftUI: Bool) -> String { + + private static func getHeader( + inputFilename: String, + extensionClassname: String, + isSwiftUI: Bool + ) -> String { """ // Generated by ResgenSwift.\(Images.toolName) \(ResgenSwiftVersion) // Images from \(inputFilename) - + import \(isSwiftUI ? "SwiftUI" : "UIKit") - + extension \(extensionClassname) { """ } - - private static func getProperties(images: [ParsedImage], staticVar: Bool, isSwiftUI: Bool) -> String { + + private static func getProperties( + images: [ParsedImage], + staticVar: Bool, + isSwiftUI: Bool + ) -> String { images .map { "\n\($0.getImageProperty(isStatic: staticVar, isSwiftUI: isSwiftUI))" } .joined(separator: "\n") } - + private static func getFooter() -> String { """ } - + """ } } diff --git a/Sources/ResgenSwift/Images/Generator/XcassetsGenerator.swift b/Sources/ResgenSwift/Images/Generator/XcassetsGenerator.swift index 82c512c..c15663c 100644 --- a/Sources/ResgenSwift/Images/Generator/XcassetsGenerator.swift +++ b/Sources/ResgenSwift/Images/Generator/XcassetsGenerator.swift @@ -1,6 +1,6 @@ // // XcassetsGenerator.swift -// +// // // Created by Thibaut Schmitt on 24/01/2022. // @@ -9,31 +9,34 @@ import Foundation import ToolCore enum OutputImageExtension: String { + case png case svg } class XcassetsGenerator { + // MARK: - Properties + 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 + imagesToGenerate.forEach { parsedImage in // swiftlint:disable:this closure_body_length // Get image path let imageData: (path: String, ext: String) = { for subfile in allSubFiles { @@ -54,14 +57,14 @@ class XcassetsGenerator { print(error.description) 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).\(OutputImageExtension.png.rawValue)" let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(OutputImageExtension.png.rawValue)" @@ -79,12 +82,14 @@ class XcassetsGenerator { print("\(parsedImage.name) -> Not regenerating") return } - + // Create imageset folder if fileManager.fileExists(atPath: imagesetPath) == false { do { - try fileManager.createDirectory(atPath: imagesetPath, - withIntermediateDirectories: true) + try fileManager.createDirectory( + atPath: imagesetPath, + withIntermediateDirectories: true + ) } catch { let error = ImagesError.createAssetFolder(imagesetPath) print(error.description) @@ -124,9 +129,7 @@ class XcassetsGenerator { Shell.shell(command1x) Shell.shell(command2x) Shell.shell(command3x) - } else { - let output = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.svg.rawValue)" let tempURL = URL(fileURLWithPath: output) @@ -143,47 +146,69 @@ class XcassetsGenerator { // 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]) + 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 guard let imagesetContentJson = parsedImage.generateContentJson(isVector: imageData.ext == "svg") else { return } let contentJsonFilePath = "\(imagesetPath)/Contents.json" - + let contentJsonFilePathURL = URL(fileURLWithPath: contentJsonFilePath) - try! imagesetContentJson.write(to: contentJsonFilePathURL, atomically: false, encoding: .utf8) - + try! imagesetContentJson.write( // swiftlint:disable:this force_try + 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 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)") + try! fileManager.removeItem(atPath: "\(xcassetsPath)/\(itemToRemove)") // swiftlint:disable:this force_try } 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") @@ -194,14 +219,14 @@ class XcassetsGenerator { command.append("\(height)") } } - + // MARK: - Helpers: bypass generation - + private func shouldGenerate(inputImagePath: String, xcassetImagePath: String, needToGenerateForSvg: Bool) -> Bool { if forceGeneration || needToGenerateForSvg { return true } - + return GeneratorChecker.isFile(inputImagePath, moreRecenThan: xcassetImagePath) } } diff --git a/Sources/ResgenSwift/Images/Images.swift b/Sources/ResgenSwift/Images/Images.swift index 4a028c4..792bbb5 100644 --- a/Sources/ResgenSwift/Images/Images.swift +++ b/Sources/ResgenSwift/Images/Images.swift @@ -1,123 +1,131 @@ // // Images.swift -// +// // // Created by Thibaut Schmitt on 24/01/2022. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore 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 = "Image" static let defaultExtensionNameUIKit = "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) - + 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, - isSwiftUI: true) - - ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate, - staticVar: options.staticMembers, - inputFilename: options.inputFilenameWithoutExt, - extensionName: options.extensionNameUIKit, - extensionFilePath: options.extensionFilePathUIKit, - isSwiftUI: false) - + ImageExtensionGenerator.generateExtensionFile( + images: imagesToGenerate, + staticVar: options.staticMembers, + inputFilename: options.inputFilenameWithoutExt, + extensionName: options.extensionName, + extensionFilePath: options.extensionFilePath, + isSwiftUI: true + ) + + ImageExtensionGenerator.generateExtensionFile( + images: imagesToGenerate, + staticVar: options.staticMembers, + inputFilename: options.inputFilenameWithoutExt, + extensionName: options.extensionNameUIKit, + extensionFilePath: options.extensionFilePathUIKit, + isSwiftUI: false + ) + 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.description) - Images.exit(withError: error) + Self.exit(withError: error) } // RSVG-Converter - _ = Images.getSvgConverterPath() + _ = Self.getSvgConverterPath() // Extension for UIKit and SwiftUI should have different name guard options.extensionName != options.extensionNameUIKit else { let error = ImagesError.extensionNamesCollision(options.extensionName) print(error.description) - Images.exit(withError: error) + Self.exit(withError: error) } - + // Check if needed to regenerate - guard GeneratorChecker.shouldGenerate(force: options.forceExecution, - inputFilePath: options.inputFile, - extensionFilePath: options.extensionFilePath) else { + 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) + return taskSvgConverter.output!.removeCharacters(from: CharacterSet.whitespacesAndNewlines) // swiftlint:disable:this force_unwrapping } - + let error = ImagesError.rsvgConvertNotFound print(error.description) - Images.exit(withError: error) + Self.exit(withError: error) } } diff --git a/Sources/ResgenSwift/Images/ImagesError.swift b/Sources/ResgenSwift/Images/ImagesError.swift index 36c98d9..d4942e7 100644 --- a/Sources/ResgenSwift/Images/ImagesError.swift +++ b/Sources/ResgenSwift/Images/ImagesError.swift @@ -1,6 +1,6 @@ // // ImagesError.swift -// +// // // Created by Thibaut Schmitt on 24/01/2022. // @@ -8,6 +8,7 @@ import Foundation enum ImagesError: Error { + case extensionNamesCollision(String) case inputFolderNotFound(String) case fileNotExists(String) @@ -17,33 +18,33 @@ enum ImagesError: Error { case writeFile(String, String) case createAssetFolder(String) case unknown(String) - + var description: String { switch self { case .extensionNamesCollision(let extensionName): return "error: [\(Fonts.toolName)] Error on extension names, extension name and SwiftUI extension name should be different (\(extensionName) is used on both)" - + 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): + + case let .getFileAttributed(filename, 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 librsvg')" - - case .writeFile(let subErrorDescription, let filename): + + case let .writeFile(subErrorDescription, 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)" } diff --git a/Sources/ResgenSwift/Images/ImagesOptions.swift b/Sources/ResgenSwift/Images/ImagesOptions.swift index 2b49cfb..03554ed 100644 --- a/Sources/ResgenSwift/Images/ImagesOptions.swift +++ b/Sources/ResgenSwift/Images/ImagesOptions.swift @@ -1,38 +1,41 @@ // // ImagiumOptions.swift -// +// // // Created by Thibaut Schmitt on 24/01/2022. // -import Foundation import ArgumentParser +import Foundation + +// swiftlint:disable no_grouping_extension 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 Image extension.") var extensionName: String = Images.defaultExtensionName - + @Option(help: "Extension name. If not specified, it will generate an UIImage extension.") var extensionNameUIKit: String = Images.defaultExtensionNameUIKit - + @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Image{extensionSuffix}.swift") var extensionSuffix: String? } @@ -40,35 +43,35 @@ struct ImagesOptions: ParsableArguments { // MARK: - Computed var extension ImagesOptions { - + // MARK: - SwiftUI - + var extensionFileName: String { - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { return "\(extensionName)+\(extensionSuffix).swift" } return "\(extensionName).swift" } - + var extensionFilePath: String { "\(extensionOutputPath)/\(extensionFileName)" } - + // MARK: - UIKit - + var extensionFileNameUIKit: String { - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { return "\(extensionNameUIKit)+\(extensionSuffix).swift" } return "\(extensionNameUIKit).swift" } - + var extensionFilePathUIKit: String { "\(extensionOutputPath)/\(extensionFileNameUIKit)" } - + // MARK: - - + var inputFilenameWithoutExt: String { URL(fileURLWithPath: inputFile) .deletingPathExtension() diff --git a/Sources/ResgenSwift/Images/Model/ConvertArgument.swift b/Sources/ResgenSwift/Images/Model/ConvertArgument.swift index c66566b..1dd03b5 100644 --- a/Sources/ResgenSwift/Images/Model/ConvertArgument.swift +++ b/Sources/ResgenSwift/Images/Model/ConvertArgument.swift @@ -8,6 +8,7 @@ import Foundation struct ConvertArgument { + let width: String? let height: String? } diff --git a/Sources/ResgenSwift/Images/Model/ImageContent.swift b/Sources/ResgenSwift/Images/Model/ImageContent.swift index fc6f37f..9982cbc 100644 --- a/Sources/ResgenSwift/Images/Model/ImageContent.swift +++ b/Sources/ResgenSwift/Images/Model/ImageContent.swift @@ -8,11 +8,13 @@ import Foundation enum TemplateRenderingIntent: String, Codable { + case template case original } struct AssetContent: Codable, Equatable { + let images: [AssetImageDescription] let info: AssetInfo let properties: AssetProperties? @@ -27,16 +29,17 @@ struct AssetContent: Codable, Equatable { self.properties = properties } - static func == (lhs: AssetContent, rhs: AssetContent) -> Bool { + static func == (lhs: Self, rhs: Self) -> Bool { guard lhs.images.count == rhs.images.count else { return false } - let lhsImages = lhs.images.sorted(by: { $0.filename < $1.filename }) - let rhsImages = rhs.images.sorted(by: { $0.filename < $1.filename }) + let lhsImages = lhs.images.sorted { $0.filename < $1.filename } + let rhsImages = rhs.images.sorted { $0.filename < $1.filename } return lhsImages == rhsImages } } struct AssetImageDescription: Codable, Equatable { + let idiom: String let scale: String? let filename: String @@ -53,11 +56,13 @@ struct AssetImageDescription: Codable, Equatable { } struct AssetInfo: Codable, Equatable { + let version: Int let author: String } struct AssetProperties: Codable, Equatable { + let preservesVectorRepresentation: Bool let templateRenderingIntent: TemplateRenderingIntent? @@ -70,6 +75,7 @@ struct AssetProperties: Codable, Equatable { } enum CodingKeys: String, CodingKey { + case preservesVectorRepresentation = "preserves-vector-representation" case templateRenderingIntent = "template-rendering-intent" } diff --git a/Sources/ResgenSwift/Images/Model/ParsedImage.swift b/Sources/ResgenSwift/Images/Model/ParsedImage.swift index 7c2b3f0..49668d9 100644 --- a/Sources/ResgenSwift/Images/Model/ParsedImage.swift +++ b/Sources/ResgenSwift/Images/Model/ParsedImage.swift @@ -1,6 +1,6 @@ // // ParsedImage.swift -// +// // // Created by Thibaut Schmitt on 24/01/2022. // @@ -8,10 +8,14 @@ import Foundation enum ImageExtension: String { + case png } struct ParsedImage { + + // MARK: - Properties + let name: String let tags: String let width: Int @@ -33,34 +37,34 @@ struct ParsedImage { } // MARK: - Convert - - var convertArguments: (x1: ConvertArgument, x2: ConvertArgument, x3: ConvertArgument) { + + var convertArguments: (x1: ConvertArgument, x2: ConvertArgument, x3: ConvertArgument) { // swiftlint:disable:this large_tuple 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 - + func generateContentJson(isVector: Bool) -> String? { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted @@ -77,9 +81,8 @@ struct ParsedImage { } func generateImageContent(isVector: Bool) -> AssetContent { - if !imageExtensions.contains(.png) && isVector { - //Generate svg + // Generate svg return AssetContent( images: [ AssetImageDescription( @@ -97,7 +100,7 @@ struct ParsedImage { ) ) } else { - //Generate png + // Generate png return AssetContent( images: [ AssetImageDescription( @@ -125,17 +128,17 @@ struct ParsedImage { } // MARK: - Extension property - + func getImageProperty(isStatic: Bool, isSwiftUI: Bool) -> String { if isSwiftUI { return """ - \(isStatic ? "static ": "")var \(name): Image { + \(isStatic ? "static " : "")var \(name): Image { Image("\(name)") } """ } return """ - \(isStatic ? "static ": "")var \(name): UIImage { + \(isStatic ? "static " : "")var \(name): UIImage { UIImage(named: "\(name)")! } """ diff --git a/Sources/ResgenSwift/Images/Model/PlatormTag.swift b/Sources/ResgenSwift/Images/Model/PlatormTag.swift index b7fca9c..a95c574 100644 --- a/Sources/ResgenSwift/Images/Model/PlatormTag.swift +++ b/Sources/ResgenSwift/Images/Model/PlatormTag.swift @@ -8,6 +8,7 @@ import Foundation enum PlatormTag: String { + case droid = "d" case ios = "i" } diff --git a/Sources/ResgenSwift/Images/Parser/ImageFileParser.swift b/Sources/ResgenSwift/Images/Parser/ImageFileParser.swift index eb2278b..a1c1527 100644 --- a/Sources/ResgenSwift/Images/Parser/ImageFileParser.swift +++ b/Sources/ResgenSwift/Images/Parser/ImageFileParser.swift @@ -7,36 +7,36 @@ import Foundation -class ImageFileParser { - +enum ImageFileParser { + static func parse(_ inputFile: String, platform: PlatormTag) -> [ParsedImage] { - let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8) + let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8) // swiftlint:disable:this force_try 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])! + return Int(splittedLine[2])! // swiftlint:disable:this force_unwrapping }() let height: Int = { if splittedLine[3] == "?" { return -1 } - return Int(splittedLine[3])! + return Int(splittedLine[3])! // swiftlint:disable:this force_unwrapping }() var imageExtensions: [ImageExtension] = [] diff --git a/Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift b/Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift index 5467dec..965e90d 100644 --- a/Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift +++ b/Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift @@ -8,23 +8,29 @@ import Foundation import ToolCore -class StringsFileGenerator { +// swiftlint:disable type_body_length file_length + +enum StringsFileGenerator { // MARK: - Strings Files - static func writeStringsFiles(sections: [Section], - langs: [String], - defaultLang: String, - tags: [String], - outputPath: String, - inputFilenameWithoutExt: String) { + 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) + stringsFilesContent[lang] = Self.generateStringsFileContent( + lang: lang, + defaultLang: defaultLang, + tags: tags, + sections: sections + ) } // Write strings file content @@ -35,7 +41,7 @@ class StringsFileGenerator { let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) do { try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) - } catch let error { + } catch { let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) print(error.description) Stringium.exit(withError: error) @@ -43,12 +49,14 @@ class StringsFileGenerator { } } - static func writeXcStringsFiles(sections: [Section], - langs: [String], - defaultLang: String, - tags: [String], - outputPath: String, - inputFilenameWithoutExt: String) { + static func writeXcStringsFiles( + sections: [Section], + langs: [String], + defaultLang: String, + tags: [String], + outputPath: String, + inputFilenameWithoutExt: String + ) { let fileContent: String = Self.generateXcStringsFileContent( langs: langs, @@ -61,17 +69,19 @@ class StringsFileGenerator { let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) do { try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) - } catch let error { + } catch { let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) print(error.description) Stringium.exit(withError: error) } } - static func generateStringsFileContent(lang: String, - defaultLang: String, - tags inputTags: [String], - sections: [Section]) -> String { + static func generateStringsFileContent( + lang: String, + defaultLang: String, + tags inputTags: [String], + sections: [Section] + ) -> String { var stringsFileContent = """ /** * Apple Strings File @@ -120,11 +130,18 @@ class StringsFileGenerator { // MARK: - XcStrings Generation - static func generateXcStringsFileContent(langs: [String], - defaultLang: String, - tags inputTags: [String], - sections: [Section]) -> String { - let rootObject = generateRootObject(langs: langs, defaultLang: defaultLang, tags: inputTags, sections: sections) + static func generateXcStringsFileContent( + langs: [String], + defaultLang: String, + tags inputTags: [String], + sections: [Section] + ) -> String { + let rootObject = generateRootObject( + langs: langs, + defaultLang: defaultLang, + tags: inputTags, + sections: sections + ) let file = generateXcStringsFileContentFromRootObject(rootObject: rootObject) return file @@ -138,7 +155,6 @@ class StringsFileGenerator { let json = try encoder.encode(rootObject) return String(decoding: json, as: UTF8.self) - } catch { debugPrint("Failed to encode: \(error)") } @@ -146,20 +162,22 @@ class StringsFileGenerator { return "" } - static func generateRootObject(langs: [String], - defaultLang: String, - tags inputTags: [String], - sections: [Section]) -> Root { + static func generateRootObject( + langs: [String], + defaultLang: String, + tags inputTags: [String], + sections: [Section] + ) -> Root { var xcStringDefinitionTab: [XCStringDefinition] = [] - sections.forEach { section in - // Check that at least one string will be generated + sections.forEach { section in // swiftlint:disable:this closure_body_length + // Check that at least one string will be generated guard section.hasOneOrMoreMatchingTags(tags: inputTags) else { return // Go to next section } - section.definitions.forEach { definition in + section.definitions.forEach { definition in // swiftlint:disable:this closure_body_length var skipDefinition = false var isNoTranslation = false @@ -190,7 +208,6 @@ class StringsFileGenerator { } else { // Search for langs in twine for (lang, value) in definition.translations where !value.isEmpty { - let localization = XCStringLocalization( lang: lang, content: XCStringLocalizationLangContent( @@ -219,7 +236,7 @@ class StringsFileGenerator { } let xcStringContainer = XCStringDefinitionContainer(strings: xcStringDefinitionTab) - + return Root( sourceLanguage: defaultLang, strings: xcStringContainer, @@ -229,28 +246,32 @@ class StringsFileGenerator { // MARK: - Extension file - static func writeExtensionFiles(sections: [Section], - defaultLang lang: String, - tags: [String], - staticVar: Bool, - inputFilename: String, - extensionName: String, - extensionFilePath: String, - extensionSuffix: String) { + static func writeExtensionFiles( + sections: [Section], + defaultLang lang: String, + tags: [String], + staticVar: Bool, + inputFilename: String, + extensionName: String, + extensionFilePath: String, + extensionSuffix: String + ) { // Get extension content - let extensionFileContent = Self.getExtensionContent(sections: sections, - defaultLang: lang, - tags: tags, - staticVar: staticVar, - inputFilename: inputFilename, - extensionName: extensionName, - extensionSuffix: extensionSuffix) + let extensionFileContent = Self.getExtensionContent( + sections: sections, + defaultLang: lang, + tags: tags, + staticVar: staticVar, + inputFilename: inputFilename, + extensionName: extensionName, + extensionSuffix: extensionSuffix + ) // Write content let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) do { try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) - } catch let error { + } catch { let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) print(error.description) Stringium.exit(withError: error) @@ -259,17 +280,32 @@ class StringsFileGenerator { // MARK: - Extension content - static func getExtensionContent(sections: [Section], - defaultLang lang: String, - tags: [String], - staticVar: Bool, - inputFilename: String, - extensionName: String, - extensionSuffix: String) -> String { + static func getExtensionContent( + sections: [Section], + defaultLang lang: String, + tags: [String], + staticVar: Bool, + inputFilename: String, + extensionName: String, + extensionSuffix: String + ) -> String { [ - Self.getHeader(stringsFilename: inputFilename, extensionClassname: extensionName), - Self.getEnumKey(sections: sections, tags: tags, extensionClassname: extensionName, extensionSuffix: extensionSuffix), - Self.getProperties(sections: sections, defaultLang: lang, tags: tags, staticVar: staticVar), + Self.getHeader( + stringsFilename: inputFilename, + extensionClassname: extensionName + ), + Self.getEnumKey( + sections: sections, + tags: tags, + extensionClassname: extensionName, + extensionSuffix: extensionSuffix + ), + Self.getProperties( + sections: sections, + defaultLang: lang, + tags: tags, + staticVar: staticVar + ), Self.getFooter() ] .joined(separator: "\n") @@ -289,7 +325,12 @@ class StringsFileGenerator { """ } - private static func getEnumKey(sections: [Section], tags: [String], extensionClassname: String, extensionSuffix: String) -> String { + private static func getEnumKey( + sections: [Section], + tags: [String], + extensionClassname: String, + extensionSuffix: String + ) -> String { var enumDefinition = "\n enum Key\(extensionSuffix.uppercasedFirst()): String {\n" // Enum diff --git a/Sources/ResgenSwift/Strings/Generator/TagsGenerator.swift b/Sources/ResgenSwift/Strings/Generator/TagsGenerator.swift index 81d6ccc..cba5740 100644 --- a/Sources/ResgenSwift/Strings/Generator/TagsGenerator.swift +++ b/Sources/ResgenSwift/Strings/Generator/TagsGenerator.swift @@ -1,71 +1,100 @@ // // TagsGenerator.swift -// +// // // Created by Thibaut Schmitt on 10/01/2022. // +import CoreVideo import Foundation import ToolCore -import CoreVideo -class TagsGenerator { - static func writeExtensionFiles(sections: [Section], lang: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) { +enum 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) - + 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 { + } catch { let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) print(error.description) Stringium.exit(withError: error) } } - + // MARK: - Extension content - - static func getExtensionContent(sections: [Section], lang: String, tags: [String], staticVar: Bool, extensionName: String) -> String { + + 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.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 { + + 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 + 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 { @@ -76,11 +105,11 @@ class TagsGenerator { } .joined(separator: "\n") } - + private static func getFooter() -> String { """ } - + """ } } diff --git a/Sources/ResgenSwift/Strings/Model/Definition.swift b/Sources/ResgenSwift/Strings/Model/Definition.swift index b059383..cd7d29b 100644 --- a/Sources/ResgenSwift/Strings/Model/Definition.swift +++ b/Sources/ResgenSwift/Strings/Model/Definition.swift @@ -1,78 +1,93 @@ // // Definition.swift -// +// // // Created by Thibaut Schmitt on 04/01/2022. // import Foundation +// swiftlint:disable force_unwrapping + class Definition { + + // MARK: - Properties + 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).isDisjoint(with: tags) { 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 } - + + let printfPlaceholderRegex = try! NSRegularExpression( // swiftlint:disable:this force_try + pattern: "%(?:\\d+\\$)?[+-]?(?:[ 0]|'.{1})?-?\\d*(?:\\.\\d+)?[blcdeEufFgGosxX@]*" + ) + printfPlaceholderRegex.enumerateMatches( + in: input, + options: [], + range: NSRange(location: 0, length: input.count) + ) { match, _, stop in // swiftlint:disable:this unused_closure_parameter + guard let 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() { @@ -80,10 +95,10 @@ class Definition { translationArguments.append(paramName) inputParameters.append("\(paramName): \(paramType)") } - + return (inputParameters: inputParameters, translationArguments: translationArguments) } - + private func getBaseProperty(lang: String, translation: String, isStatic: Bool, comment: String?) -> String { """ /// Translation in \(lang) : @@ -91,15 +106,20 @@ class Definition { /// /// Comment : /// \(comment?.isEmpty == false ? comment! : "No comment") - \(isStatic ? "static ": "")var \(name): String { + \(isStatic ? "static " : "")var \(name): String { NSLocalizedString("\(name)", tableName: kStringsFileName, bundle: Bundle.main, value: "\(translation)", comment: "\(comment ?? "")") } """ - } - - private func getBaseMethod(lang: String, translation: String, isStatic: Bool, inputParameters: [String], translationArguments: [String], comment: String?) -> String { + private func getBaseMethod( + lang: String, + translation: String, + isStatic: Bool, + inputParameters: [String], + translationArguments: [String], + comment: String? + ) -> String { """ /// Translation in \(lang) : @@ -107,12 +127,12 @@ class Definition { /// /// Comment : /// \(comment?.isEmpty == false ? comment! : "No comment") - \(isStatic ? "static ": "")func \(name)(\(inputParameters.joined(separator: ", "))) -> String { + \(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) @@ -131,24 +151,26 @@ class Definition { // 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, - comment: self.comment) + method = getBaseMethod( + lang: lang, + translation: translation, + isStatic: false, + inputParameters: parameters.inputParameters, + translationArguments: parameters.translationArguments, + comment: self.comment + ) } - + 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.description) Stringium.exit(withError: error) } - + // Generate property let property = getBaseProperty( lang: lang, @@ -156,23 +178,25 @@ class Definition { isStatic: true, comment: self.comment ) - + // 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, - comment: self.comment) + method = getBaseMethod( + lang: lang, + translation: translation, + isStatic: true, + inputParameters: parameters.inputParameters, + translationArguments: parameters.translationArguments, + comment: self.comment + ) } - + 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) @@ -192,14 +216,14 @@ class Definition { } """ } - + func getStaticProperty(forLang lang: String) -> String { guard let translation = translations[lang] else { let error = StringiumError.langNotDefined(lang, name, reference != nil) print(error.description) Stringium.exit(withError: error) } - + return """ /// Translation in \(lang) : /// \(translation) diff --git a/Sources/ResgenSwift/Strings/Model/Section.swift b/Sources/ResgenSwift/Strings/Model/Section.swift index 42241d2..680ac32 100644 --- a/Sources/ResgenSwift/Strings/Model/Section.swift +++ b/Sources/ResgenSwift/Strings/Model/Section.swift @@ -8,28 +8,35 @@ import Foundation class Section { + + // MARK: - Properties + let name: String // OnBoarding var definitions = [Definition]() - + + // MARK: - Init + init(name: String) { self.name = name } - + + // MARK: - Methods + 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 diff --git a/Sources/ResgenSwift/Strings/Model/XcString.swift b/Sources/ResgenSwift/Strings/Model/XcString.swift index 6c784d1..dbfef43 100644 --- a/Sources/ResgenSwift/Strings/Model/XcString.swift +++ b/Sources/ResgenSwift/Strings/Model/XcString.swift @@ -8,25 +8,30 @@ import SwiftUI struct DynamicKey: CodingKey { + var intValue: Int? + init?(intValue: Int) { self.intValue = intValue self.stringValue = "\(intValue)" } var stringValue: String + init?(stringValue: String) { self.stringValue = stringValue } } struct Root: Codable, Equatable { + let sourceLanguage: String let strings: XCStringDefinitionContainer let version: String } struct XCStringDefinitionContainer: Codable, Equatable { + let strings: [XCStringDefinition] func encode(to encoder: Encoder) throws { @@ -39,19 +44,19 @@ struct XCStringDefinitionContainer: Codable, Equatable { } } - static func == (lhs: XCStringDefinitionContainer, rhs: XCStringDefinitionContainer) -> Bool { - return lhs.strings.sorted(by: { - $0.title < $1.title - }) == rhs.strings.sorted(by: { $0.title < $1.title }) + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.strings.sorted { $0.title < $1.title } == rhs.strings.sorted { $0.title < $1.title } } } struct XCStringDefinition: Codable, Equatable { + let title: String // json key -> custom encoding methods let content: XCStringDefinitionContent } struct XCStringDefinitionContent: Codable, Equatable { + let comment: String? let extractionState: String var localizations: XCStringLocalizationContainer @@ -64,6 +69,7 @@ struct XCStringDefinitionContent: Codable, Equatable { } struct XCStringLocalizationContainer: Codable, Equatable { + let localizations: [XCStringLocalization] func encode(to encoder: Encoder) throws { @@ -76,34 +82,39 @@ struct XCStringLocalizationContainer: Codable, Equatable { } } - static func == (lhs: XCStringLocalizationContainer, rhs: XCStringLocalizationContainer) -> Bool { - return lhs.localizations.count == rhs.localizations.count && lhs.localizations.sorted(by: { $0.lang < $1.lang }) == rhs.localizations.sorted(by: { $0.lang < $1.lang }) + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.localizations.count == rhs.localizations.count + && lhs.localizations.sorted { $0.lang < $1.lang } == rhs.localizations.sorted { $0.lang < $1.lang } } } struct XCStringLocalization: Codable, Equatable { + let lang: String // json key -> custom encoding method let content: XCStringLocalizationLangContent } struct XCStringLocalizationLangContent: Codable, Equatable { + let stringUnit: DefaultStringUnit } -//enum VarationOrStringUnit: Encodable { -// case variations([Varation]) -// case stringUnit: (DefaultStringUnit) -// -// func encode(to encoder: any Encoder) throws { -// if let varations { -// -// } else if let { -// -// } -// } -//} +// enum VarationOrStringUnit: Encodable +// +// case variations([Varation]) +// case stringUnit: (DefaultStringUnit) +// +// func encode(to encoder: any Encoder) throws { +// if let varations { +// +// } else if let { +// +// } +// } +// } struct DefaultStringUnit: Codable, Equatable { + let state: String let value: String } diff --git a/Sources/ResgenSwift/Strings/Parser/TwineFileParser.swift b/Sources/ResgenSwift/Strings/Parser/TwineFileParser.swift index b7f6803..d23a9be 100644 --- a/Sources/ResgenSwift/Strings/Parser/TwineFileParser.swift +++ b/Sources/ResgenSwift/Strings/Parser/TwineFileParser.swift @@ -1,76 +1,74 @@ // // TwineFileParser.swift -// +// // // Created by Thibaut Schmitt on 10/01/2022. // import Foundation -class TwineFileParser { +// swiftlint:disable function_body_length + +enum TwineFileParser { + static func parse(_ inputFile: String) -> [Section] { - let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8) + let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8) // swiftlint:disable:this force_try let stringsByLines = inputFileContent.components(separatedBy: .newlines) - + var sections = [Section]() - + // Parse file - stringsByLines.forEach { + stringsByLines.forEach { // swiftlint:disable:this closure_body_length // 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 - } - + 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 @@ -83,10 +81,10 @@ class TwineFileParser { return true } } - if invalidDefinitionNames.count > 0 { + if invalidDefinitionNames.isEmpty == false { print("warning: [\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))") } - + return sections } } diff --git a/Sources/ResgenSwift/Strings/Stringium/Stringium.swift b/Sources/ResgenSwift/Strings/Stringium/Stringium.swift index a27ebfe..bfb3894 100644 --- a/Sources/ResgenSwift/Strings/Stringium/Stringium.swift +++ b/Sources/ResgenSwift/Strings/Stringium/Stringium.swift @@ -1,114 +1,122 @@ // // Stringium.swift -// +// // // Created by Thibaut Schmitt on 10/01/2022. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore 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 print(options.xcStrings) if !options.xcStrings { print("[\(Self.toolName)] Will generate strings") - StringsFileGenerator.writeStringsFiles(sections: sections, - langs: options.langs, - defaultLang: options.defaultLang, - tags: options.tags, - outputPath: options.stringsFileOutputPath, - inputFilenameWithoutExt: options.inputFilenameWithoutExt) + StringsFileGenerator.writeStringsFiles( + sections: sections, + langs: options.langs, + defaultLang: options.defaultLang, + tags: options.tags, + outputPath: options.stringsFileOutputPath, + inputFilenameWithoutExt: options.inputFilenameWithoutExt + ) } else { print("[\(Self.toolName)] Will generate xcStrings") - StringsFileGenerator.writeXcStringsFiles(sections: sections, - langs: options.langs, - defaultLang: options.defaultLang, - tags: options.tags, - outputPath: options.stringsFileOutputPath, - inputFilenameWithoutExt: options.inputFilenameWithoutExt) + StringsFileGenerator.writeXcStringsFiles( + 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, - extensionSuffix: options.extensionSuffix) + StringsFileGenerator.writeExtensionFiles( + sections: sections, + defaultLang: options.defaultLang, + tags: options.tags, + staticVar: options.staticMembers, + inputFilename: options.inputFilenameWithoutExt, + extensionName: options.extensionName, + extensionFilePath: options.extensionFilePath, + extensionSuffix: options.extensionSuffix + ) 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.description) - Stringium.exit(withError: error) + Self.exit(withError: error) } - + // Langs guard options.langs.isEmpty == false else { let error = StringiumError.langsListEmpty print(error.description) - Stringium.exit(withError: error) + Self.exit(withError: error) } - + guard options.langs.contains(options.defaultLang) else { let error = StringiumError.defaultLangsNotInLangs print(error.description) - Stringium.exit(withError: error) + Self.exit(withError: error) } - + // Check if needed to regenerate - guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, - inputFilePath: options.inputFile, - extensionFilePath: options.extensionFilePath) else { + 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 } } diff --git a/Sources/ResgenSwift/Strings/Stringium/StringiumError.swift b/Sources/ResgenSwift/Strings/Stringium/StringiumError.swift index ba12a82..b96341a 100644 --- a/Sources/ResgenSwift/Strings/Stringium/StringiumError.swift +++ b/Sources/ResgenSwift/Strings/Stringium/StringiumError.swift @@ -8,27 +8,28 @@ import Foundation enum StringiumError: Error { + case fileNotExists(String) case langsListEmpty case defaultLangsNotInLangs case writeFile(String, String) case langNotDefined(String, String, Bool) - + var description: 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): + + case let .writeFile(subErrorDescription, filename): return "error: [\(Stringium.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)" - - case .langNotDefined(let lang, let definitionName, let isReference): + + case let .langNotDefined(lang, definitionName, isReference): if isReference { return "error: [\(Stringium.toolName)] Reference are handled only by Twine. Please use it or remove reference from you strings file." } diff --git a/Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift b/Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift index 7c9c7a8..8c1a05e 100644 --- a/Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift +++ b/Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift @@ -5,31 +5,34 @@ // Created by Thibaut Schmitt on 10/01/2022. // -import Foundation import ArgumentParser +import Foundation + +// swiftlint:disable no_grouping_extension struct StringiumOptions: ParsableArguments { + @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") var forceGeneration = false @Argument(help: "Input files where strings are 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 @@ -38,7 +41,7 @@ struct StringiumOptions: ParsableArguments { @Option(help: "Extension name. If not specified, it will generate an String extension.") var extensionName: String = Stringium.defaultExtensionName - + @Option(help: "Extension suffix: {extensionName}+{extensionSuffix}.swift") var extensionSuffix: String } @@ -46,6 +49,7 @@ struct StringiumOptions: ParsableArguments { // MARK: - Private var getter extension StringiumOptions { + var stringsFileOutputPath: String { var outputPath = outputPathRaw if outputPath.last == "/" { @@ -53,13 +57,13 @@ extension StringiumOptions { } return outputPath } - + var langs: [String] { langsRaw .split(separator: " ") .map { String($0) } } - + var tags: [String] { tagsRaw .split(separator: " ") @@ -70,14 +74,15 @@ extension StringiumOptions { // MARK: - Computed var extension StringiumOptions { + var extensionFileName: String { "\(extensionName)+\(extensionSuffix).swift" } - + var extensionFilePath: String { "\(extensionOutputPath)/\(extensionFileName)" } - + var inputFilenameWithoutExt: String { URL(fileURLWithPath: inputFile) .deletingPathExtension() diff --git a/Sources/ResgenSwift/Strings/Strings.swift b/Sources/ResgenSwift/Strings/Strings.swift index 1465d33..c19dda8 100644 --- a/Sources/ResgenSwift/Strings/Strings.swift +++ b/Sources/ResgenSwift/Strings/Strings.swift @@ -5,12 +5,12 @@ // Created by Thibaut Schmitt on 10/01/2022. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore struct Strings: ParsableCommand { - + static var configuration = CommandConfiguration( abstract: "A utility for generate strings.", version: ResgenSwiftVersion, @@ -22,8 +22,8 @@ struct Strings: ParsableCommand { // A default subcommand, when provided, is automatically selected if a // subcommand is not given on the command line. - //defaultSubcommand: Twine.self + // defaultSubcommand: Twine.self ) } -//Strings.main() +// Strings.main() diff --git a/Sources/ResgenSwift/Strings/Tag/Tags.swift b/Sources/ResgenSwift/Strings/Tag/Tags.swift index 26c0c4b..b1d4482 100644 --- a/Sources/ResgenSwift/Strings/Tag/Tags.swift +++ b/Sources/ResgenSwift/Strings/Tag/Tags.swift @@ -1,78 +1,82 @@ // // Tag.swift -// +// // // Created by Thibaut Schmitt on 10/01/2022. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore 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) - + 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.description) Stringium.exit(withError: error) } - + // Check if needed to regenerate - guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, - inputFilePath: options.inputFile, - extensionFilePath: options.extensionFilePath) else { + 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 } } diff --git a/Sources/ResgenSwift/Strings/Tag/TagsOptions.swift b/Sources/ResgenSwift/Strings/Tag/TagsOptions.swift index e5372b9..0c6ba11 100644 --- a/Sources/ResgenSwift/Strings/Tag/TagsOptions.swift +++ b/Sources/ResgenSwift/Strings/Tag/TagsOptions.swift @@ -5,28 +5,31 @@ // Created by Thibaut Schmitt on 10/01/2022. // -import Foundation import ArgumentParser +import Foundation + +// swiftlint:disable no_grouping_extension 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.") var extensionName: String = Tags.defaultExtensionName - + @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Tag{extensionSuffix}.swift") var extensionSuffix: String? } @@ -34,13 +37,14 @@ struct TagsOptions: ParsableArguments { // MARK: - Computed var extension TagsOptions { + var extensionFileName: String { - if let extensionSuffix = extensionSuffix { + if let extensionSuffix { return "\(extensionName)+\(extensionSuffix).swift" } return "\(extensionName).swift" } - + var extensionFilePath: String { "\(extensionOutputPath)/\(extensionFileName)" } diff --git a/Sources/ResgenSwift/Strings/Twine/Twine.swift b/Sources/ResgenSwift/Strings/Twine/Twine.swift index b45ef00..08270dc 100644 --- a/Sources/ResgenSwift/Strings/Twine/Twine.swift +++ b/Sources/ResgenSwift/Strings/Twine/Twine.swift @@ -1,102 +1,117 @@ // // Twine.swift -// +// // // Created by Thibaut Schmitt on 10/01/2022. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore 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: String = { - #if os(macOS) +#if os(macOS) "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)/scripts/twine/twine" - #else +#else fatalError("This command should run on Mac only") - #endif +#endif }() // 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"]) + 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"]) - + 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.description) - Twine.exit(withError: error) + Self.exit(withError: error) } - + // Langs guard options.langs.isEmpty == false else { let error = TwineError.langsListEmpty print(error.description) - Twine.exit(withError: error) + Self.exit(withError: error) } - + guard options.langs.contains(options.defaultLang) else { let error = TwineError.defaultLangsNotInLangs print(error.description) - Twine.exit(withError: error) + Self.exit(withError: error) } - + // Check if needed to regenerate - guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, - inputFilePath: options.inputFile, - extensionFilePath: options.extensionFilePathGenerated) else { + 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 } } diff --git a/Sources/ResgenSwift/Strings/Twine/TwineError.swift b/Sources/ResgenSwift/Strings/Twine/TwineError.swift index 82ed17f..cca5e80 100644 --- a/Sources/ResgenSwift/Strings/Twine/TwineError.swift +++ b/Sources/ResgenSwift/Strings/Twine/TwineError.swift @@ -8,18 +8,19 @@ import Foundation enum TwineError: Error { + case fileNotExists(String) case langsListEmpty case defaultLangsNotInLangs - + var description: 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" } diff --git a/Sources/ResgenSwift/Strings/Twine/TwineOptions.swift b/Sources/ResgenSwift/Strings/Twine/TwineOptions.swift index 3d79aff..6b6641d 100644 --- a/Sources/ResgenSwift/Strings/Twine/TwineOptions.swift +++ b/Sources/ResgenSwift/Strings/Twine/TwineOptions.swift @@ -5,25 +5,28 @@ // Created by Thibaut Schmitt on 10/01/2022. // -import Foundation import ArgumentParser +import Foundation + +// swiftlint:disable no_grouping_extension 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 } @@ -31,6 +34,7 @@ struct TwineOptions: ParsableArguments { // MARK: - Private var getter extension TwineOptions { + var langs: [String] { langsRaw .split(separator: " ") @@ -41,16 +45,17 @@ extension TwineOptions { // 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" diff --git a/Sources/ResgenSwift/main.swift b/Sources/ResgenSwift/main.swift index 01bd9c2..78f5fdb 100644 --- a/Sources/ResgenSwift/main.swift +++ b/Sources/ResgenSwift/main.swift @@ -5,12 +5,12 @@ // Created by Thibaut Schmitt on 13/12/2021. // -import ToolCore -import Foundation import ArgumentParser +import Foundation +import ToolCore struct ResgenSwift: ParsableCommand { - + static var configuration = CommandConfiguration( abstract: "A utility for generate ressources.", version: ResgenSwiftVersion, @@ -30,7 +30,7 @@ struct ResgenSwift: ParsableCommand { // A default subcommand, when provided, is automatically selected if a // subcommand is not given on the command line. - //defaultSubcommand: Twine.self + // defaultSubcommand: Twine.self ) } diff --git a/Sources/ToolCore/GeneratorChecker.swift b/Sources/ToolCore/GeneratorChecker.swift index f96c6a8..c6dfb9c 100644 --- a/Sources/ToolCore/GeneratorChecker.swift +++ b/Sources/ToolCore/GeneratorChecker.swift @@ -1,43 +1,42 @@ // // GeneratorChecker.swift -// +// // // Created by Thibaut Schmitt on 22/12/2021. // import Foundation -public class GeneratorChecker { - +public enum GeneratorChecker { + /// Return `true` if inputFile is newer than extensionFile, otherwise `false` public static func shouldGenerate(force: Bool, inputFilePath: String, extensionFilePath: String) -> Bool { guard force == false else { return true } - + return Self.isFile(inputFilePath, moreRecenThan: extensionFilePath) } - + public static func isFile(_ fileOne: String, moreRecenThan fileTwo: String) -> Bool { let fileOneURL = URL(fileURLWithPath: fileOne) let fileTwoURL = URL(fileURLWithPath: fileTwo) - + let fileOneRessourceValues = try? fileOneURL.resourceValues(forKeys: [URLResourceKey.contentModificationDateKey]) let fileTwoRessourceValues = try? fileTwoURL.resourceValues(forKeys: [URLResourceKey.contentModificationDateKey]) - + guard let fileOneModificationDate = fileOneRessourceValues?.contentModificationDate, let fileTwoModificationDate = fileTwoRessourceValues?.contentModificationDate else { print("⚠️ Could not compare file modication date. ⚠️ (assume than file is newer)") // Date not available -> assume than fileOne is newer than fileTwo return true } - + if fileOneModificationDate >= fileTwoModificationDate { debugPrint("File one is more recent than file two.") return true } - + return false } - } diff --git a/Sources/ToolCore/SequenceExtensions.swift b/Sources/ToolCore/SequenceExtensions.swift index 2608276..7a1e0b6 100644 --- a/Sources/ToolCore/SequenceExtensions.swift +++ b/Sources/ToolCore/SequenceExtensions.swift @@ -7,8 +7,9 @@ import Foundation -public extension Sequence where Iterator.Element: Hashable { - func unique() -> [Iterator.Element] { +extension Sequence where Iterator.Element: Hashable { + + public func unique() -> [Iterator.Element] { var seen: [Iterator.Element: Bool] = [:] return self.filter { seen.updateValue(true, forKey: $0) == nil } } diff --git a/Sources/ToolCore/Shell.swift b/Sources/ToolCore/Shell.swift index 806d202..a6fd2ca 100644 --- a/Sources/ToolCore/Shell.swift +++ b/Sources/ToolCore/Shell.swift @@ -1,14 +1,14 @@ // // Shell.swift -// +// // // Created by Thibaut Schmitt on 22/12/2021. // import Foundation -public class Shell { - +public enum Shell { + public static var environment: [String: String] { ProcessInfo.processInfo.environment } @@ -18,31 +18,31 @@ public class Shell { launchPath: String = "/usr/bin/env", _ args: [String] ) -> (terminationStatus: Int32, output: String?) { - #if os(macOS) +#if os(macOS) let task = Process() task.launchPath = launchPath task.arguments = args - + var currentEnv = ProcessInfo.processInfo.environment for (key, value) in environment { currentEnv[key] = value } task.environment = currentEnv - + let pipe = Pipe() task.standardOutput = pipe task.launch() task.waitUntilExit() - + let data = pipe.fileHandleForReading.readDataToEndOfFile() - - guard let output: String = String(data: data, encoding: .utf8) else { + + guard let output = String(data: data, encoding: .utf8) else { return (terminationStatus: task.terminationStatus, output: nil) } - + return (terminationStatus: task.terminationStatus, output: output) - #else +#else fatalError("Shell is only available on Mac") - #endif +#endif } } diff --git a/Sources/ToolCore/StringExtensions.swift b/Sources/ToolCore/StringExtensions.swift index 4878290..3c97e07 100644 --- a/Sources/ToolCore/StringExtensions.swift +++ b/Sources/ToolCore/StringExtensions.swift @@ -1,87 +1,88 @@ // // Extensions.swift -// +// // // Created by Thibaut Schmitt on 13/12/2021. // import Foundation -public extension String { - func removeCharacters(from forbiddenChars: CharacterSet) -> String { +extension String { + + public func removeCharacters(from forbiddenChars: CharacterSet) -> String { let passed = self.unicodeScalars.filter { !forbiddenChars.contains($0) } return String(String.UnicodeScalarView(passed)) } - - func removeCharacters(from: String) -> String { - return removeCharacters(from: CharacterSet(charactersIn: from)) + + public func removeCharacters(from: String) -> String { + removeCharacters(from: CharacterSet(charactersIn: from)) } - - func replacingOccurrences(of: [String], with: String) -> Self { + + public func replacingOccurrences(of: [String], with: String) -> Self { var tmp = self - for e in of { - tmp = tmp.replacingOccurrences(of: e, with: with) + for ofValue in of { + tmp = tmp.replacingOccurrences(of: ofValue, with: with) } return tmp } - - func removeTrailingWhitespace() -> Self { + + public func removeTrailingWhitespace() -> Self { var newString = self - + while newString.last?.isWhitespace == true { newString = String(newString.dropLast()) } - + return newString } - - func removeLeadingWhitespace() -> Self { + + public func removeLeadingWhitespace() -> Self { var newString = self - + while newString.first?.isWhitespace == true { newString = String(newString.dropFirst()) } - + return newString } - - func removeLeadingTrailingWhitespace() -> Self { + + public func removeLeadingTrailingWhitespace() -> Self { var newString = self - + newString = newString.removeLeadingWhitespace() newString = newString.removeTrailingWhitespace() - + return newString } - - func escapeDoubleQuote() -> Self { + + public func escapeDoubleQuote() -> Self { replacingOccurrences(of: "\"", with: "\\\"") } - - func replaceTiltWithHomeDirectoryPath() -> Self { + + public func replaceTiltWithHomeDirectoryPath() -> Self { // See NSString.expandingTildeInPath - #if os(macOS) +#if os(macOS) replacingOccurrences(of: "~", with: "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)") - #else +#else fatalError("This command should run on Mac only") - #endif +#endif } - - func colorComponent() -> (alpha: String, red: String, green: String, blue: String) { + + public func colorComponent() -> (alpha: String, red: String, green: String, blue: String) { // swiftlint:disable:this large_tuple var alpha: String = "FF" var red: String var green: String var blue: String var colorClean = self - .replacingOccurrences(of: "#", with: "") - .replacingOccurrences(of: "0x", with: "") + .replacingOccurrences(of: "#", with: "") + .replacingOccurrences(of: "0x", with: "") if colorClean.count == 8 { alpha = String(colorClean.prefix(2)) colorClean = String(colorClean.dropFirst(2)) } - + red = String(colorClean.prefix(2)) colorClean = String(colorClean.dropFirst(2)) green = String(colorClean.prefix(2)) @@ -89,12 +90,12 @@ public extension String { blue = String(colorClean.prefix(2)) return (alpha: alpha, red: red, green: green, blue: blue) } - - func uppercasedFirst() -> String { + + public func uppercasedFirst() -> String { prefix(1).uppercased() + dropFirst() } - func replacingFirstOccurrence(of: String, with: String) -> Self { + public func replacingFirstOccurrence(of: String, with: String) -> Self { if let range = self.range(of: of) { let tmp = self.replacingOccurrences( of: of, diff --git a/Sources/ToolCore/Version.swift b/Sources/ToolCore/Version.swift index cdd646a..3f9b249 100644 --- a/Sources/ToolCore/Version.swift +++ b/Sources/ToolCore/Version.swift @@ -7,4 +7,6 @@ import Foundation +// swiftlint:disable prefixed_toplevel_constant identifier_name + public let ResgenSwiftVersion = "2.1.0" diff --git a/Tests/ResgenSwiftTests/Analytics/AnalyticsGeneratorTests.swift b/Tests/ResgenSwiftTests/Analytics/AnalyticsGeneratorTests.swift index a29e917..f12b524 100644 --- a/Tests/ResgenSwiftTests/Analytics/AnalyticsGeneratorTests.swift +++ b/Tests/ResgenSwiftTests/Analytics/AnalyticsGeneratorTests.swift @@ -51,11 +51,14 @@ final class AnalyticsGeneratorTests: XCTestCase { ] // When - AnalyticsGenerator.targets = [TrackerType.firebase] - let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], - tags: ["ios", "iosonly"], - staticVar: false, - extensionName: "GenAnalytics") + let extensionContent = AnalyticsGenerator.getExtensionContent( + targets: [TrackerType.firebase], + sections: [sectionOne, sectionTwo, sectionThree], + tags: ["ios", "iosonly"], + staticVar: false, + extensionName: "GenAnalytics" + ) + // Expect Analytics let expect = """ // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) @@ -65,6 +68,7 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Protocol protocol AnalyticsManagerProtocol { + func logScreen(name: String, path: String) func logEvent( name: String, @@ -77,6 +81,7 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Firebase class FirebaseAnalyticsManager: AnalyticsManagerProtocol { + func logScreen(name: String, path: String) { var parameters = [ AnalyticsParameterScreenName: name as NSObject @@ -98,7 +103,7 @@ final class AnalyticsGeneratorTests: XCTestCase { "action": action as NSObject, "category": category as NSObject, ] - + if let supplementaryParameters = params { for (newKey, newValue) in supplementaryParameters { if parameters.contains(where: { (key: String, value: NSObject) in @@ -106,11 +111,11 @@ final class AnalyticsGeneratorTests: XCTestCase { }) { continue } - + parameters[newKey] = newValue as? NSObject } } - + Analytics.logEvent( name.replacingOccurrences(of: [" "], with: "_"), parameters: parameters @@ -121,8 +126,9 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Manager class AnalyticsManager { + static var shared = AnalyticsManager() - + // MARK: - Properties var managers: [AnalyticsManagerProtocol] = [] @@ -138,7 +144,7 @@ final class AnalyticsGeneratorTests: XCTestCase { func configure() { managers.append(FirebaseAnalyticsManager()) } - + private func logScreen(name: String, path: String) { guard isEnabled else { return } @@ -146,7 +152,7 @@ final class AnalyticsGeneratorTests: XCTestCase { manager.logScreen(name: name, path: path) } } - + private func logEvent( name: String, action: String, @@ -154,7 +160,7 @@ final class AnalyticsGeneratorTests: XCTestCase { params: [String: Any]? ) { guard isEnabled else { return } - + managers.forEach { manager in manager.logEvent( name: name, @@ -222,11 +228,13 @@ final class AnalyticsGeneratorTests: XCTestCase { ] // When - AnalyticsGenerator.targets = [TrackerType.matomo] - let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], - tags: ["ios", "iosonly"], - staticVar: false, - extensionName: "GenAnalytics") + let extensionContent = AnalyticsGenerator.getExtensionContent( + targets: [TrackerType.matomo], + sections: [sectionOne, sectionTwo, sectionThree], + tags: ["ios", "iosonly"], + staticVar: false, + extensionName: "GenAnalytics" + ) // Expect Analytics let expect = """ // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) @@ -236,6 +244,7 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Protocol protocol AnalyticsManagerProtocol { + func logScreen(name: String, path: String) func logEvent( name: String, @@ -248,7 +257,7 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Matomo class MatomoAnalyticsManager: AnalyticsManagerProtocol { - + // MARK: - Properties private var tracker: MatomoTracker @@ -262,11 +271,11 @@ final class AnalyticsGeneratorTests: XCTestCase { siteId: siteId, baseURL: URL(string: url)! ) - + #if DEBUG tracker.dispatchInterval = 5 #endif - + #if DEBUG tracker.logger = DefaultLogger(minLevel: .verbose) #endif @@ -274,7 +283,7 @@ final class AnalyticsGeneratorTests: XCTestCase { debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") } - + // MARK: - Methods func logScreen(name: String, path: String) { @@ -295,7 +304,7 @@ final class AnalyticsGeneratorTests: XCTestCase { params: [String: Any]? ) { guard !tracker.isOptedOut else { return } - + tracker.track( eventWithCategory: category, action: action, @@ -309,8 +318,9 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Manager class AnalyticsManager { + static var shared = AnalyticsManager() - + // MARK: - Properties var managers: [AnalyticsManagerProtocol] = [] @@ -331,15 +341,15 @@ final class AnalyticsGeneratorTests: XCTestCase { ) ) } - + private func logScreen(name: String, path: String) { guard isEnabled else { return } - + managers.forEach { manager in manager.logScreen(name: name, path: path) } } - + private func logEvent( name: String, action: String, @@ -347,7 +357,7 @@ final class AnalyticsGeneratorTests: XCTestCase { params: [String: Any]? ) { guard isEnabled else { return } - + managers.forEach { manager in manager.logEvent( name: name, @@ -385,7 +395,7 @@ final class AnalyticsGeneratorTests: XCTestCase { ) } } - + """ if extensionContent != expect { @@ -415,11 +425,14 @@ final class AnalyticsGeneratorTests: XCTestCase { ] // When - AnalyticsGenerator.targets = [TrackerType.matomo, TrackerType.firebase] - let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], - tags: ["ios", "iosonly"], - staticVar: false, - extensionName: "GenAnalytics") + let extensionContent = AnalyticsGenerator.getExtensionContent( + targets: [TrackerType.matomo, TrackerType.firebase], + sections: [sectionOne, sectionTwo, sectionThree], + tags: ["ios", "iosonly"], + staticVar: false, + extensionName: "GenAnalytics" + ) + // Expect Analytics let expect = """ // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) @@ -430,6 +443,7 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Protocol protocol AnalyticsManagerProtocol { + func logScreen(name: String, path: String) func logEvent( name: String, @@ -442,7 +456,7 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Matomo class MatomoAnalyticsManager: AnalyticsManagerProtocol { - + // MARK: - Properties private var tracker: MatomoTracker @@ -456,11 +470,11 @@ final class AnalyticsGeneratorTests: XCTestCase { siteId: siteId, baseURL: URL(string: url)! ) - + #if DEBUG tracker.dispatchInterval = 5 #endif - + #if DEBUG tracker.logger = DefaultLogger(minLevel: .verbose) #endif @@ -468,13 +482,13 @@ final class AnalyticsGeneratorTests: XCTestCase { debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") } - + // MARK: - Methods func logScreen(name: String, path: String) { guard !tracker.isOptedOut else { return } guard let trackerUrl = tracker.contentBase?.absoluteString else { return } - + let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS") tracker.track( view: [name], @@ -489,7 +503,7 @@ final class AnalyticsGeneratorTests: XCTestCase { params: [String: Any]? ) { guard !tracker.isOptedOut else { return } - + tracker.track( eventWithCategory: category, action: action, @@ -499,7 +513,7 @@ final class AnalyticsGeneratorTests: XCTestCase { ) } } - + // MARK: - Firebase class FirebaseAnalyticsManager: AnalyticsManagerProtocol { @@ -524,7 +538,7 @@ final class AnalyticsGeneratorTests: XCTestCase { "action": action as NSObject, "category": category as NSObject, ] - + if let supplementaryParameters = params { for (newKey, newValue) in supplementaryParameters { if parameters.contains(where: { (key: String, value: NSObject) in @@ -532,11 +546,11 @@ final class AnalyticsGeneratorTests: XCTestCase { }) { continue } - + parameters[newKey] = newValue as? NSObject } } - + Analytics.logEvent( name.replacingOccurrences(of: [" "], with: "_"), parameters: parameters @@ -547,8 +561,9 @@ final class AnalyticsGeneratorTests: XCTestCase { // MARK: - Manager class AnalyticsManager { + static var shared = AnalyticsManager() - + // MARK: - Properties var managers: [AnalyticsManagerProtocol] = [] @@ -570,7 +585,7 @@ final class AnalyticsGeneratorTests: XCTestCase { ) managers.append(FirebaseAnalyticsManager()) } - + private func logScreen(name: String, path: String) { guard isEnabled else { return } @@ -578,7 +593,7 @@ final class AnalyticsGeneratorTests: XCTestCase { manager.logScreen(name: name, path: path) } } - + private func logEvent( name: String, action: String, @@ -586,7 +601,7 @@ final class AnalyticsGeneratorTests: XCTestCase { params: [String: Any]? ) { guard isEnabled else { return } - + managers.forEach { manager in manager.logEvent( name: name, @@ -624,7 +639,7 @@ final class AnalyticsGeneratorTests: XCTestCase { ) } } - + """ if extensionContent != expect {