40 Commits

Author SHA1 Message Date
166026a766 feat(RES-43): Fix du warning magick convert (#16)
All checks were successful
gitea-openium/resgen.swift/pipeline/head This commit looks good
Reviewed-on: #16
2025-05-05 10:36:51 +02:00
ccda606af5 feat(RES-35): Force JSONEncoder output formatting with .sortedKeys (#15)
All checks were successful
gitea-openium/resgen.swift/pipeline/head This commit looks good
Reviewed-on: #15
2025-05-05 10:07:58 +02:00
756de4f1de feat(RES-34): Fix plist font filename (#14)
All checks were successful
gitea-openium/resgen.swift/pipeline/head This commit looks good
Reviewed-on: #14
2025-05-05 09:53:05 +02:00
8442c89944 feat(RES-33): Generation de l'architecture compatible Swift 6 (#13)
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #13
2025-04-30 10:22:34 +02:00
57cedd37bb Update rsvgConvertNotFound error to update brew install command line
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2025-04-08 15:27:10 +02:00
09556ba6e0 [DEVTOOLS-186] Exporter les images de resgen en svg
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #12
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-07-17 15:18:13 +02:00
dea57dc1e2 Maj du readme
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-07-11 10:29:32 +02:00
07575bd2bf DEVTOOLS-195 Ne pas générer les svg en template par défaut
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-07-11 10:08:47 +02:00
8686ae974c Suppression des anciens assets si svg
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-06-21 14:54:20 +02:00
be4c561ea8 DEVTOOLS-192 Resgen iOS vector
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-06-21 09:18:51 +02:00
2357a40fff DEVTOOLS-186 Exporter les images de resgen en svg
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-04-22 12:05:24 +02:00
d4afa9c9e9 Maj du Readme suite aux devs sur le fichier string catalog
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-22 09:48:21 +02:00
76ef0a2d59 Merge pull request 'DEVTOOLS-185 Remplacer le json en dur des images resgen' (#11) from DEVTOOLS-186/SVG_resgen into master
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #11
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-04-19 17:02:26 +02:00
129eb135f1 DEVTOOLS-185 Remplacer le json en dur des images resgen
Some checks are pending
gitea-openium/resgen.swift/pipeline/pr-master Build started...
2024-04-19 17:02:00 +02:00
4ad15fcded Merge pull request 'xcstrings' (#10) from xcstrings into master
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #10
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-04-19 16:59:03 +02:00
fb2ddb2227 DEVTOOLS-181 Gérer le tag noTranslation pour les xcstrings 2024-04-17 09:44:09 +02:00
27f86f5c4d Correction de l'option pour le fichier xcstrings 2024-04-15 16:51:18 +02:00
209ba49e3f Gestion des commentaires 2024-04-12 17:15:15 +02:00
ba07005b13 Fix equatable properties for arrays 2024-04-12 16:45:09 +02:00
6c3f3a8982 Correction du test testGenerateXcStringsRootObject 2024-04-12 16:25:10 +02:00
0d651b810f Première implémentation des xcstrings 2024-04-12 16:09:54 +02:00
1d7fc76340 Changed the condition for moe visibility 2024-04-11 15:49:07 +02:00
5d4e461933 Affichage du commentaire même si nil ou empty 2024-04-11 14:20:54 +02:00
55264d61ad Modification de l'affichage des commentaires
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-04-11 11:18:57 +02:00
d21ad9d1ea Update JenkinsFile Xcode version
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-11 10:19:10 +02:00
0bd6c3c2d4 Correction des commentaires des strings
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-11 10:16:28 +02:00
eed20367b9 fix: Edit for NSObject in Firebase
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
PixeeBox/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 11:24:21 +01:00
43b5111d79 Actualiser Sources/ResgenSwift/Analytics/Generator/AnalyticsGenerator.swift
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 10:39:12 +01:00
2983093a9c Add Swiftlint 2023-12-13 10:39:12 +01:00
b4bbaa3bfd Fix Image 2023-12-13 10:39:12 +01:00
498c8fa4ae Fix Font 2023-12-13 10:39:12 +01:00
2957da6233 Fix Color 2023-12-13 10:39:12 +01:00
d79af06c38 docs: Add Analytics section in README
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 10:27:35 +01:00
d8937f2de6 docs: Add Analytics section in README 2023-12-13 10:23:31 +01:00
9b27f24197 docs: Add Analytics section in README 2023-12-13 10:20:53 +01:00
1d58fd5510 Edit sample file
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2023-12-12 16:58:25 +01:00
f6c49bf626 Add parse error
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 11:19:19 +01:00
f1b62d83c4 Add missing print error
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:29:19 +01:00
ee5055efa5 Retours sur la structure du code
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:24:42 +01:00
6f8e3b6664 Add error handling
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:09:24 +01:00
143 changed files with 4807 additions and 2198 deletions

View File

@ -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

6
Jenkinsfile vendored
View File

@ -1,8 +1,10 @@
library "openiumpipeline" library "openiumpipeline"
env.DEVELOPER_DIR="/Applications/Xcode-15.0.1.app/Contents/Developer" env.DEVELOPER_DIR="/Applications/Xcode-16.3.0.app/Contents/Developer"
//env.SIMULATOR_DEVICE_TYPES="iPad--7th-generation-" // env.SIMULATOR_DEVICE_TYPES="iPhone-14-Pro"
// env.SLACK_CHANNEL = "prj-skdevkit"
env.IS_PACKAGE_SWIFT=1 env.IS_PACKAGE_SWIFT=1
env.TARGETS_MACOS=1 env.TARGETS_MACOS=1
env.PACKAGE_NAME="ResgenSwift" // xcodebuild -list => Only 1 scheme
iOSpipeline() iOSpipeline()

View File

@ -1,75 +1,21 @@
{ {
"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" : "db51c407d3be4a051484a141bf0bff36c43d3b1e",
"version" : "1.8.0"
}
},
{
"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",
"location" : "https://github.com/apple/swift-argument-parser", "location" : "https://github.com/apple/swift-argument-parser",
"state" : { "state" : {
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", "revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.2.3" "version" : "1.5.0"
} }
}, },
{ {
"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"
} }
}, },
{ {

View File

@ -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), .iOS(.v15)],
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.5.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

View File

@ -81,7 +81,7 @@ swift run -c release ResgenSwift strings twine $FORCE_FLAG "./Twine/strings.txt"
2. Input translations file (must be Twine formatted) 2. Input translations file (must be Twine formatted)
3. `--langs`: langs to generate (string with space between each lang) 3. `--langs`: langs to generate (string with space between each lang)
4. `--default-lang`: default lang that will be in `Base.lproj`. It must be in `langs` as well 4. `--default-lang`: default lang that will be in `Base.lproj`. It must be in `langs` as well
4. `--extension-output-path`: path where to generate generated extension 5. `--extension-output-path`: path where to generate generated extension
### Stringium (recommended) ### Stringium (recommended)
@ -93,6 +93,7 @@ swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/string
--extension-output-path "./Strings/Generated" \ --extension-output-path "./Strings/Generated" \
--extension-name "AppString" \ --extension-name "AppString" \
--extension-suffix "GreatApp" \ --extension-suffix "GreatApp" \
--xcStrings true
--static-members true --static-members true
``` ```
@ -105,6 +106,7 @@ swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/string
4. `--extension-output-path`: path where to generate generated extension 4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of class to add the extension 5. `--extension-name` *(optional)* : name of class to add the extension
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppString+GreatApp.swift`) 6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppString+GreatApp.swift`)
6. `--xcStrings`*(optional)* : generate string catalog
7. `--static-members` *(optional)*: generate static properties or not 7. `--static-members` *(optional)*: generate static properties or not
@ -133,9 +135,74 @@ swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/tags.txt" \
> ⚠️ If extension name is not set or is `Tags`, it will generate the following typaloas `typealias Tags = String`. > ⚠️ If extension name is not set or is `Tags`, it will generate the following typaloas `typealias Tags = String`.
## Analytics
Analytics will generate all you need to analyze UX with Matomo or Firebase Analytics. Input files are formatted in YAML. This command will generate a manager for each target and an AnalyticsManager. This is this one you will need to use. And it will generate a method for all tags you have declared in the YAML file. Next, you will need to use the `configure()` method of AnalyticsManager and if you want to use matomo to set up the `siteId` and the `url` of the site.
```sh
swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/tags.txt" \
--target "matomo firebase" \
--extension-output-path "./Analytics/Generated" \
--extension-name "AppAnalytics" \
--extension-suffix "GreatApp" \
--static-members true
```
**Parameters**
1. `-f`: force generation
2. Input tags file (must be YAML formatted)
3. `--target`: target with you will log UX
4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of class to add the extension
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppAnalytics+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not
> ⚠️ If extension name is not set or is `Analytics`, it will generate the following typaloas `typealias Analytics = String`.
### YAML
```
- id: s1_def_one
name: s1 def one _TITLE_
path: s1_def_one/_TITLE_
action: Tap
category: User
tags: ios,droid
comments:
parameters:
- name: title
type: String
replaceIn: name,path
```
1. `id`: name of the method (method name will be composed of `log` + `Event|Screen` + `id`)
2. `name`: name of the tag
3. `path` *(optional with firebase)* : needed for matomo but not with firebase (log screen)
4. `action` *(optional with firebase)* : needed for matomo but not with firebase (log event)
5. `category` *(optional with firebase)* : needed for matomo but not with firebase (log event)
6. `tags`: which platform target
7. `comments` *(optional)*
8. `parameters` *(optional)*
**Parameters**
You can use parameters in generate methods.
1. `name`: name of the parameter
2. `type`: type of the parameter (Int, String, Bool, Double)
3. `replaceIn` *(optional)*
**Replace in**
This is section is equivalent of `%s | %d | %f | %@`. You can put the content of the parameter in *name*, *path*, *action*, *category*.
You need to put `_` + `NAME OF THE PARAMETER` + `_` in the target and which target you want in the value of `replaceIn`. (name need to be in uppercase)
## Images ## Images
Images generator will generate images assets along with extensions to access those images easily. Images generator will generate images assets along with extensions to access those images easily.
```sh ```sh
swift run -c release ResgenSwift images $FORCE_FLAG "./Images/images.txt" \ swift run -c release ResgenSwift images $FORCE_FLAG "./Images/images.txt" \
@ -158,6 +225,7 @@ swift run -c release ResgenSwift images $FORCE_FLAG "./Images/images.txt" \
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppImage+GreatApp.swift`) 6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppImage+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not 7. `--static-members` *(optional)*: generate static properties or not
> ⚠️ Svg images will be copied in the assets and rendered as "Original", however if those images are not rendered correctly you can force the png generation by adding the key word "png" like this: id arrow_back 15 ? png
## All at once ## All at once

View File

@ -1,4 +1,4 @@
// Generated by ResgenSwift.Color 1.2 // Generated by ResgenSwift.Color 2.1.0
import SwiftUI import SwiftUI

View File

@ -0,0 +1,21 @@
// Generated by ResgenSwift.Color 2.1.0
import UIKit
extension UIhkjhkColorYolo {
/// Color red is #FF0000 (light) or #FF0000 (dark)"
@objc var red: UIColor {
UIColor(named: "red")!
}
/// Color green_alpha_50 is #A000FF00 (light) or #A000FF00 (dark)"
@objc var green_alpha_50: UIColor {
UIColor(named: "green_alpha_50")!
}
/// Color blue_light_dark is #0000FF (light) or #0000AA (dark)"
@objc var blue_light_dark: UIColor {
UIColor(named: "blue_light_dark")!
}
}

View File

@ -1,4 +1,4 @@
// Generated by ResgenSwift.Fonts 1.2 // Generated by ResgenSwift.Fonts 2.1.0
import SwiftUI import SwiftUI

View File

@ -1,4 +1,4 @@
// Generated by ResgenSwift.Fonts 1.2 // Generated by ResgenSwift.Fonts 2.1.0
import UIKit import UIKit

View File

@ -6,16 +6,16 @@
<array/> <array/>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>Lato-Italic</string> <string>Lato-Italic.ttf</string>
<string>Lato-LightItalic</string> <string>Lato-LightItalic.ttf</string>
<string>Lato-Hairline</string> <string>Lato-Thin.ttf</string>
<string>Lato-Bold</string> <string>Lato-Bold.ttf</string>
<string>Lato-Black</string> <string>Lato-Black.ttf</string>
<string>Lato-Regular</string> <string>Lato-Regular.ttf</string>
<string>Lato-BlackItalic</string> <string>Lato-BlackItalic.ttf</string>
<string>Lato-BoldItalic</string> <string>Lato-BoldItalic.ttf</string>
<string>Lato-Light</string> <string>Lato-Light.ttf</string>
<string>Lato-HairlineItalic</string> <string>Lato-ThinItalic.ttf</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -6,16 +6,16 @@
<array/> <array/>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>Lato-Italic</string> <string>Lato-Italic.ttf</string>
<string>Lato-LightItalic</string> <string>Lato-LightItalic.ttf</string>
<string>Lato-Hairline</string> <string>Lato-Thin.ttf</string>
<string>Lato-Bold</string> <string>Lato-Bold.ttf</string>
<string>Lato-Black</string> <string>Lato-Black.ttf</string>
<string>Lato-Regular</string> <string>Lato-Regular.ttf</string>
<string>Lato-BlackItalic</string> <string>Lato-BlackItalic.ttf</string>
<string>Lato-BoldItalic</string> <string>Lato-BoldItalic.ttf</string>
<string>Lato-Light</string> <string>Lato-Light.ttf</string>
<string>Lato-HairlineItalic</string> <string>Lato-ThinItalic.ttf</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -1,4 +1,4 @@
// Generated by ResgenSwift.Images 1.2 // Generated by ResgenSwift.Images 2.1.0
// Images from sampleImages // Images from sampleImages
import SwiftUI import SwiftUI

View File

@ -0,0 +1,31 @@
// Generated by ResgenSwift.Images 2.1.0
// Images from sampleImages
import UIKit
extension UIImageYolo {
var article_notification_pull_detail: UIImage {
UIImage(named: "article_notification_pull_detail")!
}
var article_notification_pull: UIImage {
UIImage(named: "article_notification_pull")!
}
var new_article: UIImage {
UIImage(named: "new_article")!
}
var welcome_background: UIImage {
UIImage(named: "welcome_background")!
}
var article_trash: UIImage {
UIImage(named: "article_trash")!
}
var ic_close_article: UIImage {
UIImage(named: "ic_close_article")!
}
}

View File

@ -1,23 +1,16 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "article_notification_pull.svg",
"scale" : "1x", "idiom" : "universal"
"filename" : "article_notification_pull.png"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "article_notification_pull@2x.png"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "article_notification_pull@3x.png"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "ResgenSwift-Imagium",
"author" : "ResgenSwift-Imagium" "version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="111px" height="69px" viewBox="0 0 111 69" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>article_notification_pull</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="article_notification_pull">
<path d="M81.130223,39.5772 C81.130223,39.5772 99.360223,32.4231 102.116223,33.2806 C104.871223,34.1381 111.204223,51.339 110.013223,51.8526 C108.758223,52.4481 102.001223,47.0974 102.001223,47.0974 C102.001223,47.0974 100.834223,50.2797 98.387023,49.5337 C95.939623,48.7877 92.897623,44.9219 92.897623,44.9219 C92.897623,44.9219 92.488923,47.0487 89.986323,46.4577 C87.483723,45.8667 83.726623,41.567 83.726623,41.567 L81.130223,39.5772 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M99.259223,10.5629 C97.894023,10.5629 96.788623,9.4412 96.788623,8.0557 C96.788623,7.4619 96.333423,7 95.748223,7 C95.163023,7 94.707823,7.4619 94.707823,8.0557 C94.707823,9.4412 93.602423,10.5629 92.237023,10.5629 C91.651723,10.5629 91.196623,11.0247 91.196623,11.6185 C91.196623,12.2124 91.651723,12.6742 92.237023,12.6742 C93.602423,12.6742 94.707823,13.7959 94.707823,15.1814 C94.707823,15.7752 95.163023,16.2371 95.748223,16.2371 C96.333423,16.2371 96.788623,15.7752 96.788623,15.1814 C96.788623,13.7959 97.894023,12.6742 99.259223,12.6742 C99.844223,12.6742 100.300223,12.2124 100.300223,11.6185 C100.365223,11.0907 99.844223,10.5629 99.259223,10.5629 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M81.193523,11.2243 C80.403523,10.8878 79.962423,9.9515 80.333423,9.0805 C80.502023,8.6847 80.287223,8.3125 79.971223,8.1779 C79.576223,8.0096 79.204323,8.2254 79.069423,8.5421 C78.732123,9.3338 77.796523,9.7772 76.927523,9.407 C76.532523,9.2387 76.160623,9.4544 76.025723,9.7711 C75.857023,10.167 76.071823,10.5391 76.387823,10.6738 C77.177923,11.0103 77.619023,11.9466 77.248023,12.8175 C77.079423,13.2134 77.294223,13.5855 77.610223,13.7201 C78.005223,13.8884 78.377123,13.6727 78.512023,13.356 C78.849323,12.5642 79.784923,12.1209 80.653923,12.4911 C81.048923,12.6594 81.420823,12.4436 81.555723,12.1269 C81.769623,11.8439 81.588623,11.3926 81.193523,11.2243 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<line x1="12.845223" y1="48.192" x2="16.110123" y2="42" id="Path" stroke-opacity="0.8" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"></line>
<ellipse id="Oval" fill-opacity="0.1" fill="#000000" fill-rule="nonzero" cx="50.345223" cy="67.5" rx="17.5" ry="1.5"></ellipse>
<line x1="14.517923" y1="53.5959" x2="17.782723" y2="47.404" id="Path" stroke-opacity="0.8" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"></line>
<line x1="20.806323" y1="52.3897" x2="23.604823" y2="47.0823" id="Path" stroke-opacity="0.8" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"></line>
<path d="M79.876623,1.78144 C79.226423,1.78144 78.641223,1.2536 78.641223,0.52783 C78.641223,0.19794 78.381123,0 78.121023,0 C77.795923,0 77.600823,0.26392 77.600823,0.52783 C77.600823,1.18763 77.080623,1.78144 76.365423,1.78144 C76.040323,1.78144 75.845223,2.04535 75.845223,2.30927 C75.845223,2.63917 76.105323,2.8371 76.365423,2.8371 C77.015623,2.8371 77.600823,3.3649 77.600823,4.0907 C77.600823,4.4206 77.860923,4.6185 78.121023,4.6185 C78.446123,4.6185 78.641223,4.3546 78.641223,4.0907 C78.641223,3.4309 79.161423,2.8371 79.876623,2.8371 C80.201823,2.8371 80.396823,2.57319 80.396823,2.30927 C80.461923,2.04535 80.201823,1.78144 79.876623,1.78144 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M31.021323,19.8983 C31.021323,19.8983 19.126423,4.2537 16.379623,3.4694 C13.568923,2.76686 -0.694914027,14.1215 0.0264229726,15.2225 C0.756502973,16.3967 9.32942297,15.3706 9.32942297,15.3706 C9.32942297,15.3706 8.62499297,18.7201 11.136323,19.3843 C13.647723,20.0484 18.238723,18.3857 18.238723,18.3857 C18.238723,18.3857 17.521923,20.401 19.905523,21.2288 C22.289123,22.0567 27.760723,20.3628 27.760723,20.3628 L31.021323,19.8983 Z" id="Path" fill="#E0EDF3" fill-rule="nonzero"></path>
<path d="M89.710223,24.4503 L77.707723,58.391 C77.385623,59.3943 76.267023,59.8992 75.270023,59.5733 L21.900823,40.3589 C20.903823,40.033 20.400423,38.9059 20.722523,37.9026 L32.788923,3.88 C32.835323,3.6518 32.954323,3.4149 33.154823,3.2425 C33.674723,2.66083 34.456323,2.41884 35.299323,2.689 L88.604623,21.9853 C89.374923,22.2641 89.834523,23.0254 89.849423,23.7658 C89.875623,23.9852 89.829323,24.2134 89.710223,24.4503 Z" id="Path" fill="#FFD100" fill-rule="nonzero"></path>
<path d="M23.390523,38.0285 L52.772523,31.6916 C54.280623,31.3626 55.821223,31.9203 56.778023,33.1416 L75.925523,57.0459 L57.901923,31.4487 C56.910123,29.9348 55.061323,29.2655 53.361523,29.84 L23.390523,38.0285 Z" id="Path" fill="#DA9135" fill-rule="nonzero"></path>
<path d="M89.803023,23.9939 L54.122423,34.35 L33.108423,3.4707 C33.610823,2.74268 34.529023,2.41014 35.299323,2.68899 L88.604623,21.9853 C89.374923,22.2641 89.770623,23.1073 89.803023,23.9939 Z" id="Path" fill="#FFDF4F" fill-rule="nonzero"></path>
<path d="M34.370723,5.3975 L52.679723,32.148 C53.572623,33.4511 55.113223,34.0088 56.685323,33.598 L87.402923,24.8749 L56.375623,35.9356 C54.611823,36.592 52.771823,35.9959 51.835123,34.327 L34.370723,5.3975 Z" id="Path" fill="#DA9135" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,23 +1,16 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "article_notification_pull_detail.svg",
"scale" : "1x", "idiom" : "universal"
"filename" : "article_notification_pull_detail.png"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "article_notification_pull_detail@2x.png"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "article_notification_pull_detail@3x.png"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "ResgenSwift-Imagium",
"author" : "ResgenSwift-Imagium" "version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
} }
} }

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="111px" height="69px" viewBox="0 0 111 69" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>article_notification_pull_detail</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="article_notification_pull_detail" transform="translate(0.000000, 0.000000)">
<path d="M81.1301131,39.5772 C81.1301131,39.5772 99.3602131,32.4231 102.116213,33.2806 C104.871213,34.1381 111.204213,51.339 110.013213,51.8526 C108.758213,52.4481 102.001213,47.0974 102.001213,47.0974 C102.001213,47.0974 100.834213,50.2797 98.3869131,49.5337 C95.9395131,48.7877 92.8975131,44.9219 92.8975131,44.9219 C92.8975131,44.9219 92.4888131,47.0487 89.9862131,46.4577 C87.4836131,45.8667 83.7265131,41.567 83.7265131,41.567 L81.1301131,39.5772 Z" id="Path" fill="#E0EDF3" fill-rule="nonzero"></path>
<path d="M99.2592131,10.5629 C97.8940131,10.5629 96.7886131,9.4412 96.7886131,8.0557 C96.7886131,7.4619 96.3334131,7 95.7482131,7 C95.1630131,7 94.7078131,7.4619 94.7078131,8.0557 C94.7078131,9.4412 93.6024131,10.5629 92.2370131,10.5629 C91.6517131,10.5629 91.1966131,11.0247 91.1966131,11.6185 C91.1966131,12.2124 91.6517131,12.6742 92.2370131,12.6742 C93.6024131,12.6742 94.7078131,13.7959 94.7078131,15.1814 C94.7078131,15.7752 95.1630131,16.2371 95.7482131,16.2371 C96.3334131,16.2371 96.7886131,15.7752 96.7886131,15.1814 C96.7886131,13.7959 97.8940131,12.6742 99.2592131,12.6742 C99.8442131,12.6742 100.300213,12.2124 100.300213,11.6185 C100.365213,11.0907 99.8442131,10.5629 99.2592131,10.5629 Z" id="Path" fill="#E0EDF3" fill-rule="nonzero"></path>
<path d="M81.1934131,11.2243 C80.4034131,10.8878 79.9623131,9.9515 80.3333131,9.0805 C80.5019131,8.6847 80.2871131,8.3125 79.9711131,8.1779 C79.5761131,8.0096 79.2042131,8.2254 79.0693131,8.5421 C78.7320131,9.3338 77.7964131,9.7772 76.9274131,9.407 C76.5324131,9.2387 76.1605131,9.4544 76.0256131,9.7711 C75.8569131,10.167 76.0717131,10.5391 76.3877131,10.6738 C77.1778131,11.0103 77.6189131,11.9466 77.2479131,12.8175 C77.0793131,13.2134 77.2941131,13.5855 77.6101131,13.7201 C78.0051131,13.8884 78.3770131,13.6727 78.5119131,13.356 C78.8492131,12.5642 79.7848131,12.1209 80.6538131,12.4911 C81.0488131,12.6594 81.4207131,12.4436 81.5556131,12.1269 C81.7695131,11.8439 81.5885131,11.3926 81.1934131,11.2243 Z" id="Path" fill="#E0EDF3" fill-rule="nonzero"></path>
<line x1="12.8452131" y1="48.192" x2="16.1101131" y2="42" id="Path" stroke-opacity="0.8" stroke="#E0EDF3" stroke-width="2" stroke-linecap="round"></line>
<path d="M50.3452131,69 C60.0102131,69 67.8452131,68.3284 67.8452131,67.5 C67.8452131,66.6716 60.0102131,66 50.3452131,66 C40.6802131,66 32.8452131,66.6716 32.8452131,67.5 C32.8452131,68.3284 40.6802131,69 50.3452131,69 Z" id="Path" fill-opacity="0.1" fill="#000000" fill-rule="nonzero"></path>
<line x1="14.5178131" y1="53.5959" x2="17.7826131" y2="47.404" id="Path" stroke-opacity="0.8" stroke="#E0EDF3" stroke-width="2" stroke-linecap="round"></line>
<line x1="20.8064131" y1="52.3897" x2="23.6049131" y2="47.0823" id="Path" stroke-opacity="0.8" stroke="#E0EDF3" stroke-width="2" stroke-linecap="round"></line>
<path d="M79.8766131,1.78144 C79.2264131,1.78144 78.6412131,1.2536 78.6412131,0.52783 C78.6412131,0.19794 78.3811131,0 78.1210131,0 C77.7959131,0 77.6008131,0.26392 77.6008131,0.52783 C77.6008131,1.18763 77.0806131,1.78144 76.3654131,1.78144 C76.0403131,1.78144 75.8452131,2.04535 75.8452131,2.30927 C75.8452131,2.63917 76.1053131,2.8371 76.3654131,2.8371 C77.0156131,2.8371 77.6008131,3.3649 77.6008131,4.0907 C77.6008131,4.4206 77.8609131,4.6185 78.1210131,4.6185 C78.4461131,4.6185 78.6412131,4.3546 78.6412131,4.0907 C78.6412131,3.4309 79.1614131,2.8371 79.8766131,2.8371 C80.2018131,2.8371 80.3968131,2.57319 80.3968131,2.30927 C80.4619131,2.04535 80.2018131,1.78144 79.8766131,1.78144 Z" id="Path" fill="#E0EDF3" fill-rule="nonzero"></path>
<path d="M31.0213131,19.8983 C31.0213131,19.8983 19.1264131,4.2537 16.3796131,3.4694 C13.5689131,2.76686 -0.694915869,14.1215 0.0264231309,15.2225 C0.756503131,16.3967 9.32941313,15.3706 9.32941313,15.3706 C9.32941313,15.3706 8.62499313,18.7201 11.1363131,19.3843 C13.6477131,20.0484 18.2387131,18.3857 18.2387131,18.3857 C18.2387131,18.3857 17.5219131,20.401 19.9055131,21.2288 C22.2891131,22.0567 27.7607131,20.3628 27.7607131,20.3628 L31.0213131,19.8983 Z" id="Path" fill="#E0EDF3" fill-rule="nonzero"></path>
<path d="M89.7103131,24.4503 L77.7078131,58.391 C77.3857131,59.3943 76.2671131,59.8992 75.2701131,59.5733 L21.9009131,40.3589 C20.9039131,40.033 20.4005131,38.9059 20.7226131,37.9026 L32.7890131,3.88 C32.8354131,3.6518 32.9544131,3.4149 33.1549131,3.2425 C33.6748131,2.66083 34.4564131,2.41884 35.2994131,2.689 L88.6047131,21.9853 C89.3750131,22.2641 89.8346131,23.0254 89.8495131,23.7658 C89.8757131,23.9852 89.8294131,24.2134 89.7103131,24.4503 Z" id="Path" fill="#FFD100" fill-rule="nonzero"></path>
<path d="M23.3906131,38.0285 L52.7726131,31.6916 C54.2807131,31.3626 55.8213131,31.9203 56.7781131,33.1416 L75.9256131,57.0459 L57.9020131,31.4487 C56.9102131,29.9348 55.0614131,29.2655 53.3616131,29.84 L23.3906131,38.0285 Z" id="Path" fill="#DA9135" fill-rule="nonzero"></path>
<path d="M89.8030131,23.9939 L54.1224131,34.35 L33.1084131,3.4707 C33.6108131,2.74268 34.5290131,2.41014 35.2993131,2.68899 L88.6046131,21.9853 C89.3749131,22.2641 89.7706131,23.1073 89.8030131,23.9939 Z" id="Path" fill="#FFDF4F" fill-rule="nonzero"></path>
<path d="M34.3706131,5.3975 L52.6796131,32.148 C53.5725131,33.4511 55.1131131,34.0088 56.6852131,33.598 L87.4028131,24.8749 L56.3755131,35.9356 C54.6117131,36.592 52.7717131,35.9959 51.8350131,34.327 L34.3706131,5.3975 Z" id="Path" fill="#DA9135" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -1,23 +1,16 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "article_trash.svg",
"scale" : "1x", "idiom" : "universal"
"filename" : "article_trash.png"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "article_trash@2x.png"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "article_trash@3x.png"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "ResgenSwift-Imagium",
"author" : "ResgenSwift-Imagium" "version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 977 B

View File

@ -0,0 +1,4 @@
<svg width="37" height="41" viewBox="0 0 37 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4.87183L5.45856 32.1227C5.68952 33.5346 6.30709 34.8549 7.24275 35.9371C8.17841 37.0194 9.3956 37.8213 10.7593 38.2539L11.3943 38.4553C15.9682 39.9073 20.8795 39.9073 25.4534 38.4553L26.0884 38.2539C27.4518 37.8215 28.6688 37.0199 29.6044 35.938C30.5401 34.8561 31.1578 33.5362 31.3891 32.1246L35.8476 4.87183" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.4238 8.74392C28.0467 8.74392 35.8476 7.01039 35.8476 4.87196C35.8476 2.73354 28.0467 1 18.4238 1C8.80091 1 1 2.73354 1 4.87196C1 7.01039 8.80091 8.74392 18.4238 8.74392Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,23 +1,16 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "ic_close_article.svg",
"scale" : "1x", "idiom" : "universal"
"filename" : "ic_close_article.png"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "ic_close_article@2x.png"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "ic_close_article@3x.png"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "ResgenSwift-Imagium",
"author" : "ResgenSwift-Imagium" "version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

View File

@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.41 1.41L10 0L5.705 4.295L1.41 0L0 1.41L4.295 5.705L0 10L1.41 11.41L5.705 7.115L10 11.41L11.41 10L7.115 5.705L11.41 1.41Z"
fill="#EFF2F4" />
</svg>

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

View File

@ -1,23 +1,16 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "new_article.svg",
"scale" : "1x", "idiom" : "universal"
"filename" : "new_article.png"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "new_article@2x.png"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "new_article@3x.png"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "ResgenSwift-Imagium",
"author" : "ResgenSwift-Imagium" "version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,54 @@
<svg width="102" height="69" viewBox="0 0 102 69" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100.089 35.765H72.7522V66.626H100.089V35.765Z" fill="#DEF1F8"/>
<path d="M89.1215 46.2884H56.9285V66.6806H89.1215V46.2884Z" fill="#FAFAFA"/>
<path d="M100.089 46.2883H89.1216V66.6805H100.089V46.2883Z" fill="#DEF1F8"/>
<path d="M84.9199 49.5053H82.8464V53.1039H84.9199V49.5053Z" fill="#DEF1F8"/>
<path d="M80.8276 49.5053H78.7542V53.1039H80.8276V49.5053Z" fill="#DEF1F8"/>
<path d="M62.8759 60.3557H60.8025V63.9543H62.8759V60.3557Z" fill="#DEF1F8"/>
<path d="M97.7973 37.7279H95.7239V41.3266H97.7973V37.7279Z" fill="#FAFAFA"/>
<path d="M76.9538 37.7279H74.8804V41.3266H76.9538V37.7279Z" fill="#FAFAFA"/>
<path d="M36.4457 27.1985H101.509C101.728 25.2457 100.03 23.1303 98.1679 22.4794C96.251 21.8285 94.1699 21.72 92.4721 20.6351C90.3362 19.2791 89.1861 16.5127 86.7216 15.8076C84.3666 15.1566 82.0117 16.7839 79.5472 16.8924C74.5086 17.1094 70.6202 10.8715 65.6364 11.7394C62.2956 12.336 60.3788 15.8076 57.3666 17.3263C54.8473 18.6282 51.8352 18.3569 48.9873 18.7366C44.1678 19.4418 39.2388 23.3472 36.4457 27.1985Z" fill="#DEF1F8"/>
<path d="M0.455092 25.2296L32.2995 0.974118C33.9893 -0.324706 36.3288 -0.324706 38.0185 0.974118L69.8305 25.2296C70.1229 25.4569 70.2854 25.8141 70.2854 26.1713V67.7661C70.2854 68.4155 69.7655 68.9351 69.1156 68.9351H1.20246C0.552574 68.9351 0.0326646 68.4155 0.0326646 67.7661V26.1713C0.0001703 25.8141 0.162643 25.4569 0.455092 25.2296Z" fill="url(#paint0_linear)"/>
<path d="M53.973 13.183H16.3121L10.5281 17.5666V68.935H59.7245V17.5666L53.973 13.183Z" fill="#FDB918"/>
<path d="M55.9878 6.55914H14.2651C12.8353 6.55914 11.6655 7.72808 11.6655 9.15679V64.7789C11.6655 66.2076 12.8353 67.3765 14.2651 67.3765H55.9878C57.4176 67.3765 58.5874 66.2076 58.5874 64.7789V9.15679C58.5874 7.72808 57.4176 6.55914 55.9878 6.55914Z" fill="#FAFAFA"/>
<path d="M35.1265 47.2773L68.4657 67.8312C69.2455 68.3182 70.2528 67.7662 70.2528 66.8246V27.6975C70.2528 26.7883 69.2455 26.2039 68.4657 26.6909L35.1265 47.2773Z" fill="url(#paint1_linear)"/>
<path d="M0 27.6974V66.8245C0 67.7336 1.00732 68.3181 1.78719 67.831L35.1264 47.2772L1.78719 26.6908C1.00732 26.2038 0 26.7882 0 27.6974Z" fill="url(#paint2_linear)"/>
<path d="M40.3255 44.0627L38.1808 42.4392C36.3611 41.0429 33.8591 41.0429 32.0394 42.4392L29.8948 44.0627L35.0939 47.2773L40.3255 44.0627Z" fill="#E6E6E6"/>
<path d="M0 67.1491L35.1264 47.2771L29.9273 44.0626L0 67.1491Z" fill="#FDB918"/>
<path d="M70.2528 67.1491L35.1265 47.2771L40.3256 44.0626L70.2528 67.1491Z" fill="#FDB918"/>
<path d="M29.1147 17.3064C29.1147 17.3064 28.8872 19.969 35.1262 21.4627L33.7939 21.3977C33.7939 21.3977 29.6996 20.9756 28.7573 19.2547C27.8149 17.5337 29.1147 17.3064 29.1147 17.3064Z" fill="#FDB918"/>
<path d="M30.1545 15.9104C33.079 13.053 34.9312 21.4304 34.9637 21.4629C35.0287 21.4629 35.0937 21.4629 35.1587 21.4629C35.1587 21.4629 33.014 9.90333 29.4396 14.709C25.6053 19.7744 30.0245 21.3654 35.1262 21.4629C30.7719 21.333 26.8076 19.1574 30.1545 15.9104Z" fill="#FFE368"/>
<path d="M40.9753 17.3064C40.9753 17.3064 41.2028 19.969 34.9639 21.4627L36.2961 21.3977C36.2961 21.3977 40.3904 20.9756 41.3328 19.2547C42.2426 17.5337 40.9753 17.3064 40.9753 17.3064Z" fill="#FDB918"/>
<path d="M39.9353 15.9104C37.0108 13.053 35.1586 21.4304 35.1261 21.4629C35.0611 21.4629 34.9961 21.4629 34.9312 21.4629C34.9312 21.4629 37.0758 9.90333 40.6502 14.709C44.452 19.7744 40.0328 21.3654 34.9312 21.4629C39.3179 21.333 43.2497 19.1574 39.9353 15.9104Z" fill="#FFE368"/>
<path d="M27.3926 36.237V22.0798C27.3926 21.7226 27.685 21.4304 28.0425 21.4304H42.21C42.5674 21.4304 42.8599 21.7226 42.8599 22.0798V36.237C42.8599 36.5941 42.5674 36.8864 42.21 36.8864H28.0425C27.685 36.8864 27.3926 36.5941 27.3926 36.237Z" fill="#063881"/>
<path d="M26.3528 24.645V22.0798C26.3528 21.7226 26.6452 21.4304 27.0027 21.4304H43.2499C43.6073 21.4304 43.8997 21.7226 43.8997 22.0798V24.645C43.8997 25.0021 43.6073 25.2944 43.2499 25.2944H27.0027C26.6452 25.2944 26.3528 25.0021 26.3528 24.645Z" fill="#0B559E"/>
<path d="M37.5959 21.4304H32.6567V25.2944H37.5959V21.4304Z" fill="#FFE368"/>
<path d="M37.3035 25.2943H32.9492V36.8863H37.3035V25.2943Z" fill="#FDB918"/>
<path d="M1.5437 69H68.8777C70.1371 69 70.6539 67.4136 69.6204 66.6839L38.0042 44.6961C36.3248 43.427 33.9673 43.427 32.2557 44.6961L0.736338 66.6839C-0.232496 67.4136 0.284216 69 1.5437 69Z" fill="url(#paint3_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="-0.00678347" y1="34.4878" x2="70.2659" y2="34.4878" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFE368"/>
<stop offset="0.3174" stop-color="#FFE265"/>
<stop offset="0.5172" stop-color="#FFDD5D"/>
<stop offset="0.6845" stop-color="#FED54E"/>
<stop offset="0.8339" stop-color="#FECB39"/>
<stop offset="0.9701" stop-color="#FDBD1F"/>
<stop offset="1" stop-color="#FDB918"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="9.77743" y1="11.1302" x2="102.999" y2="68.0143" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFE368"/>
<stop offset="0.3387" stop-color="#FED750"/>
<stop offset="1" stop-color="#FDB918"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="-8.63037" y1="41.2967" x2="84.5914" y2="98.1808" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFE368"/>
<stop offset="0.3387" stop-color="#FED750"/>
<stop offset="1" stop-color="#FDB918"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="-10.5945" y1="38.7503" x2="87.7585" y2="99.7918" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFE368"/>
<stop offset="0.3387" stop-color="#FED750"/>
<stop offset="1" stop-color="#FDB918"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,23 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "welcome_background.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x", "scale" : "1x"
"filename" : "welcome_background.png"
}, },
{ {
"filename" : "welcome_background@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x", "scale" : "2x"
"filename" : "welcome_background@2x.png"
}, },
{ {
"filename" : "welcome_background@3x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x", "scale" : "3x"
"filename" : "welcome_background@3x.png"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "ResgenSwift-Imagium",
"author" : "ResgenSwift-Imagium" "version" : 1
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -1,4 +1,4 @@
// Generated by ResgenSwift.Strings.Stringium 1.2 // Generated by ResgenSwift.Strings.Stringium 2.1.0
import UIKit import UIKit
@ -30,6 +30,9 @@ extension String {
/// Translation in en : /// Translation in en :
/// en /// en
///
/// Comment :
/// No comment
var param_lang: String { var param_lang: String {
NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "") NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
} }
@ -38,24 +41,35 @@ extension String {
/// Translation in en : /// Translation in en :
/// Back /// Back
///
/// Comment :
/// No comment
var generic_back: String { var generic_back: String {
NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "") NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
} }
/// Translation in en : /// Translation in en :
/// Loading data... /// Loading data...
///
/// Comment :
/// No comment
var generic_loading_data: String { var generic_loading_data: String {
NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "") NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
} }
/// Translation in en : /// Translation in en :
/// Welcome \"%@\" ! /// Welcome \"%@\" !
///
/// Comment :
/// No comment
var generic_welcome_firstname_format: String { var generic_welcome_firstname_format: String {
NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "") NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "")
} }
/// Translation in en : /// Translation in en :
/// Welcome \"%@\" ! /// Welcome \"%@\" !
///
/// Comment :
/// No comment
func generic_welcome_firstname_format(arg0: String) -> String { func generic_welcome_firstname_format(arg0: String) -> String {
String(format: self.generic_welcome_firstname_format, arg0) String(format: self.generic_welcome_firstname_format, arg0)
} }
@ -64,6 +78,9 @@ extension String {
/// Translation in en : /// Translation in en :
/// 1 = 1 point ! /// 1 = 1 point !
///
/// Comment :
/// No comment
var test_equal_symbol: String { var test_equal_symbol: String {
NSLocalizedString("test_equal_symbol", tableName: kStringsFileName, bundle: Bundle.main, value: "1€ = 1 point !", comment: "") NSLocalizedString("test_equal_symbol", tableName: kStringsFileName, bundle: Bundle.main, value: "1€ = 1 point !", comment: "")
} }
@ -72,12 +89,17 @@ extension String {
/// Translation in en : /// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d /// You %%: %2$@ %1$@ Age: %3$d
///
/// Comment :
/// No comment
var placeholders_test_one: String { var placeholders_test_one: String {
NSLocalizedString("placeholders_test_one", tableName: kStringsFileName, bundle: Bundle.main, value: "You %%: %2$@ %1$@ Age: %3$d", comment: "") NSLocalizedString("placeholders_test_one", tableName: kStringsFileName, bundle: Bundle.main, value: "You %%: %2$@ %1$@ Age: %3$d", comment: "")
} }
/// Translation in en : /// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d /// You %%: %2$@ %1$@ Age: %3$d
///
/// Comment :
/// No comment
func placeholders_test_one(arg0: String, arg1: String, arg2: Int) -> String { func placeholders_test_one(arg0: String, arg1: String, arg2: Int) -> String {
String(format: self.placeholders_test_one, arg0, arg1, arg2) String(format: self.placeholders_test_one, arg0, arg1, arg2)
} }

View File

@ -1,6 +1,6 @@
/** /**
* Apple Strings File * Apple Strings File
* Generated by ResgenSwift 1.2 * Generated by ResgenSwift 2.1.0
* Language: en-us * Language: en-us
*/ */

View File

@ -1,6 +1,6 @@
/** /**
* Apple Strings File * Apple Strings File
* Generated by ResgenSwift 1.2 * Generated by ResgenSwift 2.1.0
* Language: en * Language: en
*/ */

View File

@ -1,6 +1,6 @@
/** /**
* Apple Strings File * Apple Strings File
* Generated by ResgenSwift 1.2 * Generated by ResgenSwift 2.1.0
* Language: fr * Language: fr
*/ */

View File

@ -1,11 +1,12 @@
// Generated by ResgenSwift.Analytics 1.2 // Generated by ResgenSwift.Analytics 2.1.0
import MatomoTracker import MatomoTracker
import Firebase import FirebaseAnalytics
// 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,
@ -18,7 +19,7 @@ protocol AnalyticsManagerProtocol {
// MARK: - Matomo // MARK: - Matomo
class MatomoAnalyticsManager: AnalyticsManagerProtocol { class MatomoAnalyticsManager: AnalyticsManagerProtocol {
// MARK: - Properties // MARK: - Properties
private var tracker: MatomoTracker private var tracker: MatomoTracker
@ -32,11 +33,11 @@ class MatomoAnalyticsManager: AnalyticsManagerProtocol {
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
@ -44,7 +45,7 @@ class MatomoAnalyticsManager: AnalyticsManagerProtocol {
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) {
@ -81,7 +82,7 @@ class MatomoAnalyticsManager: AnalyticsManagerProtocol {
class FirebaseAnalyticsManager: AnalyticsManagerProtocol { class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
func logScreen(name: String, path: String) { func logScreen(name: String, path: String) {
var parameters = [ var parameters = [
AnalyticsParameterScreenName: name AnalyticsParameterScreenName: name as NSObject
] ]
Analytics.logEvent( Analytics.logEvent(
@ -96,19 +97,25 @@ class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
category: String, category: String,
params: [String: Any]? params: [String: Any]?
) { ) {
var parameters: [String:Any] = [ var parameters: [String:NSObject] = [
"action": action, "action": action as NSObject,
"category": category, "category": category as NSObject,
] ]
if let supplementaryParameters = params { if let supplementaryParameters = params {
parameters.merge(supplementaryParameters) { (origin, new) -> Any in for (newKey, newValue) in supplementaryParameters {
return origin if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
} }
} }
Analytics.logEvent( Analytics.logEvent(
name, name.replacingOccurrences(of: [" "], with: "_"),
parameters: parameters parameters: parameters
) )
} }
@ -117,8 +124,9 @@ class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
// 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] = []
@ -140,7 +148,7 @@ class AnalyticsManager {
) )
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 }
@ -148,7 +156,7 @@ class AnalyticsManager {
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,
@ -169,19 +177,22 @@ class AnalyticsManager {
// MARK: - section_one // MARK: - section_one
func logScreenS1DefOne() { func logScreenS1DefOne(title: String) {
logScreen( logScreen(
name: "s1 def one", name: "s1 def one \(title)",
path: "s1_def_one/" path: "s1_def_one/\(title)"
) )
} }
func logEventS1DefTwo() { func logEventS1DefTwo(title: String, count: String) {
logEvent( logEvent(
name: "s1 def two", name: "s1 def two",
action: "test", action: "test",
category: "test", category: "test",
params: [] params: [
"title": title,
"count": count
]
) )
} }

View File

@ -1,4 +1,4 @@
// Generated by ResgenSwift.Strings.Tags 1.2 // Generated by ResgenSwift.Strings.Tags 2.1.0
import UIKit import UIKit
@ -8,12 +8,20 @@ extension Tags {
/// Translation in ium : /// Translation in ium :
/// Ecran un /// Ecran un
///
/// Comment :
/// No comment
var screen_one: String { var screen_one: String {
"Ecran un" "Ecran un"
} }
/// Translation in ium : /// Translation in ium :
/// Ecran deux /// Ecran deux
///
/// Comment :
/// No comment
var screen_two: String { var screen_two: String {
"Ecran deux" "Ecran deux"
} }

View File

@ -3,9 +3,13 @@ categories:
- id: section_one - id: section_one
screens: screens:
- id: s1_def_one - id: s1_def_one
name: s1 def one name: s1 def one _TITLE_
path: s1_def_one/ path: s1_def_one/_TITLE_
tags: ios tags: ios,droid
parameters:
- name: title
type: String
replaceIn: name,path
events: events:
- id: s1_def_two - id: s1_def_two
@ -13,6 +17,11 @@ categories:
action: test action: test
category: test category: test
tags: ios tags: ios
parameters:
- name: title
type: String
- name: count
type: String
- id: section_two - id: section_two
screens: screens:

View File

@ -3,65 +3,65 @@
FORCE_FLAG="$1" FORCE_FLAG="$1"
## Font ## Font
#swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/sampleFontsAll.txt" \ swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/sampleFontsAll.txt" \
# --extension-output-path "./Fonts/Generated" \ --extension-output-path "./Fonts/Generated" \
# --extension-name "FontYolo" \ --extension-name "FontYolo" \
# --extension-name-ui-kit "UIFontYolo" \ --extension-name-ui-kit "UIFontYolo" \
# --extension-suffix "GenAllScript" \ --extension-suffix "GenAllScript" \
# --info-plist-paths "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist" --info-plist-paths "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist"
#
#echo "\n-------------------------\n"
#
## Color
#swift run -c release ResgenSwift colors $FORCE_FLAG "./Colors/sampleColors1.txt" \
# --style all \
# --xcassets-path "./Colors/colors.xcassets" \
# --extension-output-path "./Colors/Generated/" \
# --extension-name "ColorYolo" \
# --extension-name-ui-kit "UIhkjhkColorYolo" \
# --extension-suffix "GenAllScript"
#
#echo "\n-------------------------\n"
#
## Twine
#swift run -c release ResgenSwift strings twine $FORCE_FLAG "./Twine/sampleStrings.txt" \
# --output-path "./Twine/Generated" \
# --langs "fr en en-us" \
# --default-lang "en" \
# --extension-output-path "./Twine/Generated"
#echo "\n-------------------------\n" echo "\n-------------------------\n"
## Color
swift run -c release ResgenSwift colors $FORCE_FLAG "./Colors/sampleColors1.txt" \
--style all \
--xcassets-path "./Colors/colors.xcassets" \
--extension-output-path "./Colors/Generated/" \
--extension-name "ColorYolo" \
--extension-name-ui-kit "UIhkjhkColorYolo" \
--extension-suffix "GenAllScript"
echo "\n-------------------------\n"
## Twine
swift run -c release ResgenSwift strings twine $FORCE_FLAG "./Twine/sampleStrings.txt" \
--output-path "./Twine/Generated" \
--langs "fr en en-us" \
--default-lang "en" \
--extension-output-path "./Twine/Generated"
echo "\n-------------------------\n"
## Strings ## Strings
#swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \ swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \
# --output-path "./Strings/Generated" \ --output-path "./Strings/Generated" \
# --langs "fr en en-us" \ --langs "fr en en-us" \
# --default-lang "en" \ --default-lang "en" \
# --extension-output-path "./Strings/Generated" \ --extension-output-path "./Strings/Generated" \
# --extension-name "String" \ --extension-name "String" \
# --extension-suffix "GenAllScript" --extension-suffix "GenAllScript"
#echo "\n-------------------------\n" echo "\n-------------------------\n"
## Tags ## Tags
#swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/sampleTags.txt" \ swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/sampleTags.txt" \
# --lang "ium" \ --lang "ium" \
# --extension-output-path "./Tags/Generated" \ --extension-output-path "./Tags/Generated" \
# --extension-name "Tags" \ --extension-name "Tags" \
# --extension-suffix "GenAllScript" --extension-suffix "GenAllScript"
#echo "\n-------------------------\n" #echo "\n-------------------------\n"
## Analytics # Analytics
#swift run -c release ResgenSwift analytics $FORCE_FLAG "./Tags/sampleTags.yml" \ swift run -c release ResgenSwift analytics $FORCE_FLAG "./Tags/sampleTags.yml" \
# --target "matomo firebase" \ --target "matomo firebase" \
# --extension-output-path "./Tags/Generated" \ --extension-output-path "./Tags/Generated" \
# --extension-name "Analytics" \ --extension-name "Analytics" \
# --extension-suffix "GenAllScript" --extension-suffix "GenAllScript"
#echo "\n-------------------------\n" echo "\n-------------------------\n"
#
# Images ## Images
swift run -c release ResgenSwift images $FORCE_FLAG "./Images/sampleImages.txt" \ swift run -c release ResgenSwift images $FORCE_FLAG "./Images/sampleImages.txt" \
--xcassets-path "./Images/imagium.xcassets" \ --xcassets-path "./Images/imagium.xcassets" \
--extension-output-path "./Images/Generated" \ --extension-output-path "./Images/Generated" \

View File

@ -5,62 +5,86 @@
// 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)")
// Check requirements
guard checkRequirements() else { return }
print("[\(Self.toolName)] Will generate analytics") print("[\(Self.toolName)] Will generate analytics")
// Check requirements
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")
} }
}
extension Analytics { // MARK: - Requirements
enum TargetType: CaseIterable {
case matomo private func checkRequirements() -> Bool {
case firebase let fileManager = FileManager()
var value: String { // Input file
switch self { guard fileManager.fileExists(atPath: options.inputFile) else {
case .matomo: let error = AnalyticsError.fileNotExists(options.inputFile)
"matomo" print(error.description)
case .firebase: Self.exit(withError: error)
"firebase"
}
} }
guard TrackerType.hasValidTarget(in: options.target) else {
let error = AnalyticsError.noValidTracker(options.target)
print(error.description)
Self.exit(withError: error)
}
// Check if needed to regenerate
guard GeneratorChecker.shouldGenerate(
force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath
) else {
print("[\(Self.toolName)] Analytics are already up to date :) ")
return false
}
return true
} }
} }

View File

@ -0,0 +1,40 @@
//
// AnalyticsError.swift
//
//
// Created by Loris Perret on 11/12/2023.
//
import Foundation
enum AnalyticsError: Error {
case noValidTracker(String)
case fileNotExists(String)
case missingElement(String)
case invalidParameter(String)
case parseFailed(String)
case writeFile(String, String)
var description: String {
switch self {
case .noValidTracker(let inputTargets):
return "error: [\(Analytics.toolName)] '\(inputTargets)' ne contient aucun tracker valid"
case .fileNotExists(let filename):
return "error: [\(Analytics.toolName)] File \(filename) does not exists"
case .missingElement(let element):
return "error: [\(Analytics.toolName)] Missing \(element) for Matomo"
case .invalidParameter(let reason):
return "error: [\(Analytics.toolName)] Invalid parameter \(reason)"
case .parseFailed(let baseError):
return "error: [\(Analytics.toolName)] Parse input file failed: \(baseError)"
case let .writeFile(subErrorDescription, filename):
return "error: [\(Analytics.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
}
}
}

View File

@ -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)"
} }

View File

@ -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: [Analytics.TargetType] = []
enum AnalyticsGenerator {
static func writeExtensionFiles(sections: [AnalyticsCategory], target: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
// 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 = {
Analytics.TargetType.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 = StringiumError.writeFile(extensionFilePath, error.localizedDescription) let error = AnalyticsError.writeFile(extensionFilePath, error.localizedDescription)
print(error.description) print(error.description)
Stringium.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.getAnalytics()) \(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(Analytics.TargetType.matomo) { if targets.contains(TrackerType.matomo) {
result.append("import MatomoTracker") result.append("import MatomoTracker")
} }
if targets.contains(Analytics.TargetType.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,19 +165,19 @@ 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(Analytics.TargetType.matomo) { if targets.contains(TrackerType.matomo) {
header = "func configure(siteId: String, url: String) {" header = "func configure(siteId: String, url: String) {"
} else if targets.contains(Analytics.TargetType.firebase) { } else if targets.contains(TrackerType.firebase) {
header = "func configure() {" header = "func configure() {"
} }
if targets.contains(Analytics.TargetType.matomo) { if targets.contains(TrackerType.matomo) {
content.append(""" content.append("""
managers.append( managers.append(
MatomoAnalyticsManager( MatomoAnalyticsManager(
@ -152,10 +187,10 @@ class AnalyticsGenerator {
) )
""") """)
} }
if targets.contains(Analytics.TargetType.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 getAnalytics() -> 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(Analytics.TargetType.matomo) { if targets.contains(TrackerType.matomo) {
result.append(MatomoGenerator.service.content) result.append(MatomoGenerator.service)
} }
if targets.contains(Analytics.TargetType.firebase) { if targets.contains(TrackerType.firebase) {
result.append(FirebaseGenerator.service.content) 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 {
""" """
} }
""" """
} }
} }

View File

@ -5,46 +5,49 @@
// Created by Loris Perret on 05/12/2023. // Created by Loris Perret on 05/12/2023.
// //
// CPD-OFF
import Foundation import Foundation
enum FirebaseGenerator { enum FirebaseGenerator {
case service
static var service: String {
var content: String {
[ [
FirebaseGenerator.service.header, Self.header,
FirebaseGenerator.service.logScreen, Self.logScreen,
FirebaseGenerator.service.logEvent, Self.logEvent,
FirebaseGenerator.service.footer Self.footer
] ]
.joined(separator: "\n") .joined(separator: "\n")
} }
private var header: String { // MARK: - Private vars
private static var header: String {
""" """
// MARK: - Firebase // MARK: - Firebase
class FirebaseAnalyticsManager: AnalyticsManagerProtocol { class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
""" """
} }
private 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 AnalyticsParameterScreenName: name as NSObject
] ]
Analytics.logEvent( Analytics.logEvent(
AnalyticsEventScreenView, AnalyticsEventScreenView,
parameters: parameters parameters: parameters
) )
} }
""" """
} }
private var logEvent: String { private static var logEvent: String {
""" """
func logEvent( func logEvent(
name: String, name: String,
@ -52,29 +55,37 @@ enum FirebaseGenerator {
category: String, category: String,
params: [String: Any]? params: [String: Any]?
) { ) {
var parameters: [String:Any] = [ var parameters: [String:NSObject] = [
"action": action, "action": action as NSObject,
"category": category, "category": category as NSObject,
] ]
if let supplementaryParameters = params { if let supplementaryParameters = params {
parameters.merge(supplementaryParameters) { (origin, new) -> Any in for (newKey, newValue) in supplementaryParameters {
return origin if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
} }
} }
Analytics.logEvent( Analytics.logEvent(
name, name.replacingOccurrences(of: [" "], with: "_"),
parameters: parameters parameters: parameters
) )
} }
""" """
} }
private var footer: String { private static var footer: String {
""" """
} }
""" """
} }
} }
// CPD-ON

View File

@ -1,6 +1,6 @@
// //
// MatomoGenerator.swift // MatomoGenerator.swift
// //
// //
// Created by Loris Perret on 05/12/2023. // Created by Loris Perret on 05/12/2023.
// //
@ -8,36 +8,37 @@
import Foundation import Foundation
enum MatomoGenerator { enum MatomoGenerator {
case service
static var service: String {
var content: String {
[ [
MatomoGenerator.service.header, Self.header,
MatomoGenerator.service.setup, Self.setup,
MatomoGenerator.service.logScreen, Self.logScreen,
MatomoGenerator.service.logEvent, Self.logEvent,
MatomoGenerator.service.footer Self.footer
] ]
.joined(separator: "\n") .joined(separator: "\n")
} }
private var header: String { // MARK: - Private vars
private static var header: String {
""" """
// MARK: - Matomo // MARK: - Matomo
class MatomoAnalyticsManager: AnalyticsManagerProtocol { class MatomoAnalyticsManager: AnalyticsManagerProtocol {
// MARK: - Properties // MARK: - Properties
private var tracker: MatomoTracker private var tracker: MatomoTracker
""" """
} }
private 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)")
@ -45,41 +46,41 @@ 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 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 var logEvent: String { private static var logEvent: String {
""" """
func logEvent( func logEvent(
name: String, name: String,
@ -88,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,
@ -99,11 +100,11 @@ enum MatomoGenerator {
} }
""" """
} }
private var footer: String { private static var footer: String {
""" """
} }
""" """
} }
} }

View File

@ -8,17 +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
init(id: String) { init(id: String) {
self.id = id self.id = id
} }
// 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

View File

@ -6,8 +6,12 @@
// //
import Foundation import Foundation
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 = ""
@ -17,45 +21,49 @@ class AnalyticsDefinition {
var tags: [String] = [] var tags: [String] = []
var parameters: [AnalyticsParameter] = [] var parameters: [AnalyticsParameter] = []
var type: TagType var type: TagType
// MARK: - Init
init(id: String, name: String, type: TagType) { init(id: String, name: String, type: TagType) {
self.id = id self.id = id
self.name = name self.name = name
self.type = type self.type = type
} }
// MARK: - Methods
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: - Methods // MARK: - Private Methods
private func getFuncName() -> String { private func getFuncName() -> String {
var pascalCaseTitle: String = "" var pascalCaseTitle: String = ""
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 = """
( (
@ -67,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 {
@ -84,19 +92,19 @@ 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)")
} }
if params.count > 1 { if params.count > 1 {
result = """ result = """
[ [
@ -110,7 +118,7 @@ class AnalyticsDefinition {
} else { } else {
result = "[:]" result = "[:]"
} }
if type == .screen { if type == .screen {
return """ return """
logScreen( logScreen(
@ -129,9 +137,9 @@ class AnalyticsDefinition {
""" """
} }
} }
// MARK: - Raw strings // MARK: - Raw strings
func getProperty() -> String { func getProperty() -> String {
replaceIn() replaceIn()
return """ return """
@ -140,7 +148,7 @@ class AnalyticsDefinition {
} }
""" """
} }
func getStaticProperty() -> String { func getStaticProperty() -> String {
replaceIn() replaceIn()
return """ return """
@ -150,27 +158,3 @@ class AnalyticsDefinition {
""" """
} }
} }
extension AnalyticsDefinition {
enum TagType {
case screen
case event
}
}
extension String {
func replacingFirstOccurrence(of: String, with: String) -> Self {
if let range = self.range(of: of) {
let tmp = self.replacingOccurrences(
of: of,
with: with,
options: .literal,
range: range
)
return tmp
}
return self
}
}

View File

@ -8,39 +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]?
} }
protocol AnalyticsDefinitionDTO: Codable {} struct AnalyticsDefinitionScreenDTO: Codable {
struct AnalyticsDefinitionScreenDTO: AnalyticsDefinitionDTO {
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: AnalyticsDefinitionDTO { 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?

View File

@ -8,10 +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
init(name: String, type: String) { init(name: String, type: String) {
self.name = name self.name = name
self.type = type self.type = type

View File

@ -0,0 +1,17 @@
//
// TagType.swift
//
//
// Created by Thibaut Schmitt on 08/12/2023.
//
import Foundation
extension AnalyticsDefinition {
enum TagType {
case screen
case event
}
}

View File

@ -0,0 +1,31 @@
//
// TargetType.swift
//
//
// Created by Thibaut Schmitt on 08/12/2023.
//
import Foundation
enum TrackerType: CaseIterable, Sendable {
case matomo
case firebase
var value: String {
switch self {
case .matomo:
"matomo"
case .firebase:
"firebase"
}
}
static func hasValidTarget(in targets: String) -> Bool {
for tracker in Self.allCases where targets.contains(tracker.value) {
return true
}
return false
}
}

View File

@ -9,54 +9,94 @@ 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 = GenerateError.fileNotExists(inputFile) let error = AnalyticsError.fileNotExists(inputFile)
Generate.exit(withError: error) print(error.description)
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
} catch let error { } catch {
Generate.exit(withError: error) let error = AnalyticsError.parseFailed(error.localizedDescription)
print(error.description)
Analytics.exit(withError: error)
} }
} }
private static func getParameters(fromData data: [AnalyticsParameterDTO]) -> [AnalyticsParameter] { private func getParameters(from parameters: [AnalyticsParameterDTO]) -> [AnalyticsParameter] {
var parameters: [AnalyticsParameter] = [] parameters.map { dtoParameter in
data.forEach { value in
// Type // Type
let type = dtoParameter.type.uppercasedFirst()
let type = value.type.uppercasedFirst()
guard
guard
type == "String" || type == "String" ||
type == "Int" || type == "Int" ||
type == "Double" || type == "Double" ||
type == "Bool" type == "Bool"
else { else {
let error = GenerateError.invalidParameter("type of \(value.name)") let error = AnalyticsError.invalidParameter("type of \(dtoParameter.name)")
Generate.exit(withError: error) print(error.description)
Analytics.exit(withError: error)
} }
let parameter: AnalyticsParameter = AnalyticsParameter(name: value.name, type: type) let parameter = AnalyticsParameter(
name: dtoParameter.name,
if let replaceIn = value.replaceIn { type: type
)
if let replaceIn = dtoParameter.replaceIn {
parameter.replaceIn = replaceIn.components(separatedBy: ",") parameter.replaceIn = replaceIn.components(separatedBy: ",")
} }
parameters.append(parameter) return parameter
} }
return parameters
} }
private static func getTagDefinition( private func getTagDefinition(
id: String, id: String,
name: String, name: String,
type: AnalyticsDefinition.TagType, type: AnalyticsDefinition.TagType,
@ -64,25 +104,25 @@ class AnalyticsFileParser {
comments: String?, comments: String?,
parameters: [AnalyticsParameterDTO]? parameters: [AnalyticsParameterDTO]?
) -> AnalyticsDefinition { ) -> AnalyticsDefinition {
let definition: AnalyticsDefinition = AnalyticsDefinition(id: id, name: name, type: type) let definition = AnalyticsDefinition(id: id, name: name, type: type)
definition.tags = tags.components(separatedBy: ",") definition.tags = tags
.components(separatedBy: ",")
if let comments = comments { .map { $0.removeLeadingTrailingWhitespace() }
if let comments {
definition.comments = comments definition.comments = comments
} }
if let parameters = parameters { if let parameters {
definition.parameters = Self.getParameters(fromData: parameters) definition.parameters = getParameters(from: parameters)
} }
return definition return definition
} }
private static func getTagDefinitionScreen(fromData screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] { private func getTagDefinitionScreen(from screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] {
var definitions: [AnalyticsDefinition] = [] screens.map { screen in
let definition: AnalyticsDefinition = getTagDefinition(
for screen in screens {
let definition: AnalyticsDefinition = Self.getTagDefinition(
id: screen.id, id: screen.id,
name: screen.name, name: screen.name,
type: .screen, type: .screen,
@ -90,29 +130,26 @@ class AnalyticsFileParser {
comments: screen.comments, comments: screen.comments,
parameters: screen.parameters parameters: screen.parameters
) )
if target.contains(Analytics.TargetType.matomo.value) { if target.contains(TrackerType.matomo.value) {
// Path // Path
guard let path = screen.path else { guard let path = screen.path else {
let error = GenerateError.missingElement("screen path") let error = AnalyticsError.missingElement("screen path")
Generate.exit(withError: error) print(error.description)
Analytics.exit(withError: error)
} }
definition.path = path definition.path = path
} }
definitions.append(definition) return definition
} }
return definitions
} }
private static func getTagDefinitionEvent(fromData events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] { private func getTagDefinitionEvent(from events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] {
var definitions: [AnalyticsDefinition] = [] events.map { event in
let definition: AnalyticsDefinition = getTagDefinition(
for event in events {
let definition: AnalyticsDefinition = Self.getTagDefinition(
id: event.id, id: event.id,
name: event.name, name: event.name,
type: .event, type: .event,
@ -120,54 +157,28 @@ class AnalyticsFileParser {
comments: event.comments, comments: event.comments,
parameters: event.parameters parameters: event.parameters
) )
if target.contains(Analytics.TargetType.matomo.value) { if target.contains(TrackerType.matomo.value) {
// Category // Category
guard let category = event.category else { guard let category = event.category else {
let error = GenerateError.missingElement("event category") let error = AnalyticsError.missingElement("event category")
Generate.exit(withError: error) print(error.description)
Analytics.exit(withError: error)
} }
definition.category = category definition.category = category
// Action // Action
guard let action = event.action else { guard let action = event.action else {
let error = GenerateError.missingElement("event action") let error = AnalyticsError.missingElement("event action")
Generate.exit(withError: error) print(error.description)
Analytics.exit(withError: error)
} }
definition.action = action definition.action = action
} }
definitions.append(definition) return definition
} }
return definitions
}
static func parse(_ inputFile: String, target: String) -> [AnalyticsCategory] {
self.inputFile = inputFile
self.target = target
let tagFile: AnalyticsFile = Self.parseYaml()
var sections: [AnalyticsCategory] = []
tagFile.categories.forEach { categorie in
let section: AnalyticsCategory = AnalyticsCategory(id: categorie.id)
if let screens = categorie.screens {
section.definitions.append(contentsOf: Self.getTagDefinitionScreen(fromData: screens))
}
if let events = categorie.events {
section.definitions.append(contentsOf: Self.getTagDefinitionEvent(fromData: events))
}
sections.append(section)
}
return sections
} }
} }

View File

@ -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)
} }
} }
} }

View File

@ -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)`"
} }

