Compare commits

..

5 Commits

Author SHA1 Message Date
279f13dbd5 Add SwiftLint HARD rules
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2025-04-30 17:05:02 +02:00
c3445042b7 Remove a lots of code duplication in StringsFileGeneratorTests
Some checks reported warnings
gitea-openium/resgen.swift/pipeline/head This commit is unstable
2025-04-30 15:24:39 +02:00
d66775730e Remove some unneeded #if os(macos)
Some checks reported warnings
gitea-openium/resgen.swift/pipeline/head This commit is unstable
2025-04-30 13:53:26 +02:00
1dbf4c643d Fix tests for Jenkins that always build for iOS
Some checks reported warnings
gitea-openium/resgen.swift/pipeline/head This commit is unstable
2025-04-30 12:03:57 +02:00
ae7c0abbc2 Fix plist font file name and fix all tests
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2025-04-30 11:37:17 +02:00
95 changed files with 3014 additions and 2852 deletions

View File

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

6
Jenkinsfile vendored
View File

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

View File

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

View File

@ -1,16 +1,25 @@
// swift-tools-version:5.6
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
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

View File

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

View File

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

View File

@ -3,12 +3,12 @@
FORCE_FLAG="$1"
## Font
#swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/sampleFontsAll.txt" \
# --extension-output-path "./Fonts/Generated" \
# --extension-name "FontYolo" \
# --extension-name-ui-kit "UIFontYolo" \
# --extension-suffix "GenAllScript" \
# --info-plist-paths "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist"
swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/sampleFontsAll.txt" \
--extension-output-path "./Fonts/Generated" \
--extension-name "FontYolo" \
--extension-name-ui-kit "UIFontYolo" \
--extension-suffix "GenAllScript" \
--info-plist-paths "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist"
#
#echo "\n-------------------------\n"
#

View File

@ -5,9 +5,9 @@
// Created by Loris Perret on 08/12/2023.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct Analytics: ParsableCommand {
@ -42,15 +42,17 @@ struct Analytics: ParsableCommand {
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,
AnalyticsGenerator.writeExtensionFiles(
sections: sections,
target: options.target,
tags: ["ios", "iosonly"],
staticVar: options.staticMembers,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
extensionFilePath: options.extensionFilePath
)
print("[\(Self.toolName)] Analytics generated")
}
@ -64,19 +66,21 @@ struct Analytics: ParsableCommand {
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,
guard GeneratorChecker.shouldGenerate(
force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
extensionFilePath: options.extensionFilePath
) else {
print("[\(Self.toolName)] Analytics are already up to date :) ")
return false
}

View File

@ -8,6 +8,7 @@
import Foundation
enum AnalyticsError: Error {
case noValidTracker(String)
case fileNotExists(String)
case missingElement(String)
@ -32,7 +33,7 @@ enum AnalyticsError: Error {
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)"
}
}

View File

@ -5,10 +5,13 @@
// 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
@ -34,8 +37,9 @@ 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"

View File

