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