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 | # All rules here : https://realm.github.io/SwiftLint/rule-directory.html | ||||||
|   - 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 |  | ||||||
|  |  | ||||||
|  | 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" : [ |   "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", |       "identity" : "swift-argument-parser", | ||||||
|       "kind" : "remoteSourceControl", |       "kind" : "remoteSourceControl", | ||||||
| @@ -37,39 +10,12 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "identity" : "swift-syntax", |       "identity" : "swiftlintplugin", | ||||||
|       "kind" : "remoteSourceControl", |       "kind" : "remoteSourceControl", | ||||||
|       "location" : "https://github.com/apple/swift-syntax.git", |       "location" : "https://github.com/lukepistrol/SwiftLintPlugin", | ||||||
|       "state" : { |       "state" : { | ||||||
|         "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", |         "revision" : "87454f5c9ff4d644086aec2a0df1ffba678e7f3c", | ||||||
|         "version" : "509.0.2" |         "version" : "0.57.1" | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       "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" |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -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. | // The swift-tools-version declares the minimum version of Swift required to build this package. | ||||||
|  |  | ||||||
| import PackageDescription | import PackageDescription | ||||||
|  |  | ||||||
| let package = Package( | let package = Package( | ||||||
|     name: "ResgenSwift", |     name: "ResgenSwift", | ||||||
|     platforms: [.macOS(.v12)], |     platforms: [.macOS(.v14)], | ||||||
|     dependencies: [ |     dependencies: [ | ||||||
|         // Dependencies declare other packages that this package depends on. |         // Dependencies declare other packages that this package depends on. | ||||||
|         .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), |         .package( | ||||||
|         .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.1"), |             url: "https://github.com/apple/swift-argument-parser", | ||||||
|         .package(url: "https://github.com/realm/SwiftLint.git", .upToNextMajor(from: "0.54.0")), |             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: [ | ||||||
|         // Targets are the basic building blocks of a package. A target can define a module or a test suite. |         // 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", |             name: "ResgenSwift", | ||||||
|             dependencies: [ |             dependencies: [ | ||||||
|                 "ToolCore", |                 "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 |         // Helper targets | ||||||
|   | |||||||
| @@ -5,30 +5,30 @@ | |||||||
| //  Created by Loris Perret on 08/12/2023. | //  Created by Loris Perret on 08/12/2023. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct Analytics: ParsableCommand { | struct Analytics: ParsableCommand { | ||||||
|      |  | ||||||
|     // MARK: - Command Configuration |     // MARK: - Command Configuration | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static var configuration = CommandConfiguration( | ||||||
|         abstract: "Generate analytics extension file.", |         abstract: "Generate analytics extension file.", | ||||||
|         version: ResgenSwiftVersion |         version: ResgenSwiftVersion | ||||||
|     ) |     ) | ||||||
|      |  | ||||||
|     // MARK: - Static |     // MARK: - Static | ||||||
|      |  | ||||||
|     static let toolName = "Analytics" |     static let toolName = "Analytics" | ||||||
|     static let defaultExtensionName = "Analytics" |     static let defaultExtensionName = "Analytics" | ||||||
|      |  | ||||||
|     // MARK: - Command Options |     // MARK: - Command Options | ||||||
|      |  | ||||||
|     @OptionGroup var options: AnalyticsOptions |     @OptionGroup var options: AnalyticsOptions | ||||||
|      |  | ||||||
|     // MARK: - Run |     // MARK: - Run | ||||||
|      |  | ||||||
|     mutating func run() { |     mutating func run() { | ||||||
|         print("[\(Self.toolName)] Starting analytics generation") |         print("[\(Self.toolName)] Starting analytics generation") | ||||||
|         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate analytics for target: \(options.target)") |         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 } |         guard checkRequirements() else { return } | ||||||
|  |  | ||||||
|         print("[\(Self.toolName)] Will generate analytics") |         print("[\(Self.toolName)] Will generate analytics") | ||||||
|          |  | ||||||
|         // Check requirements |         // Check requirements | ||||||
|         guard checkRequirements() else { return } |         guard checkRequirements() else { return } | ||||||
|          |  | ||||||
|         // Parse input file |         // Parse input file | ||||||
|         let sections = AnalyticsFileParser.parse(options.inputFile, target: options.target) |         let sections = AnalyticsFileParser().parse(options.inputFile, target: options.target) | ||||||
|          |  | ||||||
|         // Generate extension |         // Generate extension | ||||||
|         AnalyticsGenerator.writeExtensionFiles(sections: sections, |         AnalyticsGenerator.writeExtensionFiles( | ||||||
|                                           target: options.target, |             sections: sections, | ||||||
|                                           tags: ["ios", "iosonly"], |             target: options.target, | ||||||
|                                           staticVar: options.staticMembers, |             tags: ["ios", "iosonly"], | ||||||
|                                           extensionName: options.extensionName, |             staticVar: options.staticMembers, | ||||||
|                                           extensionFilePath: options.extensionFilePath) |             extensionName: options.extensionName, | ||||||
|          |             extensionFilePath: options.extensionFilePath | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         print("[\(Self.toolName)] Analytics generated") |         print("[\(Self.toolName)] Analytics generated") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Requirements |     // MARK: - Requirements | ||||||
|      |  | ||||||
|     private func checkRequirements() -> Bool { |     private func checkRequirements() -> Bool { | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|          |  | ||||||
|         // Input file |         // Input file | ||||||
|         guard fileManager.fileExists(atPath: options.inputFile) else { |         guard fileManager.fileExists(atPath: options.inputFile) else { | ||||||
|             let error = AnalyticsError.fileNotExists(options.inputFile) |             let error = AnalyticsError.fileNotExists(options.inputFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Analytics.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         guard TrackerType.hasValidTarget(in: options.target) else { |         guard TrackerType.hasValidTarget(in: options.target) else { | ||||||
|             let error = AnalyticsError.noValidTracker(options.target) |             let error = AnalyticsError.noValidTracker(options.target) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Analytics.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Check if needed to regenerate |         // Check if needed to regenerate | ||||||
|         guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, |         guard GeneratorChecker.shouldGenerate( | ||||||
|                                               inputFilePath: options.inputFile, |             force: options.forceGeneration, | ||||||
|                                               extensionFilePath: options.extensionFilePath) else { |             inputFilePath: options.inputFile, | ||||||
|  |             extensionFilePath: options.extensionFilePath | ||||||
|  |         ) else { | ||||||
|             print("[\(Self.toolName)] Analytics are already up to date :) ") |             print("[\(Self.toolName)] Analytics are already up to date :) ") | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,13 +8,14 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum AnalyticsError: Error { | enum AnalyticsError: Error { | ||||||
|  |  | ||||||
|     case noValidTracker(String) |     case noValidTracker(String) | ||||||
|     case fileNotExists(String) |     case fileNotExists(String) | ||||||
|     case missingElement(String) |     case missingElement(String) | ||||||
|     case invalidParameter(String) |     case invalidParameter(String) | ||||||
|     case parseFailed(String) |     case parseFailed(String) | ||||||
|     case writeFile(String, String) |     case writeFile(String, String) | ||||||
|      |  | ||||||
|     var description: String { |     var description: String { | ||||||
|         switch self { |         switch self { | ||||||
|         case .noValidTracker(let inputTargets): |         case .noValidTracker(let inputTargets): | ||||||
| @@ -22,17 +23,17 @@ enum AnalyticsError: Error { | |||||||
|  |  | ||||||
|         case .fileNotExists(let filename): |         case .fileNotExists(let filename): | ||||||
|             return "error: [\(Analytics.toolName)] File \(filename) does not exists" |             return "error: [\(Analytics.toolName)] File \(filename) does not exists" | ||||||
|              |  | ||||||
|         case .missingElement(let element): |         case .missingElement(let element): | ||||||
|             return "error: [\(Analytics.toolName)] Missing \(element) for Matomo" |             return "error: [\(Analytics.toolName)] Missing \(element) for Matomo" | ||||||
|              |  | ||||||
|         case .invalidParameter(let reason): |         case .invalidParameter(let reason): | ||||||
|             return "error: [\(Analytics.toolName)] Invalid parameter \(reason)" |             return "error: [\(Analytics.toolName)] Invalid parameter \(reason)" | ||||||
|              |  | ||||||
|         case .parseFailed(let baseError): |         case .parseFailed(let baseError): | ||||||
|             return "error: [\(Analytics.toolName)] Parse input file failed: \(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)" |             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. | //  Created by Loris Perret on 08/12/2023. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable no_grouping_extension | ||||||
|  |  | ||||||
| struct AnalyticsOptions: ParsableArguments { | struct AnalyticsOptions: ParsableArguments { | ||||||
|  |  | ||||||
|     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") |     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") | ||||||
|     var forceGeneration = false |     var forceGeneration = false | ||||||
|      |  | ||||||
|     @Argument(help: "Input files where tags ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Argument(help: "Input files where tags ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var inputFile: String |     var inputFile: String | ||||||
|      |  | ||||||
|     @Option(help: "Target(s) analytics to generate. (\"matomo\" | \"firebase\")") |     @Option(help: "Target(s) analytics to generate. (\"matomo\" | \"firebase\")") | ||||||
|     var target: String |     var target: String | ||||||
|      |  | ||||||
|     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var extensionOutputPath: String |     var extensionOutputPath: String | ||||||
|      |  | ||||||
|     @Option(help: "Tell if it will generate static properties or not") |     @Option(help: "Tell if it will generate static properties or not") | ||||||
|     var staticMembers: Bool = false |     var staticMembers: Bool = false | ||||||
|      |  | ||||||
|     @Option(help: "Extension name. If not specified, it will generate a Analytics extension.") |     @Option(help: "Extension name. If not specified, it will generate a Analytics extension.") | ||||||
|     var extensionName: String = Analytics.defaultExtensionName |     var extensionName: String = Analytics.defaultExtensionName | ||||||
|      |  | ||||||
|     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Analytics{extensionSuffix}.swift") |     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Analytics{extensionSuffix}.swift") | ||||||
|     var extensionSuffix: String? |     var extensionSuffix: String? | ||||||
| } | } | ||||||
| @@ -34,13 +37,14 @@ struct AnalyticsOptions: ParsableArguments { | |||||||
| // MARK: - Computed var | // MARK: - Computed var | ||||||
|  |  | ||||||
| extension AnalyticsOptions { | extension AnalyticsOptions { | ||||||
|  |  | ||||||
|     var extensionFileName: String { |     var extensionFileName: String { | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             return "\(extensionName)+\(extensionSuffix).swift" |             return "\(extensionName)+\(extensionSuffix).swift" | ||||||
|         } |         } | ||||||
|         return "\(extensionName).swift" |         return "\(extensionName).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePath: String { |     var extensionFilePath: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileName)" |         "\(extensionOutputPath)/\(extensionFileName)" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -5,112 +5,147 @@ | |||||||
| //  Created by Loris Perret on 08/12/2023. | //  Created by Loris Perret on 08/12/2023. | ||||||
| // | // | ||||||
|  |  | ||||||
|  | import CoreVideo | ||||||
| import Foundation | import Foundation | ||||||
| import ToolCore | import ToolCore | ||||||
| import CoreVideo |  | ||||||
|  |  | ||||||
| class AnalyticsGenerator { | // Disabled cause it's a pain to handle in generated string | ||||||
|     static var targets: [TrackerType] = [] |  | ||||||
|  |  | ||||||
|     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 |         // Get target type from enum | ||||||
|         let targetsString: [String] = target.components(separatedBy: " ") |         let targetsString: [String] = target.components(separatedBy: " ") | ||||||
|                  |         let targets = { | ||||||
|         TrackerType.allCases.forEach { enumTarget in |             var targets = [TrackerType]() | ||||||
|             if targetsString.contains(enumTarget.value) { |             TrackerType.allCases.forEach { enumTarget in | ||||||
|                 targets.append(enumTarget) |                 if targetsString.contains(enumTarget.value) { | ||||||
|  |                     targets.append(enumTarget) | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |             return targets | ||||||
|          |         }() | ||||||
|  |  | ||||||
|         // Get extension content |         // Get extension content | ||||||
|         let extensionFileContent = Self.getExtensionContent(sections: sections, |         let extensionFileContent = getExtensionContent( | ||||||
|                                                             tags: tags, |             targets: targets, | ||||||
|                                                             staticVar: staticVar, |             sections: sections, | ||||||
|                                                             extensionName: extensionName) |             tags: tags, | ||||||
|          |             staticVar: staticVar, | ||||||
|  |             extensionName: extensionName | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Write content |         // Write content | ||||||
|         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) |         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) | ||||||
|         do { |         do { | ||||||
|             try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) |             try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = AnalyticsError.writeFile(extensionFilePath, error.localizedDescription) |             let error = AnalyticsError.writeFile(extensionFilePath, error.localizedDescription) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Analytics.exit(withError: error) |             Analytics.exit(withError: error) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Extension content |     // 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), |             getHeader( | ||||||
|             Self.getProperties(sections: sections, tags: tags, staticVar: staticVar), |                 targets: targets, | ||||||
|             Self.getFooter() |                 extensionClassname: extensionName, | ||||||
|  |                 staticVar: staticVar | ||||||
|  |             ), | ||||||
|  |             getProperties( | ||||||
|  |                 sections: sections, | ||||||
|  |                 tags: tags, | ||||||
|  |                 staticVar: staticVar | ||||||
|  |             ), | ||||||
|  |             getFooter() | ||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Extension part |     // 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) |         // Generated by ResgenSwift.\(Analytics.toolName) \(ResgenSwiftVersion) | ||||||
|          |  | ||||||
|         \(Self.getImport()) |         \(getImport(targets: targets)) | ||||||
|          |  | ||||||
|         \(Self.getAnalyticsProtocol()) |         \(getAnalyticsProtocol(targets: targets)) | ||||||
|         // MARK: - Manager |         // MARK: - Manager | ||||||
|          |  | ||||||
|         class AnalyticsManager { |         class AnalyticsManager { | ||||||
|  |  | ||||||
|             static var shared = AnalyticsManager() |             static var shared = AnalyticsManager() | ||||||
|              |  | ||||||
|             // MARK: - Properties |             // MARK: - Properties | ||||||
|          |  | ||||||
|             var managers: [AnalyticsManagerProtocol] = [] |             var managers: [AnalyticsManagerProtocol] = [] | ||||||
|          |  | ||||||
|             \(Self.getEnabledContent()) |             \(getEnabledContent()) | ||||||
|          |  | ||||||
|             \(Self.getAnalyticsProperties()) |             \(getAnalyticsProperties(targets: targets)) | ||||||
|                  |  | ||||||
|             \(Self.getPrivateLogFunction()) |             \(getPrivateLogFunction()) | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getEnabledContent() -> String { |     private static func getEnabledContent() -> String { | ||||||
|         """ |         """ | ||||||
|         private var isEnabled: Bool = true |         private var isEnabled: Bool = true | ||||||
|          |  | ||||||
|             // MARK: - Methods |             // MARK: - Methods | ||||||
|          |  | ||||||
|             func setAnalyticsEnabled(_ enable: Bool) { |             func setAnalyticsEnabled(_ enable: Bool) { | ||||||
|                 isEnabled = enable |                 isEnabled = enable | ||||||
|             } |             } | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getImport() -> String { |     private static func getImport(targets: [TrackerType]) -> String { | ||||||
|         var result: [String] = [] |         var result: [String] = [] | ||||||
|          |  | ||||||
|         if targets.contains(TrackerType.matomo) { |         if targets.contains(TrackerType.matomo) { | ||||||
|             result.append("import MatomoTracker") |             result.append("import MatomoTracker") | ||||||
|         } |         } | ||||||
|         if targets.contains(TrackerType.firebase) { |         if targets.contains(TrackerType.firebase) { | ||||||
|             result.append("import FirebaseAnalytics") |             result.append("import FirebaseAnalytics") | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return result.joined(separator: "\n") |         return result.joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getPrivateLogFunction() -> String { |     private static func getPrivateLogFunction() -> String { | ||||||
|         """ |         """ | ||||||
|         private func logScreen(name: String, path: String) { |         private func logScreen(name: String, path: String) { | ||||||
|                 guard isEnabled else { return } |                 guard isEnabled else { return } | ||||||
|          |  | ||||||
|                 managers.forEach { manager in |                 managers.forEach { manager in | ||||||
|                     manager.logScreen(name: name, path: path) |                     manager.logScreen(name: name, path: path) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             private func logEvent( |             private func logEvent( | ||||||
|                 name: String, |                 name: String, | ||||||
|                 action: String, |                 action: String, | ||||||
| @@ -118,7 +153,7 @@ class AnalyticsGenerator { | |||||||
|                 params: [String: Any]? |                 params: [String: Any]? | ||||||
|             ) { |             ) { | ||||||
|                 guard isEnabled else { return } |                 guard isEnabled else { return } | ||||||
|          |  | ||||||
|                 managers.forEach { manager in |                 managers.forEach { manager in | ||||||
|                     manager.logEvent( |                     manager.logEvent( | ||||||
|                         name: name, |                         name: name, | ||||||
| @@ -130,18 +165,18 @@ class AnalyticsGenerator { | |||||||
|             } |             } | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getAnalyticsProperties() -> String { |     private static func getAnalyticsProperties(targets: [TrackerType]) -> String { | ||||||
|         var header = "" |         var header = "" | ||||||
|         var content: [String] = [] |         var content: [String] = [] | ||||||
|         let footer = "    }" |         let footer = "    }" | ||||||
|          |  | ||||||
|         if targets.contains(TrackerType.matomo) { |         if targets.contains(TrackerType.matomo) { | ||||||
|             header = "func configure(siteId: String, url: String) {" |             header = "func configure(siteId: String, url: String) {" | ||||||
|         } else if targets.contains(TrackerType.firebase) { |         } else if targets.contains(TrackerType.firebase) { | ||||||
|             header = "func configure() {" |             header = "func configure() {" | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if targets.contains(TrackerType.matomo) { |         if targets.contains(TrackerType.matomo) { | ||||||
|             content.append(""" |             content.append(""" | ||||||
|                     managers.append( |                     managers.append( | ||||||
| @@ -155,7 +190,7 @@ class AnalyticsGenerator { | |||||||
|         if targets.contains(TrackerType.firebase) { |         if targets.contains(TrackerType.firebase) { | ||||||
|             content.append("        managers.append(FirebaseAnalyticsManager())") |             content.append("        managers.append(FirebaseAnalyticsManager())") | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return [ |         return [ | ||||||
|             header, |             header, | ||||||
|             content.joined(separator: "\n"), |             content.joined(separator: "\n"), | ||||||
| @@ -163,12 +198,13 @@ class AnalyticsGenerator { | |||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getAnalyticsProtocol() -> String { |     private static func getAnalyticsProtocol(targets: [TrackerType]) -> String { | ||||||
|         let proto = """ |         let proto = """ | ||||||
|         // MARK: - Protocol |         // MARK: - Protocol | ||||||
|          |  | ||||||
|         protocol AnalyticsManagerProtocol { |         protocol AnalyticsManagerProtocol { | ||||||
|  |  | ||||||
|             func logScreen(name: String, path: String) |             func logScreen(name: String, path: String) | ||||||
|             func logEvent( |             func logEvent( | ||||||
|                 name: String, |                 name: String, | ||||||
| @@ -177,36 +213,40 @@ class AnalyticsGenerator { | |||||||
|                 params: [String: Any]? |                 params: [String: Any]? | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|          |  | ||||||
|         var result: [String] = [proto] |         var result: [String] = [proto] | ||||||
|          |  | ||||||
|         if targets.contains(TrackerType.matomo) { |         if targets.contains(TrackerType.matomo) { | ||||||
|             result.append(MatomoGenerator.service) |             result.append(MatomoGenerator.service) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if targets.contains(TrackerType.firebase) { |         if targets.contains(TrackerType.firebase) { | ||||||
|             result.append(FirebaseGenerator.service) |             result.append(FirebaseGenerator.service) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return result.joined(separator: "\n") |         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 |         sections | ||||||
|             .compactMap { section in |             .compactMap { section in | ||||||
|                 // Check that at least one string will be generated |                 // Check that at least one string will be generated | ||||||
|                 guard section.hasOneOrMoreMatchingTags(tags: tags) else { |                 guard section.hasOneOrMoreMatchingTags(tags: tags) else { | ||||||
|                     return nil// Go to next section |                     return nil// Go to next section | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 var res = "\n    // MARK: - \(section.id)" |                 var res = "\n    // MARK: - \(section.id)" | ||||||
|                 section.definitions.forEach { definition in |                 section.definitions.forEach { definition in | ||||||
|                     guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else { |                     guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else { | ||||||
|                         return // Go to next definition |                         return // Go to next definition | ||||||
|                     } |                     } | ||||||
|                      |  | ||||||
|                     if staticVar { |                     if staticVar { | ||||||
|                         res += "\n\n\(definition.getStaticProperty())" |                         res += "\n\n\(definition.getStaticProperty())" | ||||||
|                     } else { |                     } else { | ||||||
| @@ -217,11 +257,11 @@ class AnalyticsGenerator { | |||||||
|             } |             } | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getFooter() -> String { |     private static func getFooter() -> String { | ||||||
|         """ |         """ | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,14 +11,14 @@ enum FirebaseGenerator { | |||||||
|  |  | ||||||
|     static var service: String { |     static var service: String { | ||||||
|         [ |         [ | ||||||
|             FirebaseGenerator.header, |             Self.header, | ||||||
|             FirebaseGenerator.logScreen, |             Self.logScreen, | ||||||
|             FirebaseGenerator.logEvent, |             Self.logEvent, | ||||||
|             FirebaseGenerator.footer |             Self.footer | ||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Private vars |     // MARK: - Private vars | ||||||
|  |  | ||||||
|     private static var header: String { |     private static var header: String { | ||||||
| @@ -28,23 +28,23 @@ enum FirebaseGenerator { | |||||||
|         class FirebaseAnalyticsManager: AnalyticsManagerProtocol { |         class FirebaseAnalyticsManager: AnalyticsManagerProtocol { | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static var logScreen: String { |     private static var logScreen: String { | ||||||
|         """ |         """ | ||||||
|             func logScreen(name: String, path: String) { |             func logScreen(name: String, path: String) { | ||||||
|                 var parameters = [ |                 var parameters = [ | ||||||
|                     AnalyticsParameterScreenName: name as NSObject |                     AnalyticsParameterScreenName: name as NSObject | ||||||
|                 ] |                 ] | ||||||
|          |  | ||||||
|                 Analytics.logEvent( |                 Analytics.logEvent( | ||||||
|                     AnalyticsEventScreenView, |                     AnalyticsEventScreenView, | ||||||
|                     parameters: parameters |                     parameters: parameters | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static var logEvent: String { |     private static var logEvent: String { | ||||||
|         """ |         """ | ||||||
|             func logEvent( |             func logEvent( | ||||||
| @@ -57,7 +57,7 @@ enum FirebaseGenerator { | |||||||
|                     "action": action as NSObject, |                     "action": action as NSObject, | ||||||
|                     "category": category as NSObject, |                     "category": category as NSObject, | ||||||
|                 ] |                 ] | ||||||
|                  |  | ||||||
|                 if let supplementaryParameters = params { |                 if let supplementaryParameters = params { | ||||||
|                     for (newKey, newValue) in supplementaryParameters { |                     for (newKey, newValue) in supplementaryParameters { | ||||||
|                         if parameters.contains(where: { (key: String, value: NSObject) in |                         if parameters.contains(where: { (key: String, value: NSObject) in | ||||||
| @@ -65,11 +65,11 @@ enum FirebaseGenerator { | |||||||
|                         }) { |                         }) { | ||||||
|                             continue |                             continue | ||||||
|                         } |                         } | ||||||
|                          |  | ||||||
|                         parameters[newKey] = newValue as? NSObject |                         parameters[newKey] = newValue as? NSObject | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 Analytics.logEvent( |                 Analytics.logEvent( | ||||||
|                     name.replacingOccurrences(of: [" "], with: "_"), |                     name.replacingOccurrences(of: [" "], with: "_"), | ||||||
|                     parameters: parameters |                     parameters: parameters | ||||||
| @@ -77,11 +77,11 @@ enum FirebaseGenerator { | |||||||
|             } |             } | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static var footer: String { |     private static var footer: String { | ||||||
|         """ |         """ | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // | // | ||||||
| //  MatomoGenerator.swift | //  MatomoGenerator.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Loris Perret on 05/12/2023. | //  Created by Loris Perret on 05/12/2023. | ||||||
| // | // | ||||||
| @@ -11,15 +11,15 @@ enum MatomoGenerator { | |||||||
|  |  | ||||||
|     static var service: String { |     static var service: String { | ||||||
|         [ |         [ | ||||||
|             MatomoGenerator.header, |             Self.header, | ||||||
|             MatomoGenerator.setup, |             Self.setup, | ||||||
|             MatomoGenerator.logScreen, |             Self.logScreen, | ||||||
|             MatomoGenerator.logEvent, |             Self.logEvent, | ||||||
|             MatomoGenerator.footer |             Self.footer | ||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Private vars |     // MARK: - Private vars | ||||||
|  |  | ||||||
|     private static var header: String { |     private static var header: String { | ||||||
| @@ -27,18 +27,18 @@ enum MatomoGenerator { | |||||||
|         // MARK: - Matomo |         // MARK: - Matomo | ||||||
|  |  | ||||||
|         class MatomoAnalyticsManager: AnalyticsManagerProtocol { |         class MatomoAnalyticsManager: AnalyticsManagerProtocol { | ||||||
|              |  | ||||||
|             // MARK: - Properties |             // MARK: - Properties | ||||||
|          |  | ||||||
|             private var tracker: MatomoTracker |             private var tracker: MatomoTracker | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static var setup: String { |     private static var setup: String { | ||||||
|         """ |         """ | ||||||
|             // MARK: - Init |             // MARK: - Init | ||||||
|          |  | ||||||
|             init(siteId: String, url: String) { |             init(siteId: String, url: String) { | ||||||
|                 debugPrint("[Matomo service] Server URL: \\(url)") |                 debugPrint("[Matomo service] Server URL: \\(url)") | ||||||
|                 debugPrint("[Matomo service] Site ID: \\(siteId)") |                 debugPrint("[Matomo service] Site ID: \\(siteId)") | ||||||
| @@ -46,40 +46,40 @@ enum MatomoGenerator { | |||||||
|                     siteId: siteId, |                     siteId: siteId, | ||||||
|                     baseURL: URL(string: url)! |                     baseURL: URL(string: url)! | ||||||
|                 ) |                 ) | ||||||
|                  |  | ||||||
|                 #if DEBUG |                 #if DEBUG | ||||||
|                     tracker.dispatchInterval = 5 |                     tracker.dispatchInterval = 5 | ||||||
|                 #endif |                 #endif | ||||||
|                  |  | ||||||
|                 #if DEBUG |                 #if DEBUG | ||||||
|                     tracker.logger = DefaultLogger(minLevel: .verbose) |                     tracker.logger = DefaultLogger(minLevel: .verbose) | ||||||
|                 #endif |                 #endif | ||||||
|          |  | ||||||
|                 debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") |                 debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") | ||||||
|                 debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") |                 debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             // MARK: - Methods |             // MARK: - Methods | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static var logScreen: String { |     private static var logScreen: String { | ||||||
|         """ |         """ | ||||||
|             func logScreen(name: String, path: String) { |             func logScreen(name: String, path: String) { | ||||||
|                 guard !tracker.isOptedOut else { return } |                 guard !tracker.isOptedOut else { return } | ||||||
|                 guard let trackerUrl = tracker.contentBase?.absoluteString else { return } |                 guard let trackerUrl = tracker.contentBase?.absoluteString else { return } | ||||||
|          |  | ||||||
|                 let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS") |                 let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS") | ||||||
|                 tracker.track( |                 tracker.track( | ||||||
|                     view: [name], |                     view: [name], | ||||||
|                     url: urlString |                     url: urlString | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static var logEvent: String { |     private static var logEvent: String { | ||||||
|         """ |         """ | ||||||
|             func logEvent( |             func logEvent( | ||||||
| @@ -89,7 +89,7 @@ enum MatomoGenerator { | |||||||
|                 params: [String: Any]? |                 params: [String: Any]? | ||||||
|             ) { |             ) { | ||||||
|                 guard !tracker.isOptedOut else { return } |                 guard !tracker.isOptedOut else { return } | ||||||
|          |  | ||||||
|                 tracker.track( |                 tracker.track( | ||||||
|                     eventWithCategory: category, |                     eventWithCategory: category, | ||||||
|                     action: action, |                     action: action, | ||||||
| @@ -100,11 +100,11 @@ enum MatomoGenerator { | |||||||
|             } |             } | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static var footer: String { |     private static var footer: String { | ||||||
|         """ |         """ | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,21 +8,24 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| class AnalyticsCategory { | class AnalyticsCategory { | ||||||
|  |  | ||||||
|  |     // MARK: - Properties | ||||||
|  |  | ||||||
|     let id: String // OnBoarding |     let id: String // OnBoarding | ||||||
|     var definitions = [AnalyticsDefinition]() |     var definitions = [AnalyticsDefinition]() | ||||||
|      |  | ||||||
|     // MARK: - Init |     // MARK: - Init | ||||||
|  |  | ||||||
|     init(id: String) { |     init(id: String) { | ||||||
|         self.id = id |         self.id = id | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Methods |     // MARK: - Methods | ||||||
|  |  | ||||||
|     func hasOneOrMoreMatchingTags(tags: [String]) -> Bool { |     func hasOneOrMoreMatchingTags(tags: [String]) -> Bool { | ||||||
|         let allTags = definitions.flatMap { $0.tags } |         let allTags = definitions.flatMap { $0.tags } | ||||||
|         let allTagsSet = Set(allTags) |         let allTagsSet = Set(allTags) | ||||||
|          |  | ||||||
|         let intersection = Set(tags).intersection(allTagsSet) |         let intersection = Set(tags).intersection(allTagsSet) | ||||||
|         if intersection.isEmpty { |         if intersection.isEmpty { | ||||||
|             return false |             return false | ||||||
|   | |||||||
| @@ -9,6 +9,9 @@ import Foundation | |||||||
| import ToolCore | import ToolCore | ||||||
|  |  | ||||||
| class AnalyticsDefinition { | class AnalyticsDefinition { | ||||||
|  |  | ||||||
|  |     // MARK: - Properties | ||||||
|  |  | ||||||
|     let id: String |     let id: String | ||||||
|     var name: String |     var name: String | ||||||
|     var path: String = "" |     var path: String = "" | ||||||
| @@ -18,7 +21,7 @@ class AnalyticsDefinition { | |||||||
|     var tags: [String] = [] |     var tags: [String] = [] | ||||||
|     var parameters: [AnalyticsParameter] = [] |     var parameters: [AnalyticsParameter] = [] | ||||||
|     var type: TagType |     var type: TagType | ||||||
|      |  | ||||||
|     // MARK: - Init |     // MARK: - Init | ||||||
|  |  | ||||||
|     init(id: String, name: String, type: TagType) { |     init(id: String, name: String, type: TagType) { | ||||||
| @@ -26,7 +29,7 @@ class AnalyticsDefinition { | |||||||
|         self.name = name |         self.name = name | ||||||
|         self.type = type |         self.type = type | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Methods |     // MARK: - Methods | ||||||
|  |  | ||||||
|     func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool { |     func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool { | ||||||
| @@ -35,7 +38,7 @@ class AnalyticsDefinition { | |||||||
|         } |         } | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Private Methods |     // MARK: - Private Methods | ||||||
|  |  | ||||||
|     private func getFuncName() -> String { |     private func getFuncName() -> String { | ||||||
| @@ -43,24 +46,24 @@ class AnalyticsDefinition { | |||||||
|         id.components(separatedBy: "_").forEach { word in |         id.components(separatedBy: "_").forEach { word in | ||||||
|             pascalCaseTitle.append(contentsOf: word.uppercasedFirst()) |             pascalCaseTitle.append(contentsOf: word.uppercasedFirst()) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return "log\(type == .screen ? "Screen" : "Event")\(pascalCaseTitle)" |         return "log\(type == .screen ? "Screen" : "Event")\(pascalCaseTitle)" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private func getParameters() -> String { |     private func getParameters() -> String { | ||||||
|         var params = parameters |         var params = parameters | ||||||
|         var result: String |         var result: String | ||||||
|          |  | ||||||
|         if type == .screen { |         if type == .screen { | ||||||
|             params = params.filter { param in |             params = params.filter { param in | ||||||
|                 !param.replaceIn.isEmpty |                 !param.replaceIn.isEmpty | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         let paramsString = params.map { parameter in |         let paramsString = params.map { parameter in | ||||||
|             "\(parameter.name): \(parameter.type)" |             "\(parameter.name): \(parameter.type)" | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if paramsString.count > 2 { |         if paramsString.count > 2 { | ||||||
|             result = """ |             result = """ | ||||||
|             ( |             ( | ||||||
| @@ -72,10 +75,10 @@ class AnalyticsDefinition { | |||||||
|             (\(paramsString.joined(separator: ", "))) |             (\(paramsString.joined(separator: ", "))) | ||||||
|             """ |             """ | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return result |         return result | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private func replaceIn() { |     private func replaceIn() { | ||||||
|         for parameter in parameters { |         for parameter in parameters { | ||||||
|             for rep in parameter.replaceIn { |             for rep in parameter.replaceIn { | ||||||
| @@ -89,15 +92,15 @@ class AnalyticsDefinition { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private func getlogFunction() -> String { |     private func getlogFunction() -> String { | ||||||
|         var params: [String] = [] |         var params: [String] = [] | ||||||
|         var result: String |         var result: String | ||||||
|          |  | ||||||
|         let supplementaryParams = parameters.filter { param in |         let supplementaryParams = parameters.filter { param in | ||||||
|             param.replaceIn.isEmpty |             param.replaceIn.isEmpty | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         supplementaryParams.forEach { param in |         supplementaryParams.forEach { param in | ||||||
|             params.append("\"\(param.name)\": \(param.name)") |             params.append("\"\(param.name)\": \(param.name)") | ||||||
|         } |         } | ||||||
| @@ -115,7 +118,7 @@ class AnalyticsDefinition { | |||||||
|         } else { |         } else { | ||||||
|             result = "[:]" |             result = "[:]" | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if type == .screen { |         if type == .screen { | ||||||
|             return """ |             return """ | ||||||
|             logScreen( |             logScreen( | ||||||
| @@ -134,9 +137,9 @@ class AnalyticsDefinition { | |||||||
|             """ |             """ | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Raw strings |     // MARK: - Raw strings | ||||||
|      |  | ||||||
|     func getProperty() -> String { |     func getProperty() -> String { | ||||||
|         replaceIn() |         replaceIn() | ||||||
|         return """ |         return """ | ||||||
| @@ -145,7 +148,7 @@ class AnalyticsDefinition { | |||||||
|             } |             } | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getStaticProperty() -> String { |     func getStaticProperty() -> String { | ||||||
|         replaceIn() |         replaceIn() | ||||||
|         return """ |         return """ | ||||||
|   | |||||||
| @@ -8,37 +8,42 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| struct AnalyticsFile: Codable { | struct AnalyticsFile: Codable { | ||||||
|  |  | ||||||
|     var categories: [AnalyticsCategoryDTO] |     var categories: [AnalyticsCategoryDTO] | ||||||
| } | } | ||||||
|  |  | ||||||
| struct AnalyticsCategoryDTO: Codable { | struct AnalyticsCategoryDTO: Codable { | ||||||
|  |  | ||||||
|     var id: String |     var id: String | ||||||
|     var screens: [AnalyticsDefinitionScreenDTO]? |     var screens: [AnalyticsDefinitionScreenDTO]? | ||||||
|     var events: [AnalyticsDefinitionEventDTO]? |     var events: [AnalyticsDefinitionEventDTO]? | ||||||
| } | } | ||||||
|  |  | ||||||
| struct AnalyticsDefinitionScreenDTO: Codable { | struct AnalyticsDefinitionScreenDTO: Codable { | ||||||
|  |  | ||||||
|     var id: String |     var id: String | ||||||
|     var name: String |     var name: String | ||||||
|     var tags: String |     var tags: String | ||||||
|     var comments: String? |     var comments: String? | ||||||
|     var parameters: [AnalyticsParameterDTO]? |     var parameters: [AnalyticsParameterDTO]? | ||||||
|      |  | ||||||
|     var path: String? |     var path: String? | ||||||
| } | } | ||||||
|  |  | ||||||
| struct AnalyticsDefinitionEventDTO: Codable { | struct AnalyticsDefinitionEventDTO: Codable { | ||||||
|  |  | ||||||
|     var id: String |     var id: String | ||||||
|     var name: String |     var name: String | ||||||
|     var tags: String |     var tags: String | ||||||
|     var comments: String? |     var comments: String? | ||||||
|     var parameters: [AnalyticsParameterDTO]? |     var parameters: [AnalyticsParameterDTO]? | ||||||
|      |  | ||||||
|     var category: String? |     var category: String? | ||||||
|     var action: String? |     var action: String? | ||||||
| } | } | ||||||
|  |  | ||||||
| struct AnalyticsParameterDTO: Codable { | struct AnalyticsParameterDTO: Codable { | ||||||
|  |  | ||||||
|     var name: String |     var name: String | ||||||
|     var type: String |     var type: String | ||||||
|     var replaceIn: String? |     var replaceIn: String? | ||||||
|   | |||||||
| @@ -8,12 +8,15 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| class AnalyticsParameter { | class AnalyticsParameter { | ||||||
|  |  | ||||||
|  |     // MARK: - Properties | ||||||
|  |  | ||||||
|     var name: String |     var name: String | ||||||
|     var type: String |     var type: String | ||||||
|     var replaceIn: [String] = [] |     var replaceIn: [String] = [] | ||||||
|      |  | ||||||
|     // MARK: - Init |     // MARK: - Init | ||||||
|      |  | ||||||
|     init(name: String, type: String) { |     init(name: String, type: String) { | ||||||
|         self.name = name |         self.name = name | ||||||
|         self.type = type |         self.type = type | ||||||
|   | |||||||
| @@ -8,8 +8,9 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension AnalyticsDefinition { | extension AnalyticsDefinition { | ||||||
|      |  | ||||||
|     enum TagType { |     enum TagType { | ||||||
|  |  | ||||||
|         case screen |         case screen | ||||||
|         case event |         case event | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,7 +7,8 @@ | |||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum TrackerType: CaseIterable { | enum TrackerType: CaseIterable, Sendable { | ||||||
|  |  | ||||||
|     case matomo |     case matomo | ||||||
|     case firebase |     case firebase | ||||||
|  |  | ||||||
| @@ -15,6 +16,7 @@ enum TrackerType: CaseIterable { | |||||||
|         switch self { |         switch self { | ||||||
|         case .matomo: |         case .matomo: | ||||||
|             "matomo" |             "matomo" | ||||||
|  |  | ||||||
|         case .firebase: |         case .firebase: | ||||||
|             "firebase" |             "firebase" | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -9,16 +9,54 @@ import Foundation | |||||||
| import Yams | import Yams | ||||||
|  |  | ||||||
| class AnalyticsFileParser { | class AnalyticsFileParser { | ||||||
|     private static var inputFile: String = "" |  | ||||||
|     private static var target: String = "" |     // MARK: - Properties | ||||||
|      |  | ||||||
|     private static func parseYaml() -> AnalyticsFile { |     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 { |         guard let data = FileManager().contents(atPath: inputFile) else { | ||||||
|             let error = AnalyticsError.fileNotExists(inputFile) |             let error = AnalyticsError.fileNotExists(inputFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Analytics.exit(withError: error) |             Analytics.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         do { |         do { | ||||||
|             let tagFile = try YAMLDecoder().decode(AnalyticsFile.self, from: data) |             let tagFile = try YAMLDecoder().decode(AnalyticsFile.self, from: data) | ||||||
|             return tagFile |             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 |         parameters.map { dtoParameter in | ||||||
|             // Type |             // Type | ||||||
|              |  | ||||||
|             let type = dtoParameter.type.uppercasedFirst() |             let type = dtoParameter.type.uppercasedFirst() | ||||||
|  |  | ||||||
|             guard  |             guard | ||||||
|                 type == "String" || |                 type == "String" || | ||||||
|                 type == "Int" || |                 type == "Int" || | ||||||
|                 type == "Double" || |                 type == "Double" || | ||||||
| @@ -59,7 +96,7 @@ class AnalyticsFileParser { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static func getTagDefinition( |     private func getTagDefinition( | ||||||
|         id: String, |         id: String, | ||||||
|         name: String, |         name: String, | ||||||
|         type: AnalyticsDefinition.TagType, |         type: AnalyticsDefinition.TagType, | ||||||
| @@ -72,20 +109,20 @@ class AnalyticsFileParser { | |||||||
|             .components(separatedBy: ",") |             .components(separatedBy: ",") | ||||||
|             .map { $0.removeLeadingTrailingWhitespace() } |             .map { $0.removeLeadingTrailingWhitespace() } | ||||||
|  |  | ||||||
|         if let comments = comments { |         if let comments { | ||||||
|             definition.comments = comments |             definition.comments = comments | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if let parameters = parameters { |         if let parameters { | ||||||
|             definition.parameters = Self.getParameters(from: parameters) |             definition.parameters = getParameters(from: parameters) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return definition |         return definition | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getTagDefinitionScreen(from screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] { |     private func getTagDefinitionScreen(from screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] { | ||||||
|         screens.map { screen in |         screens.map { screen in | ||||||
|             let definition: AnalyticsDefinition = Self.getTagDefinition( |             let definition: AnalyticsDefinition = getTagDefinition( | ||||||
|                 id: screen.id, |                 id: screen.id, | ||||||
|                 name: screen.name, |                 name: screen.name, | ||||||
|                 type: .screen, |                 type: .screen, | ||||||
| @@ -109,10 +146,10 @@ class AnalyticsFileParser { | |||||||
|             return definition |             return definition | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getTagDefinitionEvent(from events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] { |     private func getTagDefinitionEvent(from events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] { | ||||||
|         events.map { event in |         events.map { event in | ||||||
|             let definition: AnalyticsDefinition = Self.getTagDefinition( |             let definition: AnalyticsDefinition = getTagDefinition( | ||||||
|                 id: event.id, |                 id: event.id, | ||||||
|                 name: event.name, |                 name: event.name, | ||||||
|                 type: .event, |                 type: .event, | ||||||
| @@ -144,35 +181,4 @@ class AnalyticsFileParser { | |||||||
|             return definition |             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 | //  main.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 20/12/2021. | //  Created by Thibaut Schmitt on 20/12/2021. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore | @preconcurrency import ArgumentParser | ||||||
| import Foundation | import Foundation | ||||||
| import ArgumentParser | import ToolCore | ||||||
|  |  | ||||||
| struct Colors: ParsableCommand { | struct Colors: ParsableCommand { | ||||||
|      |  | ||||||
|     // MARK: - CommandConfiguration |     // MARK: - CommandConfiguration | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static let configuration = CommandConfiguration( | ||||||
|         abstract: "A utility for generate colors assets and their getters.", |         abstract: "A utility for generate colors assets and their getters.", | ||||||
|         version: ResgenSwiftVersion |         version: ResgenSwiftVersion | ||||||
|     ) |     ) | ||||||
|      |  | ||||||
|     // MARK: - Static |     // MARK: - Static | ||||||
|      |  | ||||||
|     static let toolName = "Color" |     static let toolName = "Color" | ||||||
|     static let defaultExtensionName = "Color" |     static let defaultExtensionName = "Color" | ||||||
|     static let defaultExtensionNameUIKit = "UIColor" |     static let defaultExtensionNameUIKit = "UIColor" | ||||||
|     static let assetsColorsFolderName = "Colors" |     static let assetsColorsFolderName = "Colors" | ||||||
|      |  | ||||||
|     // MARK: - Command options |     // MARK: - Command options | ||||||
|      |  | ||||||
|     @OptionGroup var options: ColorsToolOptions |     @OptionGroup var options: ColorsToolOptions | ||||||
|      |  | ||||||
|     // MARK: - Run |     // MARK: - Run | ||||||
|      |  | ||||||
|     public func run() throws { |     func run() throws { | ||||||
|         print("[\(Self.toolName)] Starting colors generation") |         print("[\(Self.toolName)] Starting colors generation") | ||||||
|                  |  | ||||||
|         // Check requirements |         // Check requirements | ||||||
|         guard checkRequirements() else { return } |         guard checkRequirements() else { return } | ||||||
|          |  | ||||||
|         print("[\(Self.toolName)] Will generate colors") |         print("[\(Self.toolName)] Will generate colors") | ||||||
|          |  | ||||||
|         // Delete current colors |         // Delete current colors | ||||||
|         deleteCurrentColors() |         deleteCurrentColors() | ||||||
|  |  | ||||||
|         // Get colors to generate |         // Get colors to generate | ||||||
|         let parsedColors = ColorFileParser.parse(options.inputFile, |         let parsedColors = ColorFileParser.parse( | ||||||
|                                                 colorStyle: options.style) |             options.inputFile, | ||||||
|  |             colorStyle: options.style | ||||||
|  |         ) | ||||||
|         // -> Time: 0.0020350217819213867 seconds |         // -> Time: 0.0020350217819213867 seconds | ||||||
|  |  | ||||||
|         // Generate all colors in xcassets |         // Generate all colors in xcassets | ||||||
|         ColorXcassetHelper.generateXcassetColors(colors: parsedColors, |         ColorXcassetHelper.generateXcassetColors( | ||||||
|                                                  to: options.xcassetsPath) |             colors: parsedColors, | ||||||
|  |             to: options.xcassetsPath | ||||||
|  |         ) | ||||||
|         // -> Time: 3.4505380392074585 seconds |         // -> Time: 3.4505380392074585 seconds | ||||||
|  |  | ||||||
|         // Generate extension |         // Generate extension | ||||||
|         ColorExtensionGenerator.writeExtensionFile(colors: parsedColors, |         ColorExtensionGenerator.writeExtensionFile( | ||||||
|                                                    staticVar: options.staticMembers, |             colors: parsedColors, | ||||||
|                                                    extensionName: options.extensionName, |             staticVar: options.staticMembers, | ||||||
|                                                    extensionFilePath: options.extensionFilePath, |             extensionName: options.extensionName, | ||||||
|                                                    isSwiftUI: true) |             extensionFilePath: options.extensionFilePath, | ||||||
|          |             isSwiftUI: true | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Generate extension |         // Generate extension | ||||||
|         ColorExtensionGenerator.writeExtensionFile(colors: parsedColors, |         ColorExtensionGenerator.writeExtensionFile( | ||||||
|                                                    staticVar: options.staticMembers, |             colors: parsedColors, | ||||||
|                                                    extensionName: options.extensionNameUIKit, |             staticVar: options.staticMembers, | ||||||
|                                                    extensionFilePath: options.extensionFilePathUIKit, |             extensionName: options.extensionNameUIKit, | ||||||
|                                                    isSwiftUI: false) |             extensionFilePath: options.extensionFilePathUIKit, | ||||||
|          |             isSwiftUI: false | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         print("[\(Self.toolName)] Colors generated") |         print("[\(Self.toolName)] Colors generated") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Requirements |     // MARK: - Requirements | ||||||
|      |  | ||||||
|     private func checkRequirements() -> Bool { |     private func checkRequirements() -> Bool { | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|          |  | ||||||
|         // Check if input file exists |         // Check if input file exists | ||||||
|         guard fileManager.fileExists(atPath: options.inputFile) else { |         guard fileManager.fileExists(atPath: options.inputFile) else { | ||||||
|             let error = ColorsToolError.fileNotExists(options.inputFile) |             let error = ColorsToolError.fileNotExists(options.inputFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Colors.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Check if xcassets file exists |         // Check if xcassets file exists | ||||||
|         guard fileManager.fileExists(atPath: options.xcassetsPath) else { |         guard fileManager.fileExists(atPath: options.xcassetsPath) else { | ||||||
|             let error = ColorsToolError.fileNotExists(options.xcassetsPath) |             let error = ColorsToolError.fileNotExists(options.xcassetsPath) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Colors.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Extension for UIKit and SwiftUI should have different name |         // Extension for UIKit and SwiftUI should have different name | ||||||
|         guard options.extensionName != options.extensionNameUIKit else { |         guard options.extensionName != options.extensionNameUIKit else { | ||||||
|             let error = ColorsToolError.extensionNamesCollision(options.extensionName) |             let error = ColorsToolError.extensionNamesCollision(options.extensionName) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Colors.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Check if needed to regenerate |         // Check if needed to regenerate | ||||||
|         guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, |         guard GeneratorChecker.shouldGenerate( | ||||||
|                                               inputFilePath: options.inputFile, |             force: options.forceGeneration, | ||||||
|                                               extensionFilePath: options.extensionFilePath) else { |             inputFilePath: options.inputFile, | ||||||
|  |             extensionFilePath: options.extensionFilePath | ||||||
|  |         ) else { | ||||||
|             print("[\(Self.toolName)] Colors are already up to date :) ") |             print("[\(Self.toolName)] Colors are already up to date :) ") | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Helpers |     // MARK: - Helpers | ||||||
|      |  | ||||||
|     private func deleteCurrentColors() { |     private func deleteCurrentColors() { | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|         let assetsColorPath = "\(options.xcassetsPath)/Colors" |         let assetsColorPath = "\(options.xcassetsPath)/Colors" | ||||||
|          |  | ||||||
|         if fileManager.fileExists(atPath: assetsColorPath) { |         if fileManager.fileExists(atPath: assetsColorPath) { | ||||||
|             do { |             do { | ||||||
|                 try fileManager.removeItem(atPath: assetsColorPath) |                 try fileManager.removeItem(atPath: assetsColorPath) | ||||||
|             } catch { |             } catch { | ||||||
|                 let error = ColorsToolError.deleteExistingColors("\(options.xcassetsPath)/Colors") |                 let error = ColorsToolError.deleteExistingColors("\(options.xcassetsPath)/Colors") | ||||||
|                 print(error.description) |                 print(error.description) | ||||||
|                 Colors.exit(withError: error) |                 Self.exit(withError: error) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum ColorsToolError: Error { | enum ColorsToolError: Error { | ||||||
|  |  | ||||||
|     case extensionNamesCollision(String) |     case extensionNamesCollision(String) | ||||||
|     case badFormat(String) |     case badFormat(String) | ||||||
|     case writeAsset(String) |     case writeAsset(String) | ||||||
| @@ -16,30 +17,30 @@ enum ColorsToolError: Error { | |||||||
|     case fileNotExists(String) |     case fileNotExists(String) | ||||||
|     case badColorDefinition(String, String) |     case badColorDefinition(String, String) | ||||||
|     case deleteExistingColors(String) |     case deleteExistingColors(String) | ||||||
|      |  | ||||||
|     var description: String { |     var description: String { | ||||||
|         switch self { |         switch self { | ||||||
|         case .extensionNamesCollision(let extensionName): |         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)" |             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): |         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\"" |             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): |         case .writeAsset(let info): | ||||||
|             return "error: [\(Colors.toolName)] An error occured while writing color in Xcasset: \(info)" |             return "error: [\(Colors.toolName)] An error occured while writing color in Xcasset: \(info)" | ||||||
|           |  | ||||||
|         case .createAssetFolder(let assetsFolder): |         case .createAssetFolder(let assetsFolder): | ||||||
|             return "error: [\(Colors.toolName)] An error occured while creating colors folder `\(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)" |             return "error: [\(Colors.toolName)] An error occured while writing extension in \(filename): \(info)" | ||||||
|              |  | ||||||
|         case .fileNotExists(let filename): |         case .fileNotExists(let filename): | ||||||
|             return "error: [\(Colors.toolName)] File \(filename) does not exists" |             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)-" |             return "error: [\(Colors.toolName)] One of these two colors has invalid synthax: -\(lightColor)- or -\(darkColor)-" | ||||||
|              |  | ||||||
|         case .deleteExistingColors(let assetsFolder): |         case .deleteExistingColors(let assetsFolder): | ||||||
|             return "error: [\(Colors.toolName)] An error occured while deleting colors folder `\(assetsFolder)`" |             return "error: [\(Colors.toolName)] An error occured while deleting colors folder `\(assetsFolder)`" | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,38 +1,41 @@ | |||||||
| // | // | ||||||
| //  ColorsToolOptions.swift | //  ColorsToolOptions.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 17/01/2022. | //  Created by Thibaut Schmitt on 17/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable no_grouping_extension | ||||||
|  |  | ||||||
| struct ColorsToolOptions: ParsableArguments { | struct ColorsToolOptions: ParsableArguments { | ||||||
|  |  | ||||||
|     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") |     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") | ||||||
|     var forceGeneration = false |     var forceGeneration = false | ||||||
|      |  | ||||||
|     @Argument(help: "Input files where colors ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Argument(help: "Input files where colors ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var inputFile: String |     var inputFile: String | ||||||
|      |  | ||||||
|     @Option(help: "Color style to generate: light for light colors only, or all for dark and light colors") |     @Option(help: "Color style to generate: light for light colors only, or all for dark and light colors") | ||||||
|     var style: ColorStyle |     var style: ColorStyle | ||||||
|      |  | ||||||
|     @Option(help: "Path of xcassets where to generate colors", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path of xcassets where to generate colors", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var xcassetsPath: String |     var xcassetsPath: String | ||||||
|      |  | ||||||
|     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var extensionOutputPath: String |     var extensionOutputPath: String | ||||||
|      |  | ||||||
|     @Option(help: "Tell if it will generate static properties or not") |     @Option(help: "Tell if it will generate static properties or not") | ||||||
|     var staticMembers: Bool = false |     var staticMembers: Bool = false | ||||||
|      |  | ||||||
|     @Option(help: "Extension name. If not specified, it will generate an Color extension.") |     @Option(help: "Extension name. If not specified, it will generate an Color extension.") | ||||||
|     var extensionName: String = Colors.defaultExtensionName |     var extensionName: String = Colors.defaultExtensionName | ||||||
|      |  | ||||||
|     @Option(help: "SwiftUI Extension name. If not specified, it will generate an UIColor extension.") |     @Option(help: "SwiftUI Extension name. If not specified, it will generate an UIColor extension.") | ||||||
|     var extensionNameUIKit: String = Colors.defaultExtensionNameUIKit |     var extensionNameUIKit: String = Colors.defaultExtensionNameUIKit | ||||||
|      |  | ||||||
|     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift") |     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift") | ||||||
|     var extensionSuffix: String? |     var extensionSuffix: String? | ||||||
| } | } | ||||||
| @@ -40,29 +43,29 @@ struct ColorsToolOptions: ParsableArguments { | |||||||
| // MARK: - Computed var | // MARK: - Computed var | ||||||
|  |  | ||||||
| extension ColorsToolOptions { | extension ColorsToolOptions { | ||||||
|      |  | ||||||
|     // MARK: - SwiftUI |     // MARK: - SwiftUI | ||||||
|      |  | ||||||
|     var extensionFileName: String { |     var extensionFileName: String { | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             return "\(extensionName)+\(extensionSuffix).swift" |             return "\(extensionName)+\(extensionSuffix).swift" | ||||||
|         } |         } | ||||||
|         return "\(extensionName).swift" |         return "\(extensionName).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePath: String { |     var extensionFilePath: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileName)" |         "\(extensionOutputPath)/\(extensionFileName)" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - UIKit |     // MARK: - UIKit | ||||||
|      |  | ||||||
|     var extensionFileNameUIKit: String { |     var extensionFileNameUIKit: String { | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             return "\(extensionNameUIKit)+\(extensionSuffix).swift" |             return "\(extensionNameUIKit)+\(extensionSuffix).swift" | ||||||
|         } |         } | ||||||
|         return "\(extensionNameUIKit).swift" |         return "\(extensionNameUIKit).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePathUIKit: String { |     var extensionFilePathUIKit: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileNameUIKit)" |         "\(extensionOutputPath)/\(extensionFileNameUIKit)" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // | // | ||||||
| //  ColorExtensionGenerator.swift | //  ColorExtensionGenerator.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 20/12/2021. | //  Created by Thibaut Schmitt on 20/12/2021. | ||||||
| // | // | ||||||
| @@ -9,38 +9,44 @@ import Foundation | |||||||
| import ToolCore | import ToolCore | ||||||
|  |  | ||||||
| struct ColorExtensionGenerator { | struct ColorExtensionGenerator { | ||||||
|      |  | ||||||
|     let colors: [ParsedColor] |     let colors: [ParsedColor] | ||||||
|     let extensionClassname: String |     let extensionClassname: String | ||||||
|      |  | ||||||
|     // MARK: - UIKit |     // MARK: - UIKit | ||||||
|      |  | ||||||
|     static func writeExtensionFile(colors: [ParsedColor], |     static func writeExtensionFile( | ||||||
|                                    staticVar: Bool, |         colors: [ParsedColor], | ||||||
|                                    extensionName: String, |         staticVar: Bool, | ||||||
|                                    extensionFilePath: String, |         extensionName: String, | ||||||
|                                    isSwiftUI: Bool) { |         extensionFilePath: String, | ||||||
|  |         isSwiftUI: Bool | ||||||
|  |     ) { | ||||||
|         // Create extension content |         // Create extension content | ||||||
|         let extensionContent = Self.getExtensionContent(colors: colors, |         let extensionContent = Self.getExtensionContent( | ||||||
|                                                         staticVar: staticVar, |             colors: colors, | ||||||
|                                                         extensionName: extensionName, |             staticVar: staticVar, | ||||||
|                                                         isSwiftUI: isSwiftUI) |             extensionName: extensionName, | ||||||
|          |             isSwiftUI: isSwiftUI | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Write content |         // Write content | ||||||
|         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) |         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) | ||||||
|         do { |         do { | ||||||
|             try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) |             try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = ColorsToolError.writeExtension(extensionFilePath, error.localizedDescription) |             let error = ColorsToolError.writeExtension(extensionFilePath, error.localizedDescription) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Colors.exit(withError: error) |             Colors.exit(withError: error) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     static func getExtensionContent(colors: [ParsedColor], |     static func getExtensionContent( | ||||||
|                                     staticVar: Bool, |         colors: [ParsedColor], | ||||||
|                                     extensionName: String, |         staticVar: Bool, | ||||||
|                                     isSwiftUI: Bool) -> String { |         extensionName: String, | ||||||
|  |         isSwiftUI: Bool | ||||||
|  |     ) -> String { | ||||||
|         [ |         [ | ||||||
|             Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI), |             Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI), | ||||||
|             Self.getProperties(for: colors, withStaticVar: staticVar, isSwiftUI: isSwiftUI), |             Self.getProperties(for: colors, withStaticVar: staticVar, isSwiftUI: isSwiftUI), | ||||||
| @@ -48,7 +54,7 @@ struct ColorExtensionGenerator { | |||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getHeader(extensionClassname: String, isSwiftUI: Bool) -> String { |     private static func getHeader(extensionClassname: String, isSwiftUI: Bool) -> String { | ||||||
|         """ |         """ | ||||||
|         // Generated by ResgenSwift.\(Colors.toolName) \(ResgenSwiftVersion) |         // Generated by ResgenSwift.\(Colors.toolName) \(ResgenSwiftVersion) | ||||||
| @@ -58,17 +64,19 @@ struct ColorExtensionGenerator { | |||||||
|         extension \(extensionClassname) {\n |         extension \(extensionClassname) {\n | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getFooter() -> String { |     private static func getFooter() -> String { | ||||||
|         """ |         """ | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getProperties(for colors: [ParsedColor], |     private static func getProperties( | ||||||
|                                       withStaticVar staticVar: Bool, |         for colors: [ParsedColor], | ||||||
|                                       isSwiftUI: Bool) -> String { |         withStaticVar staticVar: Bool, | ||||||
|  |         isSwiftUI: Bool | ||||||
|  |     ) -> String { | ||||||
|         colors.map { |         colors.map { | ||||||
|             $0.getColorProperty(isStatic: staticVar, isSwiftUI: isSwiftUI) |             $0.getColorProperty(isStatic: staticVar, isSwiftUI: isSwiftUI) | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // | // | ||||||
| //  ColorXcassetHelper.swift | //  ColorXcassetHelper.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 20/12/2021. | //  Created by Thibaut Schmitt on 20/12/2021. | ||||||
| // | // | ||||||
| @@ -8,37 +8,39 @@ | |||||||
| import Foundation | import Foundation | ||||||
| import ToolCore | import ToolCore | ||||||
|  |  | ||||||
| struct ColorXcassetHelper { | enum ColorXcassetHelper { | ||||||
|      |  | ||||||
|     static func generateXcassetColors(colors: [ParsedColor], to xcassetsPath: String) { |     static func generateXcassetColors(colors: [ParsedColor], to xcassetsPath: String) { | ||||||
|         colors.forEach { |         colors.forEach { | ||||||
|             Self.generateColorSetAssets(from: $0, to: xcassetsPath) |             Self.generateColorSetAssets(from: $0, to: xcassetsPath) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // Generate ColorSet in XCAssets file |     // Generate ColorSet in XCAssets file | ||||||
|     private static func generateColorSetAssets(from color: ParsedColor, to xcassetsPath: String) { |     private static func generateColorSetAssets(from color: ParsedColor, to xcassetsPath: String) { | ||||||
|         // Create ColorSet |         // Create ColorSet | ||||||
|         let colorSetPath = "\(xcassetsPath)/Colors/\(color.name).colorset" |         let colorSetPath = "\(xcassetsPath)/Colors/\(color.name).colorset" | ||||||
|         let contentsJsonPath = "\(colorSetPath)/Contents.json" |         let contentsJsonPath = "\(colorSetPath)/Contents.json" | ||||||
|          |  | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|         if fileManager.fileExists(atPath: colorSetPath) == false { |         if fileManager.fileExists(atPath: colorSetPath) == false { | ||||||
|             do { |             do { | ||||||
|                 try fileManager.createDirectory(atPath: colorSetPath, |                 try fileManager.createDirectory( | ||||||
|                                             withIntermediateDirectories: true) |                     atPath: colorSetPath, | ||||||
|  |                     withIntermediateDirectories: true | ||||||
|  |                 ) | ||||||
|             } catch { |             } catch { | ||||||
|                 let error = ColorsToolError.createAssetFolder(colorSetPath) |                 let error = ColorsToolError.createAssetFolder(colorSetPath) | ||||||
|                 print(error.description) |                 print(error.description) | ||||||
|                 Colors.exit(withError: error) |                 Colors.exit(withError: error) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Write content in Contents.json |         // Write content in Contents.json | ||||||
|         let contentsJsonPathURL = URL(fileURLWithPath: contentsJsonPath) |         let contentsJsonPathURL = URL(fileURLWithPath: contentsJsonPath) | ||||||
|         do { |         do { | ||||||
|             try color.contentsJSON().write(to: contentsJsonPathURL, atomically: false, encoding: .utf8) |             try color.contentsJSON().write(to: contentsJsonPathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = ColorsToolError.writeAsset(error.localizedDescription) |             let error = ColorsToolError.writeAsset(error.localizedDescription) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Colors.exit(withError: error) |             Colors.exit(withError: error) | ||||||
|   | |||||||
| @@ -5,13 +5,14 @@ | |||||||
| //  Created by Thibaut Schmitt on 29/08/2022. | //  Created by Thibaut Schmitt on 29/08/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
| enum ColorStyle: String, Decodable, ExpressibleByArgument { | enum ColorStyle: String, Decodable, ExpressibleByArgument { | ||||||
|  |  | ||||||
|     case light |     case light | ||||||
|     case all |     case all | ||||||
|      |  | ||||||
|     static var allValueStrings: [String] { |     static var allValueStrings: [String] { | ||||||
|         [ |         [ | ||||||
|             Self.light.rawValue, |             Self.light.rawValue, | ||||||
|   | |||||||
| @@ -8,28 +8,29 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| struct ParsedColor { | struct ParsedColor { | ||||||
|  |  | ||||||
|     let name: String |     let name: String | ||||||
|     let light: String |     let light: String | ||||||
|     let dark: String |     let dark: String | ||||||
|      |  | ||||||
|     // Generate Contents.json content |     // Generate Contents.json content | ||||||
|     func contentsJSON() -> String { |     func contentsJSON() -> String { | ||||||
|         let lightARGB = light.colorComponent() |         let lightARGB = light.colorComponent() | ||||||
|         let darkARGB = dark.colorComponent() |         let darkARGB = dark.colorComponent() | ||||||
|          |  | ||||||
|         let allComponents = [ |         let allComponents = [ | ||||||
|             lightARGB.alpha, lightARGB.red, lightARGB.green, lightARGB.blue, |             lightARGB.alpha, lightARGB.red, lightARGB.green, lightARGB.blue, | ||||||
|             darkARGB.alpha, darkARGB.red, darkARGB.green, darkARGB.blue |             darkARGB.alpha, darkARGB.red, darkARGB.green, darkARGB.blue | ||||||
|         ].map { |         ].map { | ||||||
|             $0.isEmpty |             $0.isEmpty | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         guard allComponents.contains(true) == false else { |         guard allComponents.contains(true) == false else { | ||||||
|             let error = ColorsToolError.badColorDefinition(light, dark) |             let error = ColorsToolError.badColorDefinition(light, dark) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Colors.exit(withError: error) |             Colors.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return """ |         return """ | ||||||
|         { |         { | ||||||
|             "colors": [ |             "colors": [ | ||||||
| @@ -71,9 +72,9 @@ struct ParsedColor { | |||||||
|         } |         } | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - UIKit |     // MARK: - UIKit | ||||||
|      |  | ||||||
|     func getColorProperty(isStatic: Bool, isSwiftUI: Bool) -> String { |     func getColorProperty(isStatic: Bool, isSwiftUI: Bool) -> String { | ||||||
|         if isSwiftUI { |         if isSwiftUI { | ||||||
|             return """ |             return """ | ||||||
|   | |||||||
| @@ -7,44 +7,45 @@ | |||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| class ColorFileParser { | enum ColorFileParser { | ||||||
|  |  | ||||||
|     static func parse(_ inputFile: String, colorStyle: ColorStyle) -> [ParsedColor] { |     static func parse(_ inputFile: String, colorStyle: ColorStyle) -> [ParsedColor] { | ||||||
|         // Get content of input file |         // 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) |         let colorsByLines = inputFileContent.components(separatedBy: CharacterSet.newlines) | ||||||
|          |  | ||||||
|         // Iterate on each line of input file |         // Iterate on each line of input file | ||||||
|         return parseLines(lines: colorsByLines, colorStyle: colorStyle) |         return parseLines(lines: colorsByLines, colorStyle: colorStyle) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     static func parseLines(lines: [String], colorStyle: ColorStyle) -> [ParsedColor] { |     static func parseLines(lines: [String], colorStyle: ColorStyle) -> [ParsedColor] { | ||||||
|         lines |         lines | ||||||
|             .enumerated() |             .enumerated() | ||||||
|             .compactMap { lineNumber, colorLine in |             .compactMap { _, colorLine in // swiftlint:disable:this unused_enumerated | ||||||
|                 // Required format: |                                           // Required format: | ||||||
|                 // colorName = "#RGB/#ARGB", colorName "#RGB/#ARGB", colorName "#RGB/#ARGB" "#RGB/#ARGB" |                                           // colorName = "#RGB/#ARGB", colorName "#RGB/#ARGB", colorName "#RGB/#ARGB" "#RGB/#ARGB" | ||||||
|                 let colorLineCleanedUp = colorLine |                 let colorLineCleanedUp = colorLine | ||||||
|                     .removeLeadingWhitespace() |                     .removeLeadingWhitespace() | ||||||
|                     .removeTrailingWhitespace() |                     .removeTrailingWhitespace() | ||||||
|                     .replacingOccurrences(of: "=", with: "") // Keep compat with current file format |                     .replacingOccurrences(of: "=", with: "") // Keep compat with current file format | ||||||
|                  |  | ||||||
|                 guard colorLineCleanedUp.hasPrefix("#") == false, colorLineCleanedUp.isEmpty == false else { |                 guard colorLineCleanedUp.hasPrefix("#") == false, colorLineCleanedUp.isEmpty == false else { | ||||||
|                     // debugPrint("[\(Colors.toolName)] ⚠️  BadFormat or empty line (line number: \(lineNumber + 1)). Skip this line") |                     // debugPrint("[\(Colors.toolName)] ⚠️  BadFormat or empty line (line number: \(lineNumber + 1)). Skip this line") | ||||||
|                     return nil |                     return nil | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 let colorContent = colorLineCleanedUp.split(separator: " ") |                 let colorContent = colorLineCleanedUp.split(separator: " ") | ||||||
|                  |  | ||||||
|                 guard colorContent.count >= 2 else { |                 guard colorContent.count >= 2 else { | ||||||
|                     let error = ColorsToolError.badFormat(colorLine) |                     let error = ColorsToolError.badFormat(colorLine) | ||||||
|                     print(error.description) |                     print(error.description) | ||||||
|                     Colors.exit(withError: error) |                     Colors.exit(withError: error) | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 switch colorStyle { |                 switch colorStyle { | ||||||
|                 case .light: |                 case .light: | ||||||
|                     return ParsedColor(name: String(colorContent[0]), light: String(colorContent[1]), dark: String(colorContent[1])) |                     return ParsedColor(name: String(colorContent[0]), light: String(colorContent[1]), dark: String(colorContent[1])) | ||||||
|                      |  | ||||||
|                 case .all: |                 case .all: | ||||||
|                     if colorContent.count == 3 { |                     if colorContent.count == 3 { | ||||||
|                         return ParsedColor(name: String(colorContent[0]), light: String(colorContent[1]), dark: String(colorContent[2])) |                         return ParsedColor(name: String(colorContent[0]), light: String(colorContent[1]), dark: String(colorContent[2])) | ||||||
|   | |||||||
| @@ -5,31 +5,34 @@ | |||||||
| //  Created by Thibaut Schmitt on 17/01/2022. | //  Created by Thibaut Schmitt on 17/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable no_grouping_extension | ||||||
|  |  | ||||||
| struct FontsOptions: ParsableArguments { | struct FontsOptions: ParsableArguments { | ||||||
|  |  | ||||||
|     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") |     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") | ||||||
|     var forceGeneration = false |     var forceGeneration = false | ||||||
|      |  | ||||||
|     @Argument(help: "Input files where fonts ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Argument(help: "Input files where fonts ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var inputFile: String |     var inputFile: String | ||||||
|      |  | ||||||
|     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var extensionOutputPath: String |     var extensionOutputPath: String | ||||||
|      |  | ||||||
|     @Option(help: "Tell if it will generate static properties or methods") |     @Option(help: "Tell if it will generate static properties or methods") | ||||||
|     var staticMembers: Bool = false |     var staticMembers: Bool = false | ||||||
|      |  | ||||||
|     @Option(help: "Extension name. If not specified, it will generate an Font extension.") |     @Option(help: "Extension name. If not specified, it will generate an Font extension.") | ||||||
|     var extensionName: String = Fonts.defaultExtensionName |     var extensionName: String = Fonts.defaultExtensionName | ||||||
|      |  | ||||||
|     @Option(help: "Extension name. If not specified, it will generate an UIFont extension.") |     @Option(help: "Extension name. If not specified, it will generate an UIFont extension.") | ||||||
|     var extensionNameUIKit: String = Fonts.defaultExtensionNameUIKit |     var extensionNameUIKit: String = Fonts.defaultExtensionNameUIKit | ||||||
|      |  | ||||||
|     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+FontsMyApp.swift") |     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+FontsMyApp.swift") | ||||||
|     var extensionSuffix: String = "" |     var extensionSuffix: String = "" | ||||||
|      |  | ||||||
|     @Option(name: .customLong("info-plist-paths"), help: "Info.plist paths (array). Will be used to update UIAppFonts content") |     @Option(name: .customLong("info-plist-paths"), help: "Info.plist paths (array). Will be used to update UIAppFonts content") | ||||||
|     fileprivate var infoPlistPathsRaw: String = "" |     fileprivate var infoPlistPathsRaw: String = "" | ||||||
| } | } | ||||||
| @@ -37,29 +40,29 @@ struct FontsOptions: ParsableArguments { | |||||||
| // MARK: - Computed var | // MARK: - Computed var | ||||||
|  |  | ||||||
| extension FontsOptions { | extension FontsOptions { | ||||||
|      |  | ||||||
|     // MARK: - SwiftUI |     // MARK: - SwiftUI | ||||||
|      |  | ||||||
|     var extensionFileName: String { |     var extensionFileName: String { | ||||||
|         if extensionSuffix.isEmpty == false { |         if extensionSuffix.isEmpty == false { | ||||||
|             return "\(extensionName)+\(extensionSuffix).swift" |             return "\(extensionName)+\(extensionSuffix).swift" | ||||||
|         } |         } | ||||||
|         return "\(extensionName).swift" |         return "\(extensionName).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePath: String { |     var extensionFilePath: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileName)" |         "\(extensionOutputPath)/\(extensionFileName)" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - UIKit |     // MARK: - UIKit | ||||||
|      |  | ||||||
|     var extensionFileNameUIKit: String { |     var extensionFileNameUIKit: String { | ||||||
|         if extensionSuffix.isEmpty == false { |         if extensionSuffix.isEmpty == false { | ||||||
|             return "\(extensionNameUIKit)+\(extensionSuffix).swift" |             return "\(extensionNameUIKit)+\(extensionSuffix).swift" | ||||||
|         } |         } | ||||||
|         return "\(extensionNameUIKit).swift" |         return "\(extensionNameUIKit).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePathUIKit: String { |     var extensionFilePathUIKit: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileNameUIKit)" |         "\(extensionOutputPath)/\(extensionFileNameUIKit)" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,47 +1,47 @@ | |||||||
| // | // | ||||||
| //  Fonts.swift | //  Fonts.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 13/12/2021. | //  Created by Thibaut Schmitt on 13/12/2021. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct Fonts: ParsableCommand { | struct Fonts: ParsableCommand { | ||||||
|      |  | ||||||
|     // MARK: - CommandConfiguration |     // MARK: - CommandConfiguration | ||||||
|      |  | ||||||
|     static var configuration = 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.", |         abstract: "A utility to generate an helpful etension to access your custom font from code and also Info.plist UIAppsFont content.", | ||||||
|         version: ResgenSwiftVersion |         version: ResgenSwiftVersion | ||||||
|     ) |     ) | ||||||
|      |  | ||||||
|     // MARK: - Static |     // MARK: - Static | ||||||
|      |  | ||||||
|     static let toolName = "Fonts" |     static let toolName = "Fonts" | ||||||
|     static let defaultExtensionName = "Font" |     static let defaultExtensionName = "Font" | ||||||
|     static let defaultExtensionNameUIKit = "UIFont" |     static let defaultExtensionNameUIKit = "UIFont" | ||||||
|      |  | ||||||
|     // MARK: - Command Options |     // MARK: - Command Options | ||||||
|      |  | ||||||
|     @OptionGroup var options: FontsOptions |     @OptionGroup var options: FontsOptions | ||||||
|      |  | ||||||
|     // MARK: - Run |     // MARK: - Run | ||||||
|      |  | ||||||
|     public func run() throws { |     func run() throws { | ||||||
|         print("[\(Self.toolName)] Starting fonts generation") |         print("[\(Self.toolName)] Starting fonts generation") | ||||||
|         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate fonts") |         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate fonts") | ||||||
|          |  | ||||||
|         // Check requirements |         // Check requirements | ||||||
|         guard checkRequirements() else { return } |         guard checkRequirements() else { return } | ||||||
|          |  | ||||||
|         print("[\(Self.toolName)] Will generate fonts") |         print("[\(Self.toolName)] Will generate fonts") | ||||||
|          |  | ||||||
|         // Get fonts to generate |         // Get fonts to generate | ||||||
|         let fontsToGenerate = FontFileParser.parse(options.inputFile) |         let fontsToGenerate = FontFileParser.parse(options.inputFile) | ||||||
|          |  | ||||||
|         // Get real font names |         // Get real font names | ||||||
|         let inputFolder = URL(fileURLWithPath: options.inputFile) |         let inputFolder = URL(fileURLWithPath: options.inputFile) | ||||||
|             .deletingLastPathComponent() |             .deletingLastPathComponent() | ||||||
| @@ -51,7 +51,7 @@ struct Fonts: ParsableCommand { | |||||||
|             for: fontsToGenerate, |             for: fontsToGenerate, | ||||||
|             inputFolder: inputFolder |             inputFolder: inputFolder | ||||||
|         ) |         ) | ||||||
|          |  | ||||||
|         // Generate extension |         // Generate extension | ||||||
|         FontExtensionGenerator.writeExtensionFile( |         FontExtensionGenerator.writeExtensionFile( | ||||||
|             fontsNames: fontsNames, |             fontsNames: fontsNames, | ||||||
| @@ -60,7 +60,7 @@ struct Fonts: ParsableCommand { | |||||||
|             extensionFilePath: options.extensionFilePath, |             extensionFilePath: options.extensionFilePath, | ||||||
|             isSwiftUI: true |             isSwiftUI: true | ||||||
|         ) |         ) | ||||||
|          |  | ||||||
|         FontExtensionGenerator.writeExtensionFile( |         FontExtensionGenerator.writeExtensionFile( | ||||||
|             fontsNames: fontsNames, |             fontsNames: fontsNames, | ||||||
|             staticVar: options.staticMembers, |             staticVar: options.staticMembers, | ||||||
| @@ -68,40 +68,42 @@ struct Fonts: ParsableCommand { | |||||||
|             extensionFilePath: options.extensionFilePathUIKit, |             extensionFilePath: options.extensionFilePathUIKit, | ||||||
|             isSwiftUI: false |             isSwiftUI: false | ||||||
|         ) |         ) | ||||||
|          |  | ||||||
|         print("Info.plist has been updated with:") |         print("Info.plist has been updated with:") | ||||||
|         print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames, infoPlistPaths: options.infoPlistPaths))") |         print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames, infoPlistPaths: options.infoPlistPaths))") | ||||||
|          |  | ||||||
|         print("[\(Self.toolName)] Fonts generated") |         print("[\(Self.toolName)] Fonts generated") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Requirements |     // MARK: - Requirements | ||||||
|      |  | ||||||
|     private func checkRequirements() -> Bool { |     private func checkRequirements() -> Bool { | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|          |  | ||||||
|         // Check input file exists |         // Check input file exists | ||||||
|         guard fileManager.fileExists(atPath: options.inputFile) else { |         guard fileManager.fileExists(atPath: options.inputFile) else { | ||||||
|             let error = FontsToolError.fileNotExists(options.inputFile) |             let error = FontsToolError.fileNotExists(options.inputFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Fonts.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Extension for UIKit and SwiftUI should have different name |         // Extension for UIKit and SwiftUI should have different name | ||||||
|         guard options.extensionName != options.extensionNameUIKit else { |         guard options.extensionName != options.extensionNameUIKit else { | ||||||
|             let error = FontsToolError.extensionNamesCollision(options.extensionName) |             let error = FontsToolError.extensionNamesCollision(options.extensionName) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Fonts.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Check if needed to regenerate |         // Check if needed to regenerate | ||||||
|         guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, |         guard GeneratorChecker.shouldGenerate( | ||||||
|                                               inputFilePath: options.inputFile, |             force: options.forceGeneration, | ||||||
|                                               extensionFilePath: options.extensionFilePath) else { |             inputFilePath: options.inputFile, | ||||||
|  |             extensionFilePath: options.extensionFilePath | ||||||
|  |         ) else { | ||||||
|             print("[\(Self.toolName)] Fonts are already up to date :) ") |             print("[\(Self.toolName)] Fonts are already up to date :) ") | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,27 +8,28 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum FontsToolError: Error { | enum FontsToolError: Error { | ||||||
|  |  | ||||||
|     case extensionNamesCollision(String) |     case extensionNamesCollision(String) | ||||||
|     case fcScan(String, Int32, String?) |     case fcScan(String, Int32, String?) | ||||||
|     case inputFolderNotFound(String) |     case inputFolderNotFound(String) | ||||||
|     case fileNotExists(String) |     case fileNotExists(String) | ||||||
|     case writeExtension(String, String) |     case writeExtension(String, String) | ||||||
|      |  | ||||||
|     var description: String { |     var description: String { | ||||||
|         switch self { |         switch self { | ||||||
|         case .extensionNamesCollision(let extensionName): |         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)" |             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")" |             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): |         case .inputFolderNotFound(let inputFolder): | ||||||
|             return "error: [\(Fonts.toolName)] Input folder not found: \(inputFolder)" |             return "error: [\(Fonts.toolName)] Input folder not found: \(inputFolder)" | ||||||
|              |  | ||||||
|         case .fileNotExists(let filename): |         case .fileNotExists(let filename): | ||||||
|             return "error: [\(Fonts.toolName)] File \(filename) does not exists" |             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)" |             return "error: [\(Fonts.toolName)] An error occured while writing extension in \(filename): \(info)" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // | // | ||||||
| //  FontsToolHelper.swift | //  FontsToolHelper.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 13/12/2021. | //  Created by Thibaut Schmitt on 13/12/2021. | ||||||
| // | // | ||||||
| @@ -8,32 +8,32 @@ | |||||||
| import Foundation | import Foundation | ||||||
| import ToolCore | import ToolCore | ||||||
|  |  | ||||||
| class FontsToolHelper { | enum FontsToolHelper { | ||||||
|      |  | ||||||
|     static func getFontPostScriptName(for fonts: [String], inputFolder: String) -> [FontName] { |     static func getFontPostScriptName(for fonts: [String], inputFolder: String) -> [FontName] { | ||||||
|         let fontsFilenames = Self.getFontsFilenames(fromInputFolder: inputFolder) |         let fontsFilenames = Self.getFontsFilenames(fromInputFolder: inputFolder) | ||||||
|             .filter { fontNameWithPath in |             .filter { fontNameWithPath in | ||||||
|                 let fontName = URL(fileURLWithPath: fontNameWithPath) |                 let fontName = URL(fileURLWithPath: fontNameWithPath) | ||||||
|                     .deletingPathExtension() |                     .deletingPathExtension() | ||||||
|                     .lastPathComponent |                     .lastPathComponent | ||||||
|                  |  | ||||||
|                 if fonts.contains(fontName) { |                 if fonts.contains(fontName) { | ||||||
|                     return true |                     return true | ||||||
|                 } |                 } | ||||||
|                 return false |                 return false | ||||||
|             } |             } | ||||||
|          |  | ||||||
|         let fontsFilesnamesWithPath = fontsFilenames.map { |         let fontsFilesnamesWithPath = fontsFilenames.map { | ||||||
|             "\(inputFolder)/\($0)" |             "\(inputFolder)/\($0)" | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return fontsFilesnamesWithPath.compactMap { |         return fontsFilesnamesWithPath.compactMap { | ||||||
|             Self.getFontName(atPath: $0) |             Self.getFontName(atPath: $0) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Private |     // MARK: - Private | ||||||
|      |  | ||||||
|     private static func getFontsFilenames(fromInputFolder inputFolder: String) -> [String] { |     private static func getFontsFilenames(fromInputFolder inputFolder: String) -> [String] { | ||||||
|         // Get a enumerator for all files |         // Get a enumerator for all files | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
| @@ -42,26 +42,26 @@ class FontsToolHelper { | |||||||
|             print(error.description) |             print(error.description) | ||||||
|             Fonts.exit(withError: error) |             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 |         // Filters font files | ||||||
|         let fontsFileNames: [String] = (enumerator.allObjects as! [String]) |         let fontsFileNames: [String] = (enumerator.allObjects as! [String]) // swiftlint:disable:this force_cast | ||||||
|             .filter { |             .filter { | ||||||
|                 if $0.hasSuffix(".ttf") || $0.hasSuffix(".otf") { |                 if $0.hasSuffix(".ttf") || $0.hasSuffix(".otf") { | ||||||
|                     return true |                     return true | ||||||
|                 } |                 } | ||||||
|                 return false |                 return false | ||||||
|             } |             } | ||||||
|          |  | ||||||
|         return fontsFileNames |         return fontsFileNames | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getFontName(atPath path: String) -> FontName { |     private static func getFontName(atPath path: String) -> FontName { | ||||||
|         //print("fc-scan --format %{postscriptname} \(path)") |         // print("fc-scan --format %{postscriptname} \(path)") | ||||||
|         // Get real font name |         // Get real font name | ||||||
|         let task = Shell.shell(["fc-scan", "--format", "%{postscriptname}", path]) |         let task = Shell.shell(["fc-scan", "--format", "%{postscriptname}", path]) | ||||||
|          |  | ||||||
|         guard let postscriptName = task.output, task.terminationStatus == 0 else { |         guard let postscriptName = task.output, task.terminationStatus == 0 else { | ||||||
|             let error = FontsToolError.fcScan(path, task.terminationStatus, task.output) |             let error = FontsToolError.fcScan(path, task.terminationStatus, task.output) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|   | |||||||
| @@ -8,27 +8,34 @@ | |||||||
| import Foundation | import Foundation | ||||||
| import ToolCore | import ToolCore | ||||||
|  |  | ||||||
| class FontPlistGenerator { | enum FontPlistGenerator { | ||||||
|  |  | ||||||
|     static func generatePlistUIAppsFontContent(for fonts: [FontName], infoPlistPaths: [String]) -> String { |     static func generatePlistUIAppsFontContent(for fonts: [FontName], infoPlistPaths: [String]) -> String { | ||||||
|         let fontsToAddToPlist = fonts |         let fontsToAddToPlist = fonts | ||||||
|             .compactMap { $0 } |             .compactMap { $0 } | ||||||
|          |  | ||||||
|         // Update each plist |         // Update each plist | ||||||
|         infoPlistPaths.forEach { infoPlist in |         infoPlistPaths.forEach { infoPlist in | ||||||
|             // Remove UIAppFonts value |             // Remove UIAppFonts value | ||||||
|             Shell.shell(launchPath: "/usr/libexec/PlistBuddy", |             Shell.shell( | ||||||
|                         ["-c", "delete :UIAppFonts", infoPlist]) |                 launchPath: "/usr/libexec/PlistBuddy", | ||||||
|              |                 ["-c", "delete :UIAppFonts", infoPlist] | ||||||
|  |             ) | ||||||
|  |  | ||||||
|             // Add UIAppFonts empty array |             // Add UIAppFonts empty array | ||||||
|             debugPrint("Will PlistBuddy -c add :UIAppFonts array \(infoPlist)") |             debugPrint("Will PlistBuddy -c add :UIAppFonts array \(infoPlist)") | ||||||
|             Shell.shell(launchPath: "/usr/libexec/PlistBuddy", |             Shell.shell( | ||||||
|                         ["-c", "add :UIAppFonts array", infoPlist]) |                 launchPath: "/usr/libexec/PlistBuddy", | ||||||
|  |                 ["-c", "add :UIAppFonts array", infoPlist] | ||||||
|  |             ) | ||||||
|  |  | ||||||
|             // Fill array with fonts |             // Fill array with fonts | ||||||
|             fontsToAddToPlist |             fontsToAddToPlist | ||||||
|                 .forEach { fontName in |                 .forEach { fontName in | ||||||
|                     Shell.shell(launchPath: "/usr/libexec/PlistBuddy", |                     Shell.shell( | ||||||
|                                 ["-c", "add :UIAppFonts: string \(fontName.filename).\(fontName.fileExtension)", infoPlist]) |                         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\t<string>\(fontName.filename).\(fontName.fileExtension)</string>\n" | ||||||
|             } |             } | ||||||
|         plistData += "\t</array>" |         plistData += "\t</array>" | ||||||
|          |  | ||||||
|         return plistData |         return plistData | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,45 +8,51 @@ | |||||||
| import Foundation | import Foundation | ||||||
| import ToolCore | import ToolCore | ||||||
|  |  | ||||||
| class FontExtensionGenerator { | enum FontExtensionGenerator { | ||||||
|  |  | ||||||
|     private static func getFontNameEnum(fontsNames: [FontName]) -> String { |     private static func getFontNameEnum(fontsNames: [FontName]) -> String { | ||||||
|         var enumDefinition = "    enum FontName: String {\n" |         var enumDefinition = "    enum FontName: String {\n" | ||||||
|          |  | ||||||
|         fontsNames.forEach { |         fontsNames.forEach { | ||||||
|             enumDefinition += "        case \($0.fontNameSanitize) = \"\($0.postscriptName)\"\n" |             enumDefinition += "        case \($0.fontNameSanitize) = \"\($0.postscriptName)\"\n" | ||||||
|         } |         } | ||||||
|         enumDefinition += "    }\n" |         enumDefinition += "    }\n" | ||||||
|          |  | ||||||
|         return enumDefinition |         return enumDefinition | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     static func writeExtensionFile(fontsNames: [FontName], |     static func writeExtensionFile( | ||||||
|                                    staticVar: Bool, |         fontsNames: [FontName], | ||||||
|                                    extensionName: String, |         staticVar: Bool, | ||||||
|                                    extensionFilePath: String, |         extensionName: String, | ||||||
|                                    isSwiftUI: Bool) { |         extensionFilePath: String, | ||||||
|  |         isSwiftUI: Bool | ||||||
|  |     ) { | ||||||
|         // Create extension content |         // Create extension content | ||||||
|         let extensionContent = Self.getExtensionContent(fontsNames: fontsNames, |         let extensionContent = Self.getExtensionContent( | ||||||
|                                                         staticVar: staticVar, |             fontsNames: fontsNames, | ||||||
|                                                         extensionName: extensionName, |             staticVar: staticVar, | ||||||
|                                                         isSwiftUI: isSwiftUI) |             extensionName: extensionName, | ||||||
|          |             isSwiftUI: isSwiftUI | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Write content |         // Write content | ||||||
|         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) |         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) | ||||||
|         do { |         do { | ||||||
|             try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) |             try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription) |             let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Fonts.exit(withError: error) |             Fonts.exit(withError: error) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     static func getExtensionContent(fontsNames: [FontName], |     static func getExtensionContent( | ||||||
|                                     staticVar: Bool, |         fontsNames: [FontName], | ||||||
|                                     extensionName: String, |         staticVar: Bool, | ||||||
|                                     isSwiftUI: Bool) -> String { |         extensionName: String, | ||||||
|  |         isSwiftUI: Bool | ||||||
|  |     ) -> String { | ||||||
|         [ |         [ | ||||||
|             Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI), |             Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI), | ||||||
|             Self.getFontNameEnum(fontsNames: fontsNames), |             Self.getFontNameEnum(fontsNames: fontsNames), | ||||||
| @@ -55,34 +61,34 @@ class FontExtensionGenerator { | |||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getHeader(extensionClassname: String, isSwiftUI: Bool) -> String { |     private static func getHeader(extensionClassname: String, isSwiftUI: Bool) -> String { | ||||||
|         """ |         """ | ||||||
|         // Generated by ResgenSwift.\(Fonts.toolName) \(ResgenSwiftVersion) |         // Generated by ResgenSwift.\(Fonts.toolName) \(ResgenSwiftVersion) | ||||||
|          |  | ||||||
|         import \(isSwiftUI ? "SwiftUI" : "UIKit") |         import \(isSwiftUI ? "SwiftUI" : "UIKit") | ||||||
|          |  | ||||||
|         extension \(extensionClassname) {\n |         extension \(extensionClassname) {\n | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getFontMethods(fontsNames: [FontName], staticVar: Bool, isSwiftUI: Bool) -> String { |     private static func getFontMethods(fontsNames: [FontName], staticVar: Bool, isSwiftUI: Bool) -> String { | ||||||
|         let pragma = "    // MARK: - Getter" |         let pragma = "    // MARK: - Getter" | ||||||
|              |  | ||||||
|         var propertiesOrMethods: [String] = fontsNames |         var propertiesOrMethods: [String] = fontsNames | ||||||
|             .unique() |             .unique() | ||||||
|             .map { |             .map { | ||||||
|                 $0.getProperty(isStatic: staticVar, isSwiftUI: isSwiftUI) |                 $0.getProperty(isStatic: staticVar, isSwiftUI: isSwiftUI) | ||||||
|             } |             } | ||||||
|          |  | ||||||
|         propertiesOrMethods.insert(pragma, at: 0) |         propertiesOrMethods.insert(pragma, at: 0) | ||||||
|         return propertiesOrMethods.joined(separator: "\n\n") |         return propertiesOrMethods.joined(separator: "\n\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getFooter() -> String { |     private static func getFooter() -> String { | ||||||
|         """ |         """ | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ | |||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| //typealias FontName = String | // swiftlint:disable no_grouping_extension | ||||||
|  |  | ||||||
| struct FontName: Hashable { | struct FontName: Hashable { | ||||||
|  |  | ||||||
| @@ -17,10 +17,11 @@ struct FontName: Hashable { | |||||||
| } | } | ||||||
|  |  | ||||||
| extension FontName { | extension FontName { | ||||||
|  |  | ||||||
|     var fontNameSanitize: String { |     var fontNameSanitize: String { | ||||||
|         postscriptName.removeCharacters(from: "[]+-_") |         postscriptName.removeCharacters(from: "[]+-_") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getProperty(isStatic: Bool, isSwiftUI: Bool) -> String { |     func getProperty(isStatic: Bool, isSwiftUI: Bool) -> String { | ||||||
|         if isSwiftUI { |         if isSwiftUI { | ||||||
|             if isStatic { |             if isStatic { | ||||||
|   | |||||||
| @@ -7,10 +7,13 @@ | |||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| class FontFileParser { | enum FontFileParser { | ||||||
|  |  | ||||||
|     static func parse(_ inputFile: String) -> [String] { |     static func parse(_ inputFile: String) -> [String] { | ||||||
|         let inputFileContent = try! String(contentsOfFile: inputFile, |         let inputFileContent = try! String( // swiftlint:disable:this force_try | ||||||
|                                            encoding: .utf8) |             contentsOfFile: inputFile, | ||||||
|  |             encoding: .utf8 | ||||||
|  |         ) | ||||||
|         return inputFileContent.components(separatedBy: CharacterSet.newlines) |         return inputFileContent.components(separatedBy: CharacterSet.newlines) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension String { | extension String { | ||||||
|      |  | ||||||
|     func prependIfRelativePath(_ prependPath: String) -> String { |     func prependIfRelativePath(_ prependPath: String) -> String { | ||||||
|         // If path starts with "/", it's an absolute path |         // If path starts with "/", it's an absolute path | ||||||
|         if self.hasPrefix("/") { |         if self.hasPrefix("/") { | ||||||
|   | |||||||
| @@ -5,32 +5,32 @@ | |||||||
| //  Created by Thibaut Schmitt on 30/08/2022. | //  Created by Thibaut Schmitt on 30/08/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct Generate: ParsableCommand { | struct Generate: ParsableCommand { | ||||||
|      |  | ||||||
|     // MARK: - CommandConfiguration |     // MARK: - CommandConfiguration | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static var configuration = CommandConfiguration( | ||||||
|         abstract: "A utility to generate ressources based on a configuration file", |         abstract: "A utility to generate ressources based on a configuration file", | ||||||
|         version: ResgenSwiftVersion |         version: ResgenSwiftVersion | ||||||
|     ) |     ) | ||||||
|      |  | ||||||
|     // MARK: - Static |     // MARK: - Static | ||||||
|      |  | ||||||
|     static let toolName = "Generate" |     static let toolName = "Generate" | ||||||
|      |  | ||||||
|     // MARK: - Command Options |     // MARK: - Command Options | ||||||
|      |  | ||||||
|     @OptionGroup var options: GenerateOptions |     @OptionGroup var options: GenerateOptions | ||||||
|      |  | ||||||
|     // MARK: - Run |     // MARK: - Run | ||||||
|      |  | ||||||
|     public func run() throws { |     func run() throws { | ||||||
|         print("[\(Self.toolName)] Starting Resgen with configuration: \(options.configurationFile)") |         print("[\(Self.toolName)] Starting Resgen with configuration: \(options.configurationFile)") | ||||||
|          |  | ||||||
|         // Parse |         // Parse | ||||||
|         let configuration = ConfigurationFileParser.parse(options.configurationFile) |         let configuration = ConfigurationFileParser.parse(options.configurationFile) | ||||||
|         print("Found configurations :") |         print("Found configurations :") | ||||||
| @@ -41,18 +41,22 @@ struct Generate: ParsableCommand { | |||||||
|         print("  - \(configuration.strings.count) strings configuration(s)") |         print("  - \(configuration.strings.count) strings configuration(s)") | ||||||
|         print("  - \(configuration.tags.count) tags configuration(s)") |         print("  - \(configuration.tags.count) tags configuration(s)") | ||||||
|         print() |         print() | ||||||
|          |  | ||||||
|         if let architecture = configuration.architecture { |         if let architecture = configuration.architecture { | ||||||
|             ArchitectureGenerator.writeArchitecture(architecture, |             ArchitectureGenerator.writeArchitecture( | ||||||
|                                                     projectDirectory: options.projectDirectory) |                 architecture, | ||||||
|  |                 projectDirectory: options.projectDirectory | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Execute commands |         // Execute commands | ||||||
|         configuration.runnableConfigurations |         configuration.runnableConfigurations | ||||||
|             .forEach { |             .forEach { | ||||||
|                 let begin = Date() |                 let begin = Date() | ||||||
|                 $0.run(projectDirectory: options.projectDirectory, |                 $0.run( | ||||||
|                        force: options.forceGeneration) |                     projectDirectory: options.projectDirectory, | ||||||
|  |                     force: options.forceGeneration | ||||||
|  |                 ) | ||||||
|                 print("Took: \(Date().timeIntervalSince(begin))s\n") |                 print("Took: \(Date().timeIntervalSince(begin))s\n") | ||||||
|             } |             } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,26 +8,26 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum GenerateError: Error { | enum GenerateError: Error { | ||||||
|  |  | ||||||
|     case fileNotExists(String) |     case fileNotExists(String) | ||||||
|     case invalidConfigurationFile(String, String) |     case invalidConfigurationFile(String, String) | ||||||
|     case commandError([String], String) |     case commandError([String], String) | ||||||
|     case writeFile(String, String) |     case writeFile(String, String) | ||||||
|      |  | ||||||
|     var description: String { |     var description: String { | ||||||
|         switch self { |         switch self { | ||||||
|         case .fileNotExists(let filename): |         case .fileNotExists(let filename): | ||||||
|             return "error: [\(Generate.toolName)] File \(filename) does not exists" |             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)" |             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 |             let readableCommand = command | ||||||
|                 .map { $0 } |  | ||||||
|                 .joined(separator: " ") |                 .joined(separator: " ") | ||||||
|             return "error: [\(Generate.toolName)] An error occured while running command '\(readableCommand)'. Command terminate with status code: \(terminationStatus)" |             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)" |             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. | //  Created by Thibaut Schmitt on 30/08/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
| struct GenerateOptions: ParsableArguments { | struct GenerateOptions: ParsableArguments { | ||||||
|  |  | ||||||
|     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") |     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") | ||||||
|     var forceGeneration = false |     var forceGeneration = false | ||||||
|      |  | ||||||
|     @Argument(help: "Configuration file.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Argument(help: "Configuration file.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var configurationFile: String |     var configurationFile: String | ||||||
|      |  | ||||||
|     @Option(help: "Project directory. It will be added to every relative path (path that does not start with `/`", |     @Option(help: "Project directory. It will be added to every relative path (path that does not start with `/`", | ||||||
|             transform: { |             transform: { | ||||||
|         if $0.last == "/" { |         if $0.last == "/" { | ||||||
|   | |||||||
| @@ -5,10 +5,11 @@ | |||||||
| //  Created by Thibaut Schmitt on 18/11/2022. | //  Created by Thibaut Schmitt on 18/11/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
|  | enum ArchitectureGenerator { | ||||||
|  |  | ||||||
| struct ArchitectureGenerator { |  | ||||||
|     static func writeArchitecture(_ architecture: ConfigurationArchitecture, projectDirectory: String) { |     static func writeArchitecture(_ architecture: ConfigurationArchitecture, projectDirectory: String) { | ||||||
|         // Create extension content |         // Create extension content | ||||||
|         var architectureContent = [ |         var architectureContent = [ | ||||||
| @@ -16,21 +17,21 @@ struct ArchitectureGenerator { | |||||||
|             architecture.getClass() |             architecture.getClass() | ||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n\n") |             .joined(separator: "\n\n") | ||||||
|          |  | ||||||
|         architectureContent += "\n" |         architectureContent += "\n" | ||||||
|          |  | ||||||
|         let filename = "\(architecture.classname).swift" |         let filename = "\(architecture.classname).swift" | ||||||
|         guard let filePath = architecture.path?.prependIfRelativePath(projectDirectory) else { |         guard let filePath = architecture.path?.prependIfRelativePath(projectDirectory) else { | ||||||
|             let error = GenerateError.writeFile(filename, "Path of file is not defined.") |             let error = GenerateError.writeFile(filename, "Path of file is not defined.") | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Generate.exit(withError: error) |             Generate.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Write content |         // Write content | ||||||
|         let architectureFilePathURL = URL(fileURLWithPath: "\(filePath)/\(filename)") |         let architectureFilePathURL = URL(fileURLWithPath: "\(filePath)/\(filename)") | ||||||
|         do { |         do { | ||||||
|             try architectureContent.write(to: architectureFilePathURL, atomically: false, encoding: .utf8) |             try architectureContent.write(to: architectureFilePathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = GenerateError.writeFile(filename, error.localizedDescription) |             let error = GenerateError.writeFile(filename, error.localizedDescription) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Generate.exit(withError: error) |             Generate.exit(withError: error) | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| struct ConfigurationFile: Codable, CustomDebugStringConvertible { | struct ConfigurationFile: Codable, CustomDebugStringConvertible { | ||||||
|  |  | ||||||
|     var architecture: ConfigurationArchitecture? |     var architecture: ConfigurationArchitecture? | ||||||
|     var analytics: [AnalyticsConfiguration] |     var analytics: [AnalyticsConfiguration] | ||||||
|     var colors: [ColorsConfiguration] |     var colors: [ColorsConfiguration] | ||||||
| @@ -15,12 +16,12 @@ struct ConfigurationFile: Codable, CustomDebugStringConvertible { | |||||||
|     var images: [ImagesConfiguration] |     var images: [ImagesConfiguration] | ||||||
|     var strings: [StringsConfiguration] |     var strings: [StringsConfiguration] | ||||||
|     var tags: [TagsConfiguration] |     var tags: [TagsConfiguration] | ||||||
|      |  | ||||||
|     var runnableConfigurations: [Runnable] { |     var runnableConfigurations: [Runnable] { | ||||||
|         let runnables: [[Runnable]] = [analytics, colors, fonts, images, strings, tags] |         let runnables: [[Runnable]] = [analytics, colors, fonts, images, strings, tags] | ||||||
|         return Array(runnables.joined()) |         return Array(runnables.joined()) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var debugDescription: String { |     var debugDescription: String { | ||||||
|         """ |         """ | ||||||
|         \(analytics) |         \(analytics) | ||||||
| @@ -44,20 +45,21 @@ struct ConfigurationFile: Codable, CustomDebugStringConvertible { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct ConfigurationArchitecture: Codable { | struct ConfigurationArchitecture: Codable { | ||||||
|  |  | ||||||
|     let property: String |     let property: String | ||||||
|     let classname: String |     let classname: String | ||||||
|     let path: String? |     let path: String? | ||||||
|     let children: [ConfigurationArchitecture]? |     let children: [Self]? | ||||||
|      |  | ||||||
|     func getProperty(isStatic: Bool) -> String { |     func getProperty(isStatic: Bool) -> String { | ||||||
|         "    \(isStatic ? "static " : "")let \(property) = \(classname)()" |         "    \(isStatic ? "static " : "")let \(property) = \(classname)()" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getClass(generateStaticProperty: Bool = true) -> String { |     func getClass(generateStaticProperty: Bool = true) -> String { | ||||||
|         guard children?.isEmpty == false else { |         guard children?.isEmpty == false else { | ||||||
|             return "final class \(classname): Sendable {}" |             return "final class \(classname): Sendable {}" | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         let classDefinition = [ |         let classDefinition = [ | ||||||
|             "class \(classname) {", |             "class \(classname) {", | ||||||
|             children?.map { $0.getProperty(isStatic: generateStaticProperty) }.joined(separator: "\n"), |             children?.map { $0.getProperty(isStatic: generateStaticProperty) }.joined(separator: "\n"), | ||||||
| @@ -65,12 +67,12 @@ struct ConfigurationArchitecture: Codable { | |||||||
|         ] |         ] | ||||||
|             .compactMap { $0 } |             .compactMap { $0 } | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|          |  | ||||||
|         return [classDefinition, "", getSubclass()] |         return [classDefinition, "", getSubclass()] | ||||||
|             .compactMap { $0 } |             .compactMap { $0 } | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getSubclass() -> String? { |     func getSubclass() -> String? { | ||||||
|         guard let children else { return nil } |         guard let children else { return nil } | ||||||
|         return children.compactMap { arch in |         return children.compactMap { arch in | ||||||
| @@ -81,26 +83,29 @@ struct ConfigurationArchitecture: Codable { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible { | struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible { | ||||||
|  |  | ||||||
|     let inputFile: String |     let inputFile: String | ||||||
|     let target: String |     let target: String | ||||||
|     let extensionOutputPath: String |     let extensionOutputPath: String | ||||||
|     let extensionName: String? |     let extensionName: String? | ||||||
|     let extensionSuffix: String? |     let extensionSuffix: String? | ||||||
|     private let staticMembers: Bool? |     private let staticMembers: Bool? | ||||||
|      |  | ||||||
|     var staticMembersOptions: Bool { |     var staticMembersOptions: Bool { | ||||||
|         if let staticMembers = staticMembers { |         if let staticMembers { | ||||||
|             return staticMembers |             return staticMembers | ||||||
|         } |         } | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     internal init(inputFile: String, |     internal init( | ||||||
|                   target: String, |         inputFile: String, | ||||||
|                   extensionOutputPath: String, |         target: String, | ||||||
|                   extensionName: String?, |         extensionOutputPath: String, | ||||||
|                   extensionSuffix: String?, |         extensionName: String?, | ||||||
|                   staticMembers: Bool?) { |         extensionSuffix: String?, | ||||||
|  |         staticMembers: Bool? | ||||||
|  |     ) { | ||||||
|         self.inputFile = inputFile |         self.inputFile = inputFile | ||||||
|         self.target = target |         self.target = target | ||||||
|         self.extensionOutputPath = extensionOutputPath |         self.extensionOutputPath = extensionOutputPath | ||||||
| @@ -108,7 +113,7 @@ struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|         self.extensionSuffix = extensionSuffix |         self.extensionSuffix = extensionSuffix | ||||||
|         self.staticMembers = staticMembers |         self.staticMembers = staticMembers | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var debugDescription: String { |     var debugDescription: String { | ||||||
|         """ |         """ | ||||||
|         Analytics configuration: |         Analytics configuration: | ||||||
| @@ -122,6 +127,7 @@ struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct ColorsConfiguration: Codable, CustomDebugStringConvertible { | struct ColorsConfiguration: Codable, CustomDebugStringConvertible { | ||||||
|  |  | ||||||
|     let inputFile: String |     let inputFile: String | ||||||
|     let style: String |     let style: String | ||||||
|     let xcassetsPath: String |     let xcassetsPath: String | ||||||
| @@ -130,22 +136,24 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|     let extensionNameUIKit: String? |     let extensionNameUIKit: String? | ||||||
|     let extensionSuffix: String? |     let extensionSuffix: String? | ||||||
|     private let staticMembers: Bool? |     private let staticMembers: Bool? | ||||||
|      |  | ||||||
|     var staticMembersOptions: Bool { |     var staticMembersOptions: Bool { | ||||||
|         if let staticMembers = staticMembers { |         if let staticMembers { | ||||||
|             return staticMembers |             return staticMembers | ||||||
|         } |         } | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     internal init(inputFile: String, |     internal init( | ||||||
|                   style: String, |         inputFile: String, | ||||||
|                   xcassetsPath: String, |         style: String, | ||||||
|                   extensionOutputPath: String, |         xcassetsPath: String, | ||||||
|                   extensionName: String?, |         extensionOutputPath: String, | ||||||
|                   extensionNameUIKit: String?, |         extensionName: String?, | ||||||
|                   extensionSuffix: String?, |         extensionNameUIKit: String?, | ||||||
|                   staticMembers: Bool?) { |         extensionSuffix: String?, | ||||||
|  |         staticMembers: Bool? | ||||||
|  |     ) { | ||||||
|         self.inputFile = inputFile |         self.inputFile = inputFile | ||||||
|         self.style = style |         self.style = style | ||||||
|         self.xcassetsPath = xcassetsPath |         self.xcassetsPath = xcassetsPath | ||||||
| @@ -155,7 +163,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|         self.extensionSuffix = extensionSuffix |         self.extensionSuffix = extensionSuffix | ||||||
|         self.staticMembers = staticMembers |         self.staticMembers = staticMembers | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var debugDescription: String { |     var debugDescription: String { | ||||||
|         """ |         """ | ||||||
|         Colors configuration: |         Colors configuration: | ||||||
| @@ -171,6 +179,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct FontsConfiguration: Codable, CustomDebugStringConvertible { | struct FontsConfiguration: Codable, CustomDebugStringConvertible { | ||||||
|  |  | ||||||
|     let inputFile: String |     let inputFile: String | ||||||
|     let extensionOutputPath: String |     let extensionOutputPath: String | ||||||
|     let extensionName: String? |     let extensionName: String? | ||||||
| @@ -178,21 +187,23 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|     let extensionSuffix: String? |     let extensionSuffix: String? | ||||||
|     let infoPlistPaths: String? |     let infoPlistPaths: String? | ||||||
|     private let staticMembers: Bool? |     private let staticMembers: Bool? | ||||||
|      |  | ||||||
|     var staticMembersOptions: Bool { |     var staticMembersOptions: Bool { | ||||||
|         if let staticMembers = staticMembers { |         if let staticMembers { | ||||||
|             return staticMembers |             return staticMembers | ||||||
|         } |         } | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     internal init(inputFile: String, |     internal init( | ||||||
|                   extensionOutputPath: String, |         inputFile: String, | ||||||
|                   extensionName: String?, |         extensionOutputPath: String, | ||||||
|                   extensionNameUIKit: String?, |         extensionName: String?, | ||||||
|                   extensionSuffix: String?, |         extensionNameUIKit: String?, | ||||||
|                   infoPlistPaths: String?, |         extensionSuffix: String?, | ||||||
|                   staticMembers: Bool?) { |         infoPlistPaths: String?, | ||||||
|  |         staticMembers: Bool? | ||||||
|  |     ) { | ||||||
|         self.inputFile = inputFile |         self.inputFile = inputFile | ||||||
|         self.extensionOutputPath = extensionOutputPath |         self.extensionOutputPath = extensionOutputPath | ||||||
|         self.extensionName = extensionName |         self.extensionName = extensionName | ||||||
| @@ -201,7 +212,7 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|         self.infoPlistPaths = infoPlistPaths |         self.infoPlistPaths = infoPlistPaths | ||||||
|         self.staticMembers = staticMembers |         self.staticMembers = staticMembers | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var debugDescription: String { |     var debugDescription: String { | ||||||
|         """ |         """ | ||||||
|         Fonts configuration: |         Fonts configuration: | ||||||
| @@ -216,6 +227,7 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct ImagesConfiguration: Codable, CustomDebugStringConvertible { | struct ImagesConfiguration: Codable, CustomDebugStringConvertible { | ||||||
|  |  | ||||||
|     let inputFile: String |     let inputFile: String | ||||||
|     let xcassetsPath: String |     let xcassetsPath: String | ||||||
|     let extensionOutputPath: String |     let extensionOutputPath: String | ||||||
| @@ -223,21 +235,23 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|     let extensionNameUIKit: String? |     let extensionNameUIKit: String? | ||||||
|     let extensionSuffix: String? |     let extensionSuffix: String? | ||||||
|     private let staticMembers: Bool? |     private let staticMembers: Bool? | ||||||
|      |  | ||||||
|     var staticMembersOptions: Bool { |     var staticMembersOptions: Bool { | ||||||
|         if let staticMembers = staticMembers { |         if let staticMembers { | ||||||
|             return staticMembers |             return staticMembers | ||||||
|         } |         } | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     internal init(inputFile: String, |     internal init( | ||||||
|                   xcassetsPath: String, |         inputFile: String, | ||||||
|                   extensionOutputPath: String, |         xcassetsPath: String, | ||||||
|                   extensionName: String?, |         extensionOutputPath: String, | ||||||
|                   extensionNameUIKit: String?, |         extensionName: String?, | ||||||
|                   extensionSuffix: String?, |         extensionNameUIKit: String?, | ||||||
|                   staticMembers: Bool?) { |         extensionSuffix: String?, | ||||||
|  |         staticMembers: Bool? | ||||||
|  |     ) { | ||||||
|         self.inputFile = inputFile |         self.inputFile = inputFile | ||||||
|         self.xcassetsPath = xcassetsPath |         self.xcassetsPath = xcassetsPath | ||||||
|         self.extensionOutputPath = extensionOutputPath |         self.extensionOutputPath = extensionOutputPath | ||||||
| @@ -246,7 +260,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|         self.extensionSuffix = extensionSuffix |         self.extensionSuffix = extensionSuffix | ||||||
|         self.staticMembers = staticMembers |         self.staticMembers = staticMembers | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var debugDescription: String { |     var debugDescription: String { | ||||||
|         """ |         """ | ||||||
|         Images configuration: |         Images configuration: | ||||||
| @@ -261,6 +275,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct StringsConfiguration: Codable, CustomDebugStringConvertible { | struct StringsConfiguration: Codable, CustomDebugStringConvertible { | ||||||
|  |  | ||||||
|     let inputFile: String |     let inputFile: String | ||||||
|     let outputPath: String |     let outputPath: String | ||||||
|     let langs: String |     let langs: String | ||||||
| @@ -272,28 +287,30 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|     private let xcStrings: Bool? |     private let xcStrings: Bool? | ||||||
|  |  | ||||||
|     var staticMembersOptions: Bool { |     var staticMembersOptions: Bool { | ||||||
|         if let staticMembers = staticMembers { |         if let staticMembers { | ||||||
|             return staticMembers |             return staticMembers | ||||||
|         } |         } | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var xcStringsOptions: Bool { |     var xcStringsOptions: Bool { | ||||||
|         if let xcStrings = xcStrings { |         if let xcStrings { | ||||||
|             return xcStrings |             return xcStrings | ||||||
|         } |         } | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     internal init(inputFile: String, |     internal init( | ||||||
|                   outputPath: String, |         inputFile: String, | ||||||
|                   langs: String, |         outputPath: String, | ||||||
|                   defaultLang: String, |         langs: String, | ||||||
|                   extensionOutputPath: String, |         defaultLang: String, | ||||||
|                   extensionName: String?, |         extensionOutputPath: String, | ||||||
|                   extensionSuffix: String?, |         extensionName: String?, | ||||||
|                   staticMembers: Bool?, |         extensionSuffix: String?, | ||||||
|                   xcStrings: Bool?) { |         staticMembers: Bool?, | ||||||
|  |         xcStrings: Bool? | ||||||
|  |     ) { | ||||||
|         self.inputFile = inputFile |         self.inputFile = inputFile | ||||||
|         self.outputPath = outputPath |         self.outputPath = outputPath | ||||||
|         self.langs = langs |         self.langs = langs | ||||||
| @@ -304,7 +321,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|         self.staticMembers = staticMembers |         self.staticMembers = staticMembers | ||||||
|         self.xcStrings = xcStrings |         self.xcStrings = xcStrings | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var debugDescription: String { |     var debugDescription: String { | ||||||
|         """ |         """ | ||||||
|         Strings configuration: |         Strings configuration: | ||||||
| @@ -320,26 +337,29 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct TagsConfiguration: Codable, CustomDebugStringConvertible { | struct TagsConfiguration: Codable, CustomDebugStringConvertible { | ||||||
|  |  | ||||||
|     let inputFile: String |     let inputFile: String | ||||||
|     let lang: String |     let lang: String | ||||||
|     let extensionOutputPath: String |     let extensionOutputPath: String | ||||||
|     let extensionName: String? |     let extensionName: String? | ||||||
|     let extensionSuffix: String? |     let extensionSuffix: String? | ||||||
|     private let staticMembers: Bool? |     private let staticMembers: Bool? | ||||||
|      |  | ||||||
|     var staticMembersOptions: Bool { |     var staticMembersOptions: Bool { | ||||||
|         if let staticMembers = staticMembers { |         if let staticMembers { | ||||||
|             return staticMembers |             return staticMembers | ||||||
|         } |         } | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     internal init(inputFile: String, |     internal init( | ||||||
|                   lang: String, |         inputFile: String, | ||||||
|                   extensionOutputPath: String, |         lang: String, | ||||||
|                   extensionName: String?, |         extensionOutputPath: String, | ||||||
|                   extensionSuffix: String?, |         extensionName: String?, | ||||||
|                   staticMembers: Bool?) { |         extensionSuffix: String?, | ||||||
|  |         staticMembers: Bool? | ||||||
|  |     ) { | ||||||
|         self.inputFile = inputFile |         self.inputFile = inputFile | ||||||
|         self.lang = lang |         self.lang = lang | ||||||
|         self.extensionOutputPath = extensionOutputPath |         self.extensionOutputPath = extensionOutputPath | ||||||
| @@ -347,7 +367,7 @@ struct TagsConfiguration: Codable, CustomDebugStringConvertible { | |||||||
|         self.extensionSuffix = extensionSuffix |         self.extensionSuffix = extensionSuffix | ||||||
|         self.staticMembers = staticMembers |         self.staticMembers = staticMembers | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var debugDescription: String { |     var debugDescription: String { | ||||||
|         """ |         """ | ||||||
|         Tags configuration: |         Tags configuration: | ||||||
|   | |||||||
| @@ -8,14 +8,15 @@ | |||||||
| import Foundation | import Foundation | ||||||
| import Yams | import Yams | ||||||
|  |  | ||||||
| class ConfigurationFileParser { | enum ConfigurationFileParser { | ||||||
|  |  | ||||||
|     static func parse(_ configurationFile: String) -> ConfigurationFile { |     static func parse(_ configurationFile: String) -> ConfigurationFile { | ||||||
|         guard let data = FileManager().contents(atPath: configurationFile) else { |         guard let data = FileManager().contents(atPath: configurationFile) else { | ||||||
|             let error = GenerateError.fileNotExists(configurationFile) |             let error = GenerateError.fileNotExists(configurationFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Generate.exit(withError: error) |             Generate.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         do { |         do { | ||||||
|             return try YAMLDecoder().decode(ConfigurationFile.self, from: data) |             return try YAMLDecoder().decode(ConfigurationFile.self, from: data) | ||||||
|         } catch { |         } catch { | ||||||
|   | |||||||
| @@ -8,13 +8,14 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension AnalyticsConfiguration: Runnable { | extension AnalyticsConfiguration: Runnable { | ||||||
|  |  | ||||||
|     func run(projectDirectory: String, force: Bool) { |     func run(projectDirectory: String, force: Bool) { | ||||||
|         var args = [String]() |         var args = [String]() | ||||||
|          |  | ||||||
|         if force { |         if force { | ||||||
|             args += ["-f"] |             args += ["-f"] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         args += [ |         args += [ | ||||||
|             inputFile.prependIfRelativePath(projectDirectory), |             inputFile.prependIfRelativePath(projectDirectory), | ||||||
|             "--target", |             "--target", | ||||||
| @@ -24,20 +25,20 @@ extension AnalyticsConfiguration: Runnable { | |||||||
|             "--static-members", |             "--static-members", | ||||||
|             "\(staticMembersOptions)" |             "\(staticMembersOptions)" | ||||||
|         ] |         ] | ||||||
|          |  | ||||||
|         if let extensionName = extensionName { |         if let extensionName { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name", |                 "--extension-name", | ||||||
|                 extensionName |                 extensionName | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-suffix", |                 "--extension-suffix", | ||||||
|                 extensionSuffix |                 extensionSuffix | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         Analytics.main(args) |         Analytics.main(args) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,18 +8,19 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension ColorsConfiguration: Runnable { | extension ColorsConfiguration: Runnable { | ||||||
|  |  | ||||||
|     func run(projectDirectory: String, force: Bool) { |     func run(projectDirectory: String, force: Bool) { | ||||||
|         let args = getArguments(projectDirectory: projectDirectory, force: force) |         let args = getArguments(projectDirectory: projectDirectory, force: force) | ||||||
|         Colors.main(args) |         Colors.main(args) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getArguments(projectDirectory: String, force: Bool) -> [String] { |     func getArguments(projectDirectory: String, force: Bool) -> [String] { | ||||||
|         var args = [String]() |         var args = [String]() | ||||||
|          |  | ||||||
|         if force { |         if force { | ||||||
|             args += ["-f"] |             args += ["-f"] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         args += [ |         args += [ | ||||||
|             inputFile.prependIfRelativePath(projectDirectory), |             inputFile.prependIfRelativePath(projectDirectory), | ||||||
|             "--style", |             "--style", | ||||||
| @@ -31,26 +32,26 @@ extension ColorsConfiguration: Runnable { | |||||||
|             "--static-members", |             "--static-members", | ||||||
|             "\(staticMembersOptions)" |             "\(staticMembersOptions)" | ||||||
|         ] |         ] | ||||||
|          |  | ||||||
|         if let extensionName = extensionName { |         if let extensionName { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name", |                 "--extension-name", | ||||||
|                 extensionName |                 extensionName | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|         if let extensionNameUIKit = extensionNameUIKit { |         if let extensionNameUIKit { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name-ui-kit", |                 "--extension-name-ui-kit", | ||||||
|                 extensionNameUIKit |                 extensionNameUIKit | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-suffix", |                 "--extension-suffix", | ||||||
|                 extensionSuffix |                 extensionSuffix | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return args |         return args | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,18 +8,19 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension FontsConfiguration: Runnable { | extension FontsConfiguration: Runnable { | ||||||
|  |  | ||||||
|     func run(projectDirectory: String, force: Bool) { |     func run(projectDirectory: String, force: Bool) { | ||||||
|         let args = getArguments(projectDirectory: projectDirectory, force: force) |         let args = getArguments(projectDirectory: projectDirectory, force: force) | ||||||
|         Fonts.main(args) |         Fonts.main(args) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getArguments(projectDirectory: String, force: Bool) -> [String] { |     func getArguments(projectDirectory: String, force: Bool) -> [String] { | ||||||
|         var args = [String]() |         var args = [String]() | ||||||
|          |  | ||||||
|         if force { |         if force { | ||||||
|             args += ["-f"] |             args += ["-f"] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         args += [ |         args += [ | ||||||
|             inputFile.prependIfRelativePath(projectDirectory), |             inputFile.prependIfRelativePath(projectDirectory), | ||||||
|             "--extension-output-path", |             "--extension-output-path", | ||||||
| @@ -27,39 +28,39 @@ extension FontsConfiguration: Runnable { | |||||||
|             "--static-members", |             "--static-members", | ||||||
|             "\(staticMembersOptions)" |             "\(staticMembersOptions)" | ||||||
|         ] |         ] | ||||||
|          |  | ||||||
|         if let extensionName = extensionName { |         if let extensionName { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name", |                 "--extension-name", | ||||||
|                 extensionName |                 extensionName | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|         if let extensionNameUIKit = extensionNameUIKit { |         if let extensionNameUIKit { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name-ui-kit", |                 "--extension-name-ui-kit", | ||||||
|                 extensionNameUIKit |                 extensionNameUIKit | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-suffix", |                 "--extension-suffix", | ||||||
|                 extensionSuffix |                 extensionSuffix | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if let infoPlistPaths = infoPlistPaths { |         if let infoPlistPaths { | ||||||
|             let adjustedPlistPaths = infoPlistPaths |             let adjustedPlistPaths = infoPlistPaths | ||||||
|                 .split(separator: " ") |                 .split(separator: " ") | ||||||
|                 .map { String($0).prependIfRelativePath(projectDirectory) } |                 .map { String($0).prependIfRelativePath(projectDirectory) } | ||||||
|                 .joined(separator: " ") |                 .joined(separator: " ") | ||||||
|              |  | ||||||
|             args += [ |             args += [ | ||||||
|                 "--info-plist-paths", |                 "--info-plist-paths", | ||||||
|                 adjustedPlistPaths |                 adjustedPlistPaths | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return args |         return args | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // | // | ||||||
| //  ImagesConfiguration+Runnable.swift | //  ImagesConfiguration+Runnable.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 30/08/2022. | //  Created by Thibaut Schmitt on 30/08/2022. | ||||||
| // | // | ||||||
| @@ -8,18 +8,19 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension ImagesConfiguration: Runnable { | extension ImagesConfiguration: Runnable { | ||||||
|  |  | ||||||
|     func run(projectDirectory: String, force: Bool) { |     func run(projectDirectory: String, force: Bool) { | ||||||
|         let args = getArguments(projectDirectory: projectDirectory, force: force) |         let args = getArguments(projectDirectory: projectDirectory, force: force) | ||||||
|         Images.main(args) |         Images.main(args) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getArguments(projectDirectory: String, force: Bool) -> [String] { |     func getArguments(projectDirectory: String, force: Bool) -> [String] { | ||||||
|         var args = [String]() |         var args = [String]() | ||||||
|          |  | ||||||
|         if force { |         if force { | ||||||
|             args += ["-f"] // Images has a -f and -F options |             args += ["-f"] // Images has a -f and -F options | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         args += [ |         args += [ | ||||||
|             inputFile.prependIfRelativePath(projectDirectory), |             inputFile.prependIfRelativePath(projectDirectory), | ||||||
|             "--xcassets-path", |             "--xcassets-path", | ||||||
| @@ -29,26 +30,28 @@ extension ImagesConfiguration: Runnable { | |||||||
|             "--static-members", |             "--static-members", | ||||||
|             "\(staticMembersOptions)" |             "\(staticMembersOptions)" | ||||||
|         ] |         ] | ||||||
|          |  | ||||||
|         if let extensionName = extensionName { |         if let extensionName { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name", |                 "--extension-name", | ||||||
|                 extensionName |                 extensionName | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|         if let extensionNameUIKit = extensionNameUIKit { |  | ||||||
|  |         if let extensionNameUIKit { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name-ui-kit", |                 "--extension-name-ui-kit", | ||||||
|                 extensionNameUIKit |                 extensionNameUIKit | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|         if let extensionSuffix = extensionSuffix { |  | ||||||
|  |         if let extensionSuffix { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-suffix", |                 "--extension-suffix", | ||||||
|                 extensionSuffix |                 extensionSuffix | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return args |         return args | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,5 +8,6 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| protocol Runnable { | protocol Runnable { | ||||||
|  |  | ||||||
|     func run(projectDirectory: String, force: Bool) |     func run(projectDirectory: String, force: Bool) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,9 +8,10 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension StringsConfiguration: Runnable { | extension StringsConfiguration: Runnable { | ||||||
|  |  | ||||||
|     func run(projectDirectory: String, force: Bool) { |     func run(projectDirectory: String, force: Bool) { | ||||||
|         var args = [String]() |         var args = [String]() | ||||||
|          |  | ||||||
|         if force { |         if force { | ||||||
|             args += ["-f"] |             args += ["-f"] | ||||||
|         } |         } | ||||||
| @@ -30,21 +31,21 @@ extension StringsConfiguration: Runnable { | |||||||
|             "--xc-strings", |             "--xc-strings", | ||||||
|             "\(xcStringsOptions)" |             "\(xcStringsOptions)" | ||||||
|         ] |         ] | ||||||
|          |  | ||||||
|         if let extensionName = extensionName { |         if let extensionName { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name", |                 "--extension-name", | ||||||
|                 extensionName |                 extensionName | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-suffix", |                 "--extension-suffix", | ||||||
|                 extensionSuffix |                 extensionSuffix | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         Stringium.main(args) |         Stringium.main(args) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,13 +8,14 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension TagsConfiguration: Runnable { | extension TagsConfiguration: Runnable { | ||||||
|  |  | ||||||
|     func run(projectDirectory: String, force: Bool) { |     func run(projectDirectory: String, force: Bool) { | ||||||
|         var args = [String]() |         var args = [String]() | ||||||
|          |  | ||||||
|         if force { |         if force { | ||||||
|             args += ["-f"] |             args += ["-f"] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         args += [ |         args += [ | ||||||
|             inputFile.prependIfRelativePath(projectDirectory), |             inputFile.prependIfRelativePath(projectDirectory), | ||||||
|             "--lang", |             "--lang", | ||||||
| @@ -24,20 +25,20 @@ extension TagsConfiguration: Runnable { | |||||||
|             "--static-members", |             "--static-members", | ||||||
|             "\(staticMembersOptions)" |             "\(staticMembersOptions)" | ||||||
|         ] |         ] | ||||||
|          |  | ||||||
|         if let extensionName = extensionName { |         if let extensionName { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-name", |                 "--extension-name", | ||||||
|                 extensionName |                 extensionName | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             args += [ |             args += [ | ||||||
|                 "--extension-suffix", |                 "--extension-suffix", | ||||||
|                 extensionSuffix |                 extensionSuffix | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         Tags.main(args) |         Tags.main(args) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,7 +7,10 @@ | |||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable force_unwrapping | ||||||
|  |  | ||||||
| extension FileManager { | extension FileManager { | ||||||
|  |  | ||||||
|     func getAllRegularFileIn(directory: String) -> [String] { |     func getAllRegularFileIn(directory: String) -> [String] { | ||||||
|         var files = [String]() |         var files = [String]() | ||||||
|         guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { |         guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { | ||||||
| @@ -15,7 +18,7 @@ extension FileManager { | |||||||
|             print(error.description) |             print(error.description) | ||||||
|             Images.exit(withError: error) |             Images.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         for case let fileURL as URL in enumerator { |         for case let fileURL as URL in enumerator { | ||||||
|             do { |             do { | ||||||
|                 let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey]) |                 let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey]) | ||||||
| @@ -30,7 +33,7 @@ extension FileManager { | |||||||
|         } |         } | ||||||
|         return files |         return files | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getAllImageSetFolderIn(directory: String) -> [String] { |     func getAllImageSetFolderIn(directory: String) -> [String] { | ||||||
|         var files = [String]() |         var files = [String]() | ||||||
|         guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { |         guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { | ||||||
| @@ -38,7 +41,7 @@ extension FileManager { | |||||||
|             print(error.description) |             print(error.description) | ||||||
|             Images.exit(withError: error) |             Images.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         for case let fileURL as URL in enumerator { |         for case let fileURL as URL in enumerator { | ||||||
|             do { |             do { | ||||||
|                 let fileAttributes = try fileURL.resourceValues(forKeys: [.isDirectoryKey]) |                 let fileAttributes = try fileURL.resourceValues(forKeys: [.isDirectoryKey]) | ||||||
|   | |||||||
| @@ -5,42 +5,48 @@ | |||||||
| //  Created by Thibaut Schmitt on 14/02/2022. | //  Created by Thibaut Schmitt on 14/02/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
|  | enum ImageExtensionGenerator { | ||||||
|  |  | ||||||
| class ImageExtensionGenerator { |  | ||||||
|      |  | ||||||
|     // MARK: - UIKit |     // MARK: - UIKit | ||||||
|      |  | ||||||
|     static func generateExtensionFile(images: [ParsedImage], |     static func generateExtensionFile( | ||||||
|                                       staticVar: Bool, |         images: [ParsedImage], | ||||||
|                                       inputFilename: String, |         staticVar: Bool, | ||||||
|                                       extensionName: String, |         inputFilename: String, | ||||||
|                                       extensionFilePath: String, |         extensionName: String, | ||||||
|                                       isSwiftUI: Bool) { |         extensionFilePath: String, | ||||||
|  |         isSwiftUI: Bool | ||||||
|  |     ) { | ||||||
|         // Create extension conten1t |         // Create extension conten1t | ||||||
|         let extensionContent = Self.getExtensionContent(images: images, |         let extensionContent = Self.getExtensionContent( | ||||||
|                                                         staticVar: staticVar, |             images: images, | ||||||
|                                                         extensionName: extensionName, |             staticVar: staticVar, | ||||||
|                                                         inputFilename: inputFilename, |             extensionName: extensionName, | ||||||
|                                                         isSwiftUI: isSwiftUI) |             inputFilename: inputFilename, | ||||||
|          |             isSwiftUI: isSwiftUI | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Write content |         // Write content | ||||||
|         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) |         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) | ||||||
|         do { |         do { | ||||||
|             try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) |             try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription) |             let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Images.exit(withError: error) |             Images.exit(withError: error) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     static func getExtensionContent(images: [ParsedImage], |     static func getExtensionContent( | ||||||
|                                     staticVar: Bool, |         images: [ParsedImage], | ||||||
|                                     extensionName: String, |         staticVar: Bool, | ||||||
|                                     inputFilename: String, |         extensionName: String, | ||||||
|                                     isSwiftUI: Bool) -> String { |         inputFilename: String, | ||||||
|  |         isSwiftUI: Bool | ||||||
|  |     ) -> String { | ||||||
|         [ |         [ | ||||||
|             Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName, isSwiftUI: isSwiftUI), |             Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName, isSwiftUI: isSwiftUI), | ||||||
|             Self.getProperties(images: images, staticVar: staticVar, isSwiftUI: isSwiftUI), |             Self.getProperties(images: images, staticVar: staticVar, isSwiftUI: isSwiftUI), | ||||||
| @@ -48,30 +54,36 @@ class ImageExtensionGenerator { | |||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getHeader(inputFilename: String, |     private static func getHeader( | ||||||
|                                   extensionClassname: String, |         inputFilename: String, | ||||||
|                                   isSwiftUI: Bool) -> String { |         extensionClassname: String, | ||||||
|  |         isSwiftUI: Bool | ||||||
|  |     ) -> String { | ||||||
|         """ |         """ | ||||||
|         // Generated by ResgenSwift.\(Images.toolName) \(ResgenSwiftVersion) |         // Generated by ResgenSwift.\(Images.toolName) \(ResgenSwiftVersion) | ||||||
|         // Images from \(inputFilename) |         // Images from \(inputFilename) | ||||||
|          |  | ||||||
|         import \(isSwiftUI ? "SwiftUI" : "UIKit") |         import \(isSwiftUI ? "SwiftUI" : "UIKit") | ||||||
|          |  | ||||||
|         extension \(extensionClassname) { |         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 |         images | ||||||
|             .map { "\n\($0.getImageProperty(isStatic: staticVar, isSwiftUI: isSwiftUI))" } |             .map { "\n\($0.getImageProperty(isStatic: staticVar, isSwiftUI: isSwiftUI))" } | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getFooter() -> String { |     private static func getFooter() -> String { | ||||||
|         """ |         """ | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // | // | ||||||
| //  XcassetsGenerator.swift | //  XcassetsGenerator.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 24/01/2022. | //  Created by Thibaut Schmitt on 24/01/2022. | ||||||
| // | // | ||||||
| @@ -9,31 +9,34 @@ import Foundation | |||||||
| import ToolCore | import ToolCore | ||||||
|  |  | ||||||
| enum OutputImageExtension: String { | enum OutputImageExtension: String { | ||||||
|  |  | ||||||
|     case png |     case png | ||||||
|     case svg |     case svg | ||||||
| } | } | ||||||
|  |  | ||||||
| class XcassetsGenerator { | class XcassetsGenerator { | ||||||
|  |  | ||||||
|  |     // MARK: - Properties | ||||||
|  |  | ||||||
|     let forceGeneration: Bool |     let forceGeneration: Bool | ||||||
|      |  | ||||||
|     // MARK: - Init |     // MARK: - Init | ||||||
|      |  | ||||||
|     init(forceGeneration: Bool) { |     init(forceGeneration: Bool) { | ||||||
|         self.forceGeneration = forceGeneration |         self.forceGeneration = forceGeneration | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Assets generation |     // MARK: - Assets generation | ||||||
|      |  | ||||||
|     func generateXcassets(inputPath: String, imagesToGenerate: [ParsedImage], xcassetsPath: String) { |     func generateXcassets(inputPath: String, imagesToGenerate: [ParsedImage], xcassetsPath: String) { | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|         let svgConverter = Images.getSvgConverterPath() |         let svgConverter = Images.getSvgConverterPath() | ||||||
|         let allSubFiles = fileManager.getAllRegularFileIn(directory: inputPath) |         let allSubFiles = fileManager.getAllRegularFileIn(directory: inputPath) | ||||||
|          |  | ||||||
|         var generatedAssetsPaths = [String]() |         var generatedAssetsPaths = [String]() | ||||||
|          |  | ||||||
|         // Generate new assets |         // Generate new assets | ||||||
|         imagesToGenerate.forEach { parsedImage in |         imagesToGenerate.forEach { parsedImage in // swiftlint:disable:this closure_body_length | ||||||
|             // Get image path |             // Get image path | ||||||
|             let imageData: (path: String, ext: String) = { |             let imageData: (path: String, ext: String) = { | ||||||
|                 for subfile in allSubFiles { |                 for subfile in allSubFiles { | ||||||
| @@ -54,14 +57,14 @@ class XcassetsGenerator { | |||||||
|                 print(error.description) |                 print(error.description) | ||||||
|                 Images.exit(withError: error) |                 Images.exit(withError: error) | ||||||
|             }() |             }() | ||||||
|              |  | ||||||
|             // Create imageset folder name |             // Create imageset folder name | ||||||
|             let imagesetName = "\(parsedImage.name).imageset" |             let imagesetName = "\(parsedImage.name).imageset" | ||||||
|             let imagesetPath = "\(xcassetsPath)/\(imagesetName)" |             let imagesetPath = "\(xcassetsPath)/\(imagesetName)" | ||||||
|              |  | ||||||
|             // Store managed images path |             // Store managed images path | ||||||
|             generatedAssetsPaths.append(imagesetName) |             generatedAssetsPaths.append(imagesetName) | ||||||
|              |  | ||||||
|             // Generate output images path |             // Generate output images path | ||||||
|             let output1x = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.png.rawValue)" |             let output1x = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.png.rawValue)" | ||||||
|             let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(OutputImageExtension.png.rawValue)" |             let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(OutputImageExtension.png.rawValue)" | ||||||
| @@ -79,12 +82,14 @@ class XcassetsGenerator { | |||||||
|                 print("\(parsedImage.name) -> Not regenerating") |                 print("\(parsedImage.name) -> Not regenerating") | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             // Create imageset folder |             // Create imageset folder | ||||||
|             if fileManager.fileExists(atPath: imagesetPath) == false { |             if fileManager.fileExists(atPath: imagesetPath) == false { | ||||||
|                 do { |                 do { | ||||||
|                     try fileManager.createDirectory(atPath: imagesetPath, |                     try fileManager.createDirectory( | ||||||
|                                                     withIntermediateDirectories: true) |                         atPath: imagesetPath, | ||||||
|  |                         withIntermediateDirectories: true | ||||||
|  |                     ) | ||||||
|                 } catch { |                 } catch { | ||||||
|                     let error = ImagesError.createAssetFolder(imagesetPath) |                     let error = ImagesError.createAssetFolder(imagesetPath) | ||||||
|                     print(error.description) |                     print(error.description) | ||||||
| @@ -124,9 +129,7 @@ class XcassetsGenerator { | |||||||
|                     Shell.shell(command1x) |                     Shell.shell(command1x) | ||||||
|                     Shell.shell(command2x) |                     Shell.shell(command2x) | ||||||
|                     Shell.shell(command3x) |                     Shell.shell(command3x) | ||||||
|  |  | ||||||
|                 } else { |                 } else { | ||||||
|  |  | ||||||
|                     let output = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.svg.rawValue)" |                     let output = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.svg.rawValue)" | ||||||
|                     let tempURL = URL(fileURLWithPath: output) |                     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 200x300 path/to/output.png | ||||||
|                 // convert path/to/image.png -resize 200x 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 |                 // convert path/to/image.png -resize x300 path/to/output.png | ||||||
|                 Shell.shell(["convert", "\(imageData.path)", |                 Shell.shell( | ||||||
|                              "-resize", "\(convertArguments.x1.width ?? "")x\(convertArguments.x1.height ?? "")", |                     [ | ||||||
|                              output1x]) |                         "convert", | ||||||
|                 Shell.shell(["convert", "\(imageData.path)", |                         "\(imageData.path)", | ||||||
|                              "-resize", "\(convertArguments.x2.width ?? "")x\(convertArguments.x2.height ?? "")", |                         "-resize", | ||||||
|                              output2x]) |                         "\(convertArguments.x1.width ?? "")x\(convertArguments.x1.height ?? "")", | ||||||
|                 Shell.shell(["convert", "\(imageData.path)", |                         output1x | ||||||
|                              "-resize", "\(convertArguments.x3.width ?? "")x\(convertArguments.x3.height ?? "")", |                     ] | ||||||
|                              output3x]) |                 ) | ||||||
|  |                 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 |             // Write Content.json | ||||||
|             guard let imagesetContentJson = parsedImage.generateContentJson(isVector: imageData.ext == "svg") else { return } |             guard let imagesetContentJson = parsedImage.generateContentJson(isVector: imageData.ext == "svg") else { return } | ||||||
|             let contentJsonFilePath = "\(imagesetPath)/Contents.json" |             let contentJsonFilePath = "\(imagesetPath)/Contents.json" | ||||||
|              |  | ||||||
|             let contentJsonFilePathURL = URL(fileURLWithPath: contentJsonFilePath) |             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") |             print("\(parsedImage.name) -> Generated") | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Success info |         // Success info | ||||||
|         let generatedAssetsCount = generatedAssetsPaths.count |         let generatedAssetsCount = generatedAssetsPaths.count | ||||||
|         print("Images generated: \(generatedAssetsCount)") |         print("Images generated: \(generatedAssetsCount)") | ||||||
|          |  | ||||||
|         // Delete old assets |         // Delete old assets | ||||||
|         let allImagesetName = Set(fileManager.getAllImageSetFolderIn(directory: xcassetsPath))         |         let allImagesetName = Set(fileManager.getAllImageSetFolderIn(directory: xcassetsPath)) | ||||||
|         let imagesetToRemove = allImagesetName.subtracting(Set(generatedAssetsPaths)) |         let imagesetToRemove = allImagesetName.subtracting(Set(generatedAssetsPaths)) | ||||||
|          |  | ||||||
|         imagesetToRemove.forEach { |         imagesetToRemove.forEach { | ||||||
|             print("Will remove: \($0)") |             print("Will remove: \($0)") | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         imagesetToRemove.forEach { itemToRemove in |         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") |         print("Removed \(imagesetToRemove.count) images") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Helpers: SVG command |     // MARK: - Helpers: SVG command | ||||||
|      |  | ||||||
|     private func addConvertArgument(command: inout [String], convertArgument: ConvertArgument) { |     private func addConvertArgument(command: inout [String], convertArgument: ConvertArgument) { | ||||||
|         if let width = convertArgument.width, width.isEmpty == false { |         if let width = convertArgument.width, width.isEmpty == false { | ||||||
|             command.append("-w") |             command.append("-w") | ||||||
| @@ -194,14 +219,14 @@ class XcassetsGenerator { | |||||||
|             command.append("\(height)") |             command.append("\(height)") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Helpers: bypass generation |     // MARK: - Helpers: bypass generation | ||||||
|      |  | ||||||
|     private func shouldGenerate(inputImagePath: String, xcassetImagePath: String, needToGenerateForSvg: Bool) -> Bool { |     private func shouldGenerate(inputImagePath: String, xcassetImagePath: String, needToGenerateForSvg: Bool) -> Bool { | ||||||
|         if forceGeneration || needToGenerateForSvg { |         if forceGeneration || needToGenerateForSvg { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return GeneratorChecker.isFile(inputImagePath, moreRecenThan: xcassetImagePath) |         return GeneratorChecker.isFile(inputImagePath, moreRecenThan: xcassetImagePath) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,123 +1,131 @@ | |||||||
| // | // | ||||||
| //  Images.swift | //  Images.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 24/01/2022. | //  Created by Thibaut Schmitt on 24/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct Images: ParsableCommand { | struct Images: ParsableCommand { | ||||||
|      |  | ||||||
|     // MARK: - CommandConfiguration |     // MARK: - CommandConfiguration | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static var configuration = CommandConfiguration( | ||||||
|         abstract: "A utility for generate images and an extension to access them easily.", |         abstract: "A utility for generate images and an extension to access them easily.", | ||||||
|         version: ResgenSwiftVersion |         version: ResgenSwiftVersion | ||||||
|     ) |     ) | ||||||
|      |  | ||||||
|     // MARK: - Static |     // MARK: - Static | ||||||
|      |  | ||||||
|     static let toolName = "Images" |     static let toolName = "Images" | ||||||
|     static let defaultExtensionName = "Image" |     static let defaultExtensionName = "Image" | ||||||
|     static let defaultExtensionNameUIKit = "UIImage" |     static let defaultExtensionNameUIKit = "UIImage" | ||||||
|      |  | ||||||
|     // MARK: - Command Options |     // MARK: - Command Options | ||||||
|      |  | ||||||
|     @OptionGroup var options: ImagesOptions |     @OptionGroup var options: ImagesOptions | ||||||
|      |  | ||||||
|     // MARK: - Run |     // MARK: - Run | ||||||
|      |  | ||||||
|     mutating func run() { |     mutating func run() { | ||||||
|         print("[\(Self.toolName)] Starting images generation") |         print("[\(Self.toolName)] Starting images generation") | ||||||
|         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate images in xcassets \(options.xcassetsPath)") |         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate images in xcassets \(options.xcassetsPath)") | ||||||
|          |  | ||||||
|         // Check requirements |         // Check requirements | ||||||
|         guard checkRequirements() else { return } |         guard checkRequirements() else { return } | ||||||
|          |  | ||||||
|         print("[\(Self.toolName)] Will generate images") |         print("[\(Self.toolName)] Will generate images") | ||||||
|          |  | ||||||
|         // Parse input file |         // Parse input file | ||||||
|         let imagesToGenerate = ImageFileParser.parse(options.inputFile, platform: PlatormTag.ios) |         let imagesToGenerate = ImageFileParser.parse(options.inputFile, platform: PlatormTag.ios) | ||||||
|          |  | ||||||
|         // Generate xcassets files |         // Generate xcassets files | ||||||
|         let inputFolder = URL(fileURLWithPath: options.inputFile) |         let inputFolder = URL(fileURLWithPath: options.inputFile) | ||||||
|             .deletingLastPathComponent() |             .deletingLastPathComponent() | ||||||
|             .relativePath |             .relativePath | ||||||
|          |  | ||||||
|         let xcassetsGenerator = XcassetsGenerator(forceGeneration: options.forceExecutionAndGeneration) |         let xcassetsGenerator = XcassetsGenerator(forceGeneration: options.forceExecutionAndGeneration) | ||||||
|         xcassetsGenerator.generateXcassets(inputPath: inputFolder, |         xcassetsGenerator.generateXcassets( | ||||||
|                                            imagesToGenerate: imagesToGenerate, |             inputPath: inputFolder, | ||||||
|                                            xcassetsPath: options.xcassetsPath) |             imagesToGenerate: imagesToGenerate, | ||||||
|          |             xcassetsPath: options.xcassetsPath | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Generate extension |         // Generate extension | ||||||
|         ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate, |         ImageExtensionGenerator.generateExtensionFile( | ||||||
|                                                       staticVar: options.staticMembers, |             images: imagesToGenerate, | ||||||
|                                                       inputFilename: options.inputFilenameWithoutExt, |             staticVar: options.staticMembers, | ||||||
|                                                       extensionName: options.extensionName, |             inputFilename: options.inputFilenameWithoutExt, | ||||||
|                                                       extensionFilePath: options.extensionFilePath, |             extensionName: options.extensionName, | ||||||
|                                                       isSwiftUI: true) |             extensionFilePath: options.extensionFilePath, | ||||||
|          |             isSwiftUI: true | ||||||
|         ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate, |         ) | ||||||
|                                                       staticVar: options.staticMembers, |  | ||||||
|                                                       inputFilename: options.inputFilenameWithoutExt, |         ImageExtensionGenerator.generateExtensionFile( | ||||||
|                                                       extensionName: options.extensionNameUIKit, |             images: imagesToGenerate, | ||||||
|                                                       extensionFilePath: options.extensionFilePathUIKit, |             staticVar: options.staticMembers, | ||||||
|                                                       isSwiftUI: false) |             inputFilename: options.inputFilenameWithoutExt, | ||||||
|          |             extensionName: options.extensionNameUIKit, | ||||||
|  |             extensionFilePath: options.extensionFilePathUIKit, | ||||||
|  |             isSwiftUI: false | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         print("[\(Self.toolName)] Images generated") |         print("[\(Self.toolName)] Images generated") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Requirements |     // MARK: - Requirements | ||||||
|      |  | ||||||
|     private func checkRequirements() -> Bool { |     private func checkRequirements() -> Bool { | ||||||
|         guard options.forceExecutionAndGeneration == false else { |         guard options.forceExecutionAndGeneration == false else { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|  |  | ||||||
|         // Input file |         // Input file | ||||||
|         guard fileManager.fileExists(atPath: options.inputFile) else { |         guard fileManager.fileExists(atPath: options.inputFile) else { | ||||||
|             let error = ImagesError.fileNotExists(options.inputFile) |             let error = ImagesError.fileNotExists(options.inputFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Images.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // RSVG-Converter |         // RSVG-Converter | ||||||
|         _ = Images.getSvgConverterPath() |         _ = Self.getSvgConverterPath() | ||||||
|  |  | ||||||
|         // Extension for UIKit and SwiftUI should have different name |         // Extension for UIKit and SwiftUI should have different name | ||||||
|         guard options.extensionName != options.extensionNameUIKit else { |         guard options.extensionName != options.extensionNameUIKit else { | ||||||
|             let error = ImagesError.extensionNamesCollision(options.extensionName) |             let error = ImagesError.extensionNamesCollision(options.extensionName) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Images.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Check if needed to regenerate |         // Check if needed to regenerate | ||||||
|         guard GeneratorChecker.shouldGenerate(force: options.forceExecution, |         guard GeneratorChecker.shouldGenerate( | ||||||
|                                               inputFilePath: options.inputFile, |             force: options.forceExecution, | ||||||
|                                               extensionFilePath: options.extensionFilePath) else { |             inputFilePath: options.inputFile, | ||||||
|  |             extensionFilePath: options.extensionFilePath | ||||||
|  |         ) else { | ||||||
|             print("[\(Self.toolName)] Images are already up to date :) ") |             print("[\(Self.toolName)] Images are already up to date :) ") | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Helpers |     // MARK: - Helpers | ||||||
|      |  | ||||||
|     @discardableResult |     @discardableResult | ||||||
|     static func getSvgConverterPath() -> String { |     static func getSvgConverterPath() -> String { | ||||||
|         let taskSvgConverter = Shell.shell(["which", "rsvg-convert"]) |         let taskSvgConverter = Shell.shell(["which", "rsvg-convert"]) | ||||||
|         if taskSvgConverter.terminationStatus == 0 { |         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 |         let error = ImagesError.rsvgConvertNotFound | ||||||
|         print(error.description) |         print(error.description) | ||||||
|         Images.exit(withError: error) |         Self.exit(withError: error) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // | // | ||||||
| //  ImagesError.swift | //  ImagesError.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 24/01/2022. | //  Created by Thibaut Schmitt on 24/01/2022. | ||||||
| // | // | ||||||
| @@ -8,6 +8,7 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum ImagesError: Error { | enum ImagesError: Error { | ||||||
|  |  | ||||||
|     case extensionNamesCollision(String) |     case extensionNamesCollision(String) | ||||||
|     case inputFolderNotFound(String) |     case inputFolderNotFound(String) | ||||||
|     case fileNotExists(String) |     case fileNotExists(String) | ||||||
| @@ -17,33 +18,33 @@ enum ImagesError: Error { | |||||||
|     case writeFile(String, String) |     case writeFile(String, String) | ||||||
|     case createAssetFolder(String) |     case createAssetFolder(String) | ||||||
|     case unknown(String) |     case unknown(String) | ||||||
|      |  | ||||||
|     var description: String { |     var description: String { | ||||||
|         switch self { |         switch self { | ||||||
|         case .extensionNamesCollision(let extensionName): |         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)" |             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): |         case .inputFolderNotFound(let inputFolder): | ||||||
|             return "error: [\(Images.toolName)] Input folder not found: \(inputFolder)" |             return "error: [\(Images.toolName)] Input folder not found: \(inputFolder)" | ||||||
|              |  | ||||||
|         case .fileNotExists(let filename): |         case .fileNotExists(let filename): | ||||||
|             return "error: [\(Images.toolName)] File \(filename) does not exists" |             return "error: [\(Images.toolName)] File \(filename) does not exists" | ||||||
|              |  | ||||||
|         case .unknownImageExtension(let filename): |         case .unknownImageExtension(let filename): | ||||||
|             return "error: [\(Images.toolName)] File \(filename) have an unhandled file extension. Cannot generate image." |             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)" |             return "error: [\(Images.toolName)] Getting file attributes of \(filename) failed with error: \(errorDescription)" | ||||||
|              |  | ||||||
|         case .rsvgConvertNotFound: |         case .rsvgConvertNotFound: | ||||||
|             return "error: [\(Images.toolName)] Can't find rsvg-convert (can be installed with 'brew remove imagemagick && brew install librsvg')" |             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)" |             return "error: [\(Images.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)" | ||||||
|  |  | ||||||
|         case .createAssetFolder(let folder): |         case .createAssetFolder(let folder): | ||||||
|             return "error: [\(Colors.toolName)] An error occured while creating folder `\(folder)`" |             return "error: [\(Colors.toolName)] An error occured while creating folder `\(folder)`" | ||||||
|              |  | ||||||
|         case .unknown(let errorDescription): |         case .unknown(let errorDescription): | ||||||
|             return "error: [\(Images.toolName)] Unknown error: \(errorDescription)" |             return "error: [\(Images.toolName)] Unknown error: \(errorDescription)" | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,38 +1,41 @@ | |||||||
| // | // | ||||||
| //  ImagiumOptions.swift | //  ImagiumOptions.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 24/01/2022. | //  Created by Thibaut Schmitt on 24/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable no_grouping_extension | ||||||
|  |  | ||||||
| struct ImagesOptions: ParsableArguments { | struct ImagesOptions: ParsableArguments { | ||||||
|  |  | ||||||
|     @Flag(name: .customShort("f"), help: "Should force script execution") |     @Flag(name: .customShort("f"), help: "Should force script execution") | ||||||
|     var forceExecution = false |     var forceExecution = false | ||||||
|      |  | ||||||
|     @Flag(name: .customShort("F"), help: "Regenerate all images") |     @Flag(name: .customShort("F"), help: "Regenerate all images") | ||||||
|     var forceExecutionAndGeneration = false |     var forceExecutionAndGeneration = false | ||||||
|      |  | ||||||
|     @Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var inputFile: String |     var inputFile: String | ||||||
|      |  | ||||||
|     @Option(help: "Xcassets path where to generate images.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Xcassets path where to generate images.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var xcassetsPath: String |     var xcassetsPath: String | ||||||
|      |  | ||||||
|     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var extensionOutputPath: String |     var extensionOutputPath: String | ||||||
|      |  | ||||||
|     @Option(help: "Tell if it will generate static properties or not") |     @Option(help: "Tell if it will generate static properties or not") | ||||||
|     var staticMembers: Bool = false |     var staticMembers: Bool = false | ||||||
|      |  | ||||||
|     @Option(help: "Extension name. If not specified, it will generate an Image extension.") |     @Option(help: "Extension name. If not specified, it will generate an Image extension.") | ||||||
|     var extensionName: String = Images.defaultExtensionName |     var extensionName: String = Images.defaultExtensionName | ||||||
|      |  | ||||||
|     @Option(help: "Extension name. If not specified, it will generate an UIImage extension.") |     @Option(help: "Extension name. If not specified, it will generate an UIImage extension.") | ||||||
|     var extensionNameUIKit: String = Images.defaultExtensionNameUIKit |     var extensionNameUIKit: String = Images.defaultExtensionNameUIKit | ||||||
|      |  | ||||||
|     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Image{extensionSuffix}.swift") |     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Image{extensionSuffix}.swift") | ||||||
|     var extensionSuffix: String? |     var extensionSuffix: String? | ||||||
| } | } | ||||||
| @@ -40,35 +43,35 @@ struct ImagesOptions: ParsableArguments { | |||||||
| // MARK: - Computed var | // MARK: - Computed var | ||||||
|  |  | ||||||
| extension ImagesOptions { | extension ImagesOptions { | ||||||
|      |  | ||||||
|     // MARK: - SwiftUI |     // MARK: - SwiftUI | ||||||
|      |  | ||||||
|     var extensionFileName: String { |     var extensionFileName: String { | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             return "\(extensionName)+\(extensionSuffix).swift" |             return "\(extensionName)+\(extensionSuffix).swift" | ||||||
|         } |         } | ||||||
|         return "\(extensionName).swift" |         return "\(extensionName).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePath: String { |     var extensionFilePath: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileName)" |         "\(extensionOutputPath)/\(extensionFileName)" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - UIKit |     // MARK: - UIKit | ||||||
|      |  | ||||||
|     var extensionFileNameUIKit: String { |     var extensionFileNameUIKit: String { | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             return "\(extensionNameUIKit)+\(extensionSuffix).swift" |             return "\(extensionNameUIKit)+\(extensionSuffix).swift" | ||||||
|         } |         } | ||||||
|         return "\(extensionNameUIKit).swift" |         return "\(extensionNameUIKit).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePathUIKit: String { |     var extensionFilePathUIKit: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileNameUIKit)" |         "\(extensionOutputPath)/\(extensionFileNameUIKit)" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - |     // MARK: - | ||||||
|      |  | ||||||
|     var inputFilenameWithoutExt: String { |     var inputFilenameWithoutExt: String { | ||||||
|         URL(fileURLWithPath: inputFile) |         URL(fileURLWithPath: inputFile) | ||||||
|             .deletingPathExtension() |             .deletingPathExtension() | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| struct ConvertArgument { | struct ConvertArgument { | ||||||
|  |  | ||||||
|     let width: String? |     let width: String? | ||||||
|     let height: String? |     let height: String? | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,11 +8,13 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum TemplateRenderingIntent: String, Codable { | enum TemplateRenderingIntent: String, Codable { | ||||||
|  |  | ||||||
|     case template |     case template | ||||||
|     case original |     case original | ||||||
| } | } | ||||||
|  |  | ||||||
| struct AssetContent: Codable, Equatable { | struct AssetContent: Codable, Equatable { | ||||||
|  |  | ||||||
|     let images: [AssetImageDescription] |     let images: [AssetImageDescription] | ||||||
|     let info: AssetInfo |     let info: AssetInfo | ||||||
|     let properties: AssetProperties? |     let properties: AssetProperties? | ||||||
| @@ -27,16 +29,17 @@ struct AssetContent: Codable, Equatable { | |||||||
|         self.properties = properties |         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 } |         guard lhs.images.count == rhs.images.count else { return false } | ||||||
|         let lhsImages = lhs.images.sorted(by: { $0.filename < $1.filename }) |         let lhsImages = lhs.images.sorted { $0.filename < $1.filename } | ||||||
|         let rhsImages = rhs.images.sorted(by: { $0.filename < $1.filename }) |         let rhsImages = rhs.images.sorted { $0.filename < $1.filename } | ||||||
|  |  | ||||||
|         return lhsImages == rhsImages |         return lhsImages == rhsImages | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| struct AssetImageDescription: Codable, Equatable { | struct AssetImageDescription: Codable, Equatable { | ||||||
|  |  | ||||||
|     let idiom: String |     let idiom: String | ||||||
|     let scale: String? |     let scale: String? | ||||||
|     let filename: String |     let filename: String | ||||||
| @@ -53,11 +56,13 @@ struct AssetImageDescription: Codable, Equatable { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct AssetInfo: Codable, Equatable { | struct AssetInfo: Codable, Equatable { | ||||||
|  |  | ||||||
|     let version: Int |     let version: Int | ||||||
|     let author: String |     let author: String | ||||||
| } | } | ||||||
|  |  | ||||||
| struct AssetProperties: Codable, Equatable { | struct AssetProperties: Codable, Equatable { | ||||||
|  |  | ||||||
|     let preservesVectorRepresentation: Bool |     let preservesVectorRepresentation: Bool | ||||||
|     let templateRenderingIntent: TemplateRenderingIntent? |     let templateRenderingIntent: TemplateRenderingIntent? | ||||||
|  |  | ||||||
| @@ -70,6 +75,7 @@ struct AssetProperties: Codable, Equatable { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     enum CodingKeys: String, CodingKey { |     enum CodingKeys: String, CodingKey { | ||||||
|  |  | ||||||
|         case preservesVectorRepresentation = "preserves-vector-representation" |         case preservesVectorRepresentation = "preserves-vector-representation" | ||||||
|         case templateRenderingIntent = "template-rendering-intent" |         case templateRenderingIntent = "template-rendering-intent" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // | // | ||||||
| //  ParsedImage.swift | //  ParsedImage.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 24/01/2022. | //  Created by Thibaut Schmitt on 24/01/2022. | ||||||
| // | // | ||||||
| @@ -8,10 +8,14 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum ImageExtension: String { | enum ImageExtension: String { | ||||||
|  |  | ||||||
|     case png |     case png | ||||||
| } | } | ||||||
|  |  | ||||||
| struct ParsedImage { | struct ParsedImage { | ||||||
|  |  | ||||||
|  |     // MARK: - Properties | ||||||
|  |  | ||||||
|     let name: String |     let name: String | ||||||
|     let tags: String |     let tags: String | ||||||
|     let width: Int |     let width: Int | ||||||
| @@ -33,34 +37,34 @@ struct ParsedImage { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // MARK: - Convert |     // 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 width1x = "" | ||||||
|         var height1x = "" |         var height1x = "" | ||||||
|         var width2x = "" |         var width2x = "" | ||||||
|         var height2x = "" |         var height2x = "" | ||||||
|         var width3x = "" |         var width3x = "" | ||||||
|         var height3x = "" |         var height3x = "" | ||||||
|          |  | ||||||
|         if width != -1 { |         if width != -1 { | ||||||
|             width1x = "\(width)" |             width1x = "\(width)" | ||||||
|             width2x = "\(width * 2)" |             width2x = "\(width * 2)" | ||||||
|             width3x = "\(width * 3)" |             width3x = "\(width * 3)" | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if height != -1 { |         if height != -1 { | ||||||
|             height1x = "\(height)" |             height1x = "\(height)" | ||||||
|             height2x = "\(height * 2)" |             height2x = "\(height * 2)" | ||||||
|             height3x = "\(height * 3)" |             height3x = "\(height * 3)" | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return (x1: ConvertArgument(width: width1x, height: height1x), |         return (x1: ConvertArgument(width: width1x, height: height1x), | ||||||
|                 x2: ConvertArgument(width: width2x, height: height2x), |                 x2: ConvertArgument(width: width2x, height: height2x), | ||||||
|                 x3: ConvertArgument(width: width3x, height: height3x)) |                 x3: ConvertArgument(width: width3x, height: height3x)) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Assets |     // MARK: - Assets | ||||||
|      |  | ||||||
|     func generateContentJson(isVector: Bool) -> String? { |     func generateContentJson(isVector: Bool) -> String? { | ||||||
|         let encoder = JSONEncoder() |         let encoder = JSONEncoder() | ||||||
|         encoder.outputFormatting = .prettyPrinted |         encoder.outputFormatting = .prettyPrinted | ||||||
| @@ -77,9 +81,8 @@ struct ParsedImage { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     func generateImageContent(isVector: Bool) -> AssetContent { |     func generateImageContent(isVector: Bool) -> AssetContent { | ||||||
|  |  | ||||||
|         if !imageExtensions.contains(.png) && isVector { |         if !imageExtensions.contains(.png) && isVector { | ||||||
|             //Generate svg |             // Generate svg | ||||||
|             return AssetContent( |             return AssetContent( | ||||||
|                 images: [ |                 images: [ | ||||||
|                     AssetImageDescription( |                     AssetImageDescription( | ||||||
| @@ -97,7 +100,7 @@ struct ParsedImage { | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|         } else { |         } else { | ||||||
|             //Generate png |             // Generate png | ||||||
|             return AssetContent( |             return AssetContent( | ||||||
|                 images: [ |                 images: [ | ||||||
|                     AssetImageDescription( |                     AssetImageDescription( | ||||||
| @@ -125,17 +128,17 @@ struct ParsedImage { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // MARK: - Extension property |     // MARK: - Extension property | ||||||
|      |  | ||||||
|     func getImageProperty(isStatic: Bool, isSwiftUI: Bool) -> String { |     func getImageProperty(isStatic: Bool, isSwiftUI: Bool) -> String { | ||||||
|         if isSwiftUI { |         if isSwiftUI { | ||||||
|             return """ |             return """ | ||||||
|                 \(isStatic ? "static ": "")var \(name): Image { |                 \(isStatic ? "static " : "")var \(name): Image { | ||||||
|                     Image("\(name)") |                     Image("\(name)") | ||||||
|                 } |                 } | ||||||
|             """ |             """ | ||||||
|         } |         } | ||||||
|         return """ |         return """ | ||||||
|             \(isStatic ? "static ": "")var \(name): UIImage { |             \(isStatic ? "static " : "")var \(name): UIImage { | ||||||
|                 UIImage(named: "\(name)")! |                 UIImage(named: "\(name)")! | ||||||
|             } |             } | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum PlatormTag: String { | enum PlatormTag: String { | ||||||
|  |  | ||||||
|     case droid = "d" |     case droid = "d" | ||||||
|     case ios = "i" |     case ios = "i" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,36 +7,36 @@ | |||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| class ImageFileParser { | enum ImageFileParser { | ||||||
|      |  | ||||||
|     static func parse(_ inputFile: String, platform: PlatormTag) -> [ParsedImage] { |     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) |         let stringsByLines = inputFileContent.components(separatedBy: .newlines) | ||||||
|          |  | ||||||
|         return Self.parseLines(stringsByLines, platform: platform) |         return Self.parseLines(stringsByLines, platform: platform) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     static func parseLines(_ lines: [String], platform: PlatormTag) -> [ParsedImage] { |     static func parseLines(_ lines: [String], platform: PlatormTag) -> [ParsedImage] { | ||||||
|         var imagesToGenerate = [ParsedImage]() |         var imagesToGenerate = [ParsedImage]() | ||||||
|          |  | ||||||
|         lines.forEach { |         lines.forEach { | ||||||
|             guard $0.removeLeadingTrailingWhitespace().isEmpty == false, $0.first != "#" else { |             guard $0.removeLeadingTrailingWhitespace().isEmpty == false, $0.first != "#" else { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             let splittedLine = $0.split(separator: " ") |             let splittedLine = $0.split(separator: " ") | ||||||
|              |  | ||||||
|             let width: Int = { |             let width: Int = { | ||||||
|                 if splittedLine[2] == "?" { |                 if splittedLine[2] == "?" { | ||||||
|                     return -1 |                     return -1 | ||||||
|                 } |                 } | ||||||
|                 return Int(splittedLine[2])! |                 return Int(splittedLine[2])! // swiftlint:disable:this force_unwrapping | ||||||
|             }() |             }() | ||||||
|             let height: Int = { |             let height: Int = { | ||||||
|                 if splittedLine[3] == "?" { |                 if splittedLine[3] == "?" { | ||||||
|                     return -1 |                     return -1 | ||||||
|                 } |                 } | ||||||
|                 return Int(splittedLine[3])! |                 return Int(splittedLine[3])! // swiftlint:disable:this force_unwrapping | ||||||
|             }() |             }() | ||||||
|  |  | ||||||
|             var imageExtensions: [ImageExtension] = [] |             var imageExtensions: [ImageExtension] = [] | ||||||
|   | |||||||
| @@ -8,23 +8,29 @@ | |||||||
| import Foundation | import Foundation | ||||||
| import ToolCore | import ToolCore | ||||||
|  |  | ||||||
| class StringsFileGenerator { | // swiftlint:disable type_body_length file_length | ||||||
|  |  | ||||||
|  | enum StringsFileGenerator { | ||||||
|  |  | ||||||
|     // MARK: - Strings Files |     // MARK: - Strings Files | ||||||
|  |  | ||||||
|     static func writeStringsFiles(sections: [Section], |     static func writeStringsFiles( | ||||||
|                                   langs: [String], |         sections: [Section], | ||||||
|                                   defaultLang: String, |         langs: [String], | ||||||
|                                   tags: [String], |         defaultLang: String, | ||||||
|                                   outputPath: String, |         tags: [String], | ||||||
|                                   inputFilenameWithoutExt: String) { |         outputPath: String, | ||||||
|  |         inputFilenameWithoutExt: String | ||||||
|  |     ) { | ||||||
|  |  | ||||||
|         var stringsFilesContent = [String: String]() |         var stringsFilesContent = [String: String]() | ||||||
|         for lang in langs { |         for lang in langs { | ||||||
|             stringsFilesContent[lang] = Self.generateStringsFileContent(lang: lang, |             stringsFilesContent[lang] = Self.generateStringsFileContent( | ||||||
|                                                                         defaultLang: defaultLang, |                 lang: lang, | ||||||
|                                                                         tags: tags, |                 defaultLang: defaultLang, | ||||||
|                                                                         sections: sections) |                 tags: tags, | ||||||
|  |                 sections: sections | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Write strings file content |         // Write strings file content | ||||||
| @@ -35,7 +41,7 @@ class StringsFileGenerator { | |||||||
|             let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) |             let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) | ||||||
|             do { |             do { | ||||||
|                 try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) |                 try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) | ||||||
|             } catch let error { |             } catch { | ||||||
|                 let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) |                 let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) | ||||||
|                 print(error.description) |                 print(error.description) | ||||||
|                 Stringium.exit(withError: error) |                 Stringium.exit(withError: error) | ||||||
| @@ -43,12 +49,14 @@ class StringsFileGenerator { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static func writeXcStringsFiles(sections: [Section], |     static func writeXcStringsFiles( | ||||||
|                                     langs: [String], |         sections: [Section], | ||||||
|                                     defaultLang: String, |         langs: [String], | ||||||
|                                     tags: [String], |         defaultLang: String, | ||||||
|                                     outputPath: String, |         tags: [String], | ||||||
|                                     inputFilenameWithoutExt: String) { |         outputPath: String, | ||||||
|  |         inputFilenameWithoutExt: String | ||||||
|  |     ) { | ||||||
|  |  | ||||||
|         let fileContent: String = Self.generateXcStringsFileContent( |         let fileContent: String = Self.generateXcStringsFileContent( | ||||||
|             langs: langs, |             langs: langs, | ||||||
| @@ -61,17 +69,19 @@ class StringsFileGenerator { | |||||||
|         let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) |         let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) | ||||||
|         do { |         do { | ||||||
|             try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) |             try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) |             let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Stringium.exit(withError: error) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static func generateStringsFileContent(lang: String, |     static func generateStringsFileContent( | ||||||
|                                            defaultLang: String, |         lang: String, | ||||||
|                                            tags inputTags: [String], |         defaultLang: String, | ||||||
|                                            sections: [Section]) -> String { |         tags inputTags: [String], | ||||||
|  |         sections: [Section] | ||||||
|  |     ) -> String { | ||||||
|         var stringsFileContent = """ |         var stringsFileContent = """ | ||||||
|         /** |         /** | ||||||
|          * Apple Strings File |          * Apple Strings File | ||||||
| @@ -120,11 +130,18 @@ class StringsFileGenerator { | |||||||
|  |  | ||||||
|     // MARK: - XcStrings Generation |     // MARK: - XcStrings Generation | ||||||
|  |  | ||||||
|     static func generateXcStringsFileContent(langs: [String], |     static func generateXcStringsFileContent( | ||||||
|                                              defaultLang: String, |         langs: [String], | ||||||
|                                              tags inputTags: [String], |         defaultLang: String, | ||||||
|                                              sections: [Section]) -> String { |         tags inputTags: [String], | ||||||
|         let rootObject = generateRootObject(langs: langs, defaultLang: defaultLang, tags: inputTags, sections: sections) |         sections: [Section] | ||||||
|  |     ) -> String { | ||||||
|  |         let rootObject = generateRootObject( | ||||||
|  |             langs: langs, | ||||||
|  |             defaultLang: defaultLang, | ||||||
|  |             tags: inputTags, | ||||||
|  |             sections: sections | ||||||
|  |         ) | ||||||
|         let file = generateXcStringsFileContentFromRootObject(rootObject: rootObject) |         let file = generateXcStringsFileContentFromRootObject(rootObject: rootObject) | ||||||
|  |  | ||||||
|         return file |         return file | ||||||
| @@ -138,7 +155,6 @@ class StringsFileGenerator { | |||||||
|             let json = try encoder.encode(rootObject) |             let json = try encoder.encode(rootObject) | ||||||
|  |  | ||||||
|             return String(decoding: json, as: UTF8.self) |             return String(decoding: json, as: UTF8.self) | ||||||
|  |  | ||||||
|         } catch { |         } catch { | ||||||
|             debugPrint("Failed to encode: \(error)") |             debugPrint("Failed to encode: \(error)") | ||||||
|         } |         } | ||||||
| @@ -146,20 +162,22 @@ class StringsFileGenerator { | |||||||
|         return "" |         return "" | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static func generateRootObject(langs: [String], |     static func generateRootObject( | ||||||
|                                    defaultLang: String, |         langs: [String], | ||||||
|                                    tags inputTags: [String], |         defaultLang: String, | ||||||
|                                    sections: [Section]) -> Root { |         tags inputTags: [String], | ||||||
|  |         sections: [Section] | ||||||
|  |     ) -> Root { | ||||||
|  |  | ||||||
|         var xcStringDefinitionTab: [XCStringDefinition] = [] |         var xcStringDefinitionTab: [XCStringDefinition] = [] | ||||||
|  |  | ||||||
|         sections.forEach { section in |         sections.forEach { section in // swiftlint:disable:this closure_body_length | ||||||
|             // Check that at least one string will be generated |                                       // Check that at least one string will be generated | ||||||
|             guard section.hasOneOrMoreMatchingTags(tags: inputTags) else { |             guard section.hasOneOrMoreMatchingTags(tags: inputTags) else { | ||||||
|                 return // Go to next section |                 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 skipDefinition = false | ||||||
|                 var isNoTranslation = false |                 var isNoTranslation = false | ||||||
|  |  | ||||||
| @@ -190,7 +208,6 @@ class StringsFileGenerator { | |||||||
|                     } else { |                     } else { | ||||||
|                         // Search for langs in twine |                         // Search for langs in twine | ||||||
|                         for (lang, value) in definition.translations where !value.isEmpty { |                         for (lang, value) in definition.translations where !value.isEmpty { | ||||||
|  |  | ||||||
|                             let localization = XCStringLocalization( |                             let localization = XCStringLocalization( | ||||||
|                                 lang: lang, |                                 lang: lang, | ||||||
|                                 content: XCStringLocalizationLangContent( |                                 content: XCStringLocalizationLangContent( | ||||||
| @@ -219,7 +236,7 @@ class StringsFileGenerator { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         let xcStringContainer = XCStringDefinitionContainer(strings: xcStringDefinitionTab) |         let xcStringContainer = XCStringDefinitionContainer(strings: xcStringDefinitionTab) | ||||||
|          |  | ||||||
|         return Root( |         return Root( | ||||||
|             sourceLanguage: defaultLang, |             sourceLanguage: defaultLang, | ||||||
|             strings: xcStringContainer, |             strings: xcStringContainer, | ||||||
| @@ -229,28 +246,32 @@ class StringsFileGenerator { | |||||||
|  |  | ||||||
|     // MARK: - Extension file |     // MARK: - Extension file | ||||||
|  |  | ||||||
|     static func writeExtensionFiles(sections: [Section], |     static func writeExtensionFiles( | ||||||
|                                     defaultLang lang: String, |         sections: [Section], | ||||||
|                                     tags: [String], |         defaultLang lang: String, | ||||||
|                                     staticVar: Bool, |         tags: [String], | ||||||
|                                     inputFilename: String, |         staticVar: Bool, | ||||||
|                                     extensionName: String, |         inputFilename: String, | ||||||
|                                     extensionFilePath: String, |         extensionName: String, | ||||||
|                                     extensionSuffix: String) { |         extensionFilePath: String, | ||||||
|  |         extensionSuffix: String | ||||||
|  |     ) { | ||||||
|         // Get extension content |         // Get extension content | ||||||
|         let extensionFileContent = Self.getExtensionContent(sections: sections, |         let extensionFileContent = Self.getExtensionContent( | ||||||
|                                                             defaultLang: lang, |             sections: sections, | ||||||
|                                                             tags: tags, |             defaultLang: lang, | ||||||
|                                                             staticVar: staticVar, |             tags: tags, | ||||||
|                                                             inputFilename: inputFilename, |             staticVar: staticVar, | ||||||
|                                                             extensionName: extensionName, |             inputFilename: inputFilename, | ||||||
|                                                             extensionSuffix: extensionSuffix) |             extensionName: extensionName, | ||||||
|  |             extensionSuffix: extensionSuffix | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Write content |         // Write content | ||||||
|         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) |         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) | ||||||
|         do { |         do { | ||||||
|             try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) |             try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) |             let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Stringium.exit(withError: error) | ||||||
| @@ -259,17 +280,32 @@ class StringsFileGenerator { | |||||||
|  |  | ||||||
|     // MARK: - Extension content |     // MARK: - Extension content | ||||||
|  |  | ||||||
|     static func getExtensionContent(sections: [Section], |     static func getExtensionContent( | ||||||
|                                     defaultLang lang: String, |         sections: [Section], | ||||||
|                                     tags: [String], |         defaultLang lang: String, | ||||||
|                                     staticVar: Bool, |         tags: [String], | ||||||
|                                     inputFilename: String, |         staticVar: Bool, | ||||||
|                                     extensionName: String, |         inputFilename: String, | ||||||
|                                     extensionSuffix: String) -> String { |         extensionName: String, | ||||||
|  |         extensionSuffix: String | ||||||
|  |     ) -> String { | ||||||
|         [ |         [ | ||||||
|             Self.getHeader(stringsFilename: inputFilename, extensionClassname: extensionName), |             Self.getHeader( | ||||||
|             Self.getEnumKey(sections: sections, tags: tags, extensionClassname: extensionName, extensionSuffix: extensionSuffix), |                 stringsFilename: inputFilename, | ||||||
|             Self.getProperties(sections: sections, defaultLang: lang, tags: tags, staticVar: staticVar), |                 extensionClassname: extensionName | ||||||
|  |             ), | ||||||
|  |             Self.getEnumKey( | ||||||
|  |                 sections: sections, | ||||||
|  |                 tags: tags, | ||||||
|  |                 extensionClassname: extensionName, | ||||||
|  |                 extensionSuffix: extensionSuffix | ||||||
|  |             ), | ||||||
|  |             Self.getProperties( | ||||||
|  |                 sections: sections, | ||||||
|  |                 defaultLang: lang, | ||||||
|  |                 tags: tags, | ||||||
|  |                 staticVar: staticVar | ||||||
|  |             ), | ||||||
|             Self.getFooter() |             Self.getFooter() | ||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .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" |         var enumDefinition = "\n    enum Key\(extensionSuffix.uppercasedFirst()): String {\n" | ||||||
|  |  | ||||||
|         // Enum |         // Enum | ||||||
|   | |||||||
| @@ -1,71 +1,100 @@ | |||||||
| // | // | ||||||
| //  TagsGenerator.swift | //  TagsGenerator.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 10/01/2022. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
|  | import CoreVideo | ||||||
| import Foundation | import Foundation | ||||||
| import ToolCore | import ToolCore | ||||||
| import CoreVideo |  | ||||||
|  |  | ||||||
| class TagsGenerator { | enum TagsGenerator { | ||||||
|     static func writeExtensionFiles(sections: [Section], lang: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) { |  | ||||||
|  |     static func writeExtensionFiles( | ||||||
|  |         sections: [Section], | ||||||
|  |         lang: String, | ||||||
|  |         tags: [String], | ||||||
|  |         staticVar: Bool, | ||||||
|  |         extensionName: String, | ||||||
|  |         extensionFilePath: String | ||||||
|  |     ) { | ||||||
|         // Get extension content |         // Get extension content | ||||||
|         let extensionFileContent = Self.getExtensionContent(sections: sections, |         let extensionFileContent = Self.getExtensionContent( | ||||||
|                                                             lang: lang, |             sections: sections, | ||||||
|                                                             tags: tags, |             lang: lang, | ||||||
|                                                             staticVar: staticVar, |             tags: tags, | ||||||
|                                                             extensionName: extensionName) |             staticVar: staticVar, | ||||||
|          |             extensionName: extensionName | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Write content |         // Write content | ||||||
|         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) |         let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) | ||||||
|         do { |         do { | ||||||
|             try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) |             try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) | ||||||
|         } catch let error { |         } catch { | ||||||
|             let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) |             let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Stringium.exit(withError: error) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Extension content |     // 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.getHeader( | ||||||
|             Self.getProperties(sections: sections, lang: lang, tags: tags, staticVar: staticVar), |                 extensionClassname: extensionName, | ||||||
|  |                 staticVar: staticVar | ||||||
|  |             ), | ||||||
|  |             Self.getProperties( | ||||||
|  |                 sections: sections, | ||||||
|  |                 lang: lang, | ||||||
|  |                 tags: tags, | ||||||
|  |                 staticVar: staticVar | ||||||
|  |             ), | ||||||
|             Self.getFooter() |             Self.getFooter() | ||||||
|         ] |         ] | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Extension part |     // MARK: - Extension part | ||||||
|      |  | ||||||
|     private static func getHeader(extensionClassname: String, staticVar: Bool) -> String { |     private static func getHeader(extensionClassname: String, staticVar: Bool) -> String { | ||||||
|         """ |         """ | ||||||
|         // Generated by ResgenSwift.Strings.\(Tags.toolName) \(ResgenSwiftVersion) |         // Generated by ResgenSwift.Strings.\(Tags.toolName) \(ResgenSwiftVersion) | ||||||
|          |  | ||||||
|         \(staticVar ? "typelias Tags = String\n\n" : "")import UIKit |         \(staticVar ? "typelias Tags = String\n\n" : "")import UIKit | ||||||
|          |  | ||||||
|         extension \(extensionClassname) { |         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 |         sections | ||||||
|             .compactMap { section in |             .compactMap { section in | ||||||
|                 // Check that at least one string will be generated |                 // Check that at least one string will be generated | ||||||
|                 guard section.hasOneOrMoreMatchingTags(tags: tags) else { |                 guard section.hasOneOrMoreMatchingTags(tags: tags) else { | ||||||
|                     return nil// Go to next section |                     return nil // Go to next section | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 var res = "\n    // MARK: - \(section.name)" |                 var res = "\n    // MARK: - \(section.name)" | ||||||
|                 section.definitions.forEach { definition in |                 section.definitions.forEach { definition in | ||||||
|                     guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else { |                     guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else { | ||||||
|                         return // Go to next definition |                         return // Go to next definition | ||||||
|                     } |                     } | ||||||
|                      |  | ||||||
|                     if staticVar { |                     if staticVar { | ||||||
|                         res += "\n\n\(definition.getStaticProperty(forLang: lang))" |                         res += "\n\n\(definition.getStaticProperty(forLang: lang))" | ||||||
|                     } else { |                     } else { | ||||||
| @@ -76,11 +105,11 @@ class TagsGenerator { | |||||||
|             } |             } | ||||||
|             .joined(separator: "\n") |             .joined(separator: "\n") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private static func getFooter() -> String { |     private static func getFooter() -> String { | ||||||
|         """ |         """ | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,78 +1,93 @@ | |||||||
| // | // | ||||||
| //  Definition.swift | //  Definition.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 04/01/2022. | //  Created by Thibaut Schmitt on 04/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable force_unwrapping | ||||||
|  |  | ||||||
| class Definition { | class Definition { | ||||||
|  |  | ||||||
|  |     // MARK: - Properties | ||||||
|  |  | ||||||
|     let name: String |     let name: String | ||||||
|     var tags = [String]() |     var tags = [String]() | ||||||
|     var comment: String? |     var comment: String? | ||||||
|     var translations = [String: String]() |     var translations = [String: String]() | ||||||
|     var reference: String? |     var reference: String? | ||||||
|     var isPlurals = false |     var isPlurals = false | ||||||
|      |  | ||||||
|     var isValid: Bool { |     var isValid: Bool { | ||||||
|         name.isEmpty == false && |         name.isEmpty == false && | ||||||
|         translations.isEmpty == false |         translations.isEmpty == false | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     init(name: String) { |     init(name: String) { | ||||||
|         self.name = name |         self.name = name | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     static func match(_ line: String) -> Definition? { |     static func match(_ line: String) -> Definition? { | ||||||
|         guard line.range(of: "\\[(.*?)]$", options: .regularExpression, range: nil, locale: nil) != nil else { |         guard line.range(of: "\\[(.*?)]$", options: .regularExpression, range: nil, locale: nil) != nil else { | ||||||
|             return nil |             return nil | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         let definitionName = line |         let definitionName = line | ||||||
|             .replacingOccurrences(of: ["[", "]"], with: "") |             .replacingOccurrences(of: ["[", "]"], with: "") | ||||||
|             .removeLeadingTrailingWhitespace() |             .removeLeadingTrailingWhitespace() | ||||||
|          |  | ||||||
|         return Definition(name: definitionName) |         return Definition(name: definitionName) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool { |     func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool { | ||||||
|         if Set(inputTags).isDisjoint(with: tags) { |         if Set(inputTags).isDisjoint(with: tags) { | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - |     // MARK: - | ||||||
|      |  | ||||||
|     private func getStringParameters(input: String) -> (inputParameters: [String], translationArguments: [String])? { |     private func getStringParameters(input: String) -> (inputParameters: [String], translationArguments: [String])? { | ||||||
|         var methodsParameters = [String]() |         var methodsParameters = [String]() | ||||||
|          |  | ||||||
|         let printfPlaceholderRegex = try! NSRegularExpression(pattern: "%(?:\\d+\\$)?[+-]?(?:[ 0]|'.{1})?-?\\d*(?:\\.\\d+)?[blcdeEufFgGosxX@]*") |         let printfPlaceholderRegex = try! NSRegularExpression( // swiftlint:disable:this force_try | ||||||
|         printfPlaceholderRegex.enumerateMatches(in: input, options: [], range: NSRange(location: 0, length: input.count)) { match, _, stop in |             pattern: "%(?:\\d+\\$)?[+-]?(?:[ 0]|'.{1})?-?\\d*(?:\\.\\d+)?[blcdeEufFgGosxX@]*" | ||||||
|             guard let match = match else { return } |         ) | ||||||
|              |         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 { |             if let range = Range(match.range, in: input), let last = input[range].last { | ||||||
|                 switch last { |                 switch last { | ||||||
|                 case "d", "u": |                 case "d", "u": | ||||||
|                     methodsParameters.append("Int") |                     methodsParameters.append("Int") | ||||||
|  |  | ||||||
|                 case "f", "F": |                 case "f", "F": | ||||||
|                     methodsParameters.append("Double") |                     methodsParameters.append("Double") | ||||||
|  |  | ||||||
|                 case "@", "s", "c": |                 case "@", "s", "c": | ||||||
|                     methodsParameters.append("String") |                     methodsParameters.append("String") | ||||||
|  |  | ||||||
|                 case "%": |                 case "%": | ||||||
|                     // if you need to print %, you have to add %% |                     // if you need to print %, you have to add %% | ||||||
|                     break |                     break | ||||||
|  |  | ||||||
|                 default: |                 default: | ||||||
|                     break |                     break | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if methodsParameters.isEmpty { |         if methodsParameters.isEmpty { | ||||||
|             return nil |             return nil | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         var inputParameters = [String]() |         var inputParameters = [String]() | ||||||
|         var translationArguments = [String]() |         var translationArguments = [String]() | ||||||
|         for (index, paramType) in methodsParameters.enumerated() { |         for (index, paramType) in methodsParameters.enumerated() { | ||||||
| @@ -80,10 +95,10 @@ class Definition { | |||||||
|             translationArguments.append(paramName) |             translationArguments.append(paramName) | ||||||
|             inputParameters.append("\(paramName): \(paramType)") |             inputParameters.append("\(paramName): \(paramType)") | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return (inputParameters: inputParameters, translationArguments: translationArguments) |         return (inputParameters: inputParameters, translationArguments: translationArguments) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private func getBaseProperty(lang: String, translation: String, isStatic: Bool, comment: String?) -> String { |     private func getBaseProperty(lang: String, translation: String, isStatic: Bool, comment: String?) -> String { | ||||||
|         """ |         """ | ||||||
|             /// Translation in \(lang) : |             /// Translation in \(lang) : | ||||||
| @@ -91,15 +106,20 @@ class Definition { | |||||||
|             /// |             /// | ||||||
|             /// Comment : |             /// Comment : | ||||||
|             /// \(comment?.isEmpty == false ? comment! : "No 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 ?? "")") |                 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) : |             /// Translation in \(lang) : | ||||||
| @@ -107,12 +127,12 @@ class Definition { | |||||||
|             /// |             /// | ||||||
|             /// Comment : |             /// Comment : | ||||||
|             /// \(comment?.isEmpty == false ? comment! : "No 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: ", "))) |                 String(format: \(isStatic ? "Self" : "self").\(name), \(translationArguments.joined(separator: ", "))) | ||||||
|             } |             } | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getNSLocalizedStringProperty(forLang lang: String) -> String { |     func getNSLocalizedStringProperty(forLang lang: String) -> String { | ||||||
|         guard let translation = translations[lang] else { |         guard let translation = translations[lang] else { | ||||||
|             let error = StringiumError.langNotDefined(lang, name, reference != nil) |             let error = StringiumError.langNotDefined(lang, name, reference != nil) | ||||||
| @@ -131,24 +151,26 @@ class Definition { | |||||||
|         // Generate method |         // Generate method | ||||||
|         var method = "" |         var method = "" | ||||||
|         if let parameters = self.getStringParameters(input: translation) { |         if let parameters = self.getStringParameters(input: translation) { | ||||||
|             method = getBaseMethod(lang: lang, |             method = getBaseMethod( | ||||||
|                                    translation: translation, |                 lang: lang, | ||||||
|                                    isStatic: false, |                 translation: translation, | ||||||
|                                    inputParameters: parameters.inputParameters, |                 isStatic: false, | ||||||
|                                    translationArguments: parameters.translationArguments, |                 inputParameters: parameters.inputParameters, | ||||||
|                                    comment: self.comment) |                 translationArguments: parameters.translationArguments, | ||||||
|  |                 comment: self.comment | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return property + method |         return property + method | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getNSLocalizedStringStaticProperty(forLang lang: String) -> String { |     func getNSLocalizedStringStaticProperty(forLang lang: String) -> String { | ||||||
|         guard let translation = translations[lang] else { |         guard let translation = translations[lang] else { | ||||||
|             let error = StringiumError.langNotDefined(lang, name, reference != nil) |             let error = StringiumError.langNotDefined(lang, name, reference != nil) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Stringium.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Generate property |         // Generate property | ||||||
|         let property = getBaseProperty( |         let property = getBaseProperty( | ||||||
|             lang: lang, |             lang: lang, | ||||||
| @@ -156,23 +178,25 @@ class Definition { | |||||||
|             isStatic: true, |             isStatic: true, | ||||||
|             comment: self.comment |             comment: self.comment | ||||||
|         ) |         ) | ||||||
|          |  | ||||||
|         // Generate method |         // Generate method | ||||||
|         var method = "" |         var method = "" | ||||||
|         if let parameters = self.getStringParameters(input: translation) { |         if let parameters = self.getStringParameters(input: translation) { | ||||||
|             method = getBaseMethod(lang: lang, |             method = getBaseMethod( | ||||||
|                                    translation: translation, |                 lang: lang, | ||||||
|                                    isStatic: true, |                 translation: translation, | ||||||
|                                    inputParameters: parameters.inputParameters, |                 isStatic: true, | ||||||
|                                    translationArguments: parameters.translationArguments, |                 inputParameters: parameters.inputParameters, | ||||||
|                                    comment: self.comment) |                 translationArguments: parameters.translationArguments, | ||||||
|  |                 comment: self.comment | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return property + method |         return property + method | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Raw strings |     // MARK: - Raw strings | ||||||
|      |  | ||||||
|     func getProperty(forLang lang: String) -> String { |     func getProperty(forLang lang: String) -> String { | ||||||
|         guard let translation = translations[lang] else { |         guard let translation = translations[lang] else { | ||||||
|             let error = StringiumError.langNotDefined(lang, name, reference != nil) |             let error = StringiumError.langNotDefined(lang, name, reference != nil) | ||||||
| @@ -192,14 +216,14 @@ class Definition { | |||||||
|             } |             } | ||||||
|         """ |         """ | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func getStaticProperty(forLang lang: String) -> String { |     func getStaticProperty(forLang lang: String) -> String { | ||||||
|         guard let translation = translations[lang] else { |         guard let translation = translations[lang] else { | ||||||
|             let error = StringiumError.langNotDefined(lang, name, reference != nil) |             let error = StringiumError.langNotDefined(lang, name, reference != nil) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Stringium.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return """ |         return """ | ||||||
|             /// Translation in \(lang) : |             /// Translation in \(lang) : | ||||||
|             /// \(translation) |             /// \(translation) | ||||||
|   | |||||||
| @@ -8,28 +8,35 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| class Section { | class Section { | ||||||
|  |  | ||||||
|  |     // MARK: - Properties | ||||||
|  |  | ||||||
|     let name: String // OnBoarding |     let name: String // OnBoarding | ||||||
|     var definitions = [Definition]() |     var definitions = [Definition]() | ||||||
|      |  | ||||||
|  |     // MARK: - Init | ||||||
|  |  | ||||||
|     init(name: String) { |     init(name: String) { | ||||||
|         self.name = name |         self.name = name | ||||||
|     } |     } | ||||||
|      |  | ||||||
|  |     // MARK: - Methods | ||||||
|  |  | ||||||
|     static func match(_ line: String) -> Section? { |     static func match(_ line: String) -> Section? { | ||||||
|         guard line.range(of: "\\[\\[(.*?)]]$", options: .regularExpression, range: nil, locale: nil) != nil else { |         guard line.range(of: "\\[\\[(.*?)]]$", options: .regularExpression, range: nil, locale: nil) != nil else { | ||||||
|             return nil |             return nil | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         let sectionName = line |         let sectionName = line | ||||||
|             .replacingOccurrences(of: ["[", "]"], with: "") |             .replacingOccurrences(of: ["[", "]"], with: "") | ||||||
|             .removeLeadingTrailingWhitespace() |             .removeLeadingTrailingWhitespace() | ||||||
|         return Section(name: sectionName) |         return Section(name: sectionName) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func hasOneOrMoreMatchingTags(tags: [String]) -> Bool { |     func hasOneOrMoreMatchingTags(tags: [String]) -> Bool { | ||||||
|         let allTags = definitions.flatMap { $0.tags } |         let allTags = definitions.flatMap { $0.tags } | ||||||
|         let allTagsSet = Set(allTags) |         let allTagsSet = Set(allTags) | ||||||
|          |  | ||||||
|         let intersection = Set(tags).intersection(allTagsSet) |         let intersection = Set(tags).intersection(allTagsSet) | ||||||
|         if intersection.isEmpty { |         if intersection.isEmpty { | ||||||
|             return false |             return false | ||||||
|   | |||||||
| @@ -8,25 +8,30 @@ | |||||||
| import SwiftUI | import SwiftUI | ||||||
|  |  | ||||||
| struct DynamicKey: CodingKey { | struct DynamicKey: CodingKey { | ||||||
|  |  | ||||||
|     var intValue: Int? |     var intValue: Int? | ||||||
|  |  | ||||||
|     init?(intValue: Int) { |     init?(intValue: Int) { | ||||||
|         self.intValue = intValue |         self.intValue = intValue | ||||||
|         self.stringValue = "\(intValue)" |         self.stringValue = "\(intValue)" | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var stringValue: String |     var stringValue: String | ||||||
|  |  | ||||||
|     init?(stringValue: String) { |     init?(stringValue: String) { | ||||||
|         self.stringValue = stringValue |         self.stringValue = stringValue | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| struct Root: Codable, Equatable { | struct Root: Codable, Equatable { | ||||||
|  |  | ||||||
|     let sourceLanguage: String |     let sourceLanguage: String | ||||||
|     let strings: XCStringDefinitionContainer |     let strings: XCStringDefinitionContainer | ||||||
|     let version: String |     let version: String | ||||||
| } | } | ||||||
|  |  | ||||||
| struct XCStringDefinitionContainer: Codable, Equatable { | struct XCStringDefinitionContainer: Codable, Equatable { | ||||||
|  |  | ||||||
|     let strings: [XCStringDefinition] |     let strings: [XCStringDefinition] | ||||||
|  |  | ||||||
|     func encode(to encoder: Encoder) throws { |     func encode(to encoder: Encoder) throws { | ||||||
| @@ -39,19 +44,19 @@ struct XCStringDefinitionContainer: Codable, Equatable { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static func == (lhs: XCStringDefinitionContainer, rhs: XCStringDefinitionContainer) -> Bool { |     static func == (lhs: Self, rhs: Self) -> Bool { | ||||||
|         return lhs.strings.sorted(by: { |         lhs.strings.sorted { $0.title < $1.title } == rhs.strings.sorted { $0.title < $1.title } | ||||||
|             $0.title < $1.title |  | ||||||
|         }) == rhs.strings.sorted(by: { $0.title < $1.title }) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| struct XCStringDefinition: Codable, Equatable { | struct XCStringDefinition: Codable, Equatable { | ||||||
|  |  | ||||||
|     let title: String // json key -> custom encoding methods |     let title: String // json key -> custom encoding methods | ||||||
|     let content: XCStringDefinitionContent |     let content: XCStringDefinitionContent | ||||||
| } | } | ||||||
|  |  | ||||||
| struct XCStringDefinitionContent: Codable, Equatable { | struct XCStringDefinitionContent: Codable, Equatable { | ||||||
|  |  | ||||||
|     let comment: String? |     let comment: String? | ||||||
|     let extractionState: String |     let extractionState: String | ||||||
|     var localizations: XCStringLocalizationContainer |     var localizations: XCStringLocalizationContainer | ||||||
| @@ -64,6 +69,7 @@ struct XCStringDefinitionContent: Codable, Equatable { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct XCStringLocalizationContainer: Codable, Equatable { | struct XCStringLocalizationContainer: Codable, Equatable { | ||||||
|  |  | ||||||
|     let localizations: [XCStringLocalization] |     let localizations: [XCStringLocalization] | ||||||
|  |  | ||||||
|     func encode(to encoder: Encoder) throws { |     func encode(to encoder: Encoder) throws { | ||||||
| @@ -76,34 +82,39 @@ struct XCStringLocalizationContainer: Codable, Equatable { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static func == (lhs: XCStringLocalizationContainer, rhs: XCStringLocalizationContainer) -> Bool { |     static func == (lhs: Self, rhs: Self) -> Bool { | ||||||
|         return lhs.localizations.count == rhs.localizations.count && lhs.localizations.sorted(by: { $0.lang < $1.lang }) == rhs.localizations.sorted(by: { $0.lang < $1.lang }) |         lhs.localizations.count == rhs.localizations.count | ||||||
|  |         && lhs.localizations.sorted { $0.lang < $1.lang } == rhs.localizations.sorted { $0.lang < $1.lang } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| struct XCStringLocalization: Codable, Equatable { | struct XCStringLocalization: Codable, Equatable { | ||||||
|  |  | ||||||
|     let lang: String // json key -> custom encoding method |     let lang: String // json key -> custom encoding method | ||||||
|     let content: XCStringLocalizationLangContent |     let content: XCStringLocalizationLangContent | ||||||
| } | } | ||||||
|  |  | ||||||
| struct XCStringLocalizationLangContent: Codable, Equatable { | struct XCStringLocalizationLangContent: Codable, Equatable { | ||||||
|  |  | ||||||
|     let stringUnit: DefaultStringUnit |     let stringUnit: DefaultStringUnit | ||||||
| } | } | ||||||
|  |  | ||||||
| //enum VarationOrStringUnit: Encodable { | // enum VarationOrStringUnit: Encodable | ||||||
| //    case variations([Varation]) | //  | ||||||
| //    case stringUnit: (DefaultStringUnit) | //     case variations([Varation]) | ||||||
| // | //     case stringUnit: (DefaultStringUnit) | ||||||
| //    func encode(to encoder: any Encoder) throws { | //  | ||||||
| //        if let varations { | //     func encode(to encoder: any Encoder) throws { | ||||||
| // | //         if let varations { | ||||||
| //        } else if let  { | //  | ||||||
| // | //         } else if let  { | ||||||
| //        } | //  | ||||||
| //    } | //         } | ||||||
| //} | //     } | ||||||
|  | // } | ||||||
|  |  | ||||||
| struct DefaultStringUnit: Codable, Equatable { | struct DefaultStringUnit: Codable, Equatable { | ||||||
|  |  | ||||||
|     let state: String |     let state: String | ||||||
|     let value: String |     let value: String | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,76 +1,74 @@ | |||||||
| // | // | ||||||
| //  TwineFileParser.swift | //  TwineFileParser.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 10/01/2022. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| class TwineFileParser { | // swiftlint:disable function_body_length | ||||||
|  |  | ||||||
|  | enum TwineFileParser { | ||||||
|  |  | ||||||
|     static func parse(_ inputFile: String) -> [Section] { |     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) |         let stringsByLines = inputFileContent.components(separatedBy: .newlines) | ||||||
|          |  | ||||||
|         var sections = [Section]() |         var sections = [Section]() | ||||||
|          |  | ||||||
|         // Parse file |         // Parse file | ||||||
|         stringsByLines.forEach { |         stringsByLines.forEach { // swiftlint:disable:this closure_body_length | ||||||
|             // Section |             // Section | ||||||
|             if let section = Section.match($0) { |             if let section = Section.match($0) { | ||||||
|                 sections.append(section) |                 sections.append(section) | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             // Definition |             // Definition | ||||||
|             if let definition = Definition.match($0) { |             if let definition = Definition.match($0) { | ||||||
|                 sections.last?.definitions.append(definition) |                 sections.last?.definitions.append(definition) | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             // Definition content |             // Definition content | ||||||
|             if $0.isEmpty == false { |             if $0.isEmpty == false { | ||||||
|                 // fr = Test => ["fr ", " Test"] |                 // fr = Test => ["fr ", " Test"] | ||||||
|                 let splitLine = $0 |                 let splitLine = $0 | ||||||
|                     .removeLeadingTrailingWhitespace() |                     .removeLeadingTrailingWhitespace() | ||||||
|                     .split(separator: "=") |                     .split(separator: "=") | ||||||
|                  |  | ||||||
|                 guard let lastDefinition = sections.last?.definitions.last, |                 guard let lastDefinition = sections.last?.definitions.last, | ||||||
|                       let leftElement = splitLine.first else { |                       let leftElement = splitLine.first else { | ||||||
|                           return |                     return | ||||||
|                       } |                 } | ||||||
|                  |  | ||||||
|                 let rightElement: String = splitLine.dropFirst().joined(separator: "=") |                 let rightElement: String = splitLine.dropFirst().joined(separator: "=") | ||||||
|                      |  | ||||||
|                 // "fr " => "fr" |                 // "fr " => "fr" | ||||||
|                 let leftHand = String(leftElement).removeTrailingWhitespace() |                 let leftHand = String(leftElement).removeTrailingWhitespace() | ||||||
|                 // " Test" => "Test" |                 // " Test" => "Test" | ||||||
|                 let rightHand = String(rightElement).removeLeadingWhitespace() |                 let rightHand = String(rightElement).removeLeadingWhitespace() | ||||||
|                  |  | ||||||
|                 // Handle comments, tags and translation |                 // Handle comments, tags and translation | ||||||
|                 switch leftHand { |                 switch leftHand { | ||||||
|                 case "comments": |                 case "comments": | ||||||
|                     lastDefinition.comment = rightHand |                     lastDefinition.comment = rightHand | ||||||
|                      |  | ||||||
|                 case "tags": |                 case "tags": | ||||||
|                     lastDefinition.tags = rightHand |                     lastDefinition.tags = rightHand | ||||||
|                         .split(separator: ",") |                         .split(separator: ",") | ||||||
|                         .map { String($0) } |                         .map { String($0) } | ||||||
|                      |  | ||||||
|                 case "ref": |                 case "ref": | ||||||
|                     lastDefinition.reference = rightHand |                     lastDefinition.reference = rightHand | ||||||
|                      |  | ||||||
|                 default: |                 default: | ||||||
|                     lastDefinition.translations[leftHand] = rightHand.escapeDoubleQuote() |                     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 |         // Keep only valid definition | ||||||
|         var invalidDefinitionNames = [String]() |         var invalidDefinitionNames = [String]() | ||||||
|         sections.forEach { section in |         sections.forEach { section in | ||||||
| @@ -83,10 +81,10 @@ class TwineFileParser { | |||||||
|                     return true |                     return true | ||||||
|                 } |                 } | ||||||
|         } |         } | ||||||
|         if invalidDefinitionNames.count > 0 { |         if invalidDefinitionNames.isEmpty == false { | ||||||
|             print("warning: [\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))") |             print("warning: [\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))") | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return sections |         return sections | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,114 +1,122 @@ | |||||||
| // | // | ||||||
| //  Stringium.swift | //  Stringium.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 10/01/2022. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct Stringium: ParsableCommand { | struct Stringium: ParsableCommand { | ||||||
|      |  | ||||||
|     // MARK: - Command Configuration |     // MARK: - Command Configuration | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static var configuration = CommandConfiguration( | ||||||
|         abstract: "Generate strings with custom scripts.", |         abstract: "Generate strings with custom scripts.", | ||||||
|         version: ResgenSwiftVersion |         version: ResgenSwiftVersion | ||||||
|     ) |     ) | ||||||
|      |  | ||||||
|     // MARK: - Static |     // MARK: - Static | ||||||
|      |  | ||||||
|     static let toolName = "Stringium" |     static let toolName = "Stringium" | ||||||
|     static let defaultExtensionName = "String" |     static let defaultExtensionName = "String" | ||||||
|     static let noTranslationTag: String = "notranslation" |     static let noTranslationTag: String = "notranslation" | ||||||
|      |  | ||||||
|     // MARK: - Command options |     // MARK: - Command options | ||||||
|      |  | ||||||
|     @OptionGroup var options: StringiumOptions |     @OptionGroup var options: StringiumOptions | ||||||
|      |  | ||||||
|     // MARK: - Run |     // MARK: - Run | ||||||
|      |  | ||||||
|     mutating func run() { |     mutating func run() { | ||||||
|         print("[\(Self.toolName)] Starting strings generation") |         print("[\(Self.toolName)] Starting strings generation") | ||||||
|         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate strings for \(options.langs) (default lang: \(options.defaultLang)") |         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate strings for \(options.langs) (default lang: \(options.defaultLang)") | ||||||
|          |  | ||||||
|         // Check requirements |         // Check requirements | ||||||
|         guard checkRequirements() else { return } |         guard checkRequirements() else { return } | ||||||
|          |  | ||||||
|         print("[\(Self.toolName)] Will generate strings") |         print("[\(Self.toolName)] Will generate strings") | ||||||
|          |  | ||||||
|         // Parse input file |         // Parse input file | ||||||
|         let sections = TwineFileParser.parse(options.inputFile) |         let sections = TwineFileParser.parse(options.inputFile) | ||||||
|          |  | ||||||
|         // Generate strings files |         // Generate strings files | ||||||
|         print(options.xcStrings) |         print(options.xcStrings) | ||||||
|         if !options.xcStrings { |         if !options.xcStrings { | ||||||
|             print("[\(Self.toolName)] Will generate strings") |             print("[\(Self.toolName)] Will generate strings") | ||||||
|  |  | ||||||
|             StringsFileGenerator.writeStringsFiles(sections: sections, |             StringsFileGenerator.writeStringsFiles( | ||||||
|                                                    langs: options.langs, |                 sections: sections, | ||||||
|                                                    defaultLang: options.defaultLang, |                 langs: options.langs, | ||||||
|                                                    tags: options.tags, |                 defaultLang: options.defaultLang, | ||||||
|                                                    outputPath: options.stringsFileOutputPath, |                 tags: options.tags, | ||||||
|                                                    inputFilenameWithoutExt: options.inputFilenameWithoutExt) |                 outputPath: options.stringsFileOutputPath, | ||||||
|  |                 inputFilenameWithoutExt: options.inputFilenameWithoutExt | ||||||
|  |             ) | ||||||
|         } else { |         } else { | ||||||
|             print("[\(Self.toolName)] Will generate xcStrings") |             print("[\(Self.toolName)] Will generate xcStrings") | ||||||
|             StringsFileGenerator.writeXcStringsFiles(sections: sections, |             StringsFileGenerator.writeXcStringsFiles( | ||||||
|                                                      langs: options.langs, |                 sections: sections, | ||||||
|                                                      defaultLang: options.defaultLang, |                 langs: options.langs, | ||||||
|                                                      tags: options.tags, |                 defaultLang: options.defaultLang, | ||||||
|                                                      outputPath: options.stringsFileOutputPath, |                 tags: options.tags, | ||||||
|                                                      inputFilenameWithoutExt: options.inputFilenameWithoutExt) |                 outputPath: options.stringsFileOutputPath, | ||||||
|  |                 inputFilenameWithoutExt: options.inputFilenameWithoutExt | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Generate extension |         // Generate extension | ||||||
|         StringsFileGenerator.writeExtensionFiles(sections: sections, |         StringsFileGenerator.writeExtensionFiles( | ||||||
|                                                  defaultLang: options.defaultLang, |             sections: sections, | ||||||
|                                                  tags: options.tags, |             defaultLang: options.defaultLang, | ||||||
|                                                  staticVar: options.staticMembers, |             tags: options.tags, | ||||||
|                                                  inputFilename: options.inputFilenameWithoutExt, |             staticVar: options.staticMembers, | ||||||
|                                                  extensionName: options.extensionName, |             inputFilename: options.inputFilenameWithoutExt, | ||||||
|                                                  extensionFilePath: options.extensionFilePath, |             extensionName: options.extensionName, | ||||||
|                                                  extensionSuffix: options.extensionSuffix) |             extensionFilePath: options.extensionFilePath, | ||||||
|  |             extensionSuffix: options.extensionSuffix | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         print("[\(Self.toolName)] Strings generated") |         print("[\(Self.toolName)] Strings generated") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Requirements |     // MARK: - Requirements | ||||||
|      |  | ||||||
|     private func checkRequirements() -> Bool { |     private func checkRequirements() -> Bool { | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|          |  | ||||||
|         // Input file |         // Input file | ||||||
|         guard fileManager.fileExists(atPath: options.inputFile) else { |         guard fileManager.fileExists(atPath: options.inputFile) else { | ||||||
|             let error = StringiumError.fileNotExists(options.inputFile) |             let error = StringiumError.fileNotExists(options.inputFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Langs |         // Langs | ||||||
|         guard options.langs.isEmpty == false else { |         guard options.langs.isEmpty == false else { | ||||||
|             let error = StringiumError.langsListEmpty |             let error = StringiumError.langsListEmpty | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         guard options.langs.contains(options.defaultLang) else { |         guard options.langs.contains(options.defaultLang) else { | ||||||
|             let error = StringiumError.defaultLangsNotInLangs |             let error = StringiumError.defaultLangsNotInLangs | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Check if needed to regenerate |         // Check if needed to regenerate | ||||||
|         guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, |         guard GeneratorChecker.shouldGenerate( | ||||||
|                                               inputFilePath: options.inputFile, |             force: options.forceGeneration, | ||||||
|                                               extensionFilePath: options.extensionFilePath) else { |             inputFilePath: options.inputFile, | ||||||
|  |             extensionFilePath: options.extensionFilePath | ||||||
|  |         ) else { | ||||||
|             print("[\(Self.toolName)] Strings are already up to date :) ") |             print("[\(Self.toolName)] Strings are already up to date :) ") | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,27 +8,28 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum StringiumError: Error { | enum StringiumError: Error { | ||||||
|  |  | ||||||
|     case fileNotExists(String) |     case fileNotExists(String) | ||||||
|     case langsListEmpty |     case langsListEmpty | ||||||
|     case defaultLangsNotInLangs |     case defaultLangsNotInLangs | ||||||
|     case writeFile(String, String) |     case writeFile(String, String) | ||||||
|     case langNotDefined(String, String, Bool) |     case langNotDefined(String, String, Bool) | ||||||
|      |  | ||||||
|     var description: String { |     var description: String { | ||||||
|         switch self { |         switch self { | ||||||
|         case .fileNotExists(let filename): |         case .fileNotExists(let filename): | ||||||
|             return "error: [\(Stringium.toolName)] File \(filename) does not exists " |             return "error: [\(Stringium.toolName)] File \(filename) does not exists " | ||||||
|              |  | ||||||
|         case .langsListEmpty: |         case .langsListEmpty: | ||||||
|             return "error: [\(Stringium.toolName)] Langs list is empty" |             return "error: [\(Stringium.toolName)] Langs list is empty" | ||||||
|              |  | ||||||
|         case .defaultLangsNotInLangs: |         case .defaultLangsNotInLangs: | ||||||
|             return "error: [\(Stringium.toolName)] Langs list does not contains the default lang" |             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)" |             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 { |             if isReference { | ||||||
|                 return "error: [\(Stringium.toolName)] Reference are handled only by Twine. Please use it or remove reference from you strings file." |                 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. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable no_grouping_extension | ||||||
|  |  | ||||||
| struct StringiumOptions: ParsableArguments { | struct StringiumOptions: ParsableArguments { | ||||||
|  |  | ||||||
|     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") |     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") | ||||||
|     var forceGeneration = false |     var forceGeneration = false | ||||||
|  |  | ||||||
|     @Argument(help: "Input files where strings are defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Argument(help: "Input files where strings are defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var inputFile: String |     var inputFile: String | ||||||
|      |  | ||||||
|     @Option(name: .customLong("output-path"), help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(name: .customLong("output-path"), help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     fileprivate var outputPathRaw: String |     fileprivate var outputPathRaw: String | ||||||
|      |  | ||||||
|     @Option(name: .customLong("langs"), help: "Langs to generate.") |     @Option(name: .customLong("langs"), help: "Langs to generate.") | ||||||
|     fileprivate var langsRaw: String |     fileprivate var langsRaw: String | ||||||
|      |  | ||||||
|     @Option(help: "Default langs.") |     @Option(help: "Default langs.") | ||||||
|     var defaultLang: String |     var defaultLang: String | ||||||
|      |  | ||||||
|     @Option(name: .customLong("tags"), help: "Tags to generate.") |     @Option(name: .customLong("tags"), help: "Tags to generate.") | ||||||
|     fileprivate var tagsRaw: String = "ios iosonly iosOnly notranslation" |     fileprivate var tagsRaw: String = "ios iosonly iosOnly notranslation" | ||||||
|      |  | ||||||
|     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var extensionOutputPath: String |     var extensionOutputPath: String | ||||||
|      |  | ||||||
|     @Option(help: "Tell if it will generate static properties or not") |     @Option(help: "Tell if it will generate static properties or not") | ||||||
|     var staticMembers: Bool = false |     var staticMembers: Bool = false | ||||||
|  |  | ||||||
| @@ -38,7 +41,7 @@ struct StringiumOptions: ParsableArguments { | |||||||
|  |  | ||||||
|     @Option(help: "Extension name. If not specified, it will generate an String extension.") |     @Option(help: "Extension name. If not specified, it will generate an String extension.") | ||||||
|     var extensionName: String = Stringium.defaultExtensionName |     var extensionName: String = Stringium.defaultExtensionName | ||||||
|      |  | ||||||
|     @Option(help: "Extension suffix: {extensionName}+{extensionSuffix}.swift") |     @Option(help: "Extension suffix: {extensionName}+{extensionSuffix}.swift") | ||||||
|     var extensionSuffix: String |     var extensionSuffix: String | ||||||
| } | } | ||||||
| @@ -46,6 +49,7 @@ struct StringiumOptions: ParsableArguments { | |||||||
| // MARK: - Private var getter | // MARK: - Private var getter | ||||||
|  |  | ||||||
| extension StringiumOptions { | extension StringiumOptions { | ||||||
|  |  | ||||||
|     var stringsFileOutputPath: String { |     var stringsFileOutputPath: String { | ||||||
|         var outputPath = outputPathRaw |         var outputPath = outputPathRaw | ||||||
|         if outputPath.last == "/" { |         if outputPath.last == "/" { | ||||||
| @@ -53,13 +57,13 @@ extension StringiumOptions { | |||||||
|         } |         } | ||||||
|         return outputPath |         return outputPath | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var langs: [String] { |     var langs: [String] { | ||||||
|         langsRaw |         langsRaw | ||||||
|             .split(separator: " ") |             .split(separator: " ") | ||||||
|             .map { String($0) } |             .map { String($0) } | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var tags: [String] { |     var tags: [String] { | ||||||
|         tagsRaw |         tagsRaw | ||||||
|             .split(separator: " ") |             .split(separator: " ") | ||||||
| @@ -70,14 +74,15 @@ extension StringiumOptions { | |||||||
| // MARK: - Computed var | // MARK: - Computed var | ||||||
|  |  | ||||||
| extension StringiumOptions { | extension StringiumOptions { | ||||||
|  |  | ||||||
|     var extensionFileName: String { |     var extensionFileName: String { | ||||||
|         "\(extensionName)+\(extensionSuffix).swift" |         "\(extensionName)+\(extensionSuffix).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePath: String { |     var extensionFilePath: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileName)" |         "\(extensionOutputPath)/\(extensionFileName)" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var inputFilenameWithoutExt: String { |     var inputFilenameWithoutExt: String { | ||||||
|         URL(fileURLWithPath: inputFile) |         URL(fileURLWithPath: inputFile) | ||||||
|             .deletingPathExtension() |             .deletingPathExtension() | ||||||
|   | |||||||
| @@ -5,12 +5,12 @@ | |||||||
| //  Created by Thibaut Schmitt on 10/01/2022. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct Strings: ParsableCommand { | struct Strings: ParsableCommand { | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static var configuration = CommandConfiguration( | ||||||
|         abstract: "A utility for generate strings.", |         abstract: "A utility for generate strings.", | ||||||
|         version: ResgenSwiftVersion, |         version: ResgenSwiftVersion, | ||||||
| @@ -22,8 +22,8 @@ struct Strings: ParsableCommand { | |||||||
|  |  | ||||||
|         // A default subcommand, when provided, is automatically selected if a |         // A default subcommand, when provided, is automatically selected if a | ||||||
|         // subcommand is not given on the command line. |         // subcommand is not given on the command line. | ||||||
|         //defaultSubcommand: Twine.self |         // defaultSubcommand: Twine.self | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
| //Strings.main() | // Strings.main() | ||||||
|   | |||||||
| @@ -1,78 +1,82 @@ | |||||||
| // | // | ||||||
| //  Tag.swift | //  Tag.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 10/01/2022. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct Tags: ParsableCommand { | struct Tags: ParsableCommand { | ||||||
|      |  | ||||||
|     // MARK: - Command Configuration |     // MARK: - Command Configuration | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static var configuration = CommandConfiguration( | ||||||
|         abstract: "Generate tags extension file.", |         abstract: "Generate tags extension file.", | ||||||
|         version: ResgenSwiftVersion |         version: ResgenSwiftVersion | ||||||
|     ) |     ) | ||||||
|      |  | ||||||
|     // MARK: - Static |     // MARK: - Static | ||||||
|      |  | ||||||
|     static let toolName = "Tags" |     static let toolName = "Tags" | ||||||
|     static let defaultExtensionName = "Tags" |     static let defaultExtensionName = "Tags" | ||||||
|     static let noTranslationTag: String = "notranslation" |     static let noTranslationTag: String = "notranslation" | ||||||
|      |  | ||||||
|     // MARK: - Command Options |     // MARK: - Command Options | ||||||
|      |  | ||||||
|     @OptionGroup var options: TagsOptions |     @OptionGroup var options: TagsOptions | ||||||
|      |  | ||||||
|     // MARK: - Run |     // MARK: - Run | ||||||
|      |  | ||||||
|     mutating func run() { |     mutating func run() { | ||||||
|         print("[\(Self.toolName)] Starting tags generation") |         print("[\(Self.toolName)] Starting tags generation") | ||||||
|         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate strings for lang: \(options.lang)") |         print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate strings for lang: \(options.lang)") | ||||||
|          |  | ||||||
|         // Check requirements |         // Check requirements | ||||||
|         guard checkRequirements() else { return } |         guard checkRequirements() else { return } | ||||||
|          |  | ||||||
|         print("[\(Self.toolName)] Will generate tags") |         print("[\(Self.toolName)] Will generate tags") | ||||||
|          |  | ||||||
|         // Parse input file |         // Parse input file | ||||||
|         let sections = TwineFileParser.parse(options.inputFile) |         let sections = TwineFileParser.parse(options.inputFile) | ||||||
|          |  | ||||||
|         // Generate extension |         // Generate extension | ||||||
|         TagsGenerator.writeExtensionFiles(sections: sections, |         TagsGenerator.writeExtensionFiles( | ||||||
|                                           lang: options.lang, |             sections: sections, | ||||||
|                                           tags: ["ios", "iosonly", Self.noTranslationTag], |             lang: options.lang, | ||||||
|                                           staticVar: options.staticMembers, |             tags: ["ios", "iosonly", Self.noTranslationTag], | ||||||
|                                           extensionName: options.extensionName, |             staticVar: options.staticMembers, | ||||||
|                                           extensionFilePath: options.extensionFilePath) |             extensionName: options.extensionName, | ||||||
|          |             extensionFilePath: options.extensionFilePath | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         print("[\(Self.toolName)] Tags generated") |         print("[\(Self.toolName)] Tags generated") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Requirements |     // MARK: - Requirements | ||||||
|      |  | ||||||
|     private func checkRequirements() -> Bool { |     private func checkRequirements() -> Bool { | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|          |  | ||||||
|         // Input file |         // Input file | ||||||
|         guard fileManager.fileExists(atPath: options.inputFile) else { |         guard fileManager.fileExists(atPath: options.inputFile) else { | ||||||
|             let error = StringiumError.fileNotExists(options.inputFile) |             let error = StringiumError.fileNotExists(options.inputFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Stringium.exit(withError: error) |             Stringium.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Check if needed to regenerate |         // Check if needed to regenerate | ||||||
|         guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, |         guard GeneratorChecker.shouldGenerate( | ||||||
|                                               inputFilePath: options.inputFile, |             force: options.forceGeneration, | ||||||
|                                               extensionFilePath: options.extensionFilePath) else { |             inputFilePath: options.inputFile, | ||||||
|  |             extensionFilePath: options.extensionFilePath | ||||||
|  |         ) else { | ||||||
|             print("[\(Self.toolName)] Tags are already up to date :) ") |             print("[\(Self.toolName)] Tags are already up to date :) ") | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,28 +5,31 @@ | |||||||
| //  Created by Thibaut Schmitt on 10/01/2022. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable no_grouping_extension | ||||||
|  |  | ||||||
| struct TagsOptions: ParsableArguments { | struct TagsOptions: ParsableArguments { | ||||||
|  |  | ||||||
|     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") |     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") | ||||||
|     var forceGeneration = false |     var forceGeneration = false | ||||||
|      |  | ||||||
|     @Argument(help: "Input files where tags ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Argument(help: "Input files where tags ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var inputFile: String |     var inputFile: String | ||||||
|      |  | ||||||
|     @Option(help: "Lang to generate. (\"ium\" by default)") |     @Option(help: "Lang to generate. (\"ium\" by default)") | ||||||
|     var lang: String = "ium" |     var lang: String = "ium" | ||||||
|      |  | ||||||
|     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var extensionOutputPath: String |     var extensionOutputPath: String | ||||||
|      |  | ||||||
|     @Option(help: "Tell if it will generate static properties or not") |     @Option(help: "Tell if it will generate static properties or not") | ||||||
|     var staticMembers: Bool = false |     var staticMembers: Bool = false | ||||||
|      |  | ||||||
|     @Option(help: "Extension name. If not specified, it will generate a Tag extension.") |     @Option(help: "Extension name. If not specified, it will generate a Tag extension.") | ||||||
|     var extensionName: String = Tags.defaultExtensionName |     var extensionName: String = Tags.defaultExtensionName | ||||||
|      |  | ||||||
|     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Tag{extensionSuffix}.swift") |     @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Tag{extensionSuffix}.swift") | ||||||
|     var extensionSuffix: String? |     var extensionSuffix: String? | ||||||
| } | } | ||||||
| @@ -34,13 +37,14 @@ struct TagsOptions: ParsableArguments { | |||||||
| // MARK: - Computed var | // MARK: - Computed var | ||||||
|  |  | ||||||
| extension TagsOptions { | extension TagsOptions { | ||||||
|  |  | ||||||
|     var extensionFileName: String { |     var extensionFileName: String { | ||||||
|         if let extensionSuffix = extensionSuffix { |         if let extensionSuffix { | ||||||
|             return "\(extensionName)+\(extensionSuffix).swift" |             return "\(extensionName)+\(extensionSuffix).swift" | ||||||
|         } |         } | ||||||
|         return "\(extensionName).swift" |         return "\(extensionName).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePath: String { |     var extensionFilePath: String { | ||||||
|         "\(extensionOutputPath)/\(extensionFileName)" |         "\(extensionOutputPath)/\(extensionFileName)" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,102 +1,117 @@ | |||||||
| // | // | ||||||
| //  Twine.swift | //  Twine.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 10/01/2022. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct Twine: ParsableCommand { | struct Twine: ParsableCommand { | ||||||
|      |  | ||||||
|     // MARK: - Command Configuration |     // MARK: - Command Configuration | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static var configuration = CommandConfiguration( | ||||||
|         abstract: "Generate strings with twine.", |         abstract: "Generate strings with twine.", | ||||||
|         version: ResgenSwiftVersion |         version: ResgenSwiftVersion | ||||||
|     ) |     ) | ||||||
|      |  | ||||||
|     // MARK: - Static |     // MARK: - Static | ||||||
|      |  | ||||||
|     static let toolName = "Twine" |     static let toolName = "Twine" | ||||||
|     static let defaultExtensionName = "String" |     static let defaultExtensionName = "String" | ||||||
|     static let twineExecutable: String = { |     static let twineExecutable: String = { | ||||||
|         #if os(macOS) | #if os(macOS) | ||||||
|         "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)/scripts/twine/twine" |         "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)/scripts/twine/twine" | ||||||
|         #else | #else | ||||||
|         fatalError("This command should run on Mac only") |         fatalError("This command should run on Mac only") | ||||||
|         #endif | #endif | ||||||
|     }() |     }() | ||||||
|  |  | ||||||
|     // MARK: - Command Options |     // MARK: - Command Options | ||||||
|      |  | ||||||
|     @OptionGroup var options: TwineOptions |     @OptionGroup var options: TwineOptions | ||||||
|      |  | ||||||
|     // MARK: - Run |     // MARK: - Run | ||||||
|      |  | ||||||
|     mutating func run() { |     mutating func run() { | ||||||
|         print("[\(Self.toolName)] Starting strings generation") |         print("[\(Self.toolName)] Starting strings generation") | ||||||
|          |  | ||||||
|         // Check requirements |         // Check requirements | ||||||
|         guard checkRequirements() else { return } |         guard checkRequirements() else { return } | ||||||
|          |  | ||||||
|         print("[\(Self.toolName)] Will generate strings") |         print("[\(Self.toolName)] Will generate strings") | ||||||
|          |  | ||||||
|         // Generate strings files (lproj files) |         // Generate strings files (lproj files) | ||||||
|         for lang in options.langs { |         for lang in options.langs { | ||||||
|             Shell.shell([Self.twineExecutable, |             Shell.shell( | ||||||
|                         "generate-localization-file", options.inputFile, |                 [ | ||||||
|                         "--lang", "\(lang)", |                     Self.twineExecutable, | ||||||
|                         "\(options.outputPath)/\(lang).lproj/\(options.inputFilenameWithoutExt).strings", |                     "generate-localization-file", | ||||||
|                         "--tags=ios,iosonly,iosOnly"]) |                     options.inputFile, | ||||||
|  |                     "--lang", | ||||||
|  |                     "\(lang)", | ||||||
|  |                     "\(options.outputPath)/\(lang).lproj/\(options.inputFilenameWithoutExt).strings", | ||||||
|  |                     "--tags=ios,iosonly,iosOnly" | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Generate extension |         // Generate extension | ||||||
|         Shell.shell([Self.twineExecutable, |         Shell.shell( | ||||||
|                     "generate-localization-file", options.inputFile, |             [ | ||||||
|                     "--format", "apple-swift", |                 Self.twineExecutable, | ||||||
|                     "--lang", "\(options.defaultLang)", |                 "generate-localization-file", | ||||||
|                     options.extensionFilePath, |                 options.inputFile, | ||||||
|                     "--tags=ios,iosonly,iosOnly"]) |                 "--format", | ||||||
|          |                 "apple-swift", | ||||||
|  |                 "--lang", | ||||||
|  |                 "\(options.defaultLang)", | ||||||
|  |                 options.extensionFilePath, | ||||||
|  |                 "--tags=ios,iosonly,iosOnly" | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         print("[\(Self.toolName)] Strings generated") |         print("[\(Self.toolName)] Strings generated") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // MARK: - Requirements |     // MARK: - Requirements | ||||||
|      |  | ||||||
|     private func checkRequirements() -> Bool { |     private func checkRequirements() -> Bool { | ||||||
|         let fileManager = FileManager() |         let fileManager = FileManager() | ||||||
|          |  | ||||||
|         // Input file |         // Input file | ||||||
|         guard fileManager.fileExists(atPath: options.inputFile) else { |         guard fileManager.fileExists(atPath: options.inputFile) else { | ||||||
|             let error = TwineError.fileNotExists(options.inputFile) |             let error = TwineError.fileNotExists(options.inputFile) | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Twine.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Langs |         // Langs | ||||||
|         guard options.langs.isEmpty == false else { |         guard options.langs.isEmpty == false else { | ||||||
|             let error = TwineError.langsListEmpty |             let error = TwineError.langsListEmpty | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Twine.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         guard options.langs.contains(options.defaultLang) else { |         guard options.langs.contains(options.defaultLang) else { | ||||||
|             let error = TwineError.defaultLangsNotInLangs |             let error = TwineError.defaultLangsNotInLangs | ||||||
|             print(error.description) |             print(error.description) | ||||||
|             Twine.exit(withError: error) |             Self.exit(withError: error) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Check if needed to regenerate |         // Check if needed to regenerate | ||||||
|         guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, |         guard GeneratorChecker.shouldGenerate( | ||||||
|                                               inputFilePath: options.inputFile, |             force: options.forceGeneration, | ||||||
|                                               extensionFilePath: options.extensionFilePathGenerated) else { |             inputFilePath: options.inputFile, | ||||||
|  |             extensionFilePath: options.extensionFilePathGenerated | ||||||
|  |         ) else { | ||||||
|             print("[\(Self.toolName)] Strings are already up to date :) ") |             print("[\(Self.toolName)] Strings are already up to date :) ") | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,18 +8,19 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| enum TwineError: Error { | enum TwineError: Error { | ||||||
|  |  | ||||||
|     case fileNotExists(String) |     case fileNotExists(String) | ||||||
|     case langsListEmpty |     case langsListEmpty | ||||||
|     case defaultLangsNotInLangs |     case defaultLangsNotInLangs | ||||||
|      |  | ||||||
|     var description: String { |     var description: String { | ||||||
|         switch self { |         switch self { | ||||||
|         case .fileNotExists(let filename): |         case .fileNotExists(let filename): | ||||||
|             return "error: [\(Twine.toolName)] File \(filename) does not exists " |             return "error: [\(Twine.toolName)] File \(filename) does not exists " | ||||||
|              |  | ||||||
|         case .langsListEmpty: |         case .langsListEmpty: | ||||||
|             return "error: [\(Twine.toolName)] Langs list is empty" |             return "error: [\(Twine.toolName)] Langs list is empty" | ||||||
|              |  | ||||||
|         case .defaultLangsNotInLangs: |         case .defaultLangsNotInLangs: | ||||||
|             return "error: [\(Twine.toolName)] Langs list does not contains the default lang" |             return "error: [\(Twine.toolName)] Langs list does not contains the default lang" | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -5,25 +5,28 @@ | |||||||
| //  Created by Thibaut Schmitt on 10/01/2022. | //  Created by Thibaut Schmitt on 10/01/2022. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable no_grouping_extension | ||||||
|  |  | ||||||
| struct TwineOptions: ParsableArguments { | struct TwineOptions: ParsableArguments { | ||||||
|  |  | ||||||
|     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") |     @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") | ||||||
|     var forceGeneration = false |     var forceGeneration = false | ||||||
|      |  | ||||||
|     @Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var inputFile: String |     var inputFile: String | ||||||
|      |  | ||||||
|     @Option(help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var outputPath: String |     var outputPath: String | ||||||
|      |  | ||||||
|     @Option(name: .customLong("langs"), help: "Langs to generate.") |     @Option(name: .customLong("langs"), help: "Langs to generate.") | ||||||
|     fileprivate var langsRaw: String |     fileprivate var langsRaw: String | ||||||
|      |  | ||||||
|     @Option(help: "Default langs.") |     @Option(help: "Default langs.") | ||||||
|     var defaultLang: String |     var defaultLang: String | ||||||
|      |  | ||||||
|     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) |     @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) | ||||||
|     var extensionOutputPath: String |     var extensionOutputPath: String | ||||||
| } | } | ||||||
| @@ -31,6 +34,7 @@ struct TwineOptions: ParsableArguments { | |||||||
| // MARK: - Private var getter | // MARK: - Private var getter | ||||||
|  |  | ||||||
| extension TwineOptions { | extension TwineOptions { | ||||||
|  |  | ||||||
|     var langs: [String] { |     var langs: [String] { | ||||||
|         langsRaw |         langsRaw | ||||||
|             .split(separator: " ") |             .split(separator: " ") | ||||||
| @@ -41,16 +45,17 @@ extension TwineOptions { | |||||||
| // MARK: - Computed var | // MARK: - Computed var | ||||||
|  |  | ||||||
| extension TwineOptions { | extension TwineOptions { | ||||||
|  |  | ||||||
|     var inputFilenameWithoutExt: String { |     var inputFilenameWithoutExt: String { | ||||||
|         URL(fileURLWithPath: inputFile) |         URL(fileURLWithPath: inputFile) | ||||||
|             .deletingPathExtension() |             .deletingPathExtension() | ||||||
|             .lastPathComponent |             .lastPathComponent | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     var extensionFilePath: String { |     var extensionFilePath: String { | ||||||
|         "\(extensionOutputPath)/\(inputFilenameWithoutExt).swift" |         "\(extensionOutputPath)/\(inputFilenameWithoutExt).swift" | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     // "R2String+" is hardcoded in Twine formatter |     // "R2String+" is hardcoded in Twine formatter | ||||||
|     var extensionFilePathGenerated: String { |     var extensionFilePathGenerated: String { | ||||||
|         "\(extensionOutputPath)/R2String+\(inputFilenameWithoutExt).swift" |         "\(extensionOutputPath)/R2String+\(inputFilenameWithoutExt).swift" | ||||||
|   | |||||||
| @@ -5,12 +5,12 @@ | |||||||
| //  Created by Thibaut Schmitt on 13/12/2021. | //  Created by Thibaut Schmitt on 13/12/2021. | ||||||
| // | // | ||||||
|  |  | ||||||
| import ToolCore |  | ||||||
| import Foundation |  | ||||||
| import ArgumentParser | import ArgumentParser | ||||||
|  | import Foundation | ||||||
|  | import ToolCore | ||||||
|  |  | ||||||
| struct ResgenSwift: ParsableCommand { | struct ResgenSwift: ParsableCommand { | ||||||
|      |  | ||||||
|     static var configuration = CommandConfiguration( |     static var configuration = CommandConfiguration( | ||||||
|         abstract: "A utility for generate ressources.", |         abstract: "A utility for generate ressources.", | ||||||
|         version: ResgenSwiftVersion, |         version: ResgenSwiftVersion, | ||||||
| @@ -30,7 +30,7 @@ struct ResgenSwift: ParsableCommand { | |||||||
|  |  | ||||||
|         // A default subcommand, when provided, is automatically selected if a |         // A default subcommand, when provided, is automatically selected if a | ||||||
|         // subcommand is not given on the command line. |         // subcommand is not given on the command line. | ||||||
|         //defaultSubcommand: Twine.self |         // defaultSubcommand: Twine.self | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,43 +1,42 @@ | |||||||
| // | // | ||||||
| //  GeneratorChecker.swift | //  GeneratorChecker.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 22/12/2021. | //  Created by Thibaut Schmitt on 22/12/2021. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| public class GeneratorChecker { | public enum GeneratorChecker { | ||||||
|      |  | ||||||
|     /// Return `true` if  inputFile is newer than extensionFile, otherwise `false` |     /// Return `true` if  inputFile is newer than extensionFile, otherwise `false` | ||||||
|     public static func shouldGenerate(force: Bool, inputFilePath: String, extensionFilePath: String) -> Bool { |     public static func shouldGenerate(force: Bool, inputFilePath: String, extensionFilePath: String) -> Bool { | ||||||
|         guard force == false else { |         guard force == false else { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return Self.isFile(inputFilePath, moreRecenThan: extensionFilePath) |         return Self.isFile(inputFilePath, moreRecenThan: extensionFilePath) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     public static func isFile(_ fileOne: String, moreRecenThan fileTwo: String) -> Bool { |     public static func isFile(_ fileOne: String, moreRecenThan fileTwo: String) -> Bool { | ||||||
|         let fileOneURL = URL(fileURLWithPath: fileOne) |         let fileOneURL = URL(fileURLWithPath: fileOne) | ||||||
|         let fileTwoURL = URL(fileURLWithPath: fileTwo) |         let fileTwoURL = URL(fileURLWithPath: fileTwo) | ||||||
|          |  | ||||||
|         let fileOneRessourceValues = try? fileOneURL.resourceValues(forKeys: [URLResourceKey.contentModificationDateKey]) |         let fileOneRessourceValues = try? fileOneURL.resourceValues(forKeys: [URLResourceKey.contentModificationDateKey]) | ||||||
|         let fileTwoRessourceValues = try? fileTwoURL.resourceValues(forKeys: [URLResourceKey.contentModificationDateKey]) |         let fileTwoRessourceValues = try? fileTwoURL.resourceValues(forKeys: [URLResourceKey.contentModificationDateKey]) | ||||||
|          |  | ||||||
|         guard let fileOneModificationDate = fileOneRessourceValues?.contentModificationDate, |         guard let fileOneModificationDate = fileOneRessourceValues?.contentModificationDate, | ||||||
|               let fileTwoModificationDate = fileTwoRessourceValues?.contentModificationDate else { |               let fileTwoModificationDate = fileTwoRessourceValues?.contentModificationDate else { | ||||||
|             print("⚠️ Could not compare file modication date. ⚠️ (assume than file is newer)") |             print("⚠️ Could not compare file modication date. ⚠️ (assume than file is newer)") | ||||||
|             // Date not available -> assume than fileOne is newer than fileTwo |             // Date not available -> assume than fileOne is newer than fileTwo | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if fileOneModificationDate >= fileTwoModificationDate { |         if fileOneModificationDate >= fileTwoModificationDate { | ||||||
|             debugPrint("File one is more recent than file two.") |             debugPrint("File one is more recent than file two.") | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|      |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,8 +7,9 @@ | |||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| public extension Sequence where Iterator.Element: Hashable { | extension Sequence where Iterator.Element: Hashable { | ||||||
|     func unique() -> [Iterator.Element] { |  | ||||||
|  |      public func unique() -> [Iterator.Element] { | ||||||
|         var seen: [Iterator.Element: Bool] = [:] |         var seen: [Iterator.Element: Bool] = [:] | ||||||
|         return self.filter { seen.updateValue(true, forKey: $0) == nil } |         return self.filter { seen.updateValue(true, forKey: $0) == nil } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| // | // | ||||||
| //  Shell.swift | //  Shell.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 22/12/2021. | //  Created by Thibaut Schmitt on 22/12/2021. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| public class Shell { | public enum Shell { | ||||||
|      |  | ||||||
|     public static var environment: [String: String] { |     public static var environment: [String: String] { | ||||||
|         ProcessInfo.processInfo.environment |         ProcessInfo.processInfo.environment | ||||||
|     } |     } | ||||||
| @@ -18,31 +18,31 @@ public class Shell { | |||||||
|         launchPath: String = "/usr/bin/env", |         launchPath: String = "/usr/bin/env", | ||||||
|         _ args: [String] |         _ args: [String] | ||||||
|     ) -> (terminationStatus: Int32, output: String?) { |     ) -> (terminationStatus: Int32, output: String?) { | ||||||
|         #if os(macOS) | #if os(macOS) | ||||||
|         let task = Process() |         let task = Process() | ||||||
|         task.launchPath = launchPath |         task.launchPath = launchPath | ||||||
|         task.arguments = args |         task.arguments = args | ||||||
|          |  | ||||||
|         var currentEnv = ProcessInfo.processInfo.environment |         var currentEnv = ProcessInfo.processInfo.environment | ||||||
|         for (key, value) in environment { |         for (key, value) in environment { | ||||||
|             currentEnv[key] = value |             currentEnv[key] = value | ||||||
|         } |         } | ||||||
|         task.environment = currentEnv |         task.environment = currentEnv | ||||||
|          |  | ||||||
|         let pipe = Pipe() |         let pipe = Pipe() | ||||||
|         task.standardOutput = pipe |         task.standardOutput = pipe | ||||||
|         task.launch() |         task.launch() | ||||||
|         task.waitUntilExit() |         task.waitUntilExit() | ||||||
|          |  | ||||||
|         let data = pipe.fileHandleForReading.readDataToEndOfFile() |         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: nil) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return (terminationStatus: task.terminationStatus, output: output) |         return (terminationStatus: task.terminationStatus, output: output) | ||||||
|         #else | #else | ||||||
|         fatalError("Shell is only available on Mac") |         fatalError("Shell is only available on Mac") | ||||||
|         #endif | #endif | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,87 +1,88 @@ | |||||||
| // | // | ||||||
| //  Extensions.swift | //  Extensions.swift | ||||||
| //   | // | ||||||
| // | // | ||||||
| //  Created by Thibaut Schmitt on 13/12/2021. | //  Created by Thibaut Schmitt on 13/12/2021. | ||||||
| // | // | ||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| public extension String { | extension String { | ||||||
|     func removeCharacters(from forbiddenChars: CharacterSet) -> String { |  | ||||||
|  |     public func removeCharacters(from forbiddenChars: CharacterSet) -> String { | ||||||
|         let passed = self.unicodeScalars.filter { !forbiddenChars.contains($0) } |         let passed = self.unicodeScalars.filter { !forbiddenChars.contains($0) } | ||||||
|         return String(String.UnicodeScalarView(passed)) |         return String(String.UnicodeScalarView(passed)) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func removeCharacters(from: String) -> String { |     public func removeCharacters(from: String) -> String { | ||||||
|         return removeCharacters(from: CharacterSet(charactersIn: from)) |         removeCharacters(from: CharacterSet(charactersIn: from)) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func replacingOccurrences(of: [String], with: String) -> Self { |     public func replacingOccurrences(of: [String], with: String) -> Self { | ||||||
|         var tmp = self |         var tmp = self | ||||||
|         for e in of { |         for ofValue in of { | ||||||
|             tmp = tmp.replacingOccurrences(of: e, with: with) |             tmp = tmp.replacingOccurrences(of: ofValue, with: with) | ||||||
|         } |         } | ||||||
|         return tmp |         return tmp | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func removeTrailingWhitespace() -> Self { |     public func removeTrailingWhitespace() -> Self { | ||||||
|         var newString = self |         var newString = self | ||||||
|          |  | ||||||
|         while newString.last?.isWhitespace == true { |         while newString.last?.isWhitespace == true { | ||||||
|             newString = String(newString.dropLast()) |             newString = String(newString.dropLast()) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return newString |         return newString | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func removeLeadingWhitespace() -> Self { |     public func removeLeadingWhitespace() -> Self { | ||||||
|         var newString = self |         var newString = self | ||||||
|          |  | ||||||
|         while newString.first?.isWhitespace == true { |         while newString.first?.isWhitespace == true { | ||||||
|             newString = String(newString.dropFirst()) |             newString = String(newString.dropFirst()) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         return newString |         return newString | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func removeLeadingTrailingWhitespace() -> Self { |     public func removeLeadingTrailingWhitespace() -> Self { | ||||||
|         var newString = self |         var newString = self | ||||||
|          |  | ||||||
|         newString = newString.removeLeadingWhitespace() |         newString = newString.removeLeadingWhitespace() | ||||||
|         newString = newString.removeTrailingWhitespace() |         newString = newString.removeTrailingWhitespace() | ||||||
|          |  | ||||||
|         return newString |         return newString | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func escapeDoubleQuote() -> Self { |     public func escapeDoubleQuote() -> Self { | ||||||
|         replacingOccurrences(of: "\"", with: "\\\"") |         replacingOccurrences(of: "\"", with: "\\\"") | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func replaceTiltWithHomeDirectoryPath() -> Self { |     public func replaceTiltWithHomeDirectoryPath() -> Self { | ||||||
|         // See NSString.expandingTildeInPath |         // See NSString.expandingTildeInPath | ||||||
|         #if os(macOS) | #if os(macOS) | ||||||
|         replacingOccurrences(of: "~", with: "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)") |         replacingOccurrences(of: "~", with: "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)") | ||||||
|         #else | #else | ||||||
|         fatalError("This command should run on Mac only") |         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 alpha: String = "FF" | ||||||
|         var red: String |         var red: String | ||||||
|         var green: String |         var green: String | ||||||
|         var blue: String |         var blue: String | ||||||
|  |  | ||||||
|         var colorClean = self |         var colorClean = self | ||||||
|                 .replacingOccurrences(of: "#", with: "") |             .replacingOccurrences(of: "#", with: "") | ||||||
|                 .replacingOccurrences(of: "0x", with: "") |             .replacingOccurrences(of: "0x", with: "") | ||||||
|  |  | ||||||
|         if colorClean.count == 8 { |         if colorClean.count == 8 { | ||||||
|             alpha = String(colorClean.prefix(2)) |             alpha = String(colorClean.prefix(2)) | ||||||
|             colorClean = String(colorClean.dropFirst(2)) |             colorClean = String(colorClean.dropFirst(2)) | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         red = String(colorClean.prefix(2)) |         red = String(colorClean.prefix(2)) | ||||||
|         colorClean = String(colorClean.dropFirst(2)) |         colorClean = String(colorClean.dropFirst(2)) | ||||||
|         green = String(colorClean.prefix(2)) |         green = String(colorClean.prefix(2)) | ||||||
| @@ -89,12 +90,12 @@ public extension String { | |||||||
|         blue = String(colorClean.prefix(2)) |         blue = String(colorClean.prefix(2)) | ||||||
|         return (alpha: alpha, red: red, green: green, blue: blue) |         return (alpha: alpha, red: red, green: green, blue: blue) | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     func uppercasedFirst() -> String { |     public func uppercasedFirst() -> String { | ||||||
|         prefix(1).uppercased() + dropFirst() |         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) { |         if let range = self.range(of: of) { | ||||||
|             let tmp = self.replacingOccurrences( |             let tmp = self.replacingOccurrences( | ||||||
|                 of: of, |                 of: of, | ||||||
|   | |||||||
| @@ -7,4 +7,6 @@ | |||||||
|  |  | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
|  | // swiftlint:disable prefixed_toplevel_constant identifier_name | ||||||
|  |  | ||||||
| public let ResgenSwiftVersion = "2.1.0" | public let ResgenSwiftVersion = "2.1.0" | ||||||
|   | |||||||
| @@ -51,11 +51,14 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         ] |         ] | ||||||
|          |          | ||||||
|         // When |         // When | ||||||
|         AnalyticsGenerator.targets = [TrackerType.firebase] |         let extensionContent = AnalyticsGenerator.getExtensionContent( | ||||||
|         let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], |             targets: [TrackerType.firebase], | ||||||
|                                                                  tags: ["ios", "iosonly"], |             sections: [sectionOne, sectionTwo, sectionThree], | ||||||
|                                                                  staticVar: false, |             tags: ["ios", "iosonly"], | ||||||
|                                                                  extensionName: "GenAnalytics") |             staticVar: false, | ||||||
|  |             extensionName: "GenAnalytics" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Expect Analytics |         // Expect Analytics | ||||||
|         let expect = """ |         let expect = """ | ||||||
|         // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) |         // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) | ||||||
| @@ -65,6 +68,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Protocol |         // MARK: - Protocol | ||||||
|  |  | ||||||
|         protocol AnalyticsManagerProtocol { |         protocol AnalyticsManagerProtocol { | ||||||
|  |  | ||||||
|             func logScreen(name: String, path: String) |             func logScreen(name: String, path: String) | ||||||
|             func logEvent( |             func logEvent( | ||||||
|                 name: String, |                 name: String, | ||||||
| @@ -77,6 +81,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Firebase |         // MARK: - Firebase | ||||||
|  |  | ||||||
|         class FirebaseAnalyticsManager: AnalyticsManagerProtocol { |         class FirebaseAnalyticsManager: AnalyticsManagerProtocol { | ||||||
|  |  | ||||||
|             func logScreen(name: String, path: String) { |             func logScreen(name: String, path: String) { | ||||||
|                 var parameters = [ |                 var parameters = [ | ||||||
|                     AnalyticsParameterScreenName: name as NSObject |                     AnalyticsParameterScreenName: name as NSObject | ||||||
| @@ -98,7 +103,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                     "action": action as NSObject, |                     "action": action as NSObject, | ||||||
|                     "category": category as NSObject, |                     "category": category as NSObject, | ||||||
|                 ] |                 ] | ||||||
|                  |  | ||||||
|                 if let supplementaryParameters = params { |                 if let supplementaryParameters = params { | ||||||
|                     for (newKey, newValue) in supplementaryParameters { |                     for (newKey, newValue) in supplementaryParameters { | ||||||
|                         if parameters.contains(where: { (key: String, value: NSObject) in |                         if parameters.contains(where: { (key: String, value: NSObject) in | ||||||
| @@ -106,11 +111,11 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                         }) { |                         }) { | ||||||
|                             continue |                             continue | ||||||
|                         } |                         } | ||||||
|                          |  | ||||||
|                         parameters[newKey] = newValue as? NSObject |                         parameters[newKey] = newValue as? NSObject | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 Analytics.logEvent( |                 Analytics.logEvent( | ||||||
|                     name.replacingOccurrences(of: [" "], with: "_"), |                     name.replacingOccurrences(of: [" "], with: "_"), | ||||||
|                     parameters: parameters |                     parameters: parameters | ||||||
| @@ -121,8 +126,9 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Manager |         // MARK: - Manager | ||||||
|  |  | ||||||
|         class AnalyticsManager { |         class AnalyticsManager { | ||||||
|  |  | ||||||
|             static var shared = AnalyticsManager() |             static var shared = AnalyticsManager() | ||||||
|              |  | ||||||
|             // MARK: - Properties |             // MARK: - Properties | ||||||
|  |  | ||||||
|             var managers: [AnalyticsManagerProtocol] = [] |             var managers: [AnalyticsManagerProtocol] = [] | ||||||
| @@ -138,7 +144,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|             func configure() { |             func configure() { | ||||||
|                 managers.append(FirebaseAnalyticsManager()) |                 managers.append(FirebaseAnalyticsManager()) | ||||||
|             } |             } | ||||||
|                  |  | ||||||
|             private func logScreen(name: String, path: String) { |             private func logScreen(name: String, path: String) { | ||||||
|                 guard isEnabled else { return } |                 guard isEnabled else { return } | ||||||
|  |  | ||||||
| @@ -146,7 +152,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                     manager.logScreen(name: name, path: path) |                     manager.logScreen(name: name, path: path) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             private func logEvent( |             private func logEvent( | ||||||
|                 name: String, |                 name: String, | ||||||
|                 action: String, |                 action: String, | ||||||
| @@ -154,7 +160,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 params: [String: Any]? |                 params: [String: Any]? | ||||||
|             ) { |             ) { | ||||||
|                 guard isEnabled else { return } |                 guard isEnabled else { return } | ||||||
|          |  | ||||||
|                 managers.forEach { manager in |                 managers.forEach { manager in | ||||||
|                     manager.logEvent( |                     manager.logEvent( | ||||||
|                         name: name, |                         name: name, | ||||||
| @@ -222,11 +228,13 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         ] |         ] | ||||||
|          |          | ||||||
|         // When |         // When | ||||||
|         AnalyticsGenerator.targets = [TrackerType.matomo] |         let extensionContent = AnalyticsGenerator.getExtensionContent( | ||||||
|         let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], |             targets: [TrackerType.matomo], | ||||||
|                                                                  tags: ["ios", "iosonly"], |             sections: [sectionOne, sectionTwo, sectionThree], | ||||||
|                                                                  staticVar: false, |             tags: ["ios", "iosonly"], | ||||||
|                                                                  extensionName: "GenAnalytics") |             staticVar: false, | ||||||
|  |             extensionName: "GenAnalytics" | ||||||
|  |         ) | ||||||
|         // Expect Analytics |         // Expect Analytics | ||||||
|         let expect = """ |         let expect = """ | ||||||
|         // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) |         // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) | ||||||
| @@ -236,6 +244,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Protocol |         // MARK: - Protocol | ||||||
|  |  | ||||||
|         protocol AnalyticsManagerProtocol { |         protocol AnalyticsManagerProtocol { | ||||||
|  |  | ||||||
|             func logScreen(name: String, path: String) |             func logScreen(name: String, path: String) | ||||||
|             func logEvent( |             func logEvent( | ||||||
|                 name: String, |                 name: String, | ||||||
| @@ -248,7 +257,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Matomo |         // MARK: - Matomo | ||||||
|  |  | ||||||
|         class MatomoAnalyticsManager: AnalyticsManagerProtocol { |         class MatomoAnalyticsManager: AnalyticsManagerProtocol { | ||||||
|              |  | ||||||
|             // MARK: - Properties |             // MARK: - Properties | ||||||
|  |  | ||||||
|             private var tracker: MatomoTracker |             private var tracker: MatomoTracker | ||||||
| @@ -262,11 +271,11 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                     siteId: siteId, |                     siteId: siteId, | ||||||
|                     baseURL: URL(string: url)! |                     baseURL: URL(string: url)! | ||||||
|                 ) |                 ) | ||||||
|                  |  | ||||||
|                 #if DEBUG |                 #if DEBUG | ||||||
|                     tracker.dispatchInterval = 5 |                     tracker.dispatchInterval = 5 | ||||||
|                 #endif |                 #endif | ||||||
|                  |  | ||||||
|                 #if DEBUG |                 #if DEBUG | ||||||
|                     tracker.logger = DefaultLogger(minLevel: .verbose) |                     tracker.logger = DefaultLogger(minLevel: .verbose) | ||||||
|                 #endif |                 #endif | ||||||
| @@ -274,7 +283,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") |                 debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") | ||||||
|                 debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") |                 debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             // MARK: - Methods |             // MARK: - Methods | ||||||
|  |  | ||||||
|             func logScreen(name: String, path: String) { |             func logScreen(name: String, path: String) { | ||||||
| @@ -295,7 +304,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 params: [String: Any]? |                 params: [String: Any]? | ||||||
|             ) { |             ) { | ||||||
|                 guard !tracker.isOptedOut else { return } |                 guard !tracker.isOptedOut else { return } | ||||||
|          |  | ||||||
|                 tracker.track( |                 tracker.track( | ||||||
|                     eventWithCategory: category, |                     eventWithCategory: category, | ||||||
|                     action: action, |                     action: action, | ||||||
| @@ -309,8 +318,9 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Manager |         // MARK: - Manager | ||||||
|  |  | ||||||
|         class AnalyticsManager { |         class AnalyticsManager { | ||||||
|  |  | ||||||
|             static var shared = AnalyticsManager() |             static var shared = AnalyticsManager() | ||||||
|              |  | ||||||
|             // MARK: - Properties |             // MARK: - Properties | ||||||
|  |  | ||||||
|             var managers: [AnalyticsManagerProtocol] = [] |             var managers: [AnalyticsManagerProtocol] = [] | ||||||
| @@ -331,15 +341,15 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|                  |  | ||||||
|             private func logScreen(name: String, path: String) { |             private func logScreen(name: String, path: String) { | ||||||
|                 guard isEnabled else { return } |                 guard isEnabled else { return } | ||||||
|          |  | ||||||
|                 managers.forEach { manager in |                 managers.forEach { manager in | ||||||
|                     manager.logScreen(name: name, path: path) |                     manager.logScreen(name: name, path: path) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             private func logEvent( |             private func logEvent( | ||||||
|                 name: String, |                 name: String, | ||||||
|                 action: String, |                 action: String, | ||||||
| @@ -347,7 +357,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 params: [String: Any]? |                 params: [String: Any]? | ||||||
|             ) { |             ) { | ||||||
|                 guard isEnabled else { return } |                 guard isEnabled else { return } | ||||||
|          |  | ||||||
|                 managers.forEach { manager in |                 managers.forEach { manager in | ||||||
|                     manager.logEvent( |                     manager.logEvent( | ||||||
|                         name: name, |                         name: name, | ||||||
| @@ -385,7 +395,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|          |          | ||||||
|         if extensionContent != expect { |         if extensionContent != expect { | ||||||
| @@ -415,11 +425,14 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         ] |         ] | ||||||
|          |          | ||||||
|         // When |         // When | ||||||
|         AnalyticsGenerator.targets = [TrackerType.matomo, TrackerType.firebase] |         let extensionContent = AnalyticsGenerator.getExtensionContent( | ||||||
|         let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], |             targets: [TrackerType.matomo, TrackerType.firebase], | ||||||
|                                                                  tags: ["ios", "iosonly"], |             sections: [sectionOne, sectionTwo, sectionThree], | ||||||
|                                                                  staticVar: false, |             tags: ["ios", "iosonly"], | ||||||
|                                                                  extensionName: "GenAnalytics") |             staticVar: false, | ||||||
|  |             extensionName: "GenAnalytics" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // Expect Analytics |         // Expect Analytics | ||||||
|         let expect = """ |         let expect = """ | ||||||
|         // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) |         // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion) | ||||||
| @@ -430,6 +443,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Protocol |         // MARK: - Protocol | ||||||
|  |  | ||||||
|         protocol AnalyticsManagerProtocol { |         protocol AnalyticsManagerProtocol { | ||||||
|  |  | ||||||
|             func logScreen(name: String, path: String) |             func logScreen(name: String, path: String) | ||||||
|             func logEvent( |             func logEvent( | ||||||
|                 name: String, |                 name: String, | ||||||
| @@ -442,7 +456,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Matomo |         // MARK: - Matomo | ||||||
|  |  | ||||||
|         class MatomoAnalyticsManager: AnalyticsManagerProtocol { |         class MatomoAnalyticsManager: AnalyticsManagerProtocol { | ||||||
|              |  | ||||||
|             // MARK: - Properties |             // MARK: - Properties | ||||||
|  |  | ||||||
|             private var tracker: MatomoTracker |             private var tracker: MatomoTracker | ||||||
| @@ -456,11 +470,11 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                     siteId: siteId, |                     siteId: siteId, | ||||||
|                     baseURL: URL(string: url)! |                     baseURL: URL(string: url)! | ||||||
|                 ) |                 ) | ||||||
|                  |  | ||||||
|                 #if DEBUG |                 #if DEBUG | ||||||
|                     tracker.dispatchInterval = 5 |                     tracker.dispatchInterval = 5 | ||||||
|                 #endif |                 #endif | ||||||
|                  |  | ||||||
|                 #if DEBUG |                 #if DEBUG | ||||||
|                     tracker.logger = DefaultLogger(minLevel: .verbose) |                     tracker.logger = DefaultLogger(minLevel: .verbose) | ||||||
|                 #endif |                 #endif | ||||||
| @@ -468,13 +482,13 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") |                 debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")") | ||||||
|                 debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") |                 debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)") | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             // MARK: - Methods |             // MARK: - Methods | ||||||
|  |  | ||||||
|             func logScreen(name: String, path: String) { |             func logScreen(name: String, path: String) { | ||||||
|                 guard !tracker.isOptedOut else { return } |                 guard !tracker.isOptedOut else { return } | ||||||
|                 guard let trackerUrl = tracker.contentBase?.absoluteString else { return } |                 guard let trackerUrl = tracker.contentBase?.absoluteString else { return } | ||||||
|          |  | ||||||
|                 let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS") |                 let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS") | ||||||
|                 tracker.track( |                 tracker.track( | ||||||
|                     view: [name], |                     view: [name], | ||||||
| @@ -489,7 +503,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 params: [String: Any]? |                 params: [String: Any]? | ||||||
|             ) { |             ) { | ||||||
|                 guard !tracker.isOptedOut else { return } |                 guard !tracker.isOptedOut else { return } | ||||||
|          |  | ||||||
|                 tracker.track( |                 tracker.track( | ||||||
|                     eventWithCategory: category, |                     eventWithCategory: category, | ||||||
|                     action: action, |                     action: action, | ||||||
| @@ -499,7 +513,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // MARK: - Firebase |         // MARK: - Firebase | ||||||
|  |  | ||||||
|         class FirebaseAnalyticsManager: AnalyticsManagerProtocol { |         class FirebaseAnalyticsManager: AnalyticsManagerProtocol { | ||||||
| @@ -524,7 +538,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                     "action": action as NSObject, |                     "action": action as NSObject, | ||||||
|                     "category": category as NSObject, |                     "category": category as NSObject, | ||||||
|                 ] |                 ] | ||||||
|                  |  | ||||||
|                 if let supplementaryParameters = params { |                 if let supplementaryParameters = params { | ||||||
|                     for (newKey, newValue) in supplementaryParameters { |                     for (newKey, newValue) in supplementaryParameters { | ||||||
|                         if parameters.contains(where: { (key: String, value: NSObject) in |                         if parameters.contains(where: { (key: String, value: NSObject) in | ||||||
| @@ -532,11 +546,11 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                         }) { |                         }) { | ||||||
|                             continue |                             continue | ||||||
|                         } |                         } | ||||||
|                          |  | ||||||
|                         parameters[newKey] = newValue as? NSObject |                         parameters[newKey] = newValue as? NSObject | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 Analytics.logEvent( |                 Analytics.logEvent( | ||||||
|                     name.replacingOccurrences(of: [" "], with: "_"), |                     name.replacingOccurrences(of: [" "], with: "_"), | ||||||
|                     parameters: parameters |                     parameters: parameters | ||||||
| @@ -547,8 +561,9 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|         // MARK: - Manager |         // MARK: - Manager | ||||||
|  |  | ||||||
|         class AnalyticsManager { |         class AnalyticsManager { | ||||||
|  |  | ||||||
|             static var shared = AnalyticsManager() |             static var shared = AnalyticsManager() | ||||||
|              |  | ||||||
|             // MARK: - Properties |             // MARK: - Properties | ||||||
|  |  | ||||||
|             var managers: [AnalyticsManagerProtocol] = [] |             var managers: [AnalyticsManagerProtocol] = [] | ||||||
| @@ -570,7 +585,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 ) |                 ) | ||||||
|                 managers.append(FirebaseAnalyticsManager()) |                 managers.append(FirebaseAnalyticsManager()) | ||||||
|             } |             } | ||||||
|                  |  | ||||||
|             private func logScreen(name: String, path: String) { |             private func logScreen(name: String, path: String) { | ||||||
|                 guard isEnabled else { return } |                 guard isEnabled else { return } | ||||||
|          |          | ||||||
| @@ -578,7 +593,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                     manager.logScreen(name: name, path: path) |                     manager.logScreen(name: name, path: path) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              |  | ||||||
|             private func logEvent( |             private func logEvent( | ||||||
|                 name: String, |                 name: String, | ||||||
|                 action: String, |                 action: String, | ||||||
| @@ -586,7 +601,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 params: [String: Any]? |                 params: [String: Any]? | ||||||
|             ) { |             ) { | ||||||
|                 guard isEnabled else { return } |                 guard isEnabled else { return } | ||||||
|          |  | ||||||
|                 managers.forEach { manager in |                 managers.forEach { manager in | ||||||
|                     manager.logEvent( |                     manager.logEvent( | ||||||
|                         name: name, |                         name: name, | ||||||
| @@ -624,7 +639,7 @@ final class AnalyticsGeneratorTests: XCTestCase { | |||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         """ |         """ | ||||||
|          |          | ||||||
|         if extensionContent != expect { |         if extensionContent != expect { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user