@ -5,34 +5,50 @@
// 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: " ")
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,
let extensionFileContent = getExtensionContent(
targets: targets,
sections: sections,
tags: tags,
staticVar: staticVar,
extensionName: extensionName)
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)
@ -41,38 +57,57 @@ class AnalyticsGenerator {
// 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())
\(getImport(targets: targets))
\(Self.getAnalyticsProtocol())
\(getAnalyticsProtocol(targets: targets))
// MARK: - Manager
class AnalyticsManager {
static var shared = AnalyticsManager()
// MARK: - Properties
var managers: [AnalyticsManagerProtocol] = []
\(Self.getEnabledContent())
\(getEnabledContent())
\(Self.getAnalyticsProperties())
\(getAnalyticsProperties(targets: targets))
\(Self.getPrivateLogFunction())
\(getPrivateLogFunction())
"""
}
@ -88,7 +123,7 @@ class AnalyticsGenerator {
"""
}
private static func getImport() -> String {
private static func getImport(targets: [TrackerType]) -> String {
var result: [String] = []
if targets.contains(TrackerType.matomo) {
@ -131,7 +166,7 @@ class AnalyticsGenerator {
"""
}
private static func getAnalyticsProperties() -> String {
private static func getAnalyticsProperties(targets: [TrackerType]) -> String {
var header = ""
var content: [String] = []
let footer = " }"
@ -164,11 +199,12 @@ 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,
@ -193,7 +229,11 @@ class AnalyticsGenerator {
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

View File

@ -11,10 +11,10 @@ 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")
}

View File

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

View File

@ -8,6 +8,9 @@
import Foundation
class AnalyticsCategory {
// MARK: - Properties
let id: String // OnBoarding
var definitions = [AnalyticsDefinition]()

View File

@ -9,6 +9,9 @@ import Foundation
import ToolCore
class AnalyticsDefinition {
// MARK: - Properties
let id: String
var name: String
var path: String = ""

View File

@ -8,16 +8,19 @@
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
@ -28,6 +31,7 @@ struct AnalyticsDefinitionScreenDTO: Codable {
}
struct AnalyticsDefinitionEventDTO: Codable {
var id: String
var name: String
var tags: String
@ -39,6 +43,7 @@ struct AnalyticsDefinitionEventDTO: Codable {
}
struct AnalyticsParameterDTO: Codable {
var name: String
var type: String
var replaceIn: String?

View File

@ -8,6 +8,9 @@
import Foundation
class AnalyticsParameter {
// MARK: - Properties
var name: String
var type: String
var replaceIn: [String] = []

View File

@ -10,6 +10,7 @@ import Foundation
extension AnalyticsDefinition {
enum TagType {
case screen
case event
}

View File

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

View File

@ -9,10 +9,48 @@ 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)
@ -29,10 +67,9 @@ 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
@ -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,
@ -110,9 +147,9 @@ class AnalyticsFileParser {
}
}
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
}
}
}

View File

@ -5,15 +5,15 @@
// 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
)
@ -31,7 +31,7 @@ struct Colors: ParsableCommand {
// MARK: - Run
public func run() throws {
func run() throws {
print("[\(Self.toolName)] Starting colors generation")
// Check requirements
@ -43,28 +43,36 @@ struct Colors: ParsableCommand {
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,
ColorExtensionGenerator.writeExtensionFile(
colors: parsedColors,
staticVar: options.staticMembers,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath,
isSwiftUI: true)
isSwiftUI: true
)
// Generate extension
ColorExtensionGenerator.writeExtensionFile(colors: parsedColors,
ColorExtensionGenerator.writeExtensionFile(
colors: parsedColors,
staticVar: options.staticMembers,
extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: false)
isSwiftUI: false
)
print("[\(Self.toolName)] Colors generated")
}
@ -78,27 +86,29 @@ struct Colors: ParsableCommand {
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,
guard GeneratorChecker.shouldGenerate(
force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
extensionFilePath: options.extensionFilePath
) else {
print("[\(Self.toolName)] Colors are already up to date :) ")
return false
}
@ -118,7 +128,7 @@ struct Colors: ParsableCommand {
} catch {
let error = ColorsToolError.deleteExistingColors("\(options.xcassetsPath)/Colors")
print(error.description)
Colors.exit(withError: error)
Self.exit(withError: error)
}
}
}

View File

@ -8,6 +8,7 @@
import Foundation
enum ColorsToolError: Error {
case extensionNamesCollision(String)
case badFormat(String)
case writeAsset(String)
@ -31,13 +32,13 @@ enum ColorsToolError: Error {
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):

View File

@ -5,10 +5,13 @@
// 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
@ -44,7 +47,7 @@ extension ColorsToolOptions {
// MARK: - SwiftUI
var extensionFileName: String {
if let extensionSuffix = extensionSuffix {
if let extensionSuffix {
return "\(extensionName)+\(extensionSuffix).swift"
}
return "\(extensionName).swift"
@ -57,7 +60,7 @@ extension ColorsToolOptions {
// MARK: - UIKit
var extensionFileNameUIKit: String {
if let extensionSuffix = extensionSuffix {
if let extensionSuffix {
return "\(extensionNameUIKit)+\(extensionSuffix).swift"
}
return "\(extensionNameUIKit).swift"

View File

@ -15,32 +15,38 @@ struct ColorExtensionGenerator {
// MARK: - UIKit
static func writeExtensionFile(colors: [ParsedColor],
static func writeExtensionFile(
colors: [ParsedColor],
staticVar: Bool,
extensionName: String,
extensionFilePath: String,
isSwiftUI: Bool) {
isSwiftUI: Bool
) {
// Create extension content
let extensionContent = Self.getExtensionContent(colors: colors,
let extensionContent = Self.getExtensionContent(
colors: colors,
staticVar: staticVar,
extensionName: extensionName,
isSwiftUI: isSwiftUI)
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],
static func getExtensionContent(
colors: [ParsedColor],
staticVar: Bool,
extensionName: String,
isSwiftUI: Bool) -> String {
isSwiftUI: Bool
) -> String {
[
Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI),
Self.getProperties(for: colors, withStaticVar: staticVar, isSwiftUI: isSwiftUI),
@ -66,9 +72,11 @@ struct ColorExtensionGenerator {
"""
}
private static func getProperties(for colors: [ParsedColor],
private static func getProperties(
for colors: [ParsedColor],
withStaticVar staticVar: Bool,
isSwiftUI: Bool) -> String {
isSwiftUI: Bool
) -> String {
colors.map {
$0.getColorProperty(isStatic: staticVar, isSwiftUI: isSwiftUI)
}

View File

@ -8,7 +8,7 @@
import Foundation
import ToolCore
struct ColorXcassetHelper {
enum ColorXcassetHelper {
static func generateXcassetColors(colors: [ParsedColor], to xcassetsPath: String) {
colors.forEach {
@ -25,8 +25,10 @@ struct ColorXcassetHelper {
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)
@ -38,7 +40,7 @@ struct ColorXcassetHelper {
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)

View File

@ -5,10 +5,11 @@
// Created by Thibaut Schmitt on 29/08/2022.
//
import Foundation
import ArgumentParser
import Foundation
enum ColorStyle: String, Decodable, ExpressibleByArgument {
case light
case all

View File

@ -8,6 +8,7 @@
import Foundation
struct ParsedColor {
let name: String
let light: String
let dark: String

View File

@ -7,10 +7,11 @@
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
@ -20,7 +21,7 @@ class ColorFileParser {
static func parseLines(lines: [String], colorStyle: ColorStyle) -> [ParsedColor] {
lines
.enumerated()
.compactMap { lineNumber, colorLine in
.compactMap { _, colorLine in // swiftlint:disable:this unused_enumerated
// Required format:
// colorName = "#RGB/#ARGB", colorName "#RGB/#ARGB", colorName "#RGB/#ARGB" "#RGB/#ARGB"
let colorLineCleanedUp = colorLine

View File

@ -5,10 +5,13 @@
// 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

View File

@ -5,9 +5,9 @@
// Created by Thibaut Schmitt on 13/12/2021.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct Fonts: ParsableCommand {
@ -30,7 +30,7 @@ struct Fonts: ParsableCommand {
// 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")
@ -43,22 +43,31 @@ struct Fonts: ParsableCommand {
let fontsToGenerate = FontFileParser.parse(options.inputFile)
// Get real font names
let inputFolder = URL(fileURLWithPath: options.inputFile).deletingLastPathComponent().relativePath
let fontsNames = FontsToolHelper.getFontPostScriptName(for: fontsToGenerate,
inputFolder: inputFolder)
let inputFolder = URL(fileURLWithPath: options.inputFile)
.deletingLastPathComponent()
.relativePath
let fontsNames = FontsToolHelper.getFontPostScriptName(
for: fontsToGenerate,
inputFolder: inputFolder
)
// Generate extension
FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames,
FontExtensionGenerator.writeExtensionFile(
fontsNames: fontsNames,
staticVar: options.staticMembers,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath,
isSwiftUI: true)
isSwiftUI: true
)
FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames,
FontExtensionGenerator.writeExtensionFile(
fontsNames: fontsNames,
staticVar: options.staticMembers,
extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: false)
isSwiftUI: false
)
print("Info.plist has been updated with:")
print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames, infoPlistPaths: options.infoPlistPaths))")
@ -75,20 +84,22 @@ struct Fonts: ParsableCommand {
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,
guard GeneratorChecker.shouldGenerate(
force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
extensionFilePath: options.extensionFilePath
) else {
print("[\(Self.toolName)] Fonts are already up to date :) ")
return false
}

View File

@ -8,6 +8,7 @@
import Foundation
enum FontsToolError: Error {
case extensionNamesCollision(String)
case fcScan(String, Int32, String?)
case inputFolderNotFound(String)
@ -19,7 +20,7 @@ enum FontsToolError: Error {
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):
@ -28,7 +29,7 @@ enum FontsToolError: Error {
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)"
}
}

View File

@ -8,7 +8,7 @@
import Foundation
import ToolCore
class FontsToolHelper {
enum FontsToolHelper {
static func getFontPostScriptName(for fonts: [String], inputFolder: String) -> [FontName] {
let fontsFilenames = Self.getFontsFilenames(fromInputFolder: inputFolder)
@ -43,10 +43,10 @@ class FontsToolHelper {
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
@ -57,17 +57,26 @@ class FontsToolHelper {
return fontsFileNames
}
private static func getFontName(atPath path: String) -> String {
private static func getFontName(atPath path: String) -> FontName {
// print("fc-scan --format %{postscriptname} \(path)")
// Get real font name
let task = Shell.shell(["fc-scan", "--format", "%{postscriptname}", path])
guard let fontName = task.output, task.terminationStatus == 0 else {
guard let postscriptName = task.output, task.terminationStatus == 0 else {
let error = FontsToolError.fcScan(path, task.terminationStatus, task.output)
print(error.description)
Fonts.exit(withError: error)
}
return fontName
let pathURL = URL(fileURLWithPath: path)
let filename = pathURL
.deletingPathExtension()
.lastPathComponent
return FontName(
postscriptName: postscriptName,
filename: filename,
fileExtension: pathURL.pathExtension
)
}
}

View File

@ -8,7 +8,8 @@
import Foundation
import ToolCore
class FontPlistGenerator {
enum FontPlistGenerator {
static func generatePlistUIAppsFontContent(for fonts: [FontName], infoPlistPaths: [String]) -> String {
let fontsToAddToPlist = fonts
.compactMap { $0 }
@ -16,26 +17,32 @@ class FontPlistGenerator {
// 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 {
Shell.shell(launchPath: "/usr/libexec/PlistBuddy",
["-c", "add :UIAppFonts: string \($0)", infoPlist])
.forEach { fontName in
Shell.shell(
launchPath: "/usr/libexec/PlistBuddy",
["-c", "add :UIAppFonts: string \(fontName.filename).\(fontName.fileExtension)", infoPlist]
)
}
}
var plistData = "<key>UIAppFonts</key>\n\t<array>\n"
fontsToAddToPlist
.forEach {
plistData += "\t\t<string>\($0)</string>\n"
.forEach { fontName in
plistData += "\t\t<string>\(fontName.filename).\(fontName.fileExtension)</string>\n"
}
plistData += "\t</array>"

View File

@ -8,45 +8,51 @@
import Foundation
import ToolCore
class FontExtensionGenerator {
enum FontExtensionGenerator {
private static func getFontNameEnum(fontsNames: [String]) -> String {
private static func getFontNameEnum(fontsNames: [FontName]) -> String {
var enumDefinition = " enum FontName: String {\n"
fontsNames.forEach {
enumDefinition += " case \($0.fontNameSanitize) = \"\($0)\"\n"
enumDefinition += " case \($0.fontNameSanitize) = \"\($0.postscriptName)\"\n"
}
enumDefinition += " }\n"
return enumDefinition
}
static func writeExtensionFile(fontsNames: [String],
static func writeExtensionFile(
fontsNames: [FontName],
staticVar: Bool,
extensionName: String,
extensionFilePath: String,
isSwiftUI: Bool) {
isSwiftUI: Bool
) {
// Create extension content
let extensionContent = Self.getExtensionContent(fontsNames: fontsNames,
let extensionContent = Self.getExtensionContent(
fontsNames: fontsNames,
staticVar: staticVar,
extensionName: extensionName,
isSwiftUI: isSwiftUI)
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: [String],
static func getExtensionContent(
fontsNames: [FontName],
staticVar: Bool,
extensionName: String,
isSwiftUI: Bool) -> String {
isSwiftUI: Bool
) -> String {
[
Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI),
Self.getFontNameEnum(fontsNames: fontsNames),

View File

@ -7,11 +7,19 @@
import Foundation
typealias FontName = String
// swiftlint:disable no_grouping_extension
struct FontName: Hashable {
let postscriptName: String
let filename: String
let fileExtension: String
}
extension FontName {
var fontNameSanitize: String {
self.removeCharacters(from: "[]+-_")
postscriptName.removeCharacters(from: "[]+-_")
}
func getProperty(isStatic: Bool, isSwiftUI: Bool) -> String {

View File

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

View File

@ -5,9 +5,9 @@
// Created by Thibaut Schmitt on 30/08/2022.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct Generate: ParsableCommand {
@ -28,7 +28,7 @@ struct Generate: ParsableCommand {
// MARK: - Run
public func run() throws {
func run() throws {
print("[\(Self.toolName)] Starting Resgen with configuration: \(options.configurationFile)")
// Parse
@ -43,16 +43,20 @@ struct Generate: ParsableCommand {
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")
}

View File

@ -8,6 +8,7 @@
import Foundation
enum GenerateError: Error {
case fileNotExists(String)
case invalidConfigurationFile(String, String)
case commandError([String], String)
@ -18,16 +19,15 @@ enum GenerateError: Error {
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)"
}
}

View File

@ -5,10 +5,11 @@
// 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

View File

@ -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 = [
@ -30,7 +31,7 @@ struct ArchitectureGenerator {
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)

View File

@ -8,6 +8,7 @@
import Foundation
struct ConfigurationFile: Codable, CustomDebugStringConvertible {
var architecture: ConfigurationArchitecture?
var analytics: [AnalyticsConfiguration]
var colors: [ColorsConfiguration]
@ -44,10 +45,11 @@ 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)()"
@ -81,6 +83,7 @@ struct ConfigurationArchitecture: Codable {
}
struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let target: String
let extensionOutputPath: String
@ -89,18 +92,20 @@ struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible {
private let staticMembers: Bool?
var staticMembersOptions: Bool {
if let staticMembers = staticMembers {
if let staticMembers {
return staticMembers
}
return false
}
internal init(inputFile: String,
internal init(
inputFile: String,
target: String,
extensionOutputPath: String,
extensionName: String?,
extensionSuffix: String?,
staticMembers: Bool?) {
staticMembers: Bool?
) {
self.inputFile = inputFile
self.target = target
self.extensionOutputPath = extensionOutputPath
@ -122,6 +127,7 @@ struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible {
}
struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let style: String
let xcassetsPath: String
@ -132,20 +138,22 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
private let staticMembers: Bool?
var staticMembersOptions: Bool {
if let staticMembers = staticMembers {
if let staticMembers {
return staticMembers
}
return false
}
internal init(inputFile: String,
internal init(
inputFile: String,
style: String,
xcassetsPath: String,
extensionOutputPath: String,
extensionName: String?,
extensionNameUIKit: String?,
extensionSuffix: String?,
staticMembers: Bool?) {
staticMembers: Bool?
) {
self.inputFile = inputFile
self.style = style
self.xcassetsPath = xcassetsPath
@ -171,6 +179,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
}
struct FontsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let extensionOutputPath: String
let extensionName: String?
@ -180,19 +189,21 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible {
private let staticMembers: Bool?
var staticMembersOptions: Bool {
if let staticMembers = staticMembers {
if let staticMembers {
return staticMembers
}
return false
}
internal init(inputFile: String,
internal init(
inputFile: String,
extensionOutputPath: String,
extensionName: String?,
extensionNameUIKit: String?,
extensionSuffix: String?,
infoPlistPaths: String?,
staticMembers: Bool?) {
staticMembers: Bool?
) {
self.inputFile = inputFile
self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName
@ -216,6 +227,7 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible {
}
struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let xcassetsPath: String
let extensionOutputPath: String
@ -225,19 +237,21 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
private let staticMembers: Bool?
var staticMembersOptions: Bool {
if let staticMembers = staticMembers {
if let staticMembers {
return staticMembers
}
return false
}
internal init(inputFile: String,
internal init(
inputFile: String,
xcassetsPath: String,
extensionOutputPath: String,
extensionName: String?,
extensionNameUIKit: String?,
extensionSuffix: String?,
staticMembers: Bool?) {
staticMembers: Bool?
) {
self.inputFile = inputFile
self.xcassetsPath = xcassetsPath
self.extensionOutputPath = extensionOutputPath
@ -261,6 +275,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
}
struct StringsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let outputPath: String
let langs: String
@ -272,20 +287,21 @@ 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,
internal init(
inputFile: String,
outputPath: String,
langs: String,
defaultLang: String,
@ -293,7 +309,8 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
extensionName: String?,
extensionSuffix: String?,
staticMembers: Bool?,
xcStrings: Bool?) {
xcStrings: Bool?
) {
self.inputFile = inputFile
self.outputPath = outputPath
self.langs = langs
@ -320,6 +337,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
}
struct TagsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let lang: String
let extensionOutputPath: String
@ -328,18 +346,20 @@ struct TagsConfiguration: Codable, CustomDebugStringConvertible {
private let staticMembers: Bool?
var staticMembersOptions: Bool {
if let staticMembers = staticMembers {
if let staticMembers {
return staticMembers
}
return false
}
internal init(inputFile: String,
internal init(
inputFile: String,
lang: String,
extensionOutputPath: String,
extensionName: String?,
extensionSuffix: String?,
staticMembers: Bool?) {
staticMembers: Bool?
) {
self.inputFile = inputFile
self.lang = lang
self.extensionOutputPath = extensionOutputPath

View File

@ -8,7 +8,8 @@
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)

View File

@ -8,6 +8,7 @@
import Foundation
extension AnalyticsConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
var args = [String]()
@ -25,13 +26,13 @@ extension AnalyticsConfiguration: Runnable {
"\(staticMembersOptions)"
]
if let extensionName = extensionName {
if let extensionName {
args += [
"--extension-name",
extensionName
]
}
if let extensionSuffix = extensionSuffix {
if let extensionSuffix {
args += [
"--extension-suffix",
extensionSuffix

View File

@ -8,6 +8,7 @@
import Foundation
extension ColorsConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
let args = getArguments(projectDirectory: projectDirectory, force: force)
Colors.main(args)
@ -32,19 +33,19 @@ extension ColorsConfiguration: Runnable {
"\(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

View File

@ -8,6 +8,7 @@
import Foundation
extension FontsConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
let args = getArguments(projectDirectory: projectDirectory, force: force)
Fonts.main(args)
@ -28,27 +29,27 @@ extension FontsConfiguration: Runnable {
"\(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) }

View File

@ -8,6 +8,7 @@
import Foundation
extension ImagesConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
let args = getArguments(projectDirectory: projectDirectory, force: force)
Images.main(args)
@ -30,19 +31,21 @@ extension ImagesConfiguration: Runnable {
"\(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

View File

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

View File

@ -8,6 +8,7 @@
import Foundation
extension StringsConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
var args = [String]()
@ -31,14 +32,14 @@ extension StringsConfiguration: Runnable {
"\(xcStringsOptions)"
]
if let extensionName = extensionName {
if let extensionName {
args += [
"--extension-name",
extensionName
]
}
if let extensionSuffix = extensionSuffix {
if let extensionSuffix {
args += [
"--extension-suffix",
extensionSuffix

View File

@ -8,6 +8,7 @@
import Foundation
extension TagsConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
var args = [String]()
@ -25,13 +26,13 @@ extension TagsConfiguration: Runnable {
"\(staticMembersOptions)"
]
if let extensionName = extensionName {
if let extensionName {
args += [
"--extension-name",
extensionName
]
}
if let extensionSuffix = extensionSuffix {
if let extensionSuffix {
args += [
"--extension-suffix",
extensionSuffix

View File

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

View File

@ -5,42 +5,48 @@
// Created by Thibaut Schmitt on 14/02/2022.
//
import ToolCore
import Foundation
import ToolCore
class ImageExtensionGenerator {
enum ImageExtensionGenerator {
// MARK: - UIKit
static func generateExtensionFile(images: [ParsedImage],
static func generateExtensionFile(
images: [ParsedImage],
staticVar: Bool,
inputFilename: String,
extensionName: String,
extensionFilePath: String,
isSwiftUI: Bool) {
isSwiftUI: Bool
) {
// Create extension conten1t
let extensionContent = Self.getExtensionContent(images: images,
let extensionContent = Self.getExtensionContent(
images: images,
staticVar: staticVar,
extensionName: extensionName,
inputFilename: inputFilename,
isSwiftUI: isSwiftUI)
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],
static func getExtensionContent(
images: [ParsedImage],
staticVar: Bool,
extensionName: String,
inputFilename: String,
isSwiftUI: Bool) -> String {
isSwiftUI: Bool
) -> String {
[
Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName, isSwiftUI: isSwiftUI),
Self.getProperties(images: images, staticVar: staticVar, isSwiftUI: isSwiftUI),
@ -49,9 +55,11 @@ class ImageExtensionGenerator {
.joined(separator: "\n")
}
private static func getHeader(inputFilename: String,
private static func getHeader(
inputFilename: String,
extensionClassname: String,
isSwiftUI: Bool) -> String {
isSwiftUI: Bool
) -> String {
"""
// Generated by ResgenSwift.\(Images.toolName) \(ResgenSwiftVersion)
// Images from \(inputFilename)
@ -62,7 +70,11 @@ class ImageExtensionGenerator {
"""
}
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")

View File

@ -9,12 +9,15 @@ import Foundation
import ToolCore
enum OutputImageExtension: String {
case png
case svg
}
class XcassetsGenerator {
// MARK: - Properties
let forceGeneration: Bool
// MARK: - Init
@ -33,7 +36,7 @@ class XcassetsGenerator {
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 {
@ -83,8 +86,10 @@ class XcassetsGenerator {
// 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,15 +146,33 @@ 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
@ -159,7 +180,11 @@ class XcassetsGenerator {
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")
}
@ -177,7 +202,7 @@ class XcassetsGenerator {
}
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")
}

View File

@ -5,9 +5,9 @@
// Created by Thibaut Schmitt on 24/01/2022.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct Images: ParsableCommand {
@ -48,24 +48,30 @@ struct Images: ParsableCommand {
.relativePath
let xcassetsGenerator = XcassetsGenerator(forceGeneration: options.forceExecutionAndGeneration)
xcassetsGenerator.generateXcassets(inputPath: inputFolder,
xcassetsGenerator.generateXcassets(
inputPath: inputFolder,
imagesToGenerate: imagesToGenerate,
xcassetsPath: options.xcassetsPath)
xcassetsPath: options.xcassetsPath
)
// Generate extension
ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate,
ImageExtensionGenerator.generateExtensionFile(
images: imagesToGenerate,
staticVar: options.staticMembers,
inputFilename: options.inputFilenameWithoutExt,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath,
isSwiftUI: true)
isSwiftUI: true
)
ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate,
ImageExtensionGenerator.generateExtensionFile(
images: imagesToGenerate,
staticVar: options.staticMembers,
inputFilename: options.inputFilenameWithoutExt,
extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: false)
isSwiftUI: false
)
print("[\(Self.toolName)] Images generated")
}
@ -83,23 +89,25 @@ struct Images: ParsableCommand {
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,
guard GeneratorChecker.shouldGenerate(
force: options.forceExecution,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
extensionFilePath: options.extensionFilePath
) else {
print("[\(Self.toolName)] Images are already up to date :) ")
return false
}
@ -113,11 +121,11 @@ struct Images: ParsableCommand {
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)
}
}

View File

@ -8,6 +8,7 @@
import Foundation
enum ImagesError: Error {
case extensionNamesCollision(String)
case inputFolderNotFound(String)
case fileNotExists(String)
@ -32,13 +33,13 @@ enum ImagesError: Error {
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):

View File

@ -5,10 +5,13 @@
// 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
@ -44,7 +47,7 @@ extension ImagesOptions {
// MARK: - SwiftUI
var extensionFileName: String {
if let extensionSuffix = extensionSuffix {
if let extensionSuffix {
return "\(extensionName)+\(extensionSuffix).swift"
}
return "\(extensionName).swift"
@ -57,7 +60,7 @@ extension ImagesOptions {
// MARK: - UIKit
var extensionFileNameUIKit: String {
if let extensionSuffix = extensionSuffix {
if let extensionSuffix {
return "\(extensionNameUIKit)+\(extensionSuffix).swift"
}
return "\(extensionNameUIKit).swift"

View File

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

View File

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

View File

@ -8,10 +8,14 @@
import Foundation
enum ImageExtension: String {
case png
}
struct ParsedImage {
// MARK: - Properties
let name: String
let tags: String
let width: Int
@ -34,7 +38,7 @@ 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 = ""
@ -77,7 +81,6 @@ struct ParsedImage {
}
func generateImageContent(isVector: Bool) -> AssetContent {
if !imageExtensions.contains(.png) && isVector {
// Generate svg
return AssetContent(

View File

@ -8,6 +8,7 @@
import Foundation
enum PlatormTag: String {
case droid = "d"
case ios = "i"
}

View File

@ -7,10 +7,10 @@
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)
@ -30,13 +30,13 @@ class ImageFileParser {
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] = []

View File

@ -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],
static func writeStringsFiles(
sections: [Section],
langs: [String],
defaultLang: String,
tags: [String],
outputPath: String,
inputFilenameWithoutExt: String) {
inputFilenameWithoutExt: String
) {
var stringsFilesContent = [String: String]()
for lang in langs {
stringsFilesContent[lang] = Self.generateStringsFileContent(lang: lang,
stringsFilesContent[lang] = Self.generateStringsFileContent(
lang: lang,
defaultLang: defaultLang,
tags: tags,
sections: sections)
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],
static func writeXcStringsFiles(
sections: [Section],
langs: [String],
defaultLang: String,
tags: [String],
outputPath: String,
inputFilenameWithoutExt: 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,
static func generateStringsFileContent(
lang: String,
defaultLang: String,
tags inputTags: [String],
sections: [Section]) -> String {
sections: [Section]
) -> String {
var stringsFileContent = """
/**
* Apple Strings File
@ -120,11 +130,18 @@ class StringsFileGenerator {
// MARK: - XcStrings Generation
static func generateXcStringsFileContent(langs: [String],
static func generateXcStringsFileContent(
langs: [String],
defaultLang: String,
tags inputTags: [String],
sections: [Section]) -> String {
let rootObject = generateRootObject(langs: langs, defaultLang: defaultLang, tags: inputTags, sections: sections)
sections: [Section]
) -> String {
let rootObject = generateRootObject(
langs: langs,
defaultLang: defaultLang,
tags: inputTags,
sections: sections
)
let file = generateXcStringsFileContentFromRootObject(rootObject: rootObject)
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],
static func generateRootObject(
langs: [String],
defaultLang: String,
tags inputTags: [String],
sections: [Section]) -> Root {
sections: [Section]
) -> Root {
var xcStringDefinitionTab: [XCStringDefinition] = []
sections.forEach { section in
sections.forEach { section in // swiftlint:disable:this closure_body_length
// Check that at least one string will be generated
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(
@ -229,28 +246,32 @@ class StringsFileGenerator {
// MARK: - Extension file
static func writeExtensionFiles(sections: [Section],
static func writeExtensionFiles(
sections: [Section],
defaultLang lang: String,
tags: [String],
staticVar: Bool,
inputFilename: String,
extensionName: String,
extensionFilePath: String,
extensionSuffix: String) {
extensionSuffix: String
) {
// Get extension content
let extensionFileContent = Self.getExtensionContent(sections: sections,
let extensionFileContent = Self.getExtensionContent(
sections: sections,
defaultLang: lang,
tags: tags,
staticVar: staticVar,
inputFilename: inputFilename,
extensionName: extensionName,
extensionSuffix: extensionSuffix)
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],
static func getExtensionContent(
sections: [Section],
defaultLang lang: String,
tags: [String],
staticVar: Bool,
inputFilename: String,
extensionName: String,
extensionSuffix: String) -> 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

View File

@ -5,24 +5,34 @@
// 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,
let extensionFileContent = Self.getExtensionContent(
sections: sections,
lang: lang,
tags: tags,
staticVar: staticVar,
extensionName: extensionName)
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)
@ -31,10 +41,24 @@ class TagsGenerator {
// 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")
@ -52,7 +76,12 @@ class TagsGenerator {
"""
}
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

View File

@ -7,7 +7,12 @@
import Foundation
// swiftlint:disable force_unwrapping
class Definition {
// MARK: - Properties
let name: String
var tags = [String]()
var comment: String?
@ -48,21 +53,31 @@ class Definition {
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
}
@ -95,11 +110,16 @@ class Definition {
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) :
@ -131,12 +151,14 @@ class Definition {
// Generate method
var method = ""
if let parameters = self.getStringParameters(input: translation) {
method = getBaseMethod(lang: lang,
method = getBaseMethod(
lang: lang,
translation: translation,
isStatic: false,
inputParameters: parameters.inputParameters,
translationArguments: parameters.translationArguments,
comment: self.comment)
comment: self.comment
)
}
return property + method
@ -160,12 +182,14 @@ class Definition {
// Generate method
var method = ""
if let parameters = self.getStringParameters(input: translation) {
method = getBaseMethod(lang: lang,
method = getBaseMethod(
lang: lang,
translation: translation,
isStatic: true,
inputParameters: parameters.inputParameters,
translationArguments: parameters.translationArguments,
comment: self.comment)
comment: self.comment
)
}
return property + method

View File

@ -8,13 +8,20 @@
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

View File

@ -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,21 +82,25 @@ 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 {
// enum VarationOrStringUnit: Encodable
//
// case variations([Varation])
// case stringUnit: (DefaultStringUnit)
//
@ -104,6 +114,7 @@ struct XCStringLocalizationLangContent: Codable, Equatable {
// }
struct DefaultStringUnit: Codable, Equatable {
let state: String
let value: String
}

View File

@ -7,15 +7,18 @@
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)
@ -62,11 +65,6 @@ class TwineFileParser {
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
//}
}
}
}
@ -83,7 +81,7 @@ class TwineFileParser {
return true
}
}
if invalidDefinitionNames.count > 0 {
if invalidDefinitionNames.isEmpty == false {
print("warning: [\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))")
}

View File

@ -5,9 +5,9 @@
// Created by Thibaut Schmitt on 10/01/2022.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct Stringium: ParsableCommand {
@ -47,31 +47,37 @@ struct Stringium: ParsableCommand {
if !options.xcStrings {
print("[\(Self.toolName)] Will generate strings")
StringsFileGenerator.writeStringsFiles(sections: sections,
StringsFileGenerator.writeStringsFiles(
sections: sections,
langs: options.langs,
defaultLang: options.defaultLang,
tags: options.tags,
outputPath: options.stringsFileOutputPath,
inputFilenameWithoutExt: options.inputFilenameWithoutExt)
inputFilenameWithoutExt: options.inputFilenameWithoutExt
)
} else {
print("[\(Self.toolName)] Will generate xcStrings")
StringsFileGenerator.writeXcStringsFiles(sections: sections,
StringsFileGenerator.writeXcStringsFiles(
sections: sections,
langs: options.langs,
defaultLang: options.defaultLang,
tags: options.tags,
outputPath: options.stringsFileOutputPath,
inputFilenameWithoutExt: options.inputFilenameWithoutExt)
inputFilenameWithoutExt: options.inputFilenameWithoutExt
)
}
// Generate extension
StringsFileGenerator.writeExtensionFiles(sections: sections,
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)
extensionSuffix: options.extensionSuffix
)
print("[\(Self.toolName)] Strings generated")
}
@ -85,26 +91,28 @@ struct Stringium: ParsableCommand {
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,
guard GeneratorChecker.shouldGenerate(
force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
extensionFilePath: options.extensionFilePath
) else {
print("[\(Self.toolName)] Strings are already up to date :) ")
return false
}

View File

@ -8,6 +8,7 @@
import Foundation
enum StringiumError: Error {
case fileNotExists(String)
case langsListEmpty
case defaultLangsNotInLangs
@ -25,10 +26,10 @@ enum StringiumError: Error {
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."
}

View File

@ -5,10 +5,13 @@
// 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
@ -46,6 +49,7 @@ struct StringiumOptions: ParsableArguments {
// MARK: - Private var getter
extension StringiumOptions {
var stringsFileOutputPath: String {
var outputPath = outputPathRaw
if outputPath.last == "/" {
@ -70,6 +74,7 @@ extension StringiumOptions {
// MARK: - Computed var
extension StringiumOptions {
var extensionFileName: String {
"\(extensionName)+\(extensionSuffix).swift"
}

View File

@ -5,9 +5,9 @@
// Created by Thibaut Schmitt on 10/01/2022.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct Strings: ParsableCommand {

View File

@ -5,9 +5,9 @@
// Created by Thibaut Schmitt on 10/01/2022.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct Tags: ParsableCommand {
@ -43,12 +43,14 @@ struct Tags: ParsableCommand {
let sections = TwineFileParser.parse(options.inputFile)
// Generate extension
TagsGenerator.writeExtensionFiles(sections: sections,
TagsGenerator.writeExtensionFiles(
sections: sections,
lang: options.lang,
tags: ["ios", "iosonly", Self.noTranslationTag],
staticVar: options.staticMembers,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
extensionFilePath: options.extensionFilePath
)
print("[\(Self.toolName)] Tags generated")
}
@ -66,9 +68,11 @@ struct Tags: ParsableCommand {
}
// Check if needed to regenerate
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration,
guard GeneratorChecker.shouldGenerate(
force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
extensionFilePath: options.extensionFilePath
) else {
print("[\(Self.toolName)] Tags are already up to date :) ")
return false
}

View File

@ -5,10 +5,13 @@
// 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
@ -34,8 +37,9 @@ 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"

View File

@ -5,9 +5,9 @@
// Created by Thibaut Schmitt on 10/01/2022.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct Twine: ParsableCommand {
@ -22,7 +22,13 @@ struct Twine: ParsableCommand {
static let toolName = "Twine"
static let defaultExtensionName = "String"
static let twineExecutable = "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)/scripts/twine/twine"
static let twineExecutable: String = {
#if os(macOS)
"\(FileManager.default.homeDirectoryForCurrentUser.relativePath)/scripts/twine/twine"
#else
fatalError("This command should run on Mac only")
#endif
}()
// MARK: - Command Options
@ -40,20 +46,33 @@ struct Twine: ParsableCommand {
// Generate strings files (lproj files)
for lang in options.langs {
Shell.shell([Self.twineExecutable,
"generate-localization-file", options.inputFile,
"--lang", "\(lang)",
Shell.shell(
[
Self.twineExecutable,
"generate-localization-file",
options.inputFile,
"--lang",
"\(lang)",
"\(options.outputPath)/\(lang).lproj/\(options.inputFilenameWithoutExt).strings",
"--tags=ios,iosonly,iosOnly"])
"--tags=ios,iosonly,iosOnly"
]
)
}
// Generate extension
Shell.shell([Self.twineExecutable,
"generate-localization-file", options.inputFile,
"--format", "apple-swift",
"--lang", "\(options.defaultLang)",
Shell.shell(
[
Self.twineExecutable,
"generate-localization-file",
options.inputFile,
"--format",
"apple-swift",
"--lang",
"\(options.defaultLang)",
options.extensionFilePath,
"--tags=ios,iosonly,iosOnly"])
"--tags=ios,iosonly,iosOnly"
]
)
print("[\(Self.toolName)] Strings generated")
}
@ -67,26 +86,28 @@ struct Twine: ParsableCommand {
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,
guard GeneratorChecker.shouldGenerate(
force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePathGenerated) else {
extensionFilePath: options.extensionFilePathGenerated
) else {
print("[\(Self.toolName)] Strings are already up to date :) ")
return false
}

View File

@ -8,6 +8,7 @@
import Foundation
enum TwineError: Error {
case fileNotExists(String)
case langsListEmpty
case defaultLangsNotInLangs

View File

@ -5,10 +5,13 @@
// 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
@ -31,6 +34,7 @@ struct TwineOptions: ParsableArguments {
// MARK: - Private var getter
extension TwineOptions {
var langs: [String] {
langsRaw
.split(separator: " ")
@ -41,6 +45,7 @@ extension TwineOptions {
// MARK: - Computed var
extension TwineOptions {
var inputFilenameWithoutExt: String {
URL(fileURLWithPath: inputFile)
.deletingPathExtension()

View File

@ -5,9 +5,9 @@
// Created by Thibaut Schmitt on 13/12/2021.
//
import ToolCore
import Foundation
import ArgumentParser
import Foundation
import ToolCore
struct ResgenSwift: ParsableCommand {

View File

@ -7,7 +7,7 @@
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 {
@ -39,5 +39,4 @@ public class GeneratorChecker {
return false
}
}

View File

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

View File

@ -7,40 +7,18 @@
import Foundation
public class Shell {
public enum Shell {
public static var environment: [String: String] {
ProcessInfo.processInfo.environment
}
// @discardableResult
// public static func shell(launchPath: String = "/usr/bin/env", _ args: String...) -> (terminationStatus: Int32, output: String?) {
// 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
// try? task.run()
// task.waitUntilExit()
//
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
//
// guard let output: String = String(data: data, encoding: .utf8) else {
// return (terminationStatus: task.terminationStatus, output: nil)
// }
//
// return (terminationStatus: task.terminationStatus, output: output)
// }
@discardableResult
public static func shell(launchPath: String = "/usr/bin/env", _ args: [String]) -> (terminationStatus: Int32, output: String?) {
public static func shell(
launchPath: String = "/usr/bin/env",
_ args: [String]
) -> (terminationStatus: Int32, output: String?) {
#if os(macOS)
let task = Process()
task.launchPath = launchPath
task.arguments = args
@ -58,10 +36,13 @@ public class Shell {
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
fatalError("Shell is only available on Mac")
#endif
}
}

View File

@ -7,25 +7,26 @@
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 {
@ -35,7 +36,7 @@ public extension String {
return newString
}
func removeLeadingWhitespace() -> Self {
public func removeLeadingWhitespace() -> Self {
var newString = self
while newString.first?.isWhitespace == true {
@ -45,7 +46,7 @@ public extension String {
return newString
}
func removeLeadingTrailingWhitespace() -> Self {
public func removeLeadingTrailingWhitespace() -> Self {
var newString = self
newString = newString.removeLeadingWhitespace()
@ -54,16 +55,20 @@ public extension String {
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)
replacingOccurrences(of: "~", with: "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)")
#else
fatalError("This command should run on Mac only")
#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
@ -86,11 +91,11 @@ public extension String {
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,

View File

@ -7,4 +7,6 @@
import Foundation
public let ResgenSwiftVersion = "1.2"
// swiftlint:disable prefixed_toplevel_constant identifier_name
public let ResgenSwiftVersion = "2.1.0"

View File

@ -84,7 +84,7 @@ final class AnalyticsDefinitionTests: XCTestCase {
name: "Ecran un",
action: "",
category: "",
params: []
params: [:]
)
}
"""
@ -127,7 +127,7 @@ final class AnalyticsDefinitionTests: XCTestCase {
name: "Ecran un",
action: "",
category: "",
params: []
params: [:]
)
}
"""

View File

@ -51,20 +51,24 @@ final class AnalyticsGeneratorTests: XCTestCase {
]
// When
AnalyticsGenerator.targets = [TrackerType.firebase]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
let extensionContent = AnalyticsGenerator.getExtensionContent(
targets: [TrackerType.firebase],
sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"],
staticVar: false,
extensionName: "GenAnalytics")
extensionName: "GenAnalytics"
)
// Expect Analytics
let expect = """
// Generated by ResgenSwift.Analytics 1.2
// Generated by ResgenSwift.Analytics \(ResgenSwiftVersion)
import Firebase
import FirebaseAnalytics
// MARK: - Protocol
protocol AnalyticsManagerProtocol {
func logScreen(name: String, path: String)
func logEvent(
name: String,
@ -77,9 +81,10 @@ final class AnalyticsGeneratorTests: XCTestCase {
// MARK: - Firebase
class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
func logScreen(name: String, path: String) {
var parameters = [
AnalyticsParameterScreenName: name
AnalyticsParameterScreenName: name as NSObject
]
Analytics.logEvent(
@ -94,19 +99,25 @@ final class AnalyticsGeneratorTests: XCTestCase {
category: String,
params: [String: Any]?
) {
var parameters: [String:Any] = [
"action": action,
"category": category,
var parameters: [String:NSObject] = [
"action": action as NSObject,
"category": category as NSObject,
]
if let supplementaryParameters = params {
parameters.merge(supplementaryParameters) { (origin, new) -> Any in
return origin
for (newKey, newValue) in supplementaryParameters {
if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
}
}
Analytics.logEvent(
name,
name.replacingOccurrences(of: [" "], with: "_"),
parameters: parameters
)
}
@ -115,6 +126,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
// MARK: - Manager
class AnalyticsManager {
static var shared = AnalyticsManager()
// MARK: - Properties
@ -173,7 +185,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
name: "s1 def two",
action: "",
category: "",
params: []
params: [:]
)
}
@ -216,20 +228,23 @@ final class AnalyticsGeneratorTests: XCTestCase {
]
// When
AnalyticsGenerator.targets = [TrackerType.matomo]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
let extensionContent = AnalyticsGenerator.getExtensionContent(
targets: [TrackerType.matomo],
sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"],
staticVar: false,
extensionName: "GenAnalytics")
extensionName: "GenAnalytics"
)
// Expect Analytics
let expect = """
// Generated by ResgenSwift.Analytics 1.2
// Generated by ResgenSwift.Analytics \(ResgenSwiftVersion)
import MatomoTracker
// MARK: - Protocol
protocol AnalyticsManagerProtocol {
func logScreen(name: String, path: String)
func logEvent(
name: String,
@ -303,6 +318,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
// MARK: - Manager
class AnalyticsManager {
static var shared = AnalyticsManager()
// MARK: - Properties
@ -366,7 +382,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
name: "s1 def two",
action: "test",
category: "test",
params: []
params: [:]
)
}
@ -409,21 +425,25 @@ final class AnalyticsGeneratorTests: XCTestCase {
]
// When
AnalyticsGenerator.targets = [TrackerType.matomo, TrackerType.firebase]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
let extensionContent = AnalyticsGenerator.getExtensionContent(
targets: [TrackerType.matomo, TrackerType.firebase],
sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"],
staticVar: false,
extensionName: "GenAnalytics")
extensionName: "GenAnalytics"
)
// Expect Analytics
let expect = """
// Generated by ResgenSwift.Analytics 1.2
// Generated by ResgenSwift.Analytics \(ResgenSwiftVersion)
import MatomoTracker
import Firebase
import FirebaseAnalytics
// MARK: - Protocol
protocol AnalyticsManagerProtocol {
func logScreen(name: String, path: String)
func logEvent(
name: String,
@ -499,7 +519,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
func logScreen(name: String, path: String) {
var parameters = [
AnalyticsParameterScreenName: name
AnalyticsParameterScreenName: name as NSObject
]
Analytics.logEvent(
@ -514,19 +534,25 @@ final class AnalyticsGeneratorTests: XCTestCase {
category: String,
params: [String: Any]?
) {
var parameters: [String:Any] = [
"action": action,
"category": category,
var parameters: [String:NSObject] = [
"action": action as NSObject,
"category": category as NSObject,
]
if let supplementaryParameters = params {
parameters.merge(supplementaryParameters) { (origin, new) -> Any in
return origin
for (newKey, newValue) in supplementaryParameters {
if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
}
}
Analytics.logEvent(
name,
name.replacingOccurrences(of: [" "], with: "_"),
parameters: parameters
)
}
@ -535,6 +561,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
// MARK: - Manager
class AnalyticsManager {
static var shared = AnalyticsManager()
// MARK: - Properties
@ -599,7 +626,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
name: "s1 def two",
action: "test",
category: "test",
params: []
params: [:]
)
}

View File

@ -16,8 +16,8 @@ final class FontExtensionGeneratorTests: XCTestCase {
func test_uiKit_GeneratedExtensionContent() {
// Given
let fontNames: [FontName] = [
"CircularStd-Regular",
"CircularStd-Bold"
FontName(postscriptName: "CircularStd-Regular", filename: "CircularStd-Regular", fileExtension: "ttf"),
FontName(postscriptName: "CircularStd-Bold", filename: "CircularStd-Bold", fileExtension: "ttf")
]
// When
@ -59,8 +59,8 @@ final class FontExtensionGeneratorTests: XCTestCase {
func test_swiftUI_GeneratedExtensionContent() {
// Given
let fontNames: [FontName] = [
"CircularStd-Regular",
"CircularStd-Bold"
FontName(postscriptName: "CircularStd-Regular", filename: "CircularStd-Regular", fileExtension: "ttf"),
FontName(postscriptName: "CircularStd-Bold", filename: "CircularStd-Bold", fileExtension: "ttf")
]
// When

View File

@ -14,7 +14,11 @@ final class FontNameTests: XCTestCase {
func test_uiKit_GeneratedProperty_noForbiddenCharacter() {
// Given
let fontName: FontName = "CircularStdBold"
let fontName = FontName(
postscriptName: "CircularStdBold",
filename: "CircularStd-Bold",
fileExtension: "ttf"
)
// When
let property = fontName.getProperty(isStatic: true, isSwiftUI: false)
@ -31,7 +35,11 @@ final class FontNameTests: XCTestCase {
func test_uiKit_GeneratedProperty_withForbiddenCharacter() {
// Given
let fontName: FontName = "[Circular_Std+Bold-Underline]"
let fontName = FontName(
postscriptName: "[Circular_Std+Bold-Underline]",
filename: "Circular_Std+Bold-Underline",
fileExtension: "ttf"
)
// When
let property = fontName.getProperty(isStatic: true, isSwiftUI: false)
@ -48,7 +56,11 @@ final class FontNameTests: XCTestCase {
func test_uiKit_GeneratedMethod_noForbiddenCharacter() {
// Given
let fontName: FontName = "CircularStdBold"
let fontName = FontName(
postscriptName: "CircularStdBold",
filename: "CircularStd-Bold",
fileExtension: "ttf"
)
// When
let property = fontName.getProperty(isStatic: false, isSwiftUI: false)
@ -65,7 +77,11 @@ final class FontNameTests: XCTestCase {
func test_uiKit_GeneratedMethod_withForbiddenCharacter() {
// Given
let fontName: FontName = "[Circular_Std+Bold-Underline]"
let fontName = FontName(
postscriptName: "[Circular_Std+Bold-Underline]",
filename: "Circular_Std+Bold-Underline",
fileExtension: "ttf"
)
// When
let property = fontName.getProperty(isStatic: false, isSwiftUI: false)
@ -82,7 +98,11 @@ final class FontNameTests: XCTestCase {
func test_swiftUI_GeneratedProperty_noForbiddenCharacter() {
// Given
let fontName: FontName = "CircularStdBold"
let fontName = FontName(
postscriptName: "CircularStdBold",
filename: "CircularStd-Bold",
fileExtension: "ttf"
)
// When
let property = fontName.getProperty(isStatic: true, isSwiftUI: true)
@ -99,7 +119,11 @@ final class FontNameTests: XCTestCase {
func test_swiftUI_GeneratedProperty_withForbiddenCharacter() {
// Given
let fontName: FontName = "[Circular_Std+Bold-Underline]"
let fontName = FontName(
postscriptName: "[Circular_Std+Bold-Underline]",
filename: "Circular_Std+Bold-Underline",
fileExtension: "ttf"
)
// When
let property = fontName.getProperty(isStatic: true, isSwiftUI: true)
@ -116,7 +140,11 @@ final class FontNameTests: XCTestCase {
func test_swiftUI_GeneratedMethod_noForbiddenCharacter() {
// Given
let fontName: FontName = "CircularStdBold"
let fontName = FontName(
postscriptName: "CircularStdBold",
filename: "CircularStd-Bold",
fileExtension: "ttf"
)
// When
let property = fontName.getProperty(isStatic: false, isSwiftUI: true)
@ -133,7 +161,11 @@ final class FontNameTests: XCTestCase {
func test_swiftUI_GeneratedMethod_withForbiddenCharacter() {
// Given
let fontName: FontName = "[Circular_Std+Bold-Underline]"
let fontName = FontName(
postscriptName: "[Circular_Std+Bold-Underline]",
filename: "Circular_Std+Bold-Underline",
fileExtension: "ttf"
)
// When
let property = fontName.getProperty(isStatic: false, isSwiftUI: true)

View File

@ -14,8 +14,8 @@ final class FontPlistGeneratorTests: XCTestCase {
func testGeneratedPlist() {
// Given
let fontNames: [FontName] = [
"CircularStd-Regular",
"CircularStd-Bold"
FontName(postscriptName: "CircularStd-Regular", filename: "CircularStd-Regular", fileExtension: "ttf"),
FontName(postscriptName: "CircularStd-Bold", filename: "CircularStd-Bold", fileExtension: "ttf")
]
// When
@ -25,8 +25,8 @@ final class FontPlistGeneratorTests: XCTestCase {
let expect = """
<key>UIAppFonts</key>
<array>
<string>CircularStd-Regular</string>
<string>CircularStd-Bold</string>
<string>CircularStd-Regular.ttf</string>
<string>CircularStd-Bold.ttf</string>
</array>
"""

View File

@ -14,9 +14,8 @@ final class ResgenCLITests: XCTestCase {
return
}
// Mac Catalyst won't have `Process`, but it is supported for executables.
#if !targetEnvironment(macCatalyst)
// Process available on Mac only
#if os(macOS)
let fooBinary = productsDirectory.appendingPathComponent("ResgenSwift")
let process = Process()

View File

@ -0,0 +1,51 @@
//
// StringsFileGenerator+OldStringsFile.swift
// ResgenSwift
//
// Created by Thibaut Schmitt on 30/04/2025.
//
import ToolCore
extension StringsFileGeneratorTests {
static let appleStringsFileExpectationFr = """
/**
* Apple Strings File
* Generated by ResgenSwift \(ResgenSwiftVersion)
* Language: fr
*/
/********** section_one **********/
"s1_def_one" = "Section Un - Definition Un";
"s1_def_two" = "Section Un - Definition Deux";
/********** section_two **********/
"s2_def_one" = "Section Deux - Definition Un";
"s2_def_two" = "Section Deux - Definition Deux";
"""
static let appleStringsFileExpectationEn = """
/**
* Apple Strings File
* Generated by ResgenSwift \(ResgenSwiftVersion)
* Language: en
*/
/********** section_one **********/
"s1_def_one" = "Section One - Definition One";
"s1_def_two" = "Section One - Definition Two";
/********** section_two **********/
"s2_def_one" = "Section Two - Definition One";
"s2_def_two" = "Section Deux - Definition Deux";
"""
}

View File

@ -0,0 +1,91 @@
//
// StringsFileGenerator+R2ExtensionsExpectation.swift
// ResgenSwift
//
// Created by Thibaut Schmitt on 30/04/2025.
//
@testable import ResgenSwift
import ToolCore
extension StringsFileGeneratorTests {
static func getExtensionContentExpectation(
staticVar: Bool,
s1DefOneFr: String = "Section Un - Definition Un",
s1DefOneComment: String = "",
s1DefTwoFr: String = "Section Un - Definition Deux",
s1DefTwoComment: String = "",
s2DefOneFr: String = "Section Deux - Definition Un",
s2DefOneComment: String = "",
s2DefTwoFr: String = "Section Deux - Definition Deux",
s2DefTwoComment: String = "",
) -> String {
"""
// Generated by ResgenSwift.Strings.Stringium \(ResgenSwiftVersion)
import UIKit
fileprivate let kStringsFileName = "myInputFilename"
extension GenStrings {
enum KeyStrings: String {
case s1_def_one = "s1_def_one"
case s1_def_two = "s1_def_two"
case s2_def_one = "s2_def_one"
case s2_def_two = "s2_def_two"
var keyPath: KeyPath<GenStrings, String> {
switch self {
case .s1_def_one: return \\GenStrings.s1_def_one
case .s1_def_two: return \\GenStrings.s1_def_two
case .s2_def_one: return \\GenStrings.s2_def_one
case .s2_def_two: return \\GenStrings.s2_def_two
}
}
}
// MARK: - section_one
/// Translation in fr :
/// \(s1DefOneFr)
///
/// Comment :
/// \(s1DefOneComment.isEmpty ? "No comment" : s1DefOneComment)
\(staticVar ? "static " : "")var s1_def_one: String {
NSLocalizedString("s1_def_one", tableName: kStringsFileName, bundle: Bundle.main, value: "Section Un - Definition Un", comment: "\(s1DefOneComment)")
}
/// Translation in fr :
/// \(s1DefTwoFr)
///
/// Comment :
/// \(s1DefTwoComment.isEmpty ? "No comment" : s1DefTwoComment)
\(staticVar ? "static " : "")var s1_def_two: String {
NSLocalizedString("s1_def_two", tableName: kStringsFileName, bundle: Bundle.main, value: "Section Un - Definition Deux", comment: "\(s1DefTwoComment)")
}
// MARK: - section_two
/// Translation in fr :
/// \(s2DefOneFr)
///
/// Comment :
/// \(s2DefOneComment.isEmpty ? "No comment" : s2DefOneComment)
\(staticVar ? "static " : "")var s2_def_one: String {
NSLocalizedString("s2_def_one", tableName: kStringsFileName, bundle: Bundle.main, value: "Section Deux - Definition Un", comment: "\(s2DefOneComment)")
}
/// Translation in fr :
/// \(s2DefTwoFr)
///
/// Comment :
/// \(s2DefTwoComment.isEmpty ? "No comment" : s2DefTwoComment)
\(staticVar ? "static " : "")var s2_def_two: String {
NSLocalizedString("s2_def_two", tableName: kStringsFileName, bundle: Bundle.main, value: "Section Deux - Definition Deux", comment: "\(s2DefTwoComment)")
}
}
"""
}
}

View File

@ -0,0 +1,129 @@
//
// StringsFileGenerator+XCStringsExpectation.swift
// ResgenSwift
//
// Created by Thibaut Schmitt on 30/04/2025.
//
@testable import ResgenSwift
extension XCStringDefinition {
enum Mock {
static func getDefinitionSectionOne(
defOneFr: String = "Section Un - Definition Un",
defOneEn: String = "Section One - Definition One",
defOneComment: String? = nil,
defTwoFr: String = "Section Un - Definition Deux",
defTwoEn: String = "Section One - Definition Two",
defTwoComment: String? = nil
) -> [XCStringDefinition] {
[
XCStringDefinition(
title: "s1_def_one",
content: XCStringDefinitionContent(
comment: defOneComment,
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: getXCStringLocalization(
defFr: defOneFr,
defEn: defOneEn
)
)
)
),
XCStringDefinition(
title: "s1_def_two",
content: XCStringDefinitionContent(
comment: defTwoComment,
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: getXCStringLocalization(
defFr: defTwoFr,
defEn: defTwoEn
)
)
)
)
]
}
static func getDefinitionSectionTwo(
defOneFr: String = "Section Deux - Definition Un",
defOneEn: String = "Section Two - Definition One",
defOneComment: String? = nil,
defTwoFr: String = "Section Deux - Definition Deux",
defTwoEn: String? = nil,
defTwoComment: String? = nil
) -> [XCStringDefinition] {
[
XCStringDefinition(
title: "s2_def_one",
content: XCStringDefinitionContent(
comment: defOneComment,
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: getXCStringLocalization(
defFr: defOneFr,
defEn: defOneEn
)
)
)
),
XCStringDefinition(
title: "s2_def_two",
content: XCStringDefinitionContent(
comment: defTwoComment,
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: getXCStringLocalization(
defFr: defTwoFr,
defEn: defTwoEn ?? ""
)
)
)
)
]
}
// MARK: - Private methods
private static func getXCStringLocalization(
defFr: String,
defEn: String
) -> [XCStringLocalization] {
var localizations = [XCStringLocalization]()
if defFr.isEmpty == false {
localizations.append(
XCStringLocalization(
lang: "fr",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: defFr
)
)
)
)
}
if defEn.isEmpty == false {
localizations.append(
XCStringLocalization(
lang: "en",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: defEn
)
)
)
)
}
return localizations
}
}
}

View File

@ -0,0 +1,95 @@
//
// Section+Mock.swift
// ResgenSwift
//
// Created by Thibaut Schmitt on 30/04/2025.
//
@testable import ResgenSwift
extension Section {
enum Mock {
static func getSectionOne(
defOneFr: String = "Section Un - Definition Un",
defOneEn: String = "Section One - Definition One",
defOneComment: String? = nil,
defOneTag: [String] = ["ios","iosonly"],
defTwoFr: String = "Section Un - Definition Deux",
defTwoEn: String = "Section One - Definition Two",
defTwoComment: String? = nil,
defTwoTag: [String] = ["ios","iosonly"]
) -> Section {
let section = Section(name: "section_one")
section.definitions = [
Self.getDefinition(
name: "s1_def_one",
translations: ["fr": defOneFr,
"en": defOneEn],
tags: defOneTag,
comment: defOneComment
),
Self.getDefinition(
name: "s1_def_two",
translations: ["fr": defTwoFr,
"en": defTwoEn],
tags: defTwoTag,
comment: defTwoComment
)
]
return section
}
static func getSectionTwo(
defOneFr: String = "Section Deux - Definition Un",
defOneEn: String = "Section Two - Definition One",
defOneComment: String? = nil,
defOneTag: [String] = ["ios","iosonly"],
defTwoFr: String = "Section Deux - Definition Deux",
defTwoEn: String? = nil,
defTwoComment: String? = nil,
defTwoTag: [String] = ["notranslation"]
) -> Section {
let section = Section(name: "section_two")
let defTwoTranslations: [String: String] = {
var translations = ["fr": "Section Deux - Definition Deux"]
if let defTwoEn {
translations["en"] = defTwoEn
}
return translations
}()
section.definitions = [
Self.getDefinition(
name: "s2_def_one",
translations: ["fr": defOneFr,
"en": defOneEn],
tags: defOneTag,
comment: defOneComment
),
Self.getDefinition(
name: "s2_def_two",
translations: defTwoTranslations,
tags: defTwoTag,
comment: defTwoComment
)
]
return section
}
// MARK: - Private methods
private static func getDefinition(
name: String,
translations: [String: String],
tags: [String],
comment: String? = nil
) -> Definition {
let definition = Definition(name: name)
definition.tags = tags
definition.translations = translations
definition.comment = comment
return definition
}
}
}

View File

@ -0,0 +1,489 @@
//
// StringsFileGeneratorTests.swift
//
//
// Created by Thibaut Schmitt on 06/09/2022.
//
import Foundation
import XCTest
import ToolCore
@testable import ResgenSwift
final class StringsFileGeneratorTests: XCTestCase {
// MARK: - Strings File Content
func testGenerateStringsFileContent() {
// Given
let sectionOne = Section.Mock.getSectionOne()
let sectionTwo = Section.Mock.getSectionTwo()
// When
let stringsFileContentFr = StringsFileGenerator.generateStringsFileContent(
lang: "fr",
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
sections: [sectionOne, sectionTwo]
)
let stringsFileContentEn = StringsFileGenerator.generateStringsFileContent(
lang: "en",
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
sections: [sectionOne, sectionTwo]
)
// Expect
XCTAssertEqual(
stringsFileContentFr.adaptForXCTest(),
Self.appleStringsFileExpectationFr.adaptForXCTest()
)
XCTAssertEqual(
stringsFileContentEn.adaptForXCTest(),
Self.appleStringsFileExpectationEn.adaptForXCTest()
)
}
func testGenerateStringsFileContentWithComment() {
// Given
let sectionOne = Section.Mock.getSectionOne(
defOneComment: "This is a comment",
defTwoComment: "This is a comment"
)
let sectionTwo = Section.Mock.getSectionTwo(
defOneComment: "This is a comment",
defTwoComment: "This is a comment"
)
// When
let stringsFileContentFr = StringsFileGenerator.generateStringsFileContent(
lang: "fr",
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
sections: [sectionOne, sectionTwo]
)
let stringsFileContentEn = StringsFileGenerator.generateStringsFileContent(
lang: "en",
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
sections: [sectionOne, sectionTwo]
)
// Expect
XCTAssertEqual(
stringsFileContentFr.adaptForXCTest(),
Self.appleStringsFileExpectationFr.adaptForXCTest()
)
XCTAssertEqual(
stringsFileContentEn.adaptForXCTest(),
Self.appleStringsFileExpectationEn.adaptForXCTest()
)
}
// MARK: - XcString File Content
func testGenerateXcStringsRootObject() {
// Given
let sectionOne = Section.Mock.getSectionOne()
let sectionTwo = Section.Mock.getSectionTwo(
defTwoEn: "Section Two - Definition Two"
)
// When
let rootObject = StringsFileGenerator.generateRootObject(
langs: ["fr", "en"],
defaultLang: "en",
tags: ["ios", "iosonly", "notranslation"],
sections: [sectionOne, sectionTwo]
)
// Expect
let expect =
Root(
sourceLanguage: "en",
strings: XCStringDefinitionContainer(
strings: [
XCStringDefinition(
title: "s1_def_one",
content: XCStringDefinitionContent(
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: [
XCStringLocalization(
lang: "en",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: "Section One - Definition One"
)
)
),
XCStringLocalization(
lang: "fr",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: "Section Un - Definition Un"
)
)
)
]
)
)
),
XCStringDefinition(
title: "s1_def_two",
content: XCStringDefinitionContent(
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: [
XCStringLocalization(
lang: "en",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: "Section One - Definition Two"
)
)
),
XCStringLocalization(
lang: "fr",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: "Section Un - Definition Deux"
)
)
)
]
)
)
),
XCStringDefinition(
title: "s2_def_one",
content: XCStringDefinitionContent(
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: [
XCStringLocalization(
lang: "en",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: "Section Two - Definition One"
)
)
),
XCStringLocalization(
lang: "fr",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: "Section Deux - Definition Un"
)
)
)
]
)
)
),
XCStringDefinition(
title: "s2_def_two",
content: XCStringDefinitionContent(
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: [
XCStringLocalization(
lang: "en",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: "Section Two - Definition Two"
)
)
),
XCStringLocalization(
lang: "fr",
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(
state: "translated",
value: "Section Two - Definition Two"
)
)
)
]
)
)
)
]
),
version: "1.0"
)
XCTAssertEqual(rootObject, expect)
}
func testGenerateXcStringsRootObjectWithEmptyTranslations() {
// Given
let sectionOne = Section.Mock.getSectionOne(
defOneFr: "",
defTwoFr: ""
)
let sectionTwo = Section.Mock.getSectionTwo(
defOneFr: "",
defTwoFr: "",
defTwoEn: "Section Two - Definition Two"
)
// By default Section2.s2_def_two has a fr value => remove it
sectionTwo.definitions
.first { def in
def.name == "s2_def_two"
}?
.translations.removeValue(forKey: "fr")
// When
let rootObject = StringsFileGenerator.generateRootObject(
langs: ["fr", "en"],
defaultLang: "en",
tags: ["ios", "iosonly", "notranslation"],
sections: [sectionOne, sectionTwo]
)
// Expect
let expect =
Root(
sourceLanguage: "en",
strings: XCStringDefinitionContainer(
strings: [
XCStringDefinition.Mock.getDefinitionSectionOne(
defOneFr: "",
defTwoFr: "",
),
XCStringDefinition.Mock.getDefinitionSectionTwo(
defOneFr: "",
defTwoFr: "Section Two - Definition Two",
defTwoEn: "Section Two - Definition Two",
)
]
.flatMap { $0 }
),
version: "1.0"
)
XCTAssertEqual(rootObject, expect)
}
func testGenerateXcStringsRootObjectWithNoTranslations() {
// Given
let sectionOne = Section.Mock.getSectionOne(
defOneFr: "",
defOneEn: "",
defTwoFr: "",
defTwoEn : ""
)
let sectionTwo = Section.Mock.getSectionTwo(
defOneFr: "",
defOneEn: "",
defTwoFr: "",
defTwoEn : ""
)
// When
let rootObject = StringsFileGenerator.generateRootObject(
langs: ["fr", "en"],
defaultLang: "en",
tags: ["ios", "iosonly", "notranslation"],
sections: [sectionOne, sectionTwo]
)
// Expect
let expect =
Root(
sourceLanguage: "en",
strings: XCStringDefinitionContainer(
strings: [
XCStringDefinition.Mock.getDefinitionSectionOne(
defOneFr: "",
defOneEn: "",
defTwoFr: "",
defTwoEn : ""
),
XCStringDefinition.Mock.getDefinitionSectionTwo(
defOneFr: "",
defOneEn: "",
defTwoFr: "",
defTwoEn : ""
)
]
.flatMap { $0 }
),
version: "1.0"
)
XCTAssertEqual(rootObject, expect)
}
func testGenerateXcStringsRootObjectWithComments() {
// Given
let sectionOne = Section.Mock.getSectionOne(
defOneComment: "Comment 1",
defTwoComment: "Comment 2"
)
let sectionTwo = Section.Mock.getSectionTwo()
// When
let rootObject = StringsFileGenerator.generateRootObject(
langs: ["fr", "en"],
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
sections: [sectionOne, sectionTwo]
)
// Expect
let expect =
Root(
sourceLanguage: "fr",
strings: XCStringDefinitionContainer(
strings: [
XCStringDefinition.Mock.getDefinitionSectionOne(
defOneComment: "Comment 1",
defTwoComment: "Comment 2"
),
XCStringDefinition.Mock.getDefinitionSectionTwo(
// SourceLanguage is frn en definition should the same as fr
defTwoEn: "Section Deux - Definition Deux"
)
]
.flatMap { $0 }
),
version: "1.0"
)
XCTAssertEqual(rootObject, expect)
}
// MARK: - Extension Content
func testGeneratedExtensionContent() {
// Given
let sectionOne = Section.Mock.getSectionOne()
let sectionTwo = Section.Mock.getSectionTwo()
// When
let extensionContent = StringsFileGenerator.getExtensionContent(sections: [sectionOne, sectionTwo],
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
staticVar: false,
inputFilename: "myInputFilename",
extensionName: "GenStrings",
extensionSuffix: "strings")
// Expect
let expect = Self.getExtensionContentExpectation(
staticVar: false
)
if extensionContent != expect {
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
}
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
}
func testGeneratedExtensionContentWithComment() {
// Given
let sectionOne = Section.Mock.getSectionOne(
defOneComment: "This is a comment",
defTwoComment: "This is a comment"
)
let sectionTwo = Section.Mock.getSectionTwo(
defOneComment: "This is a comment",
defTwoComment: "This is a comment"
)
// When
let extensionContent = StringsFileGenerator.getExtensionContent(sections: [sectionOne, sectionTwo],
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
staticVar: false,
inputFilename: "myInputFilename",
extensionName: "GenStrings",
extensionSuffix: "strings")
// Expect
let expect = Self.getExtensionContentExpectation(
staticVar: false,
s1DefOneComment: "This is a comment",
s1DefTwoComment: "This is a comment",
s2DefOneComment: "This is a comment",
s2DefTwoComment: "This is a comment",
)
if extensionContent != expect {
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
}
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
}
// MARK: - Extension Content Static
func testGeneratedExtensionContentWithStaticVar() {
// Given
let sectionOne = Section.Mock.getSectionOne()
let sectionTwo = Section.Mock.getSectionTwo()
// When
let extensionContent = StringsFileGenerator.getExtensionContent(sections: [sectionOne, sectionTwo],
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
staticVar: true,
inputFilename: "myInputFilename",
extensionName: "GenStrings",
extensionSuffix: "strings")
// Expect
let expect = Self.getExtensionContentExpectation(
staticVar: true
)
if extensionContent != expect {
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
}
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
}
func testGeneratedExtensionContentWithStaticVarWithComment() {
// Given
let sectionOne = Section.Mock.getSectionOne(
defOneComment: "This is a comment",
defTwoComment: "This is a comment"
)
let sectionTwo = Section.Mock.getSectionTwo(
defOneComment: "This is a comment",
defTwoComment: "This is a comment"
)
// When
let extensionContent = StringsFileGenerator.getExtensionContent(sections: [sectionOne, sectionTwo],
defaultLang: "fr",
tags: ["ios", "iosonly", "notranslation"],
staticVar: true,
inputFilename: "myInputFilename",
extensionName: "GenStrings",
extensionSuffix: "strings")
// Expect
let expect = Self.getExtensionContentExpectation(
staticVar: true,
s1DefOneComment: "This is a comment",
s1DefTwoComment: "This is a comment",
s2DefOneComment: "This is a comment",
s2DefTwoComment: "This is a comment",
)
if extensionContent != expect {
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
}
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -57,12 +57,18 @@ final class TagsGeneratorTests: XCTestCase {
/// Translation in ium :
/// Some translation
///
/// Comment :
/// No comment
var s1_def_one: String {
"Some translation"
}
/// Translation in ium :
/// Some translation
///
/// Comment :
/// No comment
var s1_def_two: String {
"Some translation"
}
@ -71,6 +77,9 @@ final class TagsGeneratorTests: XCTestCase {
/// Translation in ium :
/// Some translation
///
/// Comment :
/// No comment
var s2_def_one: String {
"Some translation"
}