View File

@ -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)"
} }

View File

@ -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)
} }

View File

@ -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)

View File

@ -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,

View File

@ -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 """

View File

@ -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]))

View File

@ -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)"
} }

View File

@ -1,98 +1,109 @@
// //
// 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).deletingLastPathComponent().relativePath let inputFolder = URL(fileURLWithPath: options.inputFile)
let fontsNames = FontsToolHelper.getFontPostScriptName(for: fontsToGenerate, .deletingLastPathComponent()
inputFolder: inputFolder) .relativePath
let fontsNames = FontsToolHelper.getFontPostScriptName(
for: fontsToGenerate,
inputFolder: inputFolder
)
// Generate extension // Generate extension
FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames, FontExtensionGenerator.writeExtensionFile(
staticVar: options.staticMembers, fontsNames: fontsNames,
extensionName: options.extensionName, staticVar: options.staticMembers,
extensionFilePath: options.extensionFilePath, extensionName: options.extensionName,
isSwiftUI: true) extensionFilePath: options.extensionFilePath,
isSwiftUI: true
FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames, )
staticVar: options.staticMembers,
extensionName: options.extensionNameUIKit, FontExtensionGenerator.writeExtensionFile(
extensionFilePath: options.extensionFilePathUIKit, fontsNames: fontsNames,
isSwiftUI: false) staticVar: options.staticMembers,
extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathUIKit,
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
} }
} }

View File

@ -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)"
} }
} }

View File

@ -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,32 +42,41 @@ 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) -> String { 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 fontName = 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)
Fonts.exit(withError: error) Fonts.exit(withError: error)
} }
return fontName let pathURL = URL(fileURLWithPath: path)
let filename = pathURL
.deletingPathExtension()
.lastPathComponent
return FontName(
postscriptName: postscriptName,
filename: filename,
fileExtension: pathURL.pathExtension
)
} }
} }

View File

@ -8,37 +8,44 @@
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 { .forEach { fontName in
Shell.shell(launchPath: "/usr/libexec/PlistBuddy", Shell.shell(
["-c", "add :UIAppFonts: string \($0)", infoPlist]) launchPath: "/usr/libexec/PlistBuddy",
["-c", "add :UIAppFonts: string \(fontName.filename).\(fontName.fileExtension)", infoPlist]
)
} }
} }
var plistData = "<key>UIAppFonts</key>\n\t<array>\n" var plistData = "<key>UIAppFonts</key>\n\t<array>\n"
fontsToAddToPlist fontsToAddToPlist
.forEach { .forEach { fontName in
plistData += "\t\t<string>\($0)</string>\n" plistData += "\t\t<string>\(fontName.filename).\(fontName.fileExtension)</string>\n"
} }
plistData += "\t</array>" plistData += "\t</array>"
return plistData return plistData
} }
} }

View File

@ -8,45 +8,51 @@
import Foundation import Foundation
import ToolCore import ToolCore
class FontExtensionGenerator { enum FontExtensionGenerator {
private static func getFontNameEnum(fontsNames: [String]) -> 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)\"\n" enumDefinition += " case \($0.fontNameSanitize) = \"\($0.postscriptName)\"\n"
} }
enumDefinition += " }\n" enumDefinition += " }\n"
return enumDefinition return enumDefinition
} }
static func writeExtensionFile(fontsNames: [String], 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: [String], 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 {
""" """
} }
""" """
} }
} }

View File

@ -7,13 +7,21 @@
import Foundation import Foundation
typealias FontName = String // swiftlint:disable no_grouping_extension
struct FontName: Hashable {
let postscriptName: String
let filename: String
let fileExtension: String
}
extension FontName { extension FontName {
var fontNameSanitize: String { var fontNameSanitize: String {
self.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 {

View File

@ -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)
} }
} }

View File

@ -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("/") {

View File

@ -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")
} }

View File

@ -8,38 +8,27 @@
import Foundation import Foundation
enum GenerateError: Error { enum GenerateError: Error {
case fileNotExists(String) case fileNotExists(String)
case invalidConfigurationFile(String) case invalidConfigurationFile(String, String)
case commandError([String], String) case commandError([String], String)
case writeFile(String, String) case writeFile(String, String)
// Analytics
case missingElement(String)
case invalidParameter(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): case let .invalidConfigurationFile(filename, underneathErrorDescription):
return "error: [\(Generate.toolName)] File \(filename) is not a valid configuration file" 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)"
case .missingElement(let element):
return "error: [\(Generate.toolName)] Missing \(element) for Matomo"
case .invalidParameter(let reason):
return "error: [\(Generate.toolName)] Invalid parameter \(reason)"
} }
} }
} }

View File

@ -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 == "/" {

View File

@ -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)

View File

@ -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 "class \(classname) {}" 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
@ -269,22 +284,33 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
let extensionName: String? let extensionName: String?
let extensionSuffix: String? let extensionSuffix: String?
private let staticMembers: Bool? private let staticMembers: 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
} }
internal init(inputFile: String, var xcStringsOptions: Bool {
outputPath: String, if let xcStrings {
langs: String, return xcStrings
defaultLang: String, }
extensionOutputPath: String, return false
extensionName: String?, }
extensionSuffix: String?,
staticMembers: Bool?) { internal init(
inputFile: String,
outputPath: String,
langs: String,
defaultLang: String,
extensionOutputPath: String,
extensionName: String?,
extensionSuffix: String?,
staticMembers: Bool?,
xcStrings: Bool?
) {
self.inputFile = inputFile self.inputFile = inputFile
self.outputPath = outputPath self.outputPath = outputPath
self.langs = langs self.langs = langs
@ -293,8 +319,9 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
self.extensionName = extensionName self.extensionName = extensionName
self.extensionSuffix = extensionSuffix self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers self.staticMembers = staticMembers
self.xcStrings = xcStrings
} }
var debugDescription: String { var debugDescription: String {
""" """
Strings configuration: Strings configuration:
@ -310,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
@ -337,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:

View File

@ -8,20 +8,24 @@
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)
} }
guard let configuration = try? YAMLDecoder().decode(ConfigurationFile.self, from: data) else { do {
let error = GenerateError.invalidConfigurationFile(configurationFile) return try YAMLDecoder().decode(ConfigurationFile.self, from: data)
} catch {
let error = GenerateError.invalidConfigurationFile(
configurationFile,
error.localizedDescription.description
)
print(error.description) print(error.description)
Generate.exit(withError: error) Generate.exit(withError: error)
} }
return configuration
} }
} }

View File

@ -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)
} }
} }

View File

@ -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
} }
} }

View File

@ -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
} }
} }

View File

@ -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
} }
} }

View File

@ -8,5 +8,6 @@
import Foundation import Foundation
protocol Runnable { protocol Runnable {
func run(projectDirectory: String, force: Bool) func run(projectDirectory: String, force: Bool)
} }

View File

@ -8,13 +8,14 @@
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"]
} }
args += [ args += [
inputFile.prependIfRelativePath(projectDirectory), inputFile.prependIfRelativePath(projectDirectory),
"--output-path", "--output-path",
@ -26,23 +27,25 @@ extension StringsConfiguration: Runnable {
"--extension-output-path", "--extension-output-path",
extensionOutputPath.prependIfRelativePath(projectDirectory), extensionOutputPath.prependIfRelativePath(projectDirectory),
"--static-members", "--static-members",
"\(staticMembersOptions)" "\(staticMembersOptions)",
"--xc-strings",
"\(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)
} }
} }

View File

@ -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)
} }
} }

View File

@ -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])

View File

@ -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 {
""" """
} }
""" """
} }
} }

View File

@ -1,6 +1,6 @@
// //
// XcassetsGenerator.swift // XcassetsGenerator.swift
// //
// //
// Created by Thibaut Schmitt on 24/01/2022. // Created by Thibaut Schmitt on 24/01/2022.
// //
@ -8,29 +8,36 @@
import Foundation import Foundation
import ToolCore import ToolCore
enum OutputImageExtension: String {
case png
case svg
}
class XcassetsGenerator { class XcassetsGenerator {
static let outputImageExtension = "png" // 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 magickConvert = Images.getMagickConvertPath()
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 {
@ -51,103 +58,158 @@ 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).\(XcassetsGenerator.outputImageExtension)" let output1x = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.png.rawValue)"
let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(XcassetsGenerator.outputImageExtension)" let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(OutputImageExtension.png.rawValue)"
let output3x = "\(imagesetPath)/\(parsedImage.name)@3x.\(XcassetsGenerator.outputImageExtension)" let output3x = "\(imagesetPath)/\(parsedImage.name)@3x.\(OutputImageExtension.png.rawValue)"
// Check if we need to convert image // Check if we need to convert image
guard self.shouldGenerate(inputImagePath: imageData.path, xcassetImagePath: output1x) else {
//print("\(parsedImage.name) -> Not regenerating") var needToGenerateForSvg = false
if imageData.ext == "svg" && !parsedImage.imageExtensions.contains(.png) {
needToGenerateForSvg = true
}
guard self.shouldGenerate(inputImagePath: imageData.path, xcassetImagePath: output1x, needToGenerateForSvg: needToGenerateForSvg) else {
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)
Images.exit(withError: error) Images.exit(withError: error)
} }
} else {
do {
let documentsDirectory = try fileManager.contentsOfDirectory(atPath: imagesetPath)
for filePath in documentsDirectory {
try fileManager.removeItem(atPath: "\(imagesetPath)/\(filePath)")
}
} catch {
print("Error deleting previous assets")
}
} }
// Convert image
let convertArguments = parsedImage.convertArguments let convertArguments = parsedImage.convertArguments
if imageData.ext == "svg" { if imageData.ext == "svg" {
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -h 300 -o path/to/output.png if parsedImage.imageExtensions.contains(.png) {
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -h 300 -o path/to/output.png // /usr/local/bin/rsvg-convert path/to/image.png -w 200 -h 300 -o path/to/output.png
var command1x = ["\(svgConverter)", "\(imageData.path)"] // /usr/local/bin/rsvg-convert path/to/image.png -w 200 -o path/to/output.png
var command2x = ["\(svgConverter)", "\(imageData.path)"] // /usr/local/bin/rsvg-convert path/to/image.png -h 300 -o path/to/output.png
var command3x = ["\(svgConverter)", "\(imageData.path)"] var command1x = ["\(svgConverter)", "\(imageData.path)"]
var command2x = ["\(svgConverter)", "\(imageData.path)"]
self.addConvertArgument(command: &command1x, convertArgument: convertArguments.x1) var command3x = ["\(svgConverter)", "\(imageData.path)"]
self.addConvertArgument(command: &command2x, convertArgument: convertArguments.x2)
self.addConvertArgument(command: &command3x, convertArgument: convertArguments.x3) self.addConvertArgument(command: &command1x, convertArgument: convertArguments.x1)
self.addConvertArgument(command: &command2x, convertArgument: convertArguments.x2)
command1x.append(contentsOf: ["-o", output1x]) self.addConvertArgument(command: &command3x, convertArgument: convertArguments.x3)
command2x.append(contentsOf: ["-o", output2x])
command3x.append(contentsOf: ["-o", output3x]) command1x.append(contentsOf: ["-o", output1x])
command2x.append(contentsOf: ["-o", output2x])
Shell.shell(command1x) command3x.append(contentsOf: ["-o", output3x])
Shell.shell(command2x)
Shell.shell(command3x) Shell.shell(command1x)
Shell.shell(command2x)
Shell.shell(command3x)
} else {
let output = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.svg.rawValue)"
let tempURL = URL(fileURLWithPath: output)
do {
if FileManager.default.fileExists(atPath: tempURL.path) {
try FileManager.default.removeItem(atPath: tempURL.path)
}
try FileManager.default.copyItem(atPath: imageData.path, toPath: tempURL.path)
} catch {
print(error.localizedDescription)
}
}
} else { } else {
// 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]) "\(magickConvert)",
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(
[
"\(magickConvert)",
"\(imageData.path)",
"-resize",
"\(convertArguments.x2.width ?? "")x\(convertArguments.x2.height ?? "")",
output2x
]
)
Shell.shell(
[
"\(magickConvert)",
"\(imageData.path)",
"-resize",
"\(convertArguments.x3.width ?? "")x\(convertArguments.x3.height ?? "")",
output3x
]
)
} }
// Write Content.json // Write Content.json
let imagesetContentJson = parsedImage.contentJson 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")
@ -158,14 +220,14 @@ class XcassetsGenerator {
command.append("\(height)") command.append("\(height)")
} }
} }
// MARK: - Helpers: bypass generation // MARK: - Helpers: bypass generation
private func shouldGenerate(inputImagePath: String, xcassetImagePath: String) -> Bool { private func shouldGenerate(inputImagePath: String, xcassetImagePath: String, needToGenerateForSvg: Bool) -> Bool {
if forceGeneration { if forceGeneration || needToGenerateForSvg {
return true return true
} }
return GeneratorChecker.isFile(inputImagePath, moreRecenThan: xcassetImagePath) return GeneratorChecker.isFile(inputImagePath, moreRecenThan: xcassetImagePath)
} }
} }

View File

@ -1,123 +1,149 @@
// //
// 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)
}
@discardableResult
static func getMagickConvertPath() -> String {
// WARNING: The convert command is deprecated in IMv7, use "magick" instead of "convert"
let taskMagick = Shell.shell(["which", "magick"])
if taskMagick.terminationStatus == 0 {
return taskMagick.output!.removeCharacters(from: CharacterSet.whitespacesAndNewlines) // swiftlint:disable:this force_unwrapping
}
let taskConvert = Shell.shell(["which", "convert"])
if taskConvert.terminationStatus == 0 {
return taskMagick.output!.removeCharacters(from: CharacterSet.whitespacesAndNewlines) // swiftlint:disable:this force_unwrapping
}
let error = ImagesError.magickConvertNotFound
print(error.description)
Self.exit(withError: error)
} }
} }

View File

@ -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,42 +8,47 @@
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)
case unknownImageExtension(String) case unknownImageExtension(String)
case getFileAttributed(String, String) case getFileAttributed(String, String)
case rsvgConvertNotFound case rsvgConvertNotFound
case magickConvertNotFound
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 imagemagick --with-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 .magickConvertNotFound:
return "error: [\(Images.toolName)] Can't find magick or convert (can be installed with 'brew install imagemagick')"
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)"
} }

View File

@ -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()

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
struct ConvertArgument { struct ConvertArgument {
let width: String? let width: String?
let height: String? let height: String?
} }

Some files were not shown because too many files have changed in this diff Show More