Add SwiftLint HARD rules
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
				
			This commit is contained in:
		
							
								
								
									
										317
									
								
								.swiftlint.yml
									
									
									
									
									
								
							
							
						
						
									
										317
									
								
								.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 | ||||
|  | ||||
|    | ||||
|   | ||||
| @@ -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" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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)" | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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)" | ||||
|     } | ||||
|   | ||||
| @@ -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 { | ||||
|         """ | ||||
|         } | ||||
|          | ||||
|  | ||||
|         """ | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|         """ | ||||
|         } | ||||
|          | ||||
|  | ||||
|         """ | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|         """ | ||||
|         } | ||||
|          | ||||
|  | ||||
|         """ | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 """ | ||||
|   | ||||
| @@ -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? | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -8,8 +8,9 @@ | ||||
| import Foundation | ||||
|  | ||||
| extension AnalyticsDefinition { | ||||
|      | ||||
|  | ||||
|     enum TagType { | ||||
|  | ||||
|         case screen | ||||
|         case event | ||||
|     } | ||||
|   | ||||
| @@ -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" | ||||
|         } | ||||
|   | ||||
| @@ -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 | ||||
|             } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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)`" | ||||
|         } | ||||
|   | ||||
| @@ -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)" | ||||
|     } | ||||
|   | ||||
| @@ -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) | ||||
|         } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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 """ | ||||
|   | ||||
| @@ -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])) | ||||
|   | ||||
| @@ -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)" | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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)" | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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<string>\(fontName.filename).\(fontName.fileExtension)</string>\n" | ||||
|             } | ||||
|         plistData += "\t</array>" | ||||
|          | ||||
|  | ||||
|         return plistData | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|         """ | ||||
|         } | ||||
|          | ||||
|  | ||||
|         """ | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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("/") { | ||||
|   | ||||
| @@ -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") | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -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)" | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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 == "/" { | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,5 +8,6 @@ | ||||
| import Foundation | ||||
|  | ||||
| protocol Runnable { | ||||
|  | ||||
|     func run(projectDirectory: String, force: Bool) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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]) | ||||
|   | ||||
| @@ -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 { | ||||
|         """ | ||||
|         } | ||||
|          | ||||
|  | ||||
|         """ | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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)" | ||||
|         } | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
| import Foundation | ||||
|  | ||||
| struct ConvertArgument { | ||||
|  | ||||
|     let width: String? | ||||
|     let height: String? | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
|     } | ||||
|   | ||||
| @@ -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)")! | ||||
|             } | ||||
|         """ | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
| import Foundation | ||||
|  | ||||
| enum PlatormTag: String { | ||||
|  | ||||
|     case droid = "d" | ||||
|     case ios = "i" | ||||
| } | ||||
|   | ||||
| @@ -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] = [] | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 { | ||||
|         """ | ||||
|         } | ||||
|          | ||||
|  | ||||
|         """ | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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." | ||||
|             } | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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)" | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
|         } | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 | ||||
|     ) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
|      | ||||
| } | ||||
|   | ||||
| @@ -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 } | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -7,4 +7,6 @@ | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| // swiftlint:disable prefixed_toplevel_constant identifier_name | ||||
|  | ||||
| public let ResgenSwiftVersion = "2.1.0" | